diff --git a/data/pref.xml b/data/pref.xml
index dbc557525..aee8d7900 100644
--- a/data/pref.xml
+++ b/data/pref.xml
@@ -157,6 +157,7 @@
+
diff --git a/data/strings/en.ini b/data/strings/en.ini
index 5dfddc576..6d05e7bbd 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -1174,7 +1174,12 @@ help_enter_license = Enter &License
help_about = &About
[main_window]
-layout = User Interface Layout
+layout = Workspace Layout
+default_layout = Default
+mirrored_default_layout = Mirrored Default
+timeline = Timeline
+user_layouts = User Layouts
+new_layout = New Layout...
[mask_by_color]
title = Select Color
@@ -1205,9 +1210,9 @@ tileset = Tileset:
default_new_layer_name = New Layer
[new_layout]
-title = New UI Layout
+title = New Workspace Layout
name = Name:
-default_name = User Layout
+default_name = User Layout {}
[news_listbox]
more = More...
diff --git a/src/app/ui/dock.cpp b/src/app/ui/dock.cpp
index 8648692f0..3f2560900 100644
--- a/src/app/ui/dock.cpp
+++ b/src/app/ui/dock.cpp
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2021-2022 Igara Studio S.A.
+// Copyright (C) 2021-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@@ -363,6 +363,7 @@ bool Dock::onProcessMessage(ui::Message* msg)
case kMouseUpMessage: {
if (hasCapture()) {
releaseMouse();
+ onUserResizedDock();
}
break;
}
@@ -391,6 +392,20 @@ bool Dock::onProcessMessage(ui::Message* msg)
return Widget::onProcessMessage(msg);
}
+void Dock::onUserResizedDock()
+{
+ // Generate the UserResizedDock signal, this can be used to know
+ // when the user modified the dock configuration to save the new
+ // layout in a user/preference file.
+ UserResizedDock();
+
+ // Send the same notification for the parent (as probably eh
+ // MainWindow is listening the signal of just the root dock).
+ if (auto parentDock = dynamic_cast(parent())) {
+ parentDock->onUserResizedDock();
+ }
+}
+
void Dock::setSide(const int i, Widget* newWidget)
{
m_sides[i] = newWidget;
diff --git a/src/app/ui/dock.h b/src/app/ui/dock.h
index d0d2bfae4..065226f96 100644
--- a/src/app/ui/dock.h
+++ b/src/app/ui/dock.h
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2021-2022 Igara Studio S.A.
+// Copyright (C) 2021-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@@ -60,6 +60,7 @@ public:
gfx::Size getUserDefinedSizeAtSide(int side) const;
obs::signal Resize;
+ obs::signal UserResizedDock;
protected:
void onSizeHint(ui::SizeHintEvent& ev) override;
@@ -67,6 +68,7 @@ protected:
void onPaint(ui::PaintEvent& ev) override;
void onInitTheme(ui::InitThemeEvent& ev) override;
bool onProcessMessage(ui::Message* msg) override;
+ void onUserResizedDock();
private:
void setSide(const int i, ui::Widget* newWidget);
diff --git a/src/app/ui/layout.cpp b/src/app/ui/layout.cpp
index 2eb357d7b..7c1fb00a7 100644
--- a/src/app/ui/layout.cpp
+++ b/src/app/ui/layout.cpp
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2022 Igara Studio S.A.
+// Copyright (C) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@@ -122,18 +122,22 @@ static void load_dock_layout(const TiXmlElement* elem, Dock* dock)
LayoutPtr Layout::MakeFromXmlElement(const TiXmlElement* layoutElem)
{
auto layout = std::make_shared();
- if (auto name = layoutElem->Attribute("name"))
+ if (auto name = layoutElem->Attribute("name")) {
+ layout->m_id = name;
layout->m_name = name;
+ }
layout->m_elem.reset(layoutElem->Clone()->ToElement());
return layout;
}
// static
-LayoutPtr Layout::MakeFromDock(const std::string& name,
+LayoutPtr Layout::MakeFromDock(const std::string& id,
+ const std::string& name,
const Dock* dock)
{
auto layout = std::make_shared();
+ layout->m_id = id;
layout->m_name = name;
layout->m_elem = std::make_unique("layout");
@@ -143,6 +147,17 @@ LayoutPtr Layout::MakeFromDock(const std::string& name,
return layout;
}
+bool Layout::matchId(const std::string& id) const
+{
+ if (m_id == id)
+ return true;
+ else if ((m_id.empty() && id == kDefault) ||
+ (m_id == kDefault && id.empty()))
+ return true;
+ else
+ return false;
+}
+
bool Layout::loadLayout(Dock* dock) const
{
if (!m_elem)
diff --git a/src/app/ui/layout.h b/src/app/ui/layout.h
index 3dfac040e..0173e8177 100644
--- a/src/app/ui/layout.h
+++ b/src/app/ui/layout.h
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2022 Igara Studio S.A.
+// Copyright (C) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@@ -21,16 +21,23 @@ namespace app {
class Layout final {
public:
+ static constexpr const char* kDefault = "_default_";
+ static constexpr const char* kMirroredDefault = "_mirrored_default_";
+
static LayoutPtr MakeFromXmlElement(const TiXmlElement* layoutElem);
- static LayoutPtr MakeFromDock(const std::string& name,
+ static LayoutPtr MakeFromDock(const std::string& id,
+ const std::string& name,
const Dock* dock);
+ const std::string& id() const { return m_id; }
const std::string& name() const { return m_name; }
const TiXmlElement* xmlElement() const { return m_elem.get(); }
+ bool matchId(const std::string& id) const;
bool loadLayout(Dock* dock) const;
private:
+ std::string m_id;
std::string m_name;
std::unique_ptr m_elem;
};
diff --git a/src/app/ui/layout_selector.cpp b/src/app/ui/layout_selector.cpp
index e82e92a33..f2101a069 100644
--- a/src/app/ui/layout_selector.cpp
+++ b/src/app/ui/layout_selector.cpp
@@ -127,27 +127,36 @@ private:
class LayoutSelector::LayoutItem : public ListItem {
public:
- enum LayoutId {
+ enum LayoutOption {
DEFAULT,
- DEFAULT_MIRROR,
- SAVE_LAYOUT,
+ MIRRORED_DEFAULT,
USER_DEFINED,
+ NEW_LAYOUT,
};
LayoutItem(LayoutSelector* selector,
- const LayoutId id,
+ const LayoutOption option,
const std::string& text,
- const LayoutPtr layout = nullptr)
+ const LayoutPtr layout)
: ListItem(text)
- , m_id(id)
+ , m_option(option)
, m_selector(selector)
, m_layout(layout) {
- ASSERT((id != USER_DEFINED && layout == nullptr) ||
- (id == USER_DEFINED && layout != nullptr));
}
- Layout* layout() const {
- return m_layout.get();
+ std::string getLayoutId() const {
+ if (m_layout)
+ return m_layout->id();
+ else
+ return std::string();
+ }
+
+ bool matchId(const std::string& id) const {
+ return (m_layout && m_layout->matchId(id));
+ }
+
+ const LayoutPtr& layout() const {
+ return m_layout;
}
void setLayout(const LayoutPtr& layout) {
@@ -157,29 +166,28 @@ public:
void selectImmediately() {
MainWindow* win = App::instance()->mainWindow();
- switch (m_id) {
- case LayoutId::DEFAULT:
+ if (m_layout)
+ m_selector->m_activeLayoutId = m_layout->id();
+
+ switch (m_option) {
+ case LayoutOption::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
+ case LayoutOption::MIRRORED_DEFAULT:
+ win->setMirroredDefaultLayout();
break;
}
+ // Even Default & Mirrored Default can have a customized layout
+ // (customized default layout).
+ if (m_layout)
+ win->loadUserLayout(m_layout.get());
}
void selectAfterClose() {
MainWindow* win = App::instance()->mainWindow();
- switch (m_id) {
- case LayoutId::SAVE_LAYOUT: {
+ switch (m_option) {
+ case LayoutOption::NEW_LAYOUT: {
// Select the "Layout" separator (it's like selecting nothing)
// TODO improve the ComboBox to select a real "nothing" (with
// a placeholder text)
@@ -190,14 +198,14 @@ public:
name.getEntryWidget()->setMaxTextLength(128);
name.setFocusMagnet(true);
name.setValue(
- fmt::format("{} ({})",
- Strings::new_layout_default_name(),
+ fmt::format(Strings::new_layout_default_name(),
m_selector->m_layouts.size()+1));
window.namePlaceholder()->addChild(&name);
window.openWindowInForeground();
if (window.closer() == window.ok()) {
auto layout = Layout::MakeFromDock(name.getValue(),
+ name.getValue(),
win->customizableDock());
m_selector->addLayout(layout);
@@ -211,7 +219,7 @@ public:
}
private:
- LayoutId m_id;
+ LayoutOption m_option;
LayoutSelector* m_selector;
LayoutPtr m_layout;
};
@@ -237,6 +245,8 @@ void LayoutSelector::LayoutComboBox::onCloseListBox()
LayoutSelector::LayoutSelector(TooltipManager* tooltipManager)
: m_button(SkinTheme::instance()->parts.iconUserData())
{
+ m_activeLayoutId = Preferences::instance().general.workspaceLayout();
+
m_button.Click.connect([this](){ switchSelector(); });
m_comboBox.setVisible(false);
@@ -257,19 +267,28 @@ LayoutSelector::LayoutSelector(TooltipManager* tooltipManager)
LayoutSelector::~LayoutSelector()
{
+ Preferences::instance().general.workspaceLayout(m_activeLayoutId);
+
stopAnimation();
}
+LayoutPtr LayoutSelector::activeLayout()
+{
+ return m_layouts.getById(m_activeLayoutId);
+}
+
void LayoutSelector::addLayout(const LayoutPtr& layout)
{
bool added = m_layouts.addLayout(layout);
if (added) {
- auto item =
- m_comboBox.addItem(
- new LayoutItem(this, LayoutItem::USER_DEFINED,
- layout->name(),
- layout));
- m_comboBox.setSelectedItemIndex(item);
+ auto item = new LayoutItem(this, LayoutItem::USER_DEFINED,
+ layout->name(),
+ layout);
+ m_comboBox.insertItem(
+ m_comboBox.getItemCount()-1, // Above the "New Layout" item
+ item);
+
+ m_comboBox.setSelectedItem(item);
}
else {
for (auto item : m_comboBox) {
@@ -285,6 +304,15 @@ void LayoutSelector::addLayout(const LayoutPtr& layout)
}
}
+void LayoutSelector::updateActiveLayout(const LayoutPtr& newLayout)
+{
+ bool result = m_layouts.addLayout(newLayout);
+
+ // It means that the layout wasn't added, but replaced, when we
+ // update a layout it must be existent in the m_layouts collection.
+ ASSERT(result == false);
+}
+
void LayoutSelector::onAnimationFrame()
{
switch (animation()) {
@@ -336,18 +364,34 @@ 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(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 SeparatorInView(Strings::main_window_layout(), HORIZONTAL));
+ m_comboBox.addItem(
+ new LayoutItem(
+ this,
+ LayoutItem::DEFAULT,
+ Strings::main_window_default_layout(),
+ m_layouts.getById(Layout::kDefault)));
+ m_comboBox.addItem(
+ new LayoutItem(
+ this,
+ LayoutItem::MIRRORED_DEFAULT,
+ Strings::main_window_mirrored_default_layout(),
+ m_layouts.getById(Layout::kMirroredDefault)));
+ m_comboBox.addItem(new SeparatorInView(Strings::main_window_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..."));
+ m_comboBox.addItem(new SeparatorInView(Strings::main_window_user_layouts(), HORIZONTAL));
for (const auto& layout : m_layouts) {
- m_comboBox.addItem(new LayoutItem(this, LayoutItem::USER_DEFINED,
- layout->name(),
- layout));
+ m_comboBox.addItem(
+ new LayoutItem(
+ this, LayoutItem::USER_DEFINED,
+ layout->name(),
+ layout));
}
+ m_comboBox.addItem(
+ new LayoutItem(
+ this, LayoutItem::NEW_LAYOUT,
+ Strings::main_window_new_layout(),
+ nullptr));
}
m_comboBox.setVisible(true);
@@ -361,6 +405,9 @@ void LayoutSelector::switchSelector()
m_endSize = gfx::Size(0, 0);
}
+ if (auto item = getItemByLayoutId(m_activeLayoutId))
+ m_comboBox.setSelectedItem(item);
+
m_comboBox.setSizeHint(m_startSize);
startAnimation((expand ? ANI_EXPANDING: ANI_COLLAPSING), ANI_TICKS);
}
@@ -381,4 +428,15 @@ void LayoutSelector::setupTooltips(TooltipManager* tooltipManager)
tooltipManager->addTooltipFor(&m_button, Strings::main_window_layout(), TOP);
}
+LayoutSelector::LayoutItem* LayoutSelector::getItemByLayoutId(const std::string& id)
+{
+ for (auto child : m_comboBox) {
+ if (auto item = dynamic_cast(child)) {
+ if (item->matchId(id))
+ return item;
+ }
+ }
+ return nullptr;
+}
+
} // namespace app
diff --git a/src/app/ui/layout_selector.h b/src/app/ui/layout_selector.h
index 955aaa68a..4b3dd34e7 100644
--- a/src/app/ui/layout_selector.h
+++ b/src/app/ui/layout_selector.h
@@ -47,7 +47,11 @@ namespace app {
LayoutSelector(ui::TooltipManager* tooltipManager);
~LayoutSelector();
+ LayoutPtr activeLayout();
+ std::string activeLayoutId() const { return m_activeLayoutId; }
+
void addLayout(const LayoutPtr& layout);
+ void updateActiveLayout(const LayoutPtr& layout);
void switchSelector();
void switchSelectorFromCommand();
bool isSelectorVisible() const;
@@ -59,9 +63,11 @@ namespace app {
private:
void setupTooltips(ui::TooltipManager* tooltipManager);
+ LayoutItem* getItemByLayoutId(const std::string& id);
void onAnimationFrame() override;
void onAnimationStop(int animation) override;
+ std::string m_activeLayoutId;
LayoutComboBox m_comboBox;
IconButton m_button;
gfx::Size m_startSize;
diff --git a/src/app/ui/layouts.cpp b/src/app/ui/layouts.cpp
index 5409af865..b5b063189 100644
--- a/src/app/ui/layouts.cpp
+++ b/src/app/ui/layouts.cpp
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (c) 2022 Igara Studio S.A.
+// Copyright (c) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@@ -38,11 +38,20 @@ Layouts::~Layouts()
save(m_userLayoutsFilename);
}
+LayoutPtr Layouts::getById(const std::string& id) const
+{
+ auto it = std::find_if(m_layouts.begin(), m_layouts.end(),
+ [&id](const LayoutPtr& l){
+ return l->matchId(id);
+ });
+ return (it != m_layouts.end() ? *it: nullptr);
+}
+
bool Layouts::addLayout(const LayoutPtr& layout)
{
auto it = std::find_if(m_layouts.begin(), m_layouts.end(),
- [layout](const LayoutPtr& l){
- return l->name() == layout->name();
+ [layout](const LayoutPtr& l) {
+ return l->matchId(layout->id());
});
if (it != m_layouts.end()) {
*it = layout; // Replace existent layout
diff --git a/src/app/ui/layouts.h b/src/app/ui/layouts.h
index 784142227..8578093f0 100644
--- a/src/app/ui/layouts.h
+++ b/src/app/ui/layouts.h
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (c) 2022 Igara Studio S.A.
+// Copyright (c) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@@ -22,6 +22,8 @@ namespace app {
size_t size() const { return m_layouts.size(); }
+ LayoutPtr getById(const std::string& id) const;
+
// Returns true if the layout is added, or false if it was
// replaced.
bool addLayout(const LayoutPtr& layout);
diff --git a/src/app/ui/main_window.cpp b/src/app/ui/main_window.cpp
index c341bfc99..7830d8f00 100644
--- a/src/app/ui/main_window.cpp
+++ b/src/app/ui/main_window.cpp
@@ -42,6 +42,7 @@
#include "app/ui_context.h"
#include "base/fs.h"
#include "os/system.h"
+#include "ui/app_state.h"
#include "ui/message.h"
#include "ui/splitter.h"
#include "ui/system.h"
@@ -179,7 +180,15 @@ void MainWindow::initialize()
m_dock->top()->dock(ui::CENTER, m_menuBar.get());
m_dock->dock(ui::CENTER, m_customizableDockPlaceholder.get());
+ // After the user resizes the dock we save the updated layout
+ m_saveDockLayoutConn = m_customizableDock->UserResizedDock.connect(
+ [this]{
+ saveActiveLayout();
+ });
+
setDefaultLayout();
+ if (LayoutPtr layout = m_layoutSelector->activeLayout())
+ loadUserLayout(layout.get());
// Reconfigure workspace when the timeline position is changed.
auto& pref = Preferences::instance();
@@ -200,6 +209,7 @@ MainWindow::~MainWindow()
{
m_timelineResizeConn.disconnect();
m_colorBarResizeConn.disconnect();
+ m_saveDockLayoutConn.disconnect();
m_dock->resetDocks();
m_customizableDock->resetDocks();
@@ -412,7 +422,7 @@ void MainWindow::setDefaultLayout()
configureWorkspaceLayout();
}
-void MainWindow::setDefaultMirrorLayout()
+void MainWindow::setMirroredDefaultLayout()
{
m_timelineResizeConn.disconnect();
m_colorBarResizeConn.disconnect();
@@ -494,6 +504,11 @@ void MainWindow::onResize(ui::ResizeEvent& ev)
// inform to the UIContext that the current view has changed.
void MainWindow::onActiveViewChange()
{
+ // If we are closing the app, we just ignore all view changes (as
+ // docs will be destroyed and views closed).
+ if (get_app_state() != AppState::kNormal)
+ return;
+
// First we have to configure the MainWindow layout (e.g. show
// Timeline if needed) as UIContext::setActiveView() will configure
// several widgets (calling updateUsingEditor() functions) using the
@@ -772,4 +787,13 @@ void MainWindow::saveColorBarConfiguration()
m_colorBar->bounds().w);
}
+void MainWindow::saveActiveLayout()
+{
+ ASSERT(m_layoutSelector);
+
+ auto id = m_layoutSelector->activeLayoutId();
+ auto layout = Layout::MakeFromDock(id, id, m_customizableDock);
+ m_layoutSelector->updateActiveLayout(layout);
+}
+
} // namespace app
diff --git a/src/app/ui/main_window.h b/src/app/ui/main_window.h
index 77d560e6a..84dd59fb0 100644
--- a/src/app/ui/main_window.h
+++ b/src/app/ui/main_window.h
@@ -98,7 +98,7 @@ namespace app {
void popTimeline();
void setDefaultLayout();
- void setDefaultMirrorLayout();
+ void setMirroredDefaultLayout();
void loadUserLayout(const Layout* layout);
const Dock* customizableDock() const { return m_customizableDock; }
@@ -138,6 +138,7 @@ namespace app {
void configureWorkspaceLayout();
void saveTimelineConfiguration();
void saveColorBarConfiguration();
+ void saveActiveLayout();
ui::TooltipManager* m_tooltipManager;
Dock* m_dock;
@@ -163,6 +164,7 @@ namespace app {
#endif
obs::scoped_connection m_timelineResizeConn;
obs::scoped_connection m_colorBarResizeConn;
+ obs::scoped_connection m_saveDockLayoutConn;
};
}