Add option to save/restore user-defined layouts on memory

This happens only in memory at the moment (layouts are not saved in
disk yet), and the customization is quite simple (only size of
splitters, timeline position). But in the future we should be able to
dock elements in any place.
This commit is contained in:
David Capello 2022-08-25 16:31:42 -03:00
parent 381d9e663a
commit dc51ca25e0
11 changed files with 420 additions and 28 deletions

View File

@ -1236,6 +1236,11 @@ name = Name:
tileset = Tileset:
default_new_layer_name = New Layer
[new_layout]
title = New UI Layout
name = Name:
default_name = User Layout
[news_listbox]
more = More...
problem_loading = Problems loading news. Please retry.

View File

@ -0,0 +1,21 @@
<!-- Aseprite -->
<!-- Copyright (C) 2022 by Igara Studio S.A. -->
<gui>
<window id="new_layout" text="@.title">
<vbox>
<hbox>
<label text="@.name" />
<entry id="name" text="@.default_name"
maxsize="128" expansive="true"
magnet="true" />
</hbox>
<separator horizontal="true" />
<hbox homogeneous="true" cell_align="right">
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
<button text="@general.cancel" closewindow="true" />
</hbox>
</vbox>
</window>
</gui>

View File

@ -654,6 +654,7 @@ target_sources(app-lib PRIVATE
ui/input_chain.cpp
ui/keyboard_shortcuts.cpp
ui/layer_frame_comboboxes.cpp
ui/layout.cpp
ui/layout_selector.cpp
ui/main_menu_bar.cpp
ui/main_window.cpp

View File

@ -41,6 +41,17 @@ int side_index(int side)
return kCenterIndex; // ui::CENTER
}
int side_from_index(int index)
{
switch (index) {
case kTopIndex: return ui::TOP;
case kBottomIndex: return ui::BOTTOM;
case kLeftIndex: return ui::LEFT;
case kRightIndex: return ui::RIGHT;
}
return ui::CENTER; // kCenterIndex
}
} // anonymous namespace
void DockTabs::onSizeHint(ui::SizeHintEvent& ev)
@ -202,6 +213,25 @@ void Dock::undock(Widget* widget)
}
}
int Dock::whichSideChildIsDocked(const ui::Widget* widget) const
{
for (int i = 0; i < kSides; ++i)
if (m_sides[i] == widget)
return side_from_index(i);
return 0;
}
gfx::Size Dock::getUserDefinedSizeAtSide(int side) const
{
int i = side_index(side);
// Only EXPANSIVE sides can be user-defined (has a splitter so the
// user can expand or shrink it)
if (m_aligns[i] & EXPANSIVE)
return m_sizes[i];
else
return gfx::Size();
}
Dock* Dock::subdock(int side)
{
int i = side_index(side);
@ -431,6 +461,7 @@ void Dock::forEachSide(gfx::Rect bounds,
int spacing = (m_aligns[i] & EXPANSIVE ? childSpacing() : 0);
const gfx::Size sz = (m_aligns[i] & EXPANSIVE ? m_sizes[i] : widget->sizeHint());
gfx::Rect rc, separator;
switch (i) {
case kTopIndex:

View File

@ -53,6 +53,10 @@ public:
Dock* right() { return subdock(ui::RIGHT); }
Dock* center() { return subdock(ui::CENTER); }
// Functions useful to query/save the dock layout.
int whichSideChildIsDocked(const ui::Widget* widget) const;
gfx::Size getUserDefinedSizeAtSide(int side) const;
obs::signal<void()> Resize;
protected:

181
src/app/ui/layout.cpp Normal file
View File

@ -0,0 +1,181 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/ui/layout.h"
#include "app/app.h"
#include "app/ui/color_bar.h"
#include "app/ui/context_bar.h"
#include "app/ui/dock.h"
#include "app/ui/main_window.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui/toolbar.h"
#include "app/ui/workspace.h"
#include "app/xml_document.h"
#include "app/xml_exception.h"
#include "base/convert_to.h"
#include "ui/widget.h"
#include <cstring>
#include <sstream>
namespace app {
static void save_dock_layout(TiXmlElement& elem, const Dock* dock)
{
for (const auto child : dock->children()) {
const int side = dock->whichSideChildIsDocked(child);
const gfx::Size size = dock->getUserDefinedSizeAtSide(side);
std::string sideStr;
switch (side) {
case ui::LEFT: sideStr = "left"; break;
case ui::RIGHT: sideStr = "right"; break;
case ui::TOP: sideStr = "top"; break;
case ui::BOTTOM: sideStr = "bottom"; break;
case ui::CENTER:
// Empty side attribute
break;
}
TiXmlElement childElem("");
if (auto subdock = dynamic_cast<const Dock*>(child)) {
childElem.SetValue("dock");
if (!sideStr.empty())
childElem.SetAttribute("side", sideStr);
save_dock_layout(childElem, subdock);
}
else {
// Set the widget ID as the element name, e.g. <timeline />,
// <colorbar />, etc.
childElem.SetValue(child->id());
if (!sideStr.empty())
childElem.SetAttribute("side", sideStr);
if (size.w)
childElem.SetAttribute("width", size.w);
if (size.h)
childElem.SetAttribute("height", size.h);
}
elem.InsertEndChild(childElem);
}
}
static void load_dock_layout(const TiXmlElement* elem, Dock* dock)
{
const char* elemNameStr = elem->Value();
if (!elemNameStr) {
ASSERT(false); // Impossible?
return;
}
const std::string elemName = elemNameStr;
MainWindow* win = App::instance()->mainWindow();
ASSERT(win);
ui::Widget* widget = nullptr;
Dock* subdock = nullptr;
int side = ui::CENTER;
if (auto sideStr = elem->Attribute("side")) {
if (std::strcmp(sideStr, "left") == 0)
side = ui::LEFT;
if (std::strcmp(sideStr, "right") == 0)
side = ui::RIGHT;
if (std::strcmp(sideStr, "top") == 0)
side = ui::TOP;
if (std::strcmp(sideStr, "bottom") == 0)
side = ui::BOTTOM;
}
const char* widthStr = elem->Attribute("width");
const char* heightStr = elem->Attribute("height");
gfx::Size size;
if (widthStr)
size.w = base::convert_to<int>(std::string(widthStr));
if (heightStr)
size.h = base::convert_to<int>(std::string(heightStr));
if (elemName == "colorbar") {
widget = win->colorBar();
}
else if (elemName == "contextbar") {
widget = win->getContextBar();
}
else if (elemName == "timeline") {
widget = win->getTimeline();
}
else if (elemName == "toolbar") {
widget = win->toolBar();
}
else if (elemName == "workspace") {
widget = win->getWorkspace();
}
else if (elemName == "dock") {
subdock = dock->subdock(side);
}
if (subdock) {
auto childElem = elem->FirstChildElement();
while (childElem) {
load_dock_layout(childElem, subdock);
childElem = childElem->NextSiblingElement();
}
}
else {
dock->dock(side, widget, size);
}
}
Layout::Layout(const std::string& name, const Dock* dock) : m_name(name)
{
XmlDocumentRef doc(new TiXmlDocument());
TiXmlElement layoutsElem("layouts");
{
TiXmlElement layoutElem("layout");
layoutElem.SetAttribute("name", name);
save_dock_layout(layoutElem, dock);
layoutsElem.InsertEndChild(layoutElem);
}
TiXmlDeclaration declaration("1.0", "utf-8", "");
doc->InsertEndChild(declaration);
doc->InsertEndChild(layoutsElem);
std::stringstream s;
s << *doc;
m_data = s.str();
}
bool Layout::loadLayout(Dock* dock) const
{
XmlDocumentRef doc(new TiXmlDocument);
doc->Parse(m_data.c_str(), 0, TIXML_DEFAULT_ENCODING);
TiXmlHandle handle(doc.get());
TiXmlElement* layoutElem = handle.FirstChild("layouts").FirstChild("layout").ToElement();
if (!layoutElem)
return false;
TiXmlElement* elem = layoutElem->FirstChildElement();
while (elem) {
load_dock_layout(elem, dock);
elem = elem->NextSiblingElement();
}
return true;
}
} // namespace app

35
src/app/ui/layout.h Normal file
View File

@ -0,0 +1,35 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_UI_LAYOUT_H_INCLUDED
#define APP_UI_LAYOUT_H_INCLUDED
#pragma once
#include <memory>
#include <string>
namespace app {
class Dock;
class Layout {
public:
Layout(const std::string& name, const Dock* dock);
const std::string& name() const { return m_name; }
const std::string& data() const { return m_data; }
bool loadLayout(Dock* dock) const;
private:
std::string m_name;
std::string m_data;
};
using LayoutPtr = std::shared_ptr<Layout>;
} // namespace app
#endif

View File

@ -17,10 +17,13 @@
#include "app/ui/main_window.h"
#include "app/ui/separator_in_view.h"
#include "app/ui/skin/skin_theme.h"
#include "fmt/format.h"
#include "ui/listitem.h"
#include "ui/tooltips.h"
#include "ui/window.h"
#include "new_layout.xml.h"
#define ANI_TICKS 5
namespace app {
@ -30,29 +33,6 @@ using namespace ui;
namespace {
enum class LayoutId { DEFAULT, DEFAULT_MIRROR, CUSTOMIZE };
class LayoutItem : public ListItem {
public:
LayoutItem(const LayoutId id, const std::string& text) : ListItem(text), m_id(id) {}
void select()
{
MainWindow* win = App::instance()->mainWindow();
switch (m_id) {
case LayoutId::DEFAULT: win->setDefaultLayout(); break;
case LayoutId::DEFAULT_MIRROR: win->setDefaultMirrorLayout(); break;
case LayoutId::CUSTOMIZE:
// TODO
break;
}
}
private:
LayoutId m_id;
};
// TODO Similar ButtonSet to the one in timeline_conf.xml
class TimelineButtons : public ButtonSet {
public:
@ -65,7 +45,7 @@ public:
auto& timelinePosOption = Preferences::instance().general.timelinePosition;
setSelectedButtonFromTimelinePosition(timelinePosOption());
timelinePosOption.AfterChange.connect(
m_timelinePosConn = timelinePosOption.AfterChange.connect(
[this](gen::TimelinePosition position) { setSelectedButtonFromTimelinePosition(position); });
InitTheme.connect([this] {
@ -95,14 +75,96 @@ private:
// Show the timeline
App::instance()->mainWindow()->setTimelineVisibility(true);
}
obs::scoped_connection m_timelinePosConn;
};
}; // namespace
class LayoutSelector::LayoutItem : public ListItem {
public:
enum LayoutId {
DEFAULT,
DEFAULT_MIRROR,
SAVE_LAYOUT,
USER_DEFINED,
};
LayoutItem(LayoutSelector* selector,
const LayoutId id,
const std::string& text,
const LayoutPtr layout = nullptr)
: ListItem(text)
, m_selector(selector)
, m_id(id)
, m_layout(layout)
{
ASSERT((id != USER_DEFINED && layout == nullptr) || (id == USER_DEFINED && layout != nullptr));
}
void selectImmediately()
{
MainWindow* win = App::instance()->mainWindow();
switch (m_id) {
case LayoutId::DEFAULT: win->setDefaultLayout(); break;
case LayoutId::DEFAULT_MIRROR: win->setDefaultMirrorLayout(); break;
case LayoutId::USER_DEFINED:
ASSERT(m_layout);
if (m_layout)
win->loadUserLayout(m_layout.get());
break;
default:
// Do nothing
break;
}
}
void selectAfterClose()
{
MainWindow* win = App::instance()->mainWindow();
switch (m_id) {
case LayoutId::SAVE_LAYOUT: {
gen::NewLayout window;
window.name()->setText(
fmt::format("{} ({})", window.name()->text(), m_selector->m_layouts.size()));
window.openWindowInForeground();
if (window.closer() == window.ok()) {
auto layout = std::make_shared<Layout>(window.name()->text(), win->customizableDock());
m_selector->addLayout(std::move(layout));
}
break;
}
default:
// Do nothing
break;
}
}
private:
LayoutId m_id;
LayoutSelector* m_selector;
LayoutPtr m_layout;
};
void LayoutSelector::LayoutComboBox::onChange()
{
ComboBox::onChange();
if (auto item = dynamic_cast<LayoutItem*>(getSelectedItem())) {
item->select();
item->selectImmediately();
m_selected = item;
}
}
void LayoutSelector::LayoutComboBox::onCloseListBox()
{
ComboBox::onCloseListBox();
if (m_selected) {
m_selected->selectAfterClose();
m_selected = nullptr;
}
}
@ -131,6 +193,16 @@ LayoutSelector::~LayoutSelector()
stopAnimation();
}
void LayoutSelector::addLayout(const LayoutPtr& layout)
{
auto item = m_comboBox.addItem(
new LayoutItem(this, LayoutItem::USER_DEFINED, layout->name(), layout));
m_layouts.push_back(layout);
m_comboBox.setSelectedItemIndex(item);
}
void LayoutSelector::onAnimationFrame()
{
switch (animation()) {
@ -171,10 +243,15 @@ void LayoutSelector::switchSelector()
// Create the combobox for first time
if (m_comboBox.getItemCount() == 0) {
m_comboBox.addItem(new SeparatorInView("Layout", HORIZONTAL));
m_comboBox.addItem(new LayoutItem(LayoutId::DEFAULT, "Default"));
m_comboBox.addItem(new LayoutItem(LayoutId::DEFAULT_MIRROR, "Default / Mirror"));
m_comboBox.addItem(new LayoutItem(this, LayoutItem::DEFAULT, "Default"));
m_comboBox.addItem(new LayoutItem(this, LayoutItem::DEFAULT_MIRROR, "Default / Mirror"));
m_comboBox.addItem(new SeparatorInView("Timeline", HORIZONTAL));
m_comboBox.addItem(new TimelineButtons());
m_comboBox.addItem(new SeparatorInView("User Layouts", HORIZONTAL));
m_comboBox.addItem(new LayoutItem(this, LayoutItem::SAVE_LAYOUT, "Save..."));
for (const auto& layout : m_layouts) {
m_comboBox.addItem(new LayoutItem(this, LayoutItem::USER_DEFINED, layout->name(), layout));
}
}
m_comboBox.setVisible(true);

View File

@ -10,11 +10,13 @@
#include "app/ui/dockable.h"
#include "app/ui/icon_button.h"
#include "app/ui/layout.h"
#include "ui/animated_widget.h"
#include "ui/box.h"
#include "ui/combobox.h"
#include <memory>
#include <vector>
namespace ui {
class TooltipManager;
@ -31,14 +33,21 @@ class LayoutSelector : public ui::HBox,
ANI_COLLAPSING,
};
class LayoutItem;
class LayoutComboBox : public ui::ComboBox {
private:
void onChange() override;
void onCloseListBox() override;
LayoutItem* m_selected = nullptr;
};
public:
LayoutSelector(ui::TooltipManager* tooltipManager);
~LayoutSelector();
void addLayout(const LayoutPtr& layout);
// Dockable impl
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
@ -52,6 +61,7 @@ private:
IconButton m_button;
gfx::Size m_startSize;
gfx::Size m_endSize;
std::vector<LayoutPtr> m_layouts;
};
} // namespace app

View File

@ -156,6 +156,14 @@ void MainWindow::initialize()
m_workspace->setExpansive(true);
m_notifications->setVisible(false);
// IDs to create UI layouts from a Dock (see app::Layout
// constructor).
m_colorBar->setId("colorbar");
m_contextBar->setId("contextbar");
m_timeline->setId("timeline");
m_toolBar->setId("toolbar");
m_workspace->setId("workspace");
// Add the widgets in the boxes
addChild(m_tooltipManager);
addChild(m_dock);
@ -419,6 +427,19 @@ void MainWindow::setDefaultMirrorLayout()
configureWorkspaceLayout();
}
void MainWindow::loadUserLayout(const Layout* layout)
{
m_timelineResizeConn.disconnect();
m_colorBarResizeConn.disconnect();
m_customizableDock->resetDocks();
if (!layout->loadLayout(m_customizableDock))
setDefaultLayout();
this->layout();
}
void MainWindow::dataRecoverySessionsAreReady()
{
getHomeView()->dataRecoverySessionsAreReady();

View File

@ -34,8 +34,9 @@ class DocView;
class Dock;
class HomeView;
class INotificationDelegate;
class MainMenuBar;
class Layout;
class LayoutSelector;
class MainMenuBar;
class Notifications;
class PreviewEditorWindow;
class StatusBar;
@ -56,12 +57,15 @@ public:
MainWindow();
~MainWindow();
// TODO refactor: remove the get prefix from these functions
MainMenuBar* getMenuBar() { return m_menuBar.get(); }
ContextBar* getContextBar() { return m_contextBar.get(); }
StatusBar* statusBar() { return m_statusBar.get(); }
WorkspaceTabs* getTabsBar() { return m_tabsBar.get(); }
Timeline* getTimeline() { return m_timeline.get(); }
Workspace* getWorkspace() { return m_workspace.get(); }
ColorBar* colorBar() { return m_colorBar.get(); }
ToolBar* toolBar() { return m_toolBar.get(); }
PreviewEditorWindow* getPreviewEditor() { return m_previewEditor.get(); }
#ifdef ENABLE_UPDATER
CheckUpdateDelegate* getCheckUpdateDelegate();
@ -89,6 +93,8 @@ public:
void setDefaultLayout();
void setDefaultMirrorLayout();
void loadUserLayout(const Layout* layout);
const Dock* customizableDock() const { return m_customizableDock; }
// When crash::DataRecovery finish to search for sessions, this
// function is called.