mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Split up Window Layout loading into a loading and application stage (#1964)
* Split up Window Layout loading into a loading and application stage Previously, we were creating UI elements at while we were reading the window-layout.json file. We now read the window-layout.json file fully first, which results in a WindowLayout struct which is built up of a list of windows with a list of tabs with a root node which contains containers and splits. This WindowLayout can then be applied. This will enable PRs like #1940 to start Chatterino with Window Layouts that aren't defined in a json file. This commit has deprecated loading of v1 window layouts (we're now on v2). If a v1 window layout is there, it will just be ignored and Chatterino will boot up as if it did not have a window layout at all, and on save that old window layout will be gone. * Fix compile error for mac
This commit is contained in:
parent
7eabba959b
commit
913193f8b5
8 changed files with 484 additions and 191 deletions
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
## Unversioned
|
## 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)
|
- 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 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)
|
- 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)
|
||||||
|
|
|
@ -124,6 +124,7 @@ SOURCES += \
|
||||||
src/common/NetworkResult.cpp \
|
src/common/NetworkResult.cpp \
|
||||||
src/common/UsernameSet.cpp \
|
src/common/UsernameSet.cpp \
|
||||||
src/common/Version.cpp \
|
src/common/Version.cpp \
|
||||||
|
src/common/WindowDescriptors.cpp \
|
||||||
src/controllers/accounts/Account.cpp \
|
src/controllers/accounts/Account.cpp \
|
||||||
src/controllers/accounts/AccountController.cpp \
|
src/controllers/accounts/AccountController.cpp \
|
||||||
src/controllers/accounts/AccountModel.cpp \
|
src/controllers/accounts/AccountModel.cpp \
|
||||||
|
|
207
src/common/WindowDescriptors.cpp
Normal file
207
src/common/WindowDescriptors.cpp
Normal file
|
@ -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 <typename T>
|
||||||
|
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<SplitNodeDescriptor>(_obj));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
descriptor.items_.emplace_back(
|
||||||
|
loadNodes<ContainerNodeDescriptor>(_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<SplitNodeDescriptor>(splitRoot);
|
||||||
|
}
|
||||||
|
else if (nodeType == "horizontal" || nodeType == "vertical")
|
||||||
|
{
|
||||||
|
tab.rootNode_ =
|
||||||
|
loadNodes<ContainerNodeDescriptor>(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
|
97
src/common/WindowDescriptors.hpp
Normal file
97
src/common/WindowDescriptors.hpp
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
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<ContainerNodeDescriptor, SplitNodeDescriptor>;
|
||||||
|
|
||||||
|
struct ContainerNodeDescriptor {
|
||||||
|
qreal flexH_ = 1;
|
||||||
|
qreal flexV_ = 1;
|
||||||
|
|
||||||
|
bool vertical_ = false;
|
||||||
|
|
||||||
|
std::vector<NodeDescriptor> items_;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TabDescriptor {
|
||||||
|
QString customTitle_;
|
||||||
|
bool selected_{false};
|
||||||
|
bool highlightsEnabled_{true};
|
||||||
|
|
||||||
|
std::optional<NodeDescriptor> rootNode_;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WindowDescriptor {
|
||||||
|
enum class State {
|
||||||
|
None,
|
||||||
|
Minimized,
|
||||||
|
Maximized,
|
||||||
|
};
|
||||||
|
|
||||||
|
WindowType type_;
|
||||||
|
State state_ = State::None;
|
||||||
|
|
||||||
|
QRect geometry_;
|
||||||
|
|
||||||
|
std::vector<TabDescriptor> 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<WindowDescriptor> windows_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -23,6 +23,7 @@
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
#include "singletons/Theme.hpp"
|
#include "singletons/Theme.hpp"
|
||||||
#include "util/Clamp.hpp"
|
#include "util/Clamp.hpp"
|
||||||
|
#include "util/CombinePath.hpp"
|
||||||
#include "widgets/AccountSwitchPopup.hpp"
|
#include "widgets/AccountSwitchPopup.hpp"
|
||||||
#include "widgets/Notebook.hpp"
|
#include "widgets/Notebook.hpp"
|
||||||
#include "widgets/Window.hpp"
|
#include "widgets/Window.hpp"
|
||||||
|
@ -31,25 +32,17 @@
|
||||||
#include "widgets/splits/Split.hpp"
|
#include "widgets/splits/Split.hpp"
|
||||||
#include "widgets/splits/SplitContainer.hpp"
|
#include "widgets/splits/SplitContainer.hpp"
|
||||||
|
|
||||||
#define SETTINGS_FILENAME "/window-layout.json"
|
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
namespace {
|
namespace {
|
||||||
QJsonArray loadWindowArray(const QString &settingsPath)
|
|
||||||
{
|
const QString WINDOW_LAYOUT_FILENAME(QStringLiteral("window-layout.json"));
|
||||||
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()
|
boost::optional<bool> &shouldMoveOutOfBoundsWindow()
|
||||||
{
|
{
|
||||||
static boost::optional<bool> x;
|
static boost::optional<bool> x;
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
using SplitNode = SplitContainer::Node;
|
using SplitNode = SplitContainer::Node;
|
||||||
|
@ -86,6 +79,8 @@ void WindowManager::showAccountSelectPopup(QPoint point)
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager::WindowManager()
|
WindowManager::WindowManager()
|
||||||
|
: windowLayoutFilePath(
|
||||||
|
combinePath(getPaths()->settingsDirectory, WINDOW_LAYOUT_FILENAME))
|
||||||
{
|
{
|
||||||
qDebug() << "init WindowManager";
|
qDebug() << "init WindowManager";
|
||||||
|
|
||||||
|
@ -294,141 +289,18 @@ void WindowManager::initialize(Settings &settings, Paths &paths)
|
||||||
|
|
||||||
assert(!this->initialized_);
|
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
|
this->emotePopupPos_ = windowLayout.emotePopupPos_;
|
||||||
QString type_val = window_obj.value("type").toString();
|
|
||||||
WindowType type =
|
|
||||||
type_val == "main" ? WindowType::Main : WindowType::Popup;
|
|
||||||
|
|
||||||
if (type == WindowType::Main && mainWindow_ != nullptr)
|
this->applyWindowLayout(windowLayout);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No main window has been created from loading, create an empty one
|
||||||
if (mainWindow_ == nullptr)
|
if (mainWindow_ == nullptr)
|
||||||
{
|
{
|
||||||
mainWindow_ = &createWindow(WindowType::Main);
|
mainWindow_ = &this->createWindow(WindowType::Main);
|
||||||
mainWindow_->getNotebook().addPage(true);
|
mainWindow_->getNotebook().addPage(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -545,8 +417,7 @@ void WindowManager::save()
|
||||||
document.setObject(obj);
|
document.setObject(obj);
|
||||||
|
|
||||||
// save file
|
// save file
|
||||||
QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME;
|
QSaveFile file(this->windowLayoutFilePath);
|
||||||
QSaveFile file(settingsPath);
|
|
||||||
file.open(QIODevice::WriteOnly | QIODevice::Truncate);
|
file.open(QIODevice::WriteOnly | QIODevice::Truncate);
|
||||||
|
|
||||||
QJsonDocument::JsonFormat format =
|
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();
|
assertInGuiThread();
|
||||||
|
|
||||||
auto app = getApp();
|
auto app = getApp();
|
||||||
|
|
||||||
QString type = obj.value("type").toString();
|
if (descriptor.type_ == "twitch")
|
||||||
if (type == "twitch")
|
|
||||||
{
|
{
|
||||||
return app->twitch.server->getOrAddChannel(
|
return app->twitch.server->getOrAddChannel(descriptor.channelName_);
|
||||||
obj.value("name").toString());
|
|
||||||
}
|
}
|
||||||
else if (type == "mentions")
|
else if (descriptor.type_ == "mentions")
|
||||||
{
|
{
|
||||||
return app->twitch.server->mentionsChannel;
|
return app->twitch.server->mentionsChannel;
|
||||||
}
|
}
|
||||||
else if (type == "watching")
|
else if (descriptor.type_ == "watching")
|
||||||
{
|
{
|
||||||
return app->twitch.server->watchingChannel;
|
return app->twitch.server->watchingChannel;
|
||||||
}
|
}
|
||||||
else if (type == "whispers")
|
else if (descriptor.type_ == "whispers")
|
||||||
{
|
{
|
||||||
return app->twitch.server->whispersChannel;
|
return app->twitch.server->whispersChannel;
|
||||||
}
|
}
|
||||||
else if (type == "irc")
|
else if (descriptor.type_ == "irc")
|
||||||
{
|
{
|
||||||
return Irc::instance().getOrAddChannel(obj.value("server").toInt(-1),
|
return Irc::instance().getOrAddChannel(descriptor.server_,
|
||||||
obj.value("channel").toString());
|
descriptor.channelName_);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Channel::getEmpty();
|
return Channel::getEmpty();
|
||||||
|
@ -703,4 +572,109 @@ void WindowManager::incGeneration()
|
||||||
this->generation_++;
|
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
|
} // namespace chatterino
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include "common/Channel.hpp"
|
#include "common/Channel.hpp"
|
||||||
#include "common/FlagsEnum.hpp"
|
#include "common/FlagsEnum.hpp"
|
||||||
#include "common/Singleton.hpp"
|
#include "common/Singleton.hpp"
|
||||||
|
#include "common/WindowDescriptors.hpp"
|
||||||
#include "pajlada/settings/settinglistener.hpp"
|
#include "pajlada/settings/settinglistener.hpp"
|
||||||
#include "widgets/splits/SplitContainer.hpp"
|
#include "widgets/splits/SplitContainer.hpp"
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ public:
|
||||||
WindowManager();
|
WindowManager();
|
||||||
|
|
||||||
static void encodeChannel(IndirectChannel channel, QJsonObject &obj);
|
static void encodeChannel(IndirectChannel channel, QJsonObject &obj);
|
||||||
static IndirectChannel decodeChannel(const QJsonObject &obj);
|
static IndirectChannel decodeChannel(const SplitDescriptor &descriptor);
|
||||||
|
|
||||||
void showSettingsDialog(
|
void showSettingsDialog(
|
||||||
SettingsDialogPreference preference = SettingsDialogPreference());
|
SettingsDialogPreference preference = SettingsDialogPreference());
|
||||||
|
@ -90,6 +91,15 @@ public:
|
||||||
private:
|
private:
|
||||||
void encodeNodeRecusively(SplitContainer::Node *node, QJsonObject &obj);
|
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;
|
bool initialized_ = false;
|
||||||
|
|
||||||
QPoint emotePopupPos_;
|
QPoint emotePopupPos_;
|
||||||
|
|
|
@ -695,55 +695,67 @@ SplitContainer::Node *SplitContainer::getBaseNode()
|
||||||
return &this->baseNode_;
|
return &this->baseNode_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SplitContainer::decodeFromJson(QJsonObject &obj)
|
void SplitContainer::applyFromDescriptor(const NodeDescriptor &rootNode)
|
||||||
{
|
{
|
||||||
assert(this->baseNode_.type_ == Node::EmptyRoot);
|
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 (std::holds_alternative<SplitNodeDescriptor>(rootNode))
|
||||||
|
|
||||||
if (type == "split")
|
|
||||||
{
|
{
|
||||||
|
auto *n = std::get_if<SplitNodeDescriptor>(&rootNode);
|
||||||
|
if (!n)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &splitNode = *n;
|
||||||
auto *split = new Split(this);
|
auto *split = new Split(this);
|
||||||
split->setChannel(
|
split->setChannel(WindowManager::decodeChannel(splitNode));
|
||||||
WindowManager::decodeChannel(obj.value("data").toObject()));
|
split->setModerationMode(splitNode.moderationMode_);
|
||||||
split->setModerationMode(obj.value("moderationMode").toBool(false));
|
|
||||||
|
|
||||||
this->appendSplit(split);
|
this->appendSplit(split);
|
||||||
}
|
}
|
||||||
else if (type == "horizontal" || type == "vertical")
|
else if (std::holds_alternative<ContainerNodeDescriptor>(rootNode))
|
||||||
{
|
{
|
||||||
bool vertical = type == "vertical";
|
auto *n = std::get_if<ContainerNodeDescriptor>(&rootNode);
|
||||||
|
if (!n)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &containerNode = *n;
|
||||||
|
|
||||||
|
bool vertical = containerNode.vertical_;
|
||||||
|
|
||||||
Direction direction = vertical ? Direction::Below : Direction::Right;
|
Direction direction = vertical ? Direction::Below : Direction::Right;
|
||||||
|
|
||||||
node->type_ =
|
node->type_ =
|
||||||
vertical ? Node::VerticalContainer : Node::HorizontalContainer;
|
vertical ? Node::VerticalContainer : Node::HorizontalContainer;
|
||||||
|
|
||||||
for (QJsonValue _val : obj.value("items").toArray())
|
for (const auto &item : containerNode.items_)
|
||||||
{
|
{
|
||||||
auto _obj = _val.toObject();
|
if (std::holds_alternative<SplitNodeDescriptor>(item))
|
||||||
|
|
||||||
auto _type = _obj.value("type");
|
|
||||||
if (_type == "split")
|
|
||||||
{
|
{
|
||||||
|
auto *n = std::get_if<SplitNodeDescriptor>(&item);
|
||||||
|
if (!n)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &splitNode = *n;
|
||||||
auto *split = new Split(this);
|
auto *split = new Split(this);
|
||||||
split->setChannel(WindowManager::decodeChannel(
|
split->setChannel(WindowManager::decodeChannel(splitNode));
|
||||||
_obj.value("data").toObject()));
|
split->setModerationMode(splitNode.moderationMode_);
|
||||||
split->setModerationMode(
|
|
||||||
_obj.value("moderationMode").toBool(false));
|
|
||||||
|
|
||||||
Node *_node = new Node();
|
Node *_node = new Node();
|
||||||
_node->parent_ = node;
|
_node->parent_ = node;
|
||||||
_node->split_ = split;
|
_node->split_ = split;
|
||||||
_node->type_ = Node::_Split;
|
_node->type_ = Node::_Split;
|
||||||
|
|
||||||
_node->flexH_ = _obj.value("flexh").toDouble(1.0);
|
_node->flexH_ = splitNode.flexH_;
|
||||||
_node->flexV_ = _obj.value("flexv").toDouble(1.0);
|
_node->flexV_ = splitNode.flexV_;
|
||||||
node->children_.emplace_back(_node);
|
node->children_.emplace_back(_node);
|
||||||
|
|
||||||
this->addSplit(split);
|
this->addSplit(split);
|
||||||
|
@ -753,19 +765,7 @@ void SplitContainer::decodeNodeRecusively(QJsonObject &obj, Node *node)
|
||||||
Node *_node = new Node();
|
Node *_node = new Node();
|
||||||
_node->parent_ = node;
|
_node->parent_ = node;
|
||||||
node->children_.emplace_back(_node);
|
node->children_.emplace_back(_node);
|
||||||
this->decodeNodeRecusively(_obj, _node);
|
this->applyFromDescriptorRecursively(item, _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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/WindowDescriptors.hpp"
|
||||||
#include "widgets/BaseWidget.hpp"
|
#include "widgets/BaseWidget.hpp"
|
||||||
|
|
||||||
#include <QDragEnterEvent>
|
#include <QDragEnterEvent>
|
||||||
|
@ -184,8 +185,6 @@ public:
|
||||||
void selectNextSplit(Direction direction);
|
void selectNextSplit(Direction direction);
|
||||||
void setSelected(Split *selected_);
|
void setSelected(Split *selected_);
|
||||||
|
|
||||||
void decodeFromJson(QJsonObject &obj);
|
|
||||||
|
|
||||||
int getSplitCount();
|
int getSplitCount();
|
||||||
const std::vector<Split *> getSplits() const;
|
const std::vector<Split *> getSplits() const;
|
||||||
|
|
||||||
|
@ -201,6 +200,8 @@ public:
|
||||||
static bool isDraggingSplit;
|
static bool isDraggingSplit;
|
||||||
static Split *draggingSplit;
|
static Split *draggingSplit;
|
||||||
|
|
||||||
|
void applyFromDescriptor(const NodeDescriptor &rootNode);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
|
||||||
|
@ -214,6 +215,9 @@ protected:
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void applyFromDescriptorRecursively(const NodeDescriptor &rootNode,
|
||||||
|
Node *node);
|
||||||
|
|
||||||
void layout();
|
void layout();
|
||||||
void selectSplitRecursive(Node *node, Direction direction);
|
void selectSplitRecursive(Node *node, Direction direction);
|
||||||
void focusSplitRecursive(Node *node);
|
void focusSplitRecursive(Node *node);
|
||||||
|
@ -221,7 +225,6 @@ private:
|
||||||
|
|
||||||
void addSplit(Split *split);
|
void addSplit(Split *split);
|
||||||
|
|
||||||
void decodeNodeRecusively(QJsonObject &obj, Node *node);
|
|
||||||
Split *getTopRightSplit(Node &node);
|
Split *getTopRightSplit(Node &node);
|
||||||
|
|
||||||
void refreshTabTitle();
|
void refreshTabTitle();
|
||||||
|
|
Loading…
Reference in a new issue