diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c2c3c97..2b2f67d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Deprecate loading of "v1" window layouts. If you haven't updated Chatterino in more than 2 years, there's a chance you will lose your window layout. - Minor: Disable checking for updates on unsupported platforms (#1874) - Bugfix: Fix bug preventing users from setting the highlight color of the second entry in the "User" highlights tab (#1898) - Bugfix: Fix bug where the "check user follow state" event could trigger a network request requesting the user to follow or unfollow a user. By itself its quite harmless as it just repeats to Twitch the same follow state we had, so no follows should have been lost by this but it meant there was a rogue network request that was fired that could cause a crash (#1906) diff --git a/chatterino.pro b/chatterino.pro index 56228b399..4a1d7f100 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -124,6 +124,7 @@ SOURCES += \ src/common/NetworkResult.cpp \ src/common/UsernameSet.cpp \ src/common/Version.cpp \ + src/common/WindowDescriptors.cpp \ src/controllers/accounts/Account.cpp \ src/controllers/accounts/AccountController.cpp \ src/controllers/accounts/AccountModel.cpp \ diff --git a/src/common/WindowDescriptors.cpp b/src/common/WindowDescriptors.cpp new file mode 100644 index 000000000..7a619722a --- /dev/null +++ b/src/common/WindowDescriptors.cpp @@ -0,0 +1,207 @@ +#include "common/WindowDescriptors.hpp" + +#include "widgets/Window.hpp" + +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; + } + + template + T loadNodes(const QJsonObject &obj) + { + static_assert("loadNodes must be called with the SplitNodeDescriptor " + "or ContainerNodeDescriptor type"); + } + + template <> + SplitNodeDescriptor loadNodes(const QJsonObject &root) + { + SplitNodeDescriptor descriptor; + + descriptor.flexH_ = root.value("flexh").toDouble(1.0); + descriptor.flexV_ = root.value("flexv").toDouble(1.0); + + auto data = root.value("data").toObject(); + + SplitDescriptor::loadFromJSON(descriptor, root, data); + + return descriptor; + } + + template <> + ContainerNodeDescriptor loadNodes(const QJsonObject &root) + { + ContainerNodeDescriptor descriptor; + + descriptor.flexH_ = root.value("flexh").toDouble(1.0); + descriptor.flexV_ = root.value("flexv").toDouble(1.0); + + descriptor.vertical_ = root.value("type").toString() == "vertical"; + + for (QJsonValue _val : root.value("items").toArray()) + { + auto _obj = _val.toObject(); + + auto _type = _obj.value("type"); + if (_type == "split") + { + descriptor.items_.emplace_back( + loadNodes(_obj)); + } + else + { + descriptor.items_.emplace_back( + loadNodes(_obj)); + } + } + + return descriptor; + } + +} // namespace + +void SplitDescriptor::loadFromJSON(SplitDescriptor &descriptor, + const QJsonObject &root, + const QJsonObject &data) +{ + descriptor.type_ = data.value("type").toString(); + descriptor.server_ = data.value("server").toInt(-1); + if (data.contains("channel")) + { + descriptor.channelName_ = data.value("channel").toString(); + } + else + { + descriptor.channelName_ = data.value("name").toString(); + } +} + +WindowLayout WindowLayout::loadFromFile(const QString &path) +{ + WindowLayout layout; + + bool hasSetAMainWindow = false; + + // "deserialize" + for (const QJsonValue &window_val : loadWindowArray(path)) + { + QJsonObject window_obj = window_val.toObject(); + + WindowDescriptor window; + + // Load window type + QString type_val = window_obj.value("type").toString(); + auto type = type_val == "main" ? WindowType::Main : WindowType::Popup; + + if (type == WindowType::Main) + { + if (hasSetAMainWindow) + { + qDebug() + << "Window Layout file contains more than one Main window " + "- demoting to Popup type"; + type = WindowType::Popup; + } + hasSetAMainWindow = true; + } + + window.type_ = type; + + // Load window state + if (window_obj.value("state") == "minimized") + { + window.state_ = WindowDescriptor::State::Minimized; + } + else if (window_obj.value("state") == "maximized") + { + window.state_ = WindowDescriptor::State::Maximized; + } + + // Load window 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); + + window.geometry_ = QRect(x, y, width, height); + } + + bool hasSetASelectedTab = false; + + // Load window tabs + QJsonArray tabs = window_obj.value("tabs").toArray(); + for (QJsonValue tab_val : tabs) + { + TabDescriptor tab; + + QJsonObject tab_obj = tab_val.toObject(); + + // Load tab custom title + QJsonValue title_val = tab_obj.value("title"); + if (title_val.isString()) + { + tab.customTitle_ = title_val.toString(); + } + + // Load tab selected state + tab.selected_ = tab_obj.value("selected").toBool(false); + + if (tab.selected_) + { + if (hasSetASelectedTab) + { + qDebug() << "Window contains more than one selected tab - " + "demoting to unselected"; + tab.selected_ = false; + } + hasSetASelectedTab = true; + } + + // Load tab "highlightsEnabled" state + tab.highlightsEnabled_ = + tab_obj.value("highlightsEnabled").toBool(true); + + QJsonObject splitRoot = tab_obj.value("splits2").toObject(); + + // Load tab splits + if (!splitRoot.isEmpty()) + { + // root type + auto nodeType = splitRoot.value("type").toString(); + if (nodeType == "split") + { + tab.rootNode_ = loadNodes(splitRoot); + } + else if (nodeType == "horizontal" || nodeType == "vertical") + { + tab.rootNode_ = + loadNodes(splitRoot); + } + } + + window.tabs_.emplace_back(std::move(tab)); + } + + // Load emote popup position + QJsonObject emote_popup_obj = window_obj.value("emotePopup").toObject(); + layout.emotePopupPos_ = QPoint(emote_popup_obj.value("x").toInt(), + emote_popup_obj.value("y").toInt()); + + layout.windows_.emplace_back(std::move(window)); + } + + return layout; +} + +} // namespace chatterino diff --git a/src/common/WindowDescriptors.hpp b/src/common/WindowDescriptors.hpp new file mode 100644 index 000000000..c0709ce08 --- /dev/null +++ b/src/common/WindowDescriptors.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include + +#include +#include + +namespace chatterino { + +/** + * A WindowLayout contains one or more windows. + * Only one of those windows can be the main window + * + * Each window contains a list of tabs. + * Only one of those tabs can be marked as selected. + * + * Each tab contains a root node. + * The root node is either a: + * - Split Node (for single-split tabs), or + * - Container Node (for multi-split tabs). + * This container node would then contain a list of nodes on its own, which could be split nodes or further container nodes + **/ + +// from widgets/Window.hpp +enum class WindowType; + +struct SplitDescriptor { + // twitch or mentions or watching or whispers or irc + QString type_; + + // Twitch Channel name or IRC channel name + QString channelName_; + + // IRC server + int server_{-1}; + + // Whether "Moderation Mode" (the sword icon) is enabled in this split or not + bool moderationMode_{false}; + + static void loadFromJSON(SplitDescriptor &descriptor, + const QJsonObject &root, const QJsonObject &data); +}; + +struct SplitNodeDescriptor : SplitDescriptor { + qreal flexH_ = 1; + qreal flexV_ = 1; +}; + +struct ContainerNodeDescriptor; + +using NodeDescriptor = + std::variant; + +struct ContainerNodeDescriptor { + qreal flexH_ = 1; + qreal flexV_ = 1; + + bool vertical_ = false; + + std::vector items_; +}; + +struct TabDescriptor { + QString customTitle_; + bool selected_{false}; + bool highlightsEnabled_{true}; + + std::optional rootNode_; +}; + +struct WindowDescriptor { + enum class State { + None, + Minimized, + Maximized, + }; + + WindowType type_; + State state_ = State::None; + + QRect geometry_; + + std::vector tabs_; +}; + +class WindowLayout +{ +public: + static WindowLayout loadFromFile(const QString &path); + + // A complete window layout has a single emote popup position that is shared among all windows + QPoint emotePopupPos_; + + std::vector windows_; +}; + +} // namespace chatterino diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 53ed0fad4..d31b09ef9 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -23,6 +23,7 @@ #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "util/Clamp.hpp" +#include "util/CombinePath.hpp" #include "widgets/AccountSwitchPopup.hpp" #include "widgets/Notebook.hpp" #include "widgets/Window.hpp" @@ -31,25 +32,17 @@ #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; - } + + const QString WINDOW_LAYOUT_FILENAME(QStringLiteral("window-layout.json")); boost::optional &shouldMoveOutOfBoundsWindow() { static boost::optional x; return x; } + } // namespace using SplitNode = SplitContainer::Node; @@ -86,6 +79,8 @@ void WindowManager::showAccountSelectPopup(QPoint point) } WindowManager::WindowManager() + : windowLayoutFilePath( + combinePath(getPaths()->settingsDirectory, WINDOW_LAYOUT_FILENAME)) { qDebug() << "init WindowManager"; @@ -294,141 +289,18 @@ void WindowManager::initialize(Settings &settings, Paths &paths) 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(); + auto windowLayout = this->loadWindowLayoutFromFile(); - // get type - QString type_val = window_obj.value("type").toString(); - WindowType type = - type_val == "main" ? WindowType::Main : WindowType::Popup; + this->emotePopupPos_ = windowLayout.emotePopupPos_; - 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(); - - QJsonObject emote_popup_obj = window_obj.value("emotePopup").toObject(); - this->emotePopupPos_ = QPoint(emote_popup_obj.value("x").toInt(), - emote_popup_obj.value("y").toInt()); - - if (window_obj.value("state") == "minimized") - { - window.setWindowState(Qt::WindowMinimized); - } - else if (window_obj.value("state") == "maximized") - { - window.setWindowState(Qt::WindowMaximized); - } + this->applyWindowLayout(windowLayout); } + // No main window has been created from loading, create an empty one if (mainWindow_ == nullptr) { - mainWindow_ = &createWindow(WindowType::Main); + mainWindow_ = &this->createWindow(WindowType::Main); mainWindow_->getNotebook().addPage(true); } @@ -545,8 +417,7 @@ void WindowManager::save() document.setObject(obj); // save file - QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME; - QSaveFile file(settingsPath); + QSaveFile file(this->windowLayoutFilePath); file.open(QIODevice::WriteOnly | QIODevice::Truncate); QJsonDocument::JsonFormat format = @@ -650,34 +521,32 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj) } } -IndirectChannel WindowManager::decodeChannel(const QJsonObject &obj) +IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor) { assertInGuiThread(); auto app = getApp(); - QString type = obj.value("type").toString(); - if (type == "twitch") + if (descriptor.type_ == "twitch") { - return app->twitch.server->getOrAddChannel( - obj.value("name").toString()); + return app->twitch.server->getOrAddChannel(descriptor.channelName_); } - else if (type == "mentions") + else if (descriptor.type_ == "mentions") { return app->twitch.server->mentionsChannel; } - else if (type == "watching") + else if (descriptor.type_ == "watching") { return app->twitch.server->watchingChannel; } - else if (type == "whispers") + else if (descriptor.type_ == "whispers") { return app->twitch.server->whispersChannel; } - else if (type == "irc") + else if (descriptor.type_ == "irc") { - return Irc::instance().getOrAddChannel(obj.value("server").toInt(-1), - obj.value("channel").toString()); + return Irc::instance().getOrAddChannel(descriptor.server_, + descriptor.channelName_); } return Channel::getEmpty(); @@ -703,4 +572,109 @@ void WindowManager::incGeneration() this->generation_++; } +WindowLayout WindowManager::loadWindowLayoutFromFile() const +{ + return WindowLayout::loadFromFile(this->windowLayoutFilePath); +} + +void WindowManager::applyWindowLayout(const WindowLayout &layout) +{ + // Set emote popup position + this->emotePopupPos_ = layout.emotePopupPos_; + + for (const auto &windowData : layout.windows_) + { + auto type = windowData.type_; + + Window &window = this->createWindow(type, false); + + if (type == WindowType::Main) + { + assert(this->mainWindow_ == nullptr); + + this->mainWindow_ = &window; + } + + // get geometry + { + // out of bounds windows + auto screens = qApp->screens(); + bool outOfBounds = std::none_of( + screens.begin(), screens.end(), [&](QScreen *screen) { + return screen->availableGeometry().intersects( + windowData.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()) && + windowData.geometry_.x() != -1 && + windowData.geometry_.y() != -1 && + windowData.geometry_.width() != -1 && + windowData.geometry_.height() != -1) + { + // Have to offset x by one because qt moves the window 1px too + // far to the left:w + + window.setInitialBounds({windowData.geometry_.x(), + windowData.geometry_.y(), + windowData.geometry_.width(), + windowData.geometry_.height()}); + } + } + + // open tabs + for (const auto &tab : windowData.tabs_) + { + SplitContainer *page = window.getNotebook().addPage(false); + + // set custom title + if (!tab.customTitle_.isEmpty()) + { + page->getTab()->setCustomTitle(tab.customTitle_); + } + + // selected + if (tab.selected_) + { + window.getNotebook().select(page); + } + + // highlighting on new messages + page->getTab()->setHighlightsEnabled(tab.highlightsEnabled_); + + if (tab.rootNode_) + { + page->applyFromDescriptor(*tab.rootNode_); + } + } + window.show(); + + // Set window state + switch (windowData.state_) + { + case WindowDescriptor::State::Minimized: { + window.setWindowState(Qt::WindowMinimized); + } + break; + + case WindowDescriptor::State::Maximized: { + window.setWindowState(Qt::WindowMaximized); + } + break; + } + } +} + } // namespace chatterino diff --git a/src/singletons/WindowManager.hpp b/src/singletons/WindowManager.hpp index 7ffe52e11..e0c107462 100644 --- a/src/singletons/WindowManager.hpp +++ b/src/singletons/WindowManager.hpp @@ -3,6 +3,7 @@ #include "common/Channel.hpp" #include "common/FlagsEnum.hpp" #include "common/Singleton.hpp" +#include "common/WindowDescriptors.hpp" #include "pajlada/settings/settinglistener.hpp" #include "widgets/splits/SplitContainer.hpp" @@ -25,7 +26,7 @@ public: WindowManager(); static void encodeChannel(IndirectChannel channel, QJsonObject &obj); - static IndirectChannel decodeChannel(const QJsonObject &obj); + static IndirectChannel decodeChannel(const SplitDescriptor &descriptor); void showSettingsDialog( SettingsDialogPreference preference = SettingsDialogPreference()); @@ -90,6 +91,15 @@ public: private: void encodeNodeRecusively(SplitContainer::Node *node, QJsonObject &obj); + // Load window layout from the window-layout.json file + WindowLayout loadWindowLayoutFromFile() const; + + // Apply a window layout for this window manager. + void applyWindowLayout(const WindowLayout &layout); + + // Contains the full path to the window layout file, e.g. /home/pajlada/.local/share/Chatterino/Settings/window-layout.json + const QString windowLayoutFilePath; + bool initialized_ = false; QPoint emotePopupPos_; diff --git a/src/widgets/splits/SplitContainer.cpp b/src/widgets/splits/SplitContainer.cpp index 81dd6f178..1904af5ec 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -695,55 +695,67 @@ SplitContainer::Node *SplitContainer::getBaseNode() return &this->baseNode_; } -void SplitContainer::decodeFromJson(QJsonObject &obj) +void SplitContainer::applyFromDescriptor(const NodeDescriptor &rootNode) { assert(this->baseNode_.type_ == Node::EmptyRoot); - this->decodeNodeRecusively(obj, &this->baseNode_); + this->applyFromDescriptorRecursively(rootNode, &this->baseNode_); } -void SplitContainer::decodeNodeRecusively(QJsonObject &obj, Node *node) +void SplitContainer::applyFromDescriptorRecursively( + const NodeDescriptor &rootNode, Node *node) { - QString type = obj.value("type").toString(); - - if (type == "split") + if (std::holds_alternative(rootNode)) { + auto *n = std::get_if(&rootNode); + if (!n) + { + return; + } + const auto &splitNode = *n; auto *split = new Split(this); - split->setChannel( - WindowManager::decodeChannel(obj.value("data").toObject())); - split->setModerationMode(obj.value("moderationMode").toBool(false)); + split->setChannel(WindowManager::decodeChannel(splitNode)); + split->setModerationMode(splitNode.moderationMode_); this->appendSplit(split); } - else if (type == "horizontal" || type == "vertical") + else if (std::holds_alternative(rootNode)) { - bool vertical = type == "vertical"; + auto *n = std::get_if(&rootNode); + if (!n) + { + return; + } + const auto &containerNode = *n; + + bool vertical = containerNode.vertical_; Direction direction = vertical ? Direction::Below : Direction::Right; node->type_ = vertical ? Node::VerticalContainer : Node::HorizontalContainer; - for (QJsonValue _val : obj.value("items").toArray()) + for (const auto &item : containerNode.items_) { - auto _obj = _val.toObject(); - - auto _type = _obj.value("type"); - if (_type == "split") + if (std::holds_alternative(item)) { + auto *n = std::get_if(&item); + if (!n) + { + return; + } + const auto &splitNode = *n; auto *split = new Split(this); - split->setChannel(WindowManager::decodeChannel( - _obj.value("data").toObject())); - split->setModerationMode( - _obj.value("moderationMode").toBool(false)); + split->setChannel(WindowManager::decodeChannel(splitNode)); + split->setModerationMode(splitNode.moderationMode_); Node *_node = new Node(); _node->parent_ = node; _node->split_ = split; _node->type_ = Node::_Split; - _node->flexH_ = _obj.value("flexh").toDouble(1.0); - _node->flexV_ = _obj.value("flexv").toDouble(1.0); + _node->flexH_ = splitNode.flexH_; + _node->flexV_ = splitNode.flexV_; node->children_.emplace_back(_node); this->addSplit(split); @@ -753,19 +765,7 @@ void SplitContainer::decodeNodeRecusively(QJsonObject &obj, Node *node) Node *_node = new Node(); _node->parent_ = node; node->children_.emplace_back(_node); - this->decodeNodeRecusively(_obj, _node); - } - } - - for (int i = 0; i < 2; i++) - { - if (node->getChildren().size() < 2) - { - auto *split = new Split(this); - split->setChannel( - WindowManager::decodeChannel(obj.value("data").toObject())); - - this->insertSplit(split, direction, node); + this->applyFromDescriptorRecursively(item, _node); } } } diff --git a/src/widgets/splits/SplitContainer.hpp b/src/widgets/splits/SplitContainer.hpp index 67b669531..40f12df91 100644 --- a/src/widgets/splits/SplitContainer.hpp +++ b/src/widgets/splits/SplitContainer.hpp @@ -1,5 +1,6 @@ #pragma once +#include "common/WindowDescriptors.hpp" #include "widgets/BaseWidget.hpp" #include @@ -184,8 +185,6 @@ public: void selectNextSplit(Direction direction); void setSelected(Split *selected_); - void decodeFromJson(QJsonObject &obj); - int getSplitCount(); const std::vector getSplits() const; @@ -201,6 +200,8 @@ public: static bool isDraggingSplit; static Split *draggingSplit; + void applyFromDescriptor(const NodeDescriptor &rootNode); + protected: void paintEvent(QPaintEvent *event) override; @@ -214,6 +215,9 @@ protected: void resizeEvent(QResizeEvent *event) override; private: + void applyFromDescriptorRecursively(const NodeDescriptor &rootNode, + Node *node); + void layout(); void selectSplitRecursive(Node *node, Direction direction); void focusSplitRecursive(Node *node); @@ -221,7 +225,6 @@ private: void addSplit(Split *split); - void decodeNodeRecusively(QJsonObject &obj, Node *node); Split *getTopRightSplit(Node &node); void refreshTabTitle();