mirror-chatterino2/src/singletons/WindowManager.cpp

688 lines
18 KiB
C++
Raw Normal View History

2018-07-15 14:03:41 +02:00
#include "singletons/WindowManager.hpp"
2018-06-26 14:09:39 +02:00
#include "Application.hpp"
2018-06-26 17:20:03 +02:00
#include "debug/AssertInGuiThread.hpp"
2018-06-26 14:09:39 +02:00
#include "debug/Log.hpp"
#include "messages/MessageElement.hpp"
2019-09-11 00:10:49 +02:00
#include "providers/irc/Irc2.hpp"
#include "providers/irc/IrcChannel2.hpp"
#include "providers/irc/IrcServer.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
2018-06-28 19:46:45 +02:00
#include "singletons/Fonts.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
2018-06-28 20:03:04 +02:00
#include "singletons/Theme.hpp"
2018-06-26 14:09:39 +02:00
#include "util/Clamp.hpp"
#include "widgets/AccountSwitchPopup.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/Window.hpp"
2018-06-26 15:11:45 +02:00
#include "widgets/dialogs/SettingsDialog.hpp"
#include "widgets/helper/NotebookTab.hpp"
#include "widgets/splits/Split.hpp"
#include "widgets/splits/SplitContainer.hpp"
2017-01-15 16:38:30 +01:00
2018-07-15 14:03:41 +02:00
#include <QDebug>
#include <QDesktopWidget>
2018-05-16 14:55:45 +02:00
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QSaveFile>
#include <QScreen>
#include <boost/optional.hpp>
#include <chrono>
2018-06-21 13:02:34 +02:00
#define SETTINGS_FILENAME "/window-layout.json"
2017-01-18 21:30:23 +01:00
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
2018-06-26 17:06:17 +02:00
using SplitNode = SplitContainer::Node;
using SplitDirection = SplitContainer::Direction;
2018-05-16 14:55:45 +02:00
void WindowManager::showSettingsDialog(SettingsDialogPreference preference)
2018-01-24 15:08:22 +01:00
{
QTimer::singleShot(
80, [preference] { SettingsDialog::showDialog(preference); });
2018-01-24 15:08:22 +01:00
}
void WindowManager::showAccountSelectPopup(QPoint point)
{
// static QWidget *lastFocusedWidget = nullptr;
static AccountSwitchPopup *w = new AccountSwitchPopup();
2018-01-24 15:08:22 +01:00
2018-10-21 13:43:02 +02:00
if (w->hasFocus())
{
2018-01-24 15:08:22 +01:00
w->hide();
// if (lastFocusedWidget) {
// lastFocusedWidget->setFocus();
// }
return;
}
// lastFocusedWidget = this->focusWidget();
w->refresh();
QPoint buttonPos = point;
2019-09-08 12:43:12 +02:00
w->move(buttonPos.x() - 30, buttonPos.y());
2018-01-24 15:08:22 +01:00
w->show();
w->setFocus();
}
WindowManager::WindowManager()
2017-04-13 19:25:33 +02:00
{
qDebug() << "init WindowManager";
auto settings = getSettings();
2018-07-06 19:23:47 +02:00
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);
2018-10-31 19:45:51 +01:00
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(); //
});
}
2018-08-07 07:55:31 +02:00
MessageElementFlags WindowManager::getWordFlags()
{
2018-07-06 19:23:47 +02:00
return this->wordFlags_;
}
void WindowManager::updateWordTypeMask()
{
2018-08-07 07:55:31 +02:00
using MEF = MessageElementFlag;
auto settings = getSettings();
// text
2018-08-07 07:55:31 +02:00
auto flags = MessageElementFlags(MEF::Text);
// timestamp
2018-10-21 13:43:02 +02:00
if (settings->showTimestamps)
{
2018-08-07 07:55:31 +02:00
flags.set(MEF::Timestamp);
}
// emotes
if (settings->enableEmoteImages)
{
flags.set(MEF::EmoteImages);
}
flags.set(MEF::EmoteText);
flags.set(MEF::EmojiText);
// bits
2018-08-07 07:55:31 +02:00
flags.set(MEF::BitsAmount);
2018-11-14 17:26:08 +01:00
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
2018-08-07 07:55:31 +02:00
flags.set(MEF::Username);
// misc
2018-08-07 07:55:31 +02:00
flags.set(MEF::AlwaysShow);
flags.set(MEF::Collapsed);
2018-10-31 19:45:51 +01:00
flags.set(settings->boldUsernames ? MEF::BoldUsername
2018-11-14 17:26:08 +01:00
: MEF::NonBoldUsername);
2018-10-31 19:45:51 +01:00
flags.set(settings->lowercaseDomains ? MEF::LowercaseLink
2018-11-14 17:26:08 +01:00
: MEF::OriginalLink);
// update flags
2018-08-07 07:55:31 +02:00
MessageElementFlags newFlags = static_cast<MessageElementFlags>(flags);
2018-10-21 13:43:02 +02:00
if (newFlags != this->wordFlags_)
{
2018-07-06 19:23:47 +02:00
this->wordFlags_ = newFlags;
this->wordFlagsChanged.invoke();
}
2017-04-13 19:25:33 +02:00
}
2017-01-18 21:30:23 +01:00
void WindowManager::layoutChannelViews(Channel *channel)
2017-01-15 16:38:30 +01:00
{
2018-11-14 17:26:08 +01:00
this->layoutRequested.invoke(channel);
}
void WindowManager::forceLayoutChannelViews()
{
this->incGeneration();
this->layoutChannelViews(nullptr);
}
2017-04-12 17:46:44 +02:00
void WindowManager::repaintVisibleChatWidgets(Channel *channel)
{
2018-11-14 17:26:08 +01:00
this->layoutRequested.invoke(channel);
2017-01-15 16:38:30 +01:00
}
2017-01-26 21:04:01 +01:00
2017-04-12 17:46:44 +02:00
void WindowManager::repaintGifEmotes()
2017-02-07 00:03:15 +01:00
{
2018-11-14 17:26:08 +01:00
this->gifRepaintRequested.invoke();
2017-02-07 00:03:15 +01:00
}
// void WindowManager::updateAll()
//{
// if (this->mainWindow != nullptr) {
// this->mainWindow->update();
// }
//}
2017-02-02 01:23:26 +01:00
2018-06-26 17:06:17 +02:00
Window &WindowManager::getMainWindow()
2017-04-13 19:25:33 +02:00
{
2018-06-26 17:06:17 +02:00
assertInGuiThread();
2018-07-06 19:23:47 +02:00
return *this->mainWindow_;
2017-04-13 19:25:33 +02:00
}
2018-06-26 17:06:17 +02:00
Window &WindowManager::getSelectedWindow()
2017-11-12 17:21:50 +01:00
{
2018-06-26 17:06:17 +02:00
assertInGuiThread();
2018-07-06 19:23:47 +02:00
return *this->selectedWindow_;
2017-11-12 17:21:50 +01:00
}
Window &WindowManager::createWindow(WindowType type, bool show)
2017-11-12 17:21:50 +01:00
{
2018-06-26 17:06:17 +02:00
assertInGuiThread();
2018-06-26 17:06:17 +02:00
auto *window = new Window(type);
2018-07-06 19:23:47 +02:00
this->windows_.push_back(window);
if (show)
{
window->show();
}
2017-11-12 17:21:50 +01:00
2018-10-21 13:43:02 +02:00
if (type != WindowType::Main)
{
window->setAttribute(Qt::WA_DeleteOnClose);
QObject::connect(window, &QWidget::destroyed, [this, window] {
2018-08-06 21:17:03 +02:00
for (auto it = this->windows_.begin(); it != this->windows_.end();
2018-10-21 13:43:02 +02:00
it++)
{
if (*it == window)
{
2018-07-06 19:23:47 +02:00
this->windows_.erase(it);
break;
}
}
});
}
2017-11-12 17:21:50 +01:00
return *window;
}
2017-12-14 00:25:06 +01:00
int WindowManager::windowCount()
{
2018-07-06 19:23:47 +02:00
return this->windows_.size();
2017-12-14 00:25:06 +01:00
}
2018-06-26 17:06:17 +02:00
Window *WindowManager::windowAt(int index)
2017-12-14 00:25:06 +01:00
{
2018-06-26 17:06:17 +02:00
assertInGuiThread();
2018-10-21 13:43:02 +02:00
if (index < 0 || (size_t)index >= this->windows_.size())
{
2017-12-14 00:25:06 +01:00
return nullptr;
}
2018-08-11 14:20:53 +02:00
log("getting window at bad index {}", index);
2017-12-14 00:25:06 +01:00
2018-07-06 19:23:47 +02:00
return this->windows_.at(index);
2017-12-14 00:25:06 +01:00
}
2018-08-02 14:23:27 +02:00
void WindowManager::initialize(Settings &settings, Paths &paths)
2017-01-26 21:04:01 +01:00
{
2018-06-26 17:06:17 +02:00
assertInGuiThread();
2018-08-02 14:23:27 +02:00
getApp()->themes->repaintVisibleChatWidgets_.connect(
[this] { this->repaintVisibleChatWidgets(); });
2018-07-06 19:23:47 +02:00
assert(!this->initialized_);
// load file
2018-07-07 11:41:01 +02:00
QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME;
QJsonArray windows_arr = loadWindowArray(settingsPath);
// "deserialize"
2018-10-21 13:43:02 +02:00
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;
2018-10-21 13:43:02 +02:00
if (type == WindowType::Main && mainWindow_ != nullptr)
{
type = WindowType::Popup;
}
Window &window = createWindow(type, false);
2018-10-21 13:43:02 +02:00
if (type == WindowType::Main)
{
2018-07-06 19:23:47 +02:00
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)
2018-10-21 13:43:02 +02:00
{
// Have to offset x by one because qt moves the window 1px too
// far to the left:w
2019-09-01 14:06:30 +02:00
window.setInitialBounds({x, y, width, height});
}
}
// load tabs
QJsonArray tabs = window_obj.value("tabs").toArray();
2018-10-21 13:43:02 +02:00
for (QJsonValue tab_val : tabs)
{
2018-06-26 17:06:17 +02:00
SplitContainer *page = window.getNotebook().addPage(false);
QJsonObject tab_obj = tab_val.toObject();
// set custom title
QJsonValue title_val = tab_obj.value("title");
2018-10-21 13:43:02 +02:00
if (title_val.isString())
{
2018-06-01 16:01:49 +02:00
page->getTab()->setCustomTitle(title_val.toString());
}
2018-04-10 15:59:53 +02:00
// selected
2018-10-21 13:43:02 +02:00
if (tab_obj.value("selected").toBool(false))
{
2018-05-23 04:22:17 +02:00
window.getNotebook().select(page);
2018-04-10 15:59:53 +02:00
}
// highlighting on new messages
bool val = tab_obj.value("highlightsEnabled").toBool(true);
page->getTab()->setHighlightsEnabled(val);
// load splits
2018-05-16 14:55:45 +02:00
QJsonObject splitRoot = tab_obj.value("splits2").toObject();
2018-10-21 13:43:02 +02:00
if (!splitRoot.isEmpty())
{
2018-05-23 04:22:17 +02:00
page->decodeFromJson(splitRoot);
2018-05-16 14:55:45 +02:00
continue;
}
// fallback load splits (old)
int colNr = 0;
2018-10-21 13:43:02 +02:00
for (QJsonValue column_val : tab_obj.value("splits").toArray())
{
for (QJsonValue split_val : column_val.toArray())
{
2018-06-26 17:06:17 +02:00
Split *split = new Split(page);
QJsonObject split_obj = split_val.toObject();
2018-05-16 14:55:45 +02:00
split->setChannel(decodeChannel(split_obj));
2018-05-23 04:22:17 +02:00
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);
}
}
2018-10-21 13:43:02 +02:00
if (mainWindow_ == nullptr)
{
mainWindow_ = &createWindow(WindowType::Main);
2018-07-06 19:23:47 +02:00
mainWindow_->getNotebook().addPage(true);
}
2018-08-06 21:17:03 +02:00
settings.timestampFormat.connect(
[this](auto, auto) { this->layoutChannelViews(); });
2018-07-07 11:41:01 +02:00
2018-08-06 21:17:03 +02:00
settings.emoteScale.connect(
[this](auto, auto) { this->forceLayoutChannelViews(); });
2018-07-07 11:41:01 +02:00
2018-08-06 21:17:03 +02:00
settings.timestampFormat.connect(
[this](auto, auto) { this->forceLayoutChannelViews(); });
2018-10-31 19:45:51 +01:00
settings.alternateMessages.connect(
2018-07-07 11:41:01 +02:00
[this](auto, auto) { this->forceLayoutChannelViews(); });
2018-08-06 21:17:03 +02:00
settings.separateMessages.connect(
[this](auto, auto) { this->forceLayoutChannelViews(); });
2018-08-02 14:23:27 +02:00
settings.collpseMessagesMinLines.connect(
2018-07-07 11:41:01 +02:00
[this](auto, auto) { this->forceLayoutChannelViews(); });
2018-07-06 19:23:47 +02:00
this->initialized_ = true;
}
void WindowManager::save()
{
log("[WindowManager] Saving");
2018-06-26 17:06:17 +02:00
assertInGuiThread();
QJsonDocument document;
// "serialize"
QJsonArray window_arr;
2018-10-21 13:43:02 +02:00
for (Window *window : this->windows_)
{
QJsonObject window_obj;
// window type
2018-10-21 13:43:02 +02:00
switch (window->getType())
{
case WindowType::Main:
window_obj.insert("type", "main");
break;
2018-07-07 11:41:01 +02:00
case WindowType::Popup:
window_obj.insert("type", "popup");
break;
2018-07-07 11:41:01 +02:00
case WindowType::Attached:;
}
2018-10-29 22:11:42 +01:00
if (window->isMaximized())
{
window_obj.insert("state", "maximized");
}
else if (window->isMinimized())
{
window_obj.insert("state", "minimized");
}
// window geometry
2019-09-01 14:06:30 +02:00
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;
2018-08-06 21:17:03 +02:00
for (int tab_i = 0; tab_i < window->getNotebook().getPageCount();
2018-10-21 13:43:02 +02:00
tab_i++)
{
QJsonObject tab_obj;
2018-08-06 21:17:03 +02:00
SplitContainer *tab = dynamic_cast<SplitContainer *>(
window->getNotebook().getPageAt(tab_i));
2018-05-23 04:22:17 +02:00
assert(tab != nullptr);
// custom tab title
2018-10-21 13:43:02 +02:00
if (tab->getTab()->hasCustomTitle())
{
2018-06-01 16:01:49 +02:00
tab_obj.insert("title", tab->getTab()->getCustomTitle());
}
2018-04-10 15:59:53 +02:00
// selected
2018-10-21 13:43:02 +02:00
if (window->getNotebook().getSelectedPage() == tab)
{
2018-04-10 15:59:53 +02:00
tab_obj.insert("selected", true);
}
// highlighting on new messages
tab_obj.insert("highlightsEnabled",
tab->getTab()->hasHighlightsEnabled());
// splits
2018-05-16 14:55:45 +02:00
QJsonObject splits;
2018-05-16 14:55:45 +02:00
this->encodeNodeRecusively(tab->getBaseNode(), splits);
2018-05-16 14:55:45 +02:00
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);
2018-05-16 14:55:45 +02:00
QJsonDocument::JsonFormat format =
#ifdef _DEBUG
QJsonDocument::JsonFormat::Compact
#else
(QJsonDocument::JsonFormat)0
#endif
;
file.write(document.toJson(format));
file.commit();
}
2018-10-07 12:55:44 +02:00
void WindowManager::sendAlert()
{
int flashDuration = 2500;
2018-10-21 13:43:02 +02:00
if (getSettings()->longAlerts)
{
2018-10-07 12:55:44 +02:00
flashDuration = 0;
}
2018-10-07 15:37:17 +02:00
QApplication::alert(this->getMainWindow().window(), flashDuration);
2018-10-07 12:55:44 +02:00
}
void WindowManager::queueSave()
{
using namespace std::chrono_literals;
this->saveTimer->start(10s);
}
2018-05-16 14:55:45 +02:00
void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj)
{
2018-10-21 13:43:02 +02:00
switch (node->getType())
{
case SplitNode::_Split:
{
2018-05-16 14:55:45 +02:00
obj.insert("type", "split");
2019-03-24 15:38:09 +01:00
obj.insert("moderationMode", node->getSplit()->getModerationMode());
2018-05-16 14:55:45 +02:00
QJsonObject split;
encodeChannel(node->getSplit()->getIndirectChannel(), split);
obj.insert("data", split);
obj.insert("flexh", node->getHorizontalFlex());
obj.insert("flexv", node->getVerticalFlex());
2018-10-21 13:43:02 +02:00
}
break;
2018-05-16 14:55:45 +02:00
case SplitNode::HorizontalContainer:
2018-10-21 13:43:02 +02:00
case SplitNode::VerticalContainer:
{
2018-08-06 21:17:03 +02:00
obj.insert("type", node->getType() == SplitNode::HorizontalContainer
? "horizontal"
: "vertical");
2018-05-16 14:55:45 +02:00
QJsonArray items_arr;
2018-10-21 13:43:02 +02:00
for (const std::unique_ptr<SplitNode> &n : node->getChildren())
{
2018-05-16 14:55:45 +02:00
QJsonObject subObj;
this->encodeNodeRecusively(n.get(), subObj);
items_arr.append(subObj);
}
obj.insert("items", items_arr);
2018-10-21 13:43:02 +02:00
}
break;
2018-05-16 14:55:45 +02:00
}
}
void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj)
{
2018-06-26 17:06:17 +02:00
assertInGuiThread();
2018-10-21 13:43:02 +02:00
switch (channel.getType())
{
case Channel::Type::Twitch:
{
obj.insert("type", "twitch");
2018-08-02 14:23:27 +02:00
obj.insert("name", channel.get()->getName());
2018-10-21 13:43:02 +02:00
}
break;
case Channel::Type::TwitchMentions:
{
obj.insert("type", "mentions");
2018-10-21 13:43:02 +02:00
}
break;
case Channel::Type::TwitchWatching:
{
obj.insert("type", "watching");
2018-10-21 13:43:02 +02:00
}
break;
case Channel::Type::TwitchWhispers:
{
obj.insert("type", "whispers");
2018-10-21 13:43:02 +02:00
}
break;
2019-09-11 00:10:49 +02:00
case Channel::Type::Irc:
{
if (auto ircChannel =
dynamic_cast<IrcChannel *>(channel.get().get()))
{
obj.insert("type", "irc");
2019-09-14 18:38:09 +02:00
if (ircChannel->server())
{
obj.insert("server", ircChannel->server()->id());
}
2019-09-11 00:10:49 +02:00
obj.insert("channel", ircChannel->getName());
}
}
break;
}
}
IndirectChannel WindowManager::decodeChannel(const QJsonObject &obj)
{
2018-06-26 17:06:17 +02:00
assertInGuiThread();
auto app = getApp();
QString type = obj.value("type").toString();
2018-10-21 13:43:02 +02:00
if (type == "twitch")
{
2018-08-06 21:17:03 +02:00
return app->twitch.server->getOrAddChannel(
obj.value("name").toString());
2018-10-21 13:43:02 +02:00
}
else if (type == "mentions")
{
return app->twitch.server->mentionsChannel;
2018-10-21 13:43:02 +02:00
}
else if (type == "watching")
{
return app->twitch.server->watchingChannel;
2018-10-21 13:43:02 +02:00
}
else if (type == "whispers")
{
return app->twitch.server->whispersChannel;
}
2019-09-11 00:10:49 +02:00
else if (type == "irc")
{
return Irc::getInstance().getOrAddChannel(
obj.value("server").toInt(-1), obj.value("channel").toString());
}
return Channel::getEmpty();
}
void WindowManager::closeAll()
{
2018-06-26 17:06:17 +02:00
assertInGuiThread();
2018-10-21 13:43:02 +02:00
for (Window *window : windows_)
{
window->close();
}
2017-01-26 21:04:01 +01:00
}
int WindowManager::getGeneration() const
{
2018-07-06 19:23:47 +02:00
return this->generation_;
}
void WindowManager::incGeneration()
{
2018-07-06 19:23:47 +02:00
this->generation_++;
}
2017-04-14 17:52:22 +02:00
} // namespace chatterino