mirror-chatterino2/src/singletons/WindowManager.cpp
Rasmus Karlsson 7a08d73434 Fix preview on hover not working when Animated emotes options was disabled.
Fixes #1546

This change introduces a "window timer" that runs every 100ms that we
use to update the pixmap if necessary, since there is no signal for "let
me know when this image is done loading".
2020-02-15 17:16:10 +01:00

685 lines
18 KiB
C++

#include "singletons/WindowManager.hpp"
#include <QDebug>
#include <QDesktopWidget>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QSaveFile>
#include <QScreen>
#include <boost/optional.hpp>
#include <chrono>
#include "Application.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "messages/MessageElement.hpp"
#include "providers/irc/Irc2.hpp"
#include "providers/irc/IrcChannel2.hpp"
#include "providers/irc/IrcServer.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Fonts.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "util/Clamp.hpp"
#include "widgets/AccountSwitchPopup.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/Window.hpp"
#include "widgets/dialogs/SettingsDialog.hpp"
#include "widgets/helper/NotebookTab.hpp"
#include "widgets/splits/Split.hpp"
#include "widgets/splits/SplitContainer.hpp"
#define SETTINGS_FILENAME "/window-layout.json"
namespace chatterino {
namespace {
QJsonArray loadWindowArray(const QString &settingsPath)
{
QFile file(settingsPath);
file.open(QIODevice::ReadOnly);
QByteArray data = file.readAll();
QJsonDocument document = QJsonDocument::fromJson(data);
QJsonArray windows_arr = document.object().value("windows").toArray();
return windows_arr;
}
boost::optional<bool> &shouldMoveOutOfBoundsWindow()
{
static boost::optional<bool> x;
return x;
}
} // namespace
using SplitNode = SplitContainer::Node;
using SplitDirection = SplitContainer::Direction;
void WindowManager::showSettingsDialog(SettingsDialogPreference preference)
{
QTimer::singleShot(
80, [preference] { SettingsDialog::showDialog(preference); });
}
void WindowManager::showAccountSelectPopup(QPoint point)
{
// static QWidget *lastFocusedWidget = nullptr;
static AccountSwitchPopup *w = new AccountSwitchPopup();
if (w->hasFocus())
{
w->hide();
// if (lastFocusedWidget) {
// lastFocusedWidget->setFocus();
// }
return;
}
// lastFocusedWidget = this->focusWidget();
w->refresh();
QPoint buttonPos = point;
w->move(buttonPos.x() - 30, buttonPos.y());
w->show();
w->setFocus();
}
WindowManager::WindowManager()
{
qDebug() << "init WindowManager";
auto settings = getSettings();
this->wordFlagsListener_.addSetting(settings->showTimestamps);
this->wordFlagsListener_.addSetting(settings->showBadgesGlobalAuthority);
this->wordFlagsListener_.addSetting(settings->showBadgesChannelAuthority);
this->wordFlagsListener_.addSetting(settings->showBadgesSubscription);
this->wordFlagsListener_.addSetting(settings->showBadgesVanity);
this->wordFlagsListener_.addSetting(settings->showBadgesChatterino);
this->wordFlagsListener_.addSetting(settings->enableEmoteImages);
this->wordFlagsListener_.addSetting(settings->boldUsernames);
this->wordFlagsListener_.addSetting(settings->lowercaseDomains);
this->wordFlagsListener_.setCB([this] {
this->updateWordTypeMask(); //
});
this->saveTimer = new QTimer;
this->saveTimer->setSingleShot(true);
QObject::connect(this->saveTimer, &QTimer::timeout, [] {
getApp()->windows->save(); //
});
this->miscUpdateTimer_.start(100);
QObject::connect(&this->miscUpdateTimer_, &QTimer::timeout, [this] {
this->miscUpdate.invoke(); //
});
}
MessageElementFlags WindowManager::getWordFlags()
{
return this->wordFlags_;
}
void WindowManager::updateWordTypeMask()
{
using MEF = MessageElementFlag;
auto settings = getSettings();
// text
auto flags = MessageElementFlags(MEF::Text);
// timestamp
if (settings->showTimestamps)
{
flags.set(MEF::Timestamp);
}
// emotes
if (settings->enableEmoteImages)
{
flags.set(MEF::EmoteImages);
}
flags.set(MEF::EmoteText);
flags.set(MEF::EmojiText);
// bits
flags.set(MEF::BitsAmount);
flags.set(settings->animateEmotes ? MEF::BitsAnimated : MEF::BitsStatic);
// badges
flags.set(settings->showBadgesGlobalAuthority ? MEF::BadgeGlobalAuthority
: MEF::None);
flags.set(settings->showBadgesChannelAuthority ? MEF::BadgeChannelAuthority
: MEF::None);
flags.set(settings->showBadgesSubscription ? MEF::BadgeSubscription
: MEF::None);
flags.set(settings->showBadgesVanity ? MEF::BadgeVanity : MEF::None);
flags.set(settings->showBadgesChatterino ? MEF::BadgeChatterino
: MEF::None);
// username
flags.set(MEF::Username);
// misc
flags.set(MEF::AlwaysShow);
flags.set(MEF::Collapsed);
flags.set(settings->boldUsernames ? MEF::BoldUsername
: MEF::NonBoldUsername);
flags.set(settings->lowercaseDomains ? MEF::LowercaseLink
: MEF::OriginalLink);
// update flags
MessageElementFlags newFlags = static_cast<MessageElementFlags>(flags);
if (newFlags != this->wordFlags_)
{
this->wordFlags_ = newFlags;
this->wordFlagsChanged.invoke();
}
}
void WindowManager::layoutChannelViews(Channel *channel)
{
this->layoutRequested.invoke(channel);
}
void WindowManager::forceLayoutChannelViews()
{
this->incGeneration();
this->layoutChannelViews(nullptr);
}
void WindowManager::repaintVisibleChatWidgets(Channel *channel)
{
this->layoutRequested.invoke(channel);
}
void WindowManager::repaintGifEmotes()
{
this->gifRepaintRequested.invoke();
}
// void WindowManager::updateAll()
//{
// if (this->mainWindow != nullptr) {
// this->mainWindow->update();
// }
//}
Window &WindowManager::getMainWindow()
{
assertInGuiThread();
return *this->mainWindow_;
}
Window &WindowManager::getSelectedWindow()
{
assertInGuiThread();
return *this->selectedWindow_;
}
Window &WindowManager::createWindow(WindowType type, bool show)
{
assertInGuiThread();
auto *window = new Window(type);
this->windows_.push_back(window);
if (show)
{
window->show();
}
if (type != WindowType::Main)
{
window->setAttribute(Qt::WA_DeleteOnClose);
QObject::connect(window, &QWidget::destroyed, [this, window] {
for (auto it = this->windows_.begin(); it != this->windows_.end();
it++)
{
if (*it == window)
{
this->windows_.erase(it);
break;
}
}
});
}
return *window;
}
int WindowManager::windowCount()
{
return this->windows_.size();
}
Window *WindowManager::windowAt(int index)
{
assertInGuiThread();
if (index < 0 || (size_t)index >= this->windows_.size())
{
return nullptr;
}
qDebug() << "getting window at bad index" << index;
return this->windows_.at(index);
}
void WindowManager::initialize(Settings &settings, Paths &paths)
{
assertInGuiThread();
getApp()->themes->repaintVisibleChatWidgets_.connect(
[this] { this->repaintVisibleChatWidgets(); });
assert(!this->initialized_);
// load file
QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME;
QJsonArray windows_arr = loadWindowArray(settingsPath);
// "deserialize"
for (QJsonValue window_val : windows_arr)
{
QJsonObject window_obj = window_val.toObject();
// get type
QString type_val = window_obj.value("type").toString();
WindowType type =
type_val == "main" ? WindowType::Main : WindowType::Popup;
if (type == WindowType::Main && mainWindow_ != nullptr)
{
type = WindowType::Popup;
}
Window &window = createWindow(type, false);
if (type == WindowType::Main)
{
mainWindow_ = &window;
}
// get geometry
{
int x = window_obj.value("x").toInt(-1);
int y = window_obj.value("y").toInt(-1);
int width = window_obj.value("width").toInt(-1);
int height = window_obj.value("height").toInt(-1);
QRect geometry{x, y, width, height};
// out of bounds windows
auto screens = qApp->screens();
bool outOfBounds = std::none_of(
screens.begin(), screens.end(), [&](QScreen *screen) {
return screen->availableGeometry().intersects(geometry);
});
// ask if move into bounds
auto &&should = shouldMoveOutOfBoundsWindow();
if (outOfBounds && !should)
{
should =
QMessageBox(QMessageBox::Icon::Warning,
"Windows out of bounds",
"Some windows were detected out of bounds. "
"Should they be moved into bounds?",
QMessageBox::Yes | QMessageBox::No)
.exec() == QMessageBox::Yes;
}
if ((!outOfBounds || !should.value()) && x != -1 && y != -1 &&
width != -1 && height != -1)
{
// Have to offset x by one because qt moves the window 1px too
// far to the left:w
window.setInitialBounds({x, y, width, height});
}
}
// load tabs
QJsonArray tabs = window_obj.value("tabs").toArray();
for (QJsonValue tab_val : tabs)
{
SplitContainer *page = window.getNotebook().addPage(false);
QJsonObject tab_obj = tab_val.toObject();
// set custom title
QJsonValue title_val = tab_obj.value("title");
if (title_val.isString())
{
page->getTab()->setCustomTitle(title_val.toString());
}
// selected
if (tab_obj.value("selected").toBool(false))
{
window.getNotebook().select(page);
}
// highlighting on new messages
bool val = tab_obj.value("highlightsEnabled").toBool(true);
page->getTab()->setHighlightsEnabled(val);
// load splits
QJsonObject splitRoot = tab_obj.value("splits2").toObject();
if (!splitRoot.isEmpty())
{
page->decodeFromJson(splitRoot);
continue;
}
// fallback load splits (old)
int colNr = 0;
for (QJsonValue column_val : tab_obj.value("splits").toArray())
{
for (QJsonValue split_val : column_val.toArray())
{
Split *split = new Split(page);
QJsonObject split_obj = split_val.toObject();
split->setChannel(decodeChannel(split_obj));
page->appendSplit(split);
}
colNr++;
}
}
window.show();
if (window_obj.value("state") == "minimized")
{
window.setWindowState(Qt::WindowMinimized);
}
else if (window_obj.value("state") == "maximized")
{
window.setWindowState(Qt::WindowMaximized);
}
}
if (mainWindow_ == nullptr)
{
mainWindow_ = &createWindow(WindowType::Main);
mainWindow_->getNotebook().addPage(true);
}
settings.timestampFormat.connect(
[this](auto, auto) { this->layoutChannelViews(); });
settings.emoteScale.connect(
[this](auto, auto) { this->forceLayoutChannelViews(); });
settings.timestampFormat.connect(
[this](auto, auto) { this->forceLayoutChannelViews(); });
settings.alternateMessages.connect(
[this](auto, auto) { this->forceLayoutChannelViews(); });
settings.separateMessages.connect(
[this](auto, auto) { this->forceLayoutChannelViews(); });
settings.collpseMessagesMinLines.connect(
[this](auto, auto) { this->forceLayoutChannelViews(); });
this->initialized_ = true;
}
void WindowManager::save()
{
qDebug() << "[WindowManager] Saving";
assertInGuiThread();
QJsonDocument document;
// "serialize"
QJsonArray window_arr;
for (Window *window : this->windows_)
{
QJsonObject window_obj;
// window type
switch (window->getType())
{
case WindowType::Main:
window_obj.insert("type", "main");
break;
case WindowType::Popup:
window_obj.insert("type", "popup");
break;
case WindowType::Attached:;
}
if (window->isMaximized())
{
window_obj.insert("state", "maximized");
}
else if (window->isMinimized())
{
window_obj.insert("state", "minimized");
}
// window geometry
auto rect = window->getBounds();
window_obj.insert("x", rect.x());
window_obj.insert("y", rect.y());
window_obj.insert("width", rect.width());
window_obj.insert("height", rect.height());
// window tabs
QJsonArray tabs_arr;
for (int tab_i = 0; tab_i < window->getNotebook().getPageCount();
tab_i++)
{
QJsonObject tab_obj;
SplitContainer *tab = dynamic_cast<SplitContainer *>(
window->getNotebook().getPageAt(tab_i));
assert(tab != nullptr);
// custom tab title
if (tab->getTab()->hasCustomTitle())
{
tab_obj.insert("title", tab->getTab()->getCustomTitle());
}
// selected
if (window->getNotebook().getSelectedPage() == tab)
{
tab_obj.insert("selected", true);
}
// highlighting on new messages
tab_obj.insert("highlightsEnabled",
tab->getTab()->hasHighlightsEnabled());
// splits
QJsonObject splits;
this->encodeNodeRecusively(tab->getBaseNode(), splits);
tab_obj.insert("splits2", splits);
tabs_arr.append(tab_obj);
}
window_obj.insert("tabs", tabs_arr);
window_arr.append(window_obj);
}
QJsonObject obj;
obj.insert("windows", window_arr);
document.setObject(obj);
// save file
QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME;
QSaveFile file(settingsPath);
file.open(QIODevice::WriteOnly | QIODevice::Truncate);
QJsonDocument::JsonFormat format =
#ifdef _DEBUG
QJsonDocument::JsonFormat::Compact
#else
(QJsonDocument::JsonFormat)0
#endif
;
file.write(document.toJson(format));
file.commit();
}
void WindowManager::sendAlert()
{
int flashDuration = 2500;
if (getSettings()->longAlerts)
{
flashDuration = 0;
}
QApplication::alert(this->getMainWindow().window(), flashDuration);
}
void WindowManager::queueSave()
{
using namespace std::chrono_literals;
this->saveTimer->start(10s);
}
void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj)
{
switch (node->getType())
{
case SplitNode::_Split: {
obj.insert("type", "split");
obj.insert("moderationMode", node->getSplit()->getModerationMode());
QJsonObject split;
encodeChannel(node->getSplit()->getIndirectChannel(), split);
obj.insert("data", split);
obj.insert("flexh", node->getHorizontalFlex());
obj.insert("flexv", node->getVerticalFlex());
}
break;
case SplitNode::HorizontalContainer:
case SplitNode::VerticalContainer: {
obj.insert("type", node->getType() == SplitNode::HorizontalContainer
? "horizontal"
: "vertical");
QJsonArray items_arr;
for (const std::unique_ptr<SplitNode> &n : node->getChildren())
{
QJsonObject subObj;
this->encodeNodeRecusively(n.get(), subObj);
items_arr.append(subObj);
}
obj.insert("items", items_arr);
}
break;
}
}
void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj)
{
assertInGuiThread();
switch (channel.getType())
{
case Channel::Type::Twitch: {
obj.insert("type", "twitch");
obj.insert("name", channel.get()->getName());
}
break;
case Channel::Type::TwitchMentions: {
obj.insert("type", "mentions");
}
break;
case Channel::Type::TwitchWatching: {
obj.insert("type", "watching");
}
break;
case Channel::Type::TwitchWhispers: {
obj.insert("type", "whispers");
}
break;
case Channel::Type::Irc: {
if (auto ircChannel =
dynamic_cast<IrcChannel *>(channel.get().get()))
{
obj.insert("type", "irc");
if (ircChannel->server())
{
obj.insert("server", ircChannel->server()->id());
}
obj.insert("channel", ircChannel->getName());
}
}
break;
}
}
IndirectChannel WindowManager::decodeChannel(const QJsonObject &obj)
{
assertInGuiThread();
auto app = getApp();
QString type = obj.value("type").toString();
if (type == "twitch")
{
return app->twitch.server->getOrAddChannel(
obj.value("name").toString());
}
else if (type == "mentions")
{
return app->twitch.server->mentionsChannel;
}
else if (type == "watching")
{
return app->twitch.server->watchingChannel;
}
else if (type == "whispers")
{
return app->twitch.server->whispersChannel;
}
else if (type == "irc")
{
return Irc::instance().getOrAddChannel(obj.value("server").toInt(-1),
obj.value("channel").toString());
}
return Channel::getEmpty();
}
void WindowManager::closeAll()
{
assertInGuiThread();
for (Window *window : windows_)
{
window->close();
}
}
int WindowManager::getGeneration() const
{
return this->generation_;
}
void WindowManager::incGeneration()
{
this->generation_++;
}
} // namespace chatterino