Skip to content

Commit

Permalink
Split up Window Layout loading into a loading and application stage (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
pajlada authored Sep 19, 2020
1 parent 7eabba9 commit 913193f
Show file tree
Hide file tree
Showing 8 changed files with 484 additions and 191 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions chatterino.pro
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
207 changes: 207 additions & 0 deletions src/common/WindowDescriptors.cpp
Original file line number Diff line number Diff line change
@@ -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 changes: 97 additions & 0 deletions src/common/WindowDescriptors.hpp
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 913193f

Please sign in to comment.