diff --git a/data/widgets/main_window.xml b/data/widgets/main_window.xml
deleted file mode 100644
index 9c3098dd9..000000000
--- a/data/widgets/main_window.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index 5a84b7ccb..37626a30c 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -609,6 +609,7 @@ target_sources(app-lib PRIVATE
ui/context_bar.cpp
ui/dithering_selector.cpp
ui/doc_view.cpp
+ ui/dock.cpp
ui/drop_down_button.cpp
ui/dynamics_popup.cpp
ui/editor/brush_preview.cpp
@@ -653,6 +654,7 @@ target_sources(app-lib PRIVATE
ui/input_chain.cpp
ui/keyboard_shortcuts.cpp
ui/layer_frame_comboboxes.cpp
+ ui/layout_selector.cpp
ui/main_menu_bar.cpp
ui/main_window.cpp
ui/mini_help_button.cpp
diff --git a/src/app/ui/color_bar.cpp b/src/app/ui/color_bar.cpp
index 64008b002..f48b0a567 100644
--- a/src/app/ui/color_bar.cpp
+++ b/src/app/ui/color_bar.cpp
@@ -138,8 +138,8 @@ void ColorBar::ScrollableView::onInitTheme(InitThemeEvent& ev)
ColorBar* ColorBar::m_instance = NULL;
-ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
- : Box(align)
+ColorBar::ColorBar(TooltipManager* tooltipManager)
+ : Box(VERTICAL)
, m_editPal(1)
, m_buttons(int(PalButton::MAX))
, m_tilesButton(1)
diff --git a/src/app/ui/color_bar.h b/src/app/ui/color_bar.h
index 9895db55b..799631fd2 100644
--- a/src/app/ui/color_bar.h
+++ b/src/app/ui/color_bar.h
@@ -17,6 +17,7 @@
#include "app/tileset_mode.h"
#include "app/ui/button_set.h"
#include "app/ui/color_button.h"
+#include "app/ui/dockable.h"
#include "app/ui/input_chain_element.h"
#include "app/ui/palette_view.h"
#include "app/ui/tile_button.h"
@@ -50,7 +51,8 @@ class ColorBar : public ui::Box,
public PaletteViewDelegate,
public ContextObserver,
public DocObserver,
- public InputChainElement {
+ public InputChainElement,
+ public Dockable {
static ColorBar* m_instance;
public:
@@ -65,7 +67,7 @@ public:
static ColorBar* instance() { return m_instance; }
- ColorBar(int align, ui::TooltipManager* tooltipManager);
+ ColorBar(ui::TooltipManager* tooltipManager);
~ColorBar();
void setPixelFormat(doc::PixelFormat pixelFormat);
@@ -123,6 +125,13 @@ public:
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;
+ // Dockable impl
+ int dockableAt() const override
+ {
+ // TODO split the ColorBar in different dockable widgets
+ return ui::LEFT | ui::RIGHT | ui::EXPANSIVE;
+ }
+
obs::signal ChangeSelection;
protected:
diff --git a/src/app/ui/context_bar.h b/src/app/ui/context_bar.h
index 1db6754f2..70044b600 100644
--- a/src/app/ui/context_bar.h
+++ b/src/app/ui/context_bar.h
@@ -17,6 +17,7 @@
#include "app/tools/tool_loop_modifiers.h"
#include "app/ui/context_bar_observer.h"
#include "app/ui/doc_observer_widget.h"
+#include "app/ui/dockable.h"
#include "app/ui/font_entry.h"
#include "doc/brush.h"
#include "obs/connection.h"
@@ -60,7 +61,8 @@ class Transformation;
class ContextBar : public DocObserverWidget,
public obs::observable,
- public tools::ActiveToolObserver {
+ public tools::ActiveToolObserver,
+ public Dockable {
public:
ContextBar(ui::TooltipManager* tooltipManager, ColorBar* colorBar);
~ContextBar();
@@ -100,6 +102,9 @@ public:
// For freehand with dynamics
const tools::DynamicsOptions& getDynamics() const;
+ // Dockable impl
+ int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
+
// Signals
obs::signal BrushChange;
obs::signal FontChange;
diff --git a/src/app/ui/dock.cpp b/src/app/ui/dock.cpp
new file mode 100644
index 000000000..de5fdde15
--- /dev/null
+++ b/src/app/ui/dock.cpp
@@ -0,0 +1,476 @@
+// Aseprite
+// Copyright (C) 2021 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/dock.h"
+
+#include "app/ui/dockable.h"
+#include "app/ui/skin/skin_theme.h"
+#include "ui/cursor_type.h"
+#include "ui/message.h"
+#include "ui/paint_event.h"
+#include "ui/resize_event.h"
+#include "ui/scale.h"
+#include "ui/size_hint_event.h"
+#include "ui/system.h"
+#include "ui/widget.h"
+
+namespace app {
+
+using namespace app::skin;
+using namespace ui;
+
+namespace {
+
+enum { kTopIndex, kBottomIndex, kLeftIndex, kRightIndex, kCenterIndex };
+
+int side_index(int side)
+{
+ switch (side) {
+ case ui::TOP: return kTopIndex;
+ case ui::BOTTOM: return kBottomIndex;
+ case ui::LEFT: return kLeftIndex;
+ case ui::RIGHT: return kRightIndex;
+ }
+ return kCenterIndex; // ui::CENTER
+}
+
+} // anonymous namespace
+
+void DockTabs::onSizeHint(ui::SizeHintEvent& ev)
+{
+ gfx::Size sz;
+ for (auto child : children()) {
+ if (child->isVisible())
+ sz |= child->sizeHint();
+ }
+ sz.h += textHeight();
+ ev.setSizeHint(sz);
+}
+
+void DockTabs::onResize(ui::ResizeEvent& ev)
+{
+ auto bounds = ev.bounds();
+ setBoundsQuietly(bounds);
+ bounds = childrenBounds();
+ bounds.y += textHeight();
+ bounds.h -= textHeight();
+
+ for (auto child : children()) {
+ child->setBounds(bounds);
+ }
+}
+
+void DockTabs::onPaint(ui::PaintEvent& ev)
+{
+ Graphics* g = ev.graphics();
+ g->fillRect(gfx::rgba(0, 0, 255), clientBounds());
+}
+
+Dock::Dock()
+{
+ for (int i = 0; i < kSides; ++i) {
+ m_sides[i] = nullptr;
+ m_aligns[i] = 0;
+ m_sizes[i] = gfx::Size(0, 0);
+ }
+
+ InitTheme.connect([this] {
+ if (auto p = parent())
+ setBgColor(p->bgColor());
+ });
+ initTheme();
+}
+
+void Dock::reset()
+{
+ for (int i = 0; i < kSides; ++i) {
+ auto child = m_sides[i];
+ if (!child)
+ continue;
+ else if (auto subdock = dynamic_cast(child)) {
+ subdock->reset();
+ }
+ else if (auto tabs = dynamic_cast(child)) {
+ for (auto child2 : tabs->children()) {
+ if (auto subdock2 = dynamic_cast(child2))
+ subdock2->reset();
+ }
+ }
+ m_sides[i] = nullptr;
+ }
+ removeAllChildren();
+}
+
+void Dock::dock(int side, ui::Widget* widget, const gfx::Size& prefSize)
+{
+ ASSERT(widget);
+
+ const int i = side_index(side);
+ if (!m_sides[i]) {
+ setSide(i, widget);
+ addChild(widget);
+
+ if (prefSize != gfx::Size(0, 0))
+ m_sizes[i] = prefSize;
+ }
+ else if (auto subdock = dynamic_cast(m_sides[i])) {
+ subdock->dock(CENTER, widget, prefSize);
+ }
+ else if (auto tabs = dynamic_cast(m_sides[i])) {
+ tabs->addChild(widget);
+ }
+ // If this side already contains a widget, we create a DockTabs in
+ // this side.
+ else {
+ auto oldWidget = m_sides[i];
+ auto newTabs = new DockTabs;
+ replaceChild(oldWidget, newTabs);
+ newTabs->addChild(oldWidget);
+ newTabs->addChild(widget);
+ setSide(i, newTabs);
+ }
+}
+
+void Dock::dockRelativeTo(ui::Widget* relative,
+ int side,
+ ui::Widget* widget,
+ const gfx::Size& prefSize)
+{
+ ASSERT(relative);
+
+ Widget* parent = relative->parent();
+ ASSERT(parent);
+
+ Dock* subdock = new Dock;
+ parent->replaceChild(relative, subdock);
+ subdock->dock(CENTER, relative);
+ subdock->dock(side, widget, prefSize);
+
+ // Fix the m_sides item if the parent is a Dock
+ if (auto relativeDock = dynamic_cast(parent)) {
+ for (int i = 0; i < kSides; ++i) {
+ if (relativeDock->m_sides[i] == relative) {
+ relativeDock->setSide(i, subdock);
+ break;
+ }
+ }
+ }
+}
+
+void Dock::undock(Widget* widget)
+{
+ Widget* parent = widget->parent();
+ if (!parent)
+ return; // Already undocked
+
+ if (auto parentDock = dynamic_cast(parent)) {
+ parentDock->removeChild(widget);
+
+ for (int i = 0; i < kSides; ++i) {
+ if (parentDock->m_sides[i] == widget) {
+ parentDock->setSide(i, nullptr);
+ break;
+ }
+ }
+
+ if (parentDock != this && parentDock->children().empty()) {
+ undock(parentDock);
+ }
+ }
+ else if (auto parentTabs = dynamic_cast(parent)) {
+ parentTabs->removeChild(widget);
+
+ if (parentTabs->children().empty()) {
+ undock(parentTabs);
+ }
+ }
+ else {
+ parent->removeChild(widget);
+ }
+}
+
+Dock* Dock::subdock(int side)
+{
+ int i = side_index(side);
+ if (auto subdock = dynamic_cast(m_sides[i]))
+ return subdock;
+
+ auto oldWidget = m_sides[i];
+ auto newSubdock = new Dock;
+ setSide(i, newSubdock);
+
+ if (oldWidget) {
+ replaceChild(oldWidget, newSubdock);
+ newSubdock->dock(CENTER, oldWidget);
+ }
+ else
+ addChild(newSubdock);
+
+ return newSubdock;
+}
+
+void Dock::onSizeHint(ui::SizeHintEvent& ev)
+{
+ gfx::Size sz = border().size();
+
+ if (m_sides[kLeftIndex])
+ sz.w += m_sides[kLeftIndex]->sizeHint().w + childSpacing();
+ if (m_sides[kRightIndex])
+ sz.w += m_sides[kRightIndex]->sizeHint().w + childSpacing();
+ if (m_sides[kTopIndex])
+ sz.h += m_sides[kTopIndex]->sizeHint().h + childSpacing();
+ if (m_sides[kBottomIndex])
+ sz.h += m_sides[kBottomIndex]->sizeHint().h + childSpacing();
+ if (m_sides[kCenterIndex]) {
+ sz += m_sides[kCenterIndex]->sizeHint();
+ }
+
+ ev.setSizeHint(sz);
+}
+
+void Dock::onResize(ui::ResizeEvent& ev)
+{
+ auto bounds = ev.bounds();
+ setBoundsQuietly(bounds);
+ bounds = childrenBounds();
+
+ updateDockVisibility();
+
+ forEachSide(bounds,
+ [bounds](ui::Widget* widget,
+ const gfx::Rect& widgetBounds,
+ const gfx::Rect& separator,
+ const int index) { widget->setBounds(widgetBounds); });
+}
+
+void Dock::onPaint(ui::PaintEvent& ev)
+{
+ Graphics* g = ev.graphics();
+ g->fillRect(bgColor(), clientBounds());
+}
+
+void Dock::onInitTheme(ui::InitThemeEvent& ev)
+{
+ Widget::onInitTheme(ev);
+ setBorder(gfx::Border(0));
+ setChildSpacing(4 * ui::guiscale());
+}
+
+bool Dock::onProcessMessage(ui::Message* msg)
+{
+ switch (msg->type()) {
+ case kMouseDownMessage: {
+ const gfx::Point pos = static_cast(msg)->position();
+
+ m_capturedSide = -1;
+ forEachSide(childrenBounds(),
+ [this, pos](ui::Widget* widget,
+ const gfx::Rect& widgetBounds,
+ const gfx::Rect& separator,
+ const int index) {
+ if (separator.contains(pos)) {
+ m_capturedWidget = widget;
+ m_capturedSide = index;
+ m_startSize = m_sizes[index];
+ m_startPos = pos;
+ }
+ });
+
+ if (m_capturedSide >= 0) {
+ captureMouse();
+ return true;
+ }
+ break;
+ }
+
+ case kMouseMoveMessage: {
+ if (hasCapture()) {
+ if (m_capturedSide >= 0) {
+ const gfx::Point pos = static_cast(msg)->position();
+ gfx::Size& sz = m_sizes[m_capturedSide];
+
+ switch (m_capturedSide) {
+ case kTopIndex: sz.h = (m_startSize.h + pos.y - m_startPos.y); break;
+ case kBottomIndex: sz.h = (m_startSize.h - pos.y + m_startPos.y); break;
+ case kLeftIndex: sz.w = (m_startSize.w + pos.x - m_startPos.x); break;
+ case kRightIndex: sz.w = (m_startSize.w - pos.x + m_startPos.x); break;
+ }
+
+ layout();
+ }
+ }
+ break;
+ }
+
+ case kMouseUpMessage: {
+ if (hasCapture()) {
+ releaseMouse();
+ }
+ break;
+ }
+
+ case kSetCursorMessage: {
+ const gfx::Point pos = static_cast(msg)->position();
+ ui::CursorType cursor = ui::kArrowCursor;
+ forEachSide(childrenBounds(),
+ [pos, &cursor](ui::Widget* widget,
+ const gfx::Rect& widgetBounds,
+ const gfx::Rect& separator,
+ const int index) {
+ if (separator.contains(pos)) {
+ if (index == kTopIndex || index == kBottomIndex) {
+ cursor = ui::kSizeNSCursor;
+ }
+ else if (index == kLeftIndex || index == kRightIndex) {
+ cursor = ui::kSizeWECursor;
+ }
+ }
+ });
+ ui::set_mouse_cursor(cursor);
+ return true;
+ }
+ }
+ return Widget::onProcessMessage(msg);
+}
+
+void Dock::setSide(const int i, Widget* newWidget)
+{
+ m_sides[i] = newWidget;
+ m_aligns[i] = calcAlign(i);
+
+ if (newWidget) {
+ m_sizes[i] = newWidget->sizeHint();
+ }
+}
+
+int Dock::calcAlign(const int i)
+{
+ Widget* widget = m_sides[i];
+ int align = 0;
+ if (!widget) {
+ // Do nothing
+ }
+ else if (auto subdock = dynamic_cast(widget)) {
+ align = subdock->calcAlign(i);
+ }
+ else if (auto tabs = dynamic_cast(widget)) {
+ for (auto child : tabs->children()) {
+ if (auto subdock2 = dynamic_cast(widget))
+ align |= subdock2->calcAlign(i);
+ else if (auto dockable = dynamic_cast(child)) {
+ align = dockable->dockableAt();
+ }
+ }
+ }
+ else if (auto dockable2 = dynamic_cast(widget)) {
+ align = dockable2->dockableAt();
+ }
+ return align;
+}
+
+void Dock::updateDockVisibility()
+{
+ bool visible = false;
+ setVisible(true);
+ for (int i = 0; i < kSides; ++i) {
+ Widget* widget = m_sides[i];
+ if (!widget)
+ continue;
+
+ if (auto subdock = dynamic_cast(widget)) {
+ subdock->updateDockVisibility();
+ }
+ else if (auto tabs = dynamic_cast(widget)) {
+ bool visible2 = false;
+ for (auto child : tabs->children()) {
+ if (auto subdock2 = dynamic_cast(widget)) {
+ subdock2->updateDockVisibility();
+ }
+ if (child->isVisible()) {
+ visible2 = true;
+ }
+ }
+ tabs->setVisible(visible2);
+ if (visible2)
+ visible = true;
+ }
+
+ if (widget->isVisible()) {
+ visible = true;
+ }
+ }
+ setVisible(visible);
+}
+
+void Dock::forEachSide(gfx::Rect bounds,
+ std::function f)
+{
+ for (int i = 0; i < kSides; ++i) {
+ auto widget = m_sides[i];
+ if (!widget || !widget->isVisible()) {
+ continue;
+ }
+
+ 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:
+ rc = gfx::Rect(bounds.x, bounds.y, bounds.w, sz.h);
+ bounds.y += rc.h;
+ bounds.h -= rc.h;
+
+ if (spacing > 0) {
+ separator = gfx::Rect(bounds.x, bounds.y, bounds.w, spacing);
+ bounds.y += spacing;
+ bounds.h -= spacing;
+ }
+ break;
+ case kBottomIndex:
+ rc = gfx::Rect(bounds.x, bounds.y2() - sz.h, bounds.w, sz.h);
+ bounds.h -= rc.h;
+
+ if (spacing > 0) {
+ separator = gfx::Rect(bounds.x, bounds.y2() - spacing, bounds.w, spacing);
+ bounds.h -= spacing;
+ }
+ break;
+ case kLeftIndex:
+ rc = gfx::Rect(bounds.x, bounds.y, sz.w, bounds.h);
+ bounds.x += rc.w;
+ bounds.w -= rc.w;
+
+ if (spacing > 0) {
+ separator = gfx::Rect(bounds.x, bounds.y, spacing, bounds.h);
+ bounds.x += spacing;
+ bounds.w -= spacing;
+ }
+ break;
+ case kRightIndex:
+ rc = gfx::Rect(bounds.x2() - sz.w, bounds.y, sz.w, bounds.h);
+ bounds.w -= rc.w;
+
+ if (spacing > 0) {
+ separator = gfx::Rect(bounds.x2() - spacing, bounds.y, spacing, bounds.h);
+ bounds.w -= spacing;
+ }
+ break;
+ case kCenterIndex: rc = bounds; break;
+ }
+
+ f(widget, rc, separator, i);
+ }
+}
+
+} // namespace app
diff --git a/src/app/ui/dock.h b/src/app/ui/dock.h
new file mode 100644
index 000000000..231b45dd3
--- /dev/null
+++ b/src/app/ui/dock.h
@@ -0,0 +1,86 @@
+// Aseprite
+// Copyright (C) 2021 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_UI_DOCK_H_INCLUDED
+#define APP_UI_DOCK_H_INCLUDED
+#pragma once
+
+#include "gfx/rect.h"
+#include "gfx/size.h"
+#include "ui/widget.h"
+
+#include
+#include
+#include
+#include
+
+namespace app {
+
+class DockTabs : public ui::Widget {
+public:
+protected:
+ void onSizeHint(ui::SizeHintEvent& ev) override;
+ void onResize(ui::ResizeEvent& ev) override;
+ void onPaint(ui::PaintEvent& ev) override;
+};
+
+class Dock : public ui::Widget {
+public:
+ static constexpr const int kSides = 5;
+
+ Dock();
+
+ void reset();
+
+ // side = ui::LEFT, or ui::RIGHT, etc.
+ void dock(int side, ui::Widget* widget, const gfx::Size& prefSize = gfx::Size());
+
+ void dockRelativeTo(ui::Widget* relative,
+ int side,
+ ui::Widget* widget,
+ const gfx::Size& prefSize = gfx::Size());
+
+ void undock(ui::Widget* widget);
+
+ Dock* subdock(int side);
+
+ Dock* top() { return subdock(ui::TOP); }
+ Dock* bottom() { return subdock(ui::BOTTOM); }
+ Dock* left() { return subdock(ui::LEFT); }
+ Dock* right() { return subdock(ui::RIGHT); }
+ Dock* center() { return subdock(ui::CENTER); }
+
+protected:
+ void onSizeHint(ui::SizeHintEvent& ev) override;
+ void onResize(ui::ResizeEvent& ev) override;
+ void onPaint(ui::PaintEvent& ev) override;
+ void onInitTheme(ui::InitThemeEvent& ev) override;
+ bool onProcessMessage(ui::Message* msg) override;
+
+private:
+ void setSide(const int i, ui::Widget* newWidget);
+ int calcAlign(const int i);
+ void updateDockVisibility();
+ void forEachSide(gfx::Rect bounds,
+ std::function f);
+
+ std::array m_sides;
+ std::array m_aligns;
+ std::array m_sizes;
+
+ // Used to drag-and-drop sides.
+ ui::Widget* m_capturedWidget = nullptr;
+ int m_capturedSide;
+ gfx::Size m_startSize;
+ gfx::Point m_startPos;
+};
+
+} // namespace app
+
+#endif
diff --git a/src/app/ui/dockable.h b/src/app/ui/dockable.h
new file mode 100644
index 000000000..5b1eaf186
--- /dev/null
+++ b/src/app/ui/dockable.h
@@ -0,0 +1,33 @@
+// Aseprite
+// Copyright (C) 2021 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_UI_DOCKABLE_H_INCLUDED
+#define APP_UI_DOCKABLE_H_INCLUDED
+#pragma once
+
+#include "ui/base.h"
+
+namespace app {
+
+class Dockable {
+public:
+ virtual ~Dockable() {}
+
+ // LEFT = can be docked at the left side
+ // TOP = can be docked at the top
+ // RIGHT = can be docked at the right side
+ // BOTTOM = can be docked at the bottom
+ // CENTER = can be docked at the center
+ // EXPANSIVE = can be resized (e.g. add a splitter when docked at sides)
+ virtual int dockableAt() const
+ {
+ return ui::LEFT | ui::TOP | ui::RIGHT | ui::BOTTOM | ui::CENTER | ui::EXPANSIVE;
+ }
+};
+
+} // namespace app
+
+#endif
diff --git a/src/app/ui/layout_selector.cpp b/src/app/ui/layout_selector.cpp
new file mode 100644
index 000000000..16e8ed0e7
--- /dev/null
+++ b/src/app/ui/layout_selector.cpp
@@ -0,0 +1,134 @@
+// Aseprite
+// Copyright (C) 2021 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_selector.h"
+
+#include "app/app.h"
+#include "app/ui/main_window.h"
+#include "app/ui/separator_in_view.h"
+#include "app/ui/skin/skin_theme.h"
+#include "ui/listitem.h"
+#include "ui/window.h"
+
+#define ANI_TICKS 5
+
+namespace app {
+
+using namespace app::skin;
+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;
+};
+
+}; // namespace
+
+void LayoutSelector::LayoutComboBox::onChange()
+{
+ if (auto item = dynamic_cast(getSelectedItem())) {
+ item->select();
+ }
+}
+
+LayoutSelector::LayoutSelector() : m_button("", "\xc3\xb7")
+{
+ m_button.Click.connect([this]() { switchSelector(); });
+
+ m_comboBox.setVisible(false);
+
+ addChild(&m_comboBox);
+ addChild(&m_button);
+}
+
+LayoutSelector::~LayoutSelector()
+{
+ stopAnimation();
+}
+
+void LayoutSelector::onAnimationFrame()
+{
+ switch (animation()) {
+ case ANI_NONE: break;
+ case ANI_EXPANDING:
+ case ANI_COLLAPSING: {
+ const double t = animationTime();
+ m_comboBox.setSizeHint(gfx::Size((1.0 - t) * m_startSize.w + t * m_endSize.w,
+ (1.0 - t) * m_startSize.h + t * m_endSize.h));
+ break;
+ }
+ }
+
+ if (auto win = window())
+ win->layout();
+}
+
+void LayoutSelector::onAnimationStop(int animation)
+{
+ switch (animation) {
+ case ANI_EXPANDING: m_comboBox.setSizeHint(m_endSize); break;
+ case ANI_COLLAPSING:
+ m_comboBox.setVisible(false);
+ m_comboBox.setSizeHint(m_endSize);
+ break;
+ }
+
+ if (auto win = window())
+ win->layout();
+}
+
+void LayoutSelector::switchSelector()
+{
+ bool expand;
+ if (!m_comboBox.isVisible()) {
+ expand = true;
+
+ // Create the combobox for first time
+ if (m_comboBox.getItemCount() == 0) {
+ m_comboBox.addItem(new LayoutItem(LayoutId::DEFAULT, "Default"));
+ m_comboBox.addItem(new LayoutItem(LayoutId::DEFAULT_MIRROR, "Default / Mirror"));
+ }
+
+ m_comboBox.setVisible(true);
+ m_comboBox.resetSizeHint();
+ m_startSize = gfx::Size(0, 0);
+ m_endSize = m_comboBox.sizeHint();
+ }
+ else {
+ expand = false;
+ m_startSize = m_comboBox.bounds().size();
+ m_endSize = gfx::Size(0, 0);
+ }
+
+ m_comboBox.setSizeHint(m_startSize);
+ startAnimation((expand ? ANI_EXPANDING : ANI_COLLAPSING), ANI_TICKS);
+}
+
+} // namespace app
diff --git a/src/app/ui/layout_selector.h b/src/app/ui/layout_selector.h
new file mode 100644
index 000000000..cedbf8821
--- /dev/null
+++ b/src/app/ui/layout_selector.h
@@ -0,0 +1,54 @@
+// Aseprite
+// Copyright (C) 2021-2022 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_UI_LAYOUT_SELECTOR_H_INCLUDED
+#define APP_UI_LAYOUT_SELECTOR_H_INCLUDED
+#pragma once
+
+#include "app/ui/dockable.h"
+#include "ui/animated_widget.h"
+#include "ui/box.h"
+#include "ui/combobox.h"
+#include "ui/link_label.h"
+
+#include
+
+namespace app {
+
+class LayoutSelector : public ui::HBox,
+ public ui::AnimatedWidget,
+ public Dockable {
+ enum Ani : int {
+ ANI_NONE,
+ ANI_EXPANDING,
+ ANI_COLLAPSING,
+ };
+
+ class LayoutComboBox : public ui::ComboBox {
+ void onChange() override;
+ };
+
+public:
+ LayoutSelector();
+ ~LayoutSelector();
+
+ // Dockable impl
+ int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
+
+private:
+ void onAnimationFrame() override;
+ void onAnimationStop(int animation) override;
+ void switchSelector();
+
+ LayoutComboBox m_comboBox;
+ ui::LinkLabel m_button;
+ gfx::Size m_startSize;
+ gfx::Size m_endSize;
+};
+
+} // namespace app
+
+#endif
diff --git a/src/app/ui/main_menu_bar.h b/src/app/ui/main_menu_bar.h
index 15bed1cd2..bcb223489 100644
--- a/src/app/ui/main_menu_bar.h
+++ b/src/app/ui/main_menu_bar.h
@@ -9,18 +9,23 @@
#define APP_UI_MAIN_MENU_BAR_H_INCLUDED
#pragma once
+#include "app/ui/dockable.h"
#include "obs/connection.h"
#include "ui/menu.h"
namespace app {
-class MainMenuBar : public ui::MenuBar {
+class MainMenuBar : public ui::MenuBar,
+ public Dockable {
public:
MainMenuBar();
void queueReload();
void reload();
+ // Dockable impl
+ int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
+
private:
obs::scoped_connection m_extKeys;
obs::scoped_connection m_extScripts;
diff --git a/src/app/ui/main_window.cpp b/src/app/ui/main_window.cpp
index 87c81c8d5..c6caa0a18 100644
--- a/src/app/ui/main_window.cpp
+++ b/src/app/ui/main_window.cpp
@@ -24,9 +24,11 @@
#include "app/ui/color_bar.h"
#include "app/ui/context_bar.h"
#include "app/ui/doc_view.h"
+#include "app/ui/dock.h"
#include "app/ui/editor/editor.h"
#include "app/ui/editor/editor_view.h"
#include "app/ui/home_view.h"
+#include "app/ui/layout_selector.h"
#include "app/ui/main_menu_bar.h"
#include "app/ui/notifications.h"
#include "app/ui/preview_editor.h"
@@ -83,7 +85,10 @@ public:
};
MainWindow::MainWindow()
- : m_mode(NormalMode)
+ : ui::Window(ui::Window::DesktopWindow)
+ , m_dock(new Dock)
+ , m_customizableDock(new Dock)
+ , m_mode(NormalMode)
, m_homeView(nullptr)
, m_scalePanic(nullptr)
, m_browserView(nullptr)
@@ -105,8 +110,9 @@ MainWindow::MainWindow()
// Refer to https://github.com/aseprite/aseprite/issues/3914
void MainWindow::initialize()
{
- m_tooltipManager = new TooltipManager();
- m_menuBar = new MainMenuBar();
+ m_tooltipManager = new TooltipManager;
+ m_menuBar = new MainMenuBar;
+ m_layoutSelector = new LayoutSelector;
// Register commands to load menus+shortcuts for these commands
Editor::registerCommands();
@@ -123,7 +129,7 @@ void MainWindow::initialize()
m_tabsBar = new WorkspaceTabs(this);
m_workspace = new Workspace();
m_previewEditor = new PreviewEditorWindow();
- m_colorBar = new ColorBar(colorBarPlaceholder()->align(), m_tooltipManager);
+ m_colorBar = new ColorBar(m_tooltipManager);
m_contextBar = new ContextBar(m_tooltipManager, m_colorBar);
// The timeline (AniControls) tooltips will use the keyboard
@@ -148,19 +154,24 @@ void MainWindow::initialize()
// Add the widgets in the boxes
addChild(m_tooltipManager);
- menuBarPlaceholder()->addChild(m_menuBar);
- menuBarPlaceholder()->addChild(m_notifications);
- contextBarPlaceholder()->addChild(m_contextBar);
- colorBarPlaceholder()->addChild(m_colorBar);
- toolBarPlaceholder()->addChild(m_toolBar);
- statusBarPlaceholder()->addChild(m_statusBar);
- tabsPlaceholder()->addChild(m_tabsBar);
- workspacePlaceholder()->addChild(m_workspace);
- timelinePlaceholder()->addChild(m_timeline);
+ addChild(m_dock);
- // Default splitter positions
- colorBarSplitter()->setPosition(m_colorBar->sizeHint().w);
- timelineSplitter()->setPosition(75);
+ auto customizableDockPlaceholder = new Widget;
+ customizableDockPlaceholder->addChild(m_customizableDock);
+ customizableDockPlaceholder->InitTheme.connect([this] {
+ auto theme = static_cast(this->theme());
+ m_customizableDock->setBgColor(theme->colors.workspace());
+ });
+ customizableDockPlaceholder->initTheme();
+
+ m_dock->top()->right()->dock(ui::RIGHT, m_notifications);
+ m_dock->top()->right()->dock(ui::CENTER, m_layoutSelector);
+ m_dock->top()->dock(ui::BOTTOM, m_tabsBar);
+ m_dock->top()->dock(ui::CENTER, m_menuBar);
+ m_dock->dock(ui::CENTER, customizableDockPlaceholder);
+ m_dock->dock(ui::BOTTOM, m_statusBar);
+
+ setDefaultLayout();
// Reconfigure workspace when the timeline position is changed.
auto& pref = Preferences::instance();
@@ -179,6 +190,9 @@ void MainWindow::initialize()
MainWindow::~MainWindow()
{
+ m_dock->reset();
+ m_customizableDock->reset();
+
delete m_scalePanic;
#ifdef ENABLE_SCRIPTING
@@ -254,7 +268,7 @@ void MainWindow::showNotification(INotificationDelegate* del)
{
m_notifications->addLink(del);
m_notifications->setVisible(true);
- m_notifications->parent()->layout();
+ layout();
}
void MainWindow::showHomeOnOpen()
@@ -360,6 +374,34 @@ void MainWindow::popTimeline()
setTimelineVisibility(true);
}
+void MainWindow::setDefaultLayout()
+{
+ m_customizableDock->reset();
+ m_customizableDock->dock(ui::LEFT, m_colorBar);
+ m_customizableDock->center()->dock(ui::TOP, m_contextBar);
+ m_customizableDock->center()->dock(ui::RIGHT, m_toolBar);
+ m_customizableDock->center()->center()->dock(ui::BOTTOM,
+ m_timeline,
+ gfx::Size(64 * guiscale(), 64 * guiscale()));
+ m_customizableDock->center()->center()->dock(ui::CENTER, m_workspace);
+
+ layout();
+}
+
+void MainWindow::setDefaultMirrorLayout()
+{
+ m_customizableDock->reset();
+ m_customizableDock->dock(ui::RIGHT, m_colorBar);
+ m_customizableDock->center()->dock(ui::TOP, m_contextBar);
+ m_customizableDock->center()->dock(ui::LEFT, m_toolBar);
+ m_customizableDock->center()->center()->dock(ui::BOTTOM,
+ m_timeline,
+ gfx::Size(64 * guiscale(), 64 * guiscale()));
+ m_customizableDock->center()->center()->dock(ui::CENTER, m_workspace);
+
+ layout();
+}
+
void MainWindow::dataRecoverySessionsAreReady()
{
getHomeView()->dataRecoverySessionsAreReady();
@@ -375,24 +417,15 @@ bool MainWindow::onProcessMessage(ui::Message* msg)
void MainWindow::onInitTheme(ui::InitThemeEvent& ev)
{
- app::gen::MainWindow::onInitTheme(ev);
+ ui::Window::onInitTheme(ev);
+ noBorderNoChildSpacing();
if (m_previewEditor)
m_previewEditor->initTheme();
}
-void MainWindow::onSaveLayout(SaveLayoutEvent& ev)
-{
- // Invert the timeline splitter position before we save the setting.
- if (Preferences::instance().general.timelinePosition() == gen::TimelinePosition::LEFT) {
- timelineSplitter()->setPosition(100 - timelineSplitter()->getPosition());
- }
-
- Window::onSaveLayout(ev);
-}
-
void MainWindow::onResize(ui::ResizeEvent& ev)
{
- app::gen::MainWindow::onResize(ev);
+ ui::Window::onResize(ev);
os::Window* nativeWindow = (display() ? display()->nativeWindow() : nullptr);
if (nativeWindow && nativeWindow->screen()) {
@@ -593,16 +626,21 @@ void MainWindow::configureWorkspaceLayout()
bool isDoc = (getDocView() != nullptr);
if (os::System::instance()->menus() == nullptr || pref.general.showMenuBar()) {
- m_menuBar->resetMaxSize();
+ if (!m_menuBar->parent())
+ m_dock->top()->dock(CENTER, m_menuBar);
}
else {
- m_menuBar->setMaxSize(gfx::Size(0, 0));
+ if (m_menuBar->parent())
+ m_dock->undock(m_menuBar);
}
m_menuBar->setVisible(normal);
m_notifications->setVisible(normal && m_notifications->hasNotifications());
m_tabsBar->setVisible(normal);
- colorBarPlaceholder()->setVisible(normal && isDoc);
+
+ // TODO set visibility of color bar widgets
+ m_colorBar->setVisible(normal && isDoc);
+
m_toolBar->setVisible(normal && isDoc);
m_statusBar->setVisible(normal);
m_contextBar->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode));
@@ -610,41 +648,22 @@ void MainWindow::configureWorkspaceLayout()
// Configure timeline
{
auto timelinePosition = pref.general.timelinePosition();
- bool invertWidgets = false;
- int align = VERTICAL;
+ int side = ui::BOTTOM;
+
+ m_customizableDock->undock(m_timeline);
+
switch (timelinePosition) {
- case gen::TimelinePosition::LEFT:
- align = HORIZONTAL;
- invertWidgets = true;
- break;
- case gen::TimelinePosition::RIGHT: align = HORIZONTAL; break;
- case gen::TimelinePosition::BOTTOM: break;
+ case gen::TimelinePosition::LEFT: side = ui::LEFT; break;
+ case gen::TimelinePosition::RIGHT: side = ui::RIGHT; break;
+ case gen::TimelinePosition::BOTTOM: side = ui::BOTTOM; break;
}
- timelineSplitter()->setAlign(align);
- timelinePlaceholder()->setVisible(
- isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode) &&
- pref.general.visibleTimeline());
+ m_customizableDock->center()->center()->dock(side,
+ m_timeline,
+ gfx::Size(64 * guiscale(), 64 * guiscale()));
- bool invertSplitterPos = false;
- if (invertWidgets) {
- if (timelineSplitter()->firstChild() == workspacePlaceholder() &&
- timelineSplitter()->lastChild() == timelinePlaceholder()) {
- timelineSplitter()->removeChild(workspacePlaceholder());
- timelineSplitter()->addChild(workspacePlaceholder());
- invertSplitterPos = true;
- }
- }
- else {
- if (timelineSplitter()->firstChild() == timelinePlaceholder() &&
- timelineSplitter()->lastChild() == workspacePlaceholder()) {
- timelineSplitter()->removeChild(timelinePlaceholder());
- timelineSplitter()->addChild(timelinePlaceholder());
- invertSplitterPos = true;
- }
- }
- if (invertSplitterPos)
- timelineSplitter()->setPosition(100 - timelineSplitter()->getPosition());
+ m_timeline->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode) &&
+ pref.general.visibleTimeline());
}
if (m_contextBar->isVisible()) {
@@ -652,7 +671,6 @@ void MainWindow::configureWorkspaceLayout()
}
layout();
- invalidate();
}
} // namespace app
diff --git a/src/app/ui/main_window.h b/src/app/ui/main_window.h
index 118c52db4..39be6ab90 100644
--- a/src/app/ui/main_window.h
+++ b/src/app/ui/main_window.h
@@ -12,7 +12,7 @@
#include "app/ui/tabs.h"
#include "ui/window.h"
-#include "main_window.xml.h"
+#include
namespace ui {
class Splitter;
@@ -30,13 +30,16 @@ class ColorBar;
class ContextBar;
class DevConsoleView;
class DocView;
+class Dock;
class HomeView;
class INotificationDelegate;
class MainMenuBar;
+class LayoutSelector;
class Notifications;
class PreviewEditorWindow;
class StatusBar;
class Timeline;
+class ToolBar;
class Workspace;
class WorkspaceTabs;
@@ -44,7 +47,7 @@ namespace crash {
class DataRecovery;
}
-class MainWindow : public app::gen::MainWindow,
+class MainWindow : public ui::Window,
public TabsDelegate {
public:
enum Mode { NormalMode, ContextBarAndTimelineMode, EditorOnlyMode };
@@ -83,6 +86,9 @@ public:
void setTimelineVisibility(bool visible);
void popTimeline();
+ void setDefaultLayout();
+ void setDefaultMirrorLayout();
+
// When crash::DataRecovery finish to search for sessions, this
// function is called.
void dataRecoverySessionsAreReady();
@@ -109,7 +115,6 @@ public:
protected:
bool onProcessMessage(ui::Message* msg) override;
void onInitTheme(ui::InitThemeEvent& ev) override;
- void onSaveLayout(ui::SaveLayoutEvent& ev) override;
void onResize(ui::ResizeEvent& ev) override;
void onBeforeViewChange();
void onActiveViewChange();
@@ -123,11 +128,14 @@ private:
void configureWorkspaceLayout();
ui::TooltipManager* m_tooltipManager;
+ Dock* m_dock;
+ Dock* m_customizableDock;
MainMenuBar* m_menuBar;
+ LayoutSelector* m_layoutSelector;
StatusBar* m_statusBar;
ColorBar* m_colorBar;
ContextBar* m_contextBar;
- ui::Widget* m_toolBar;
+ ToolBar* m_toolBar;
WorkspaceTabs* m_tabsBar;
Mode m_mode;
Timeline* m_timeline;
diff --git a/src/app/ui/notifications.h b/src/app/ui/notifications.h
index ecd79890e..d39e27bda 100644
--- a/src/app/ui/notifications.h
+++ b/src/app/ui/notifications.h
@@ -9,6 +9,7 @@
#define APP_UI_NOTIFICATIONS_H_INCLUDED
#pragma once
+#include "app/ui/dockable.h"
#include "ui/button.h"
#include "ui/menu.h"
@@ -19,13 +20,17 @@ class Style;
namespace app {
class INotificationDelegate;
-class Notifications : public ui::Button {
+class Notifications : public ui::Button,
+ public Dockable {
public:
Notifications();
void addLink(INotificationDelegate* del);
bool hasNotifications() const { return m_popup.hasChildren(); }
+ // Dockable impl
+ int dockableAt() const override { return ui::TOP | ui::BOTTOM | ui::LEFT | ui::RIGHT; }
+
protected:
void onSizeHint(ui::SizeHintEvent& ev) override;
void onPaint(ui::PaintEvent& ev) override;
diff --git a/src/app/ui/status_bar.h b/src/app/ui/status_bar.h
index 90075fe72..7a4308642 100644
--- a/src/app/ui/status_bar.h
+++ b/src/app/ui/status_bar.h
@@ -13,6 +13,7 @@
#include "app/context_observer.h"
#include "app/tools/active_tool_observer.h"
#include "app/ui/doc_observer_widget.h"
+#include "app/ui/dockable.h"
#include "base/time.h"
#include "doc/tile.h"
#include "ui/base.h"
@@ -44,7 +45,8 @@ class Tool;
}
class StatusBar : public DocObserverWidget,
- public tools::ActiveToolObserver {
+ public tools::ActiveToolObserver,
+ public Dockable {
static StatusBar* m_instance;
public:
@@ -72,6 +74,9 @@ public:
void showBackupIcon(BackupIcon icon);
+ // Dockable impl
+ int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
+
protected:
void onInitTheme(ui::InitThemeEvent& ev) override;
void onResize(ui::ResizeEvent& ev) override;
diff --git a/src/app/ui/tabs.h b/src/app/ui/tabs.h
index a06fbfad2..80a109b15 100644
--- a/src/app/ui/tabs.h
+++ b/src/app/ui/tabs.h
@@ -9,6 +9,7 @@
#define APP_UI_TABS_H_INCLUDED
#pragma once
+#include "app/ui/dockable.h"
#include "base/ref.h"
#include "text/fwd.h"
#include "ui/animated_widget.h"
@@ -118,7 +119,8 @@ public:
// Tabs control. Used to show opened documents.
class Tabs : public ui::Widget,
- public ui::AnimatedWidget {
+ public ui::AnimatedWidget,
+ public Dockable {
struct Tab {
TabView* view;
std::string text;
@@ -181,6 +183,9 @@ public:
void removeDropViewPreview();
int getDropTabIndex() const { return m_dropNewIndex; }
+ // Dockable impl
+ int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
+
protected:
bool onProcessMessage(ui::Message* msg) override;
void onInitTheme(ui::InitThemeEvent& ev) override;
diff --git a/src/app/ui/timeline/timeline.h b/src/app/ui/timeline/timeline.h
index 3793ed0dd..4e6efa13d 100644
--- a/src/app/ui/timeline/timeline.h
+++ b/src/app/ui/timeline/timeline.h
@@ -13,6 +13,7 @@
#include "app/docs_observer.h"
#include "app/loop_tag.h"
#include "app/pref/preferences.h"
+#include "app/ui/dockable.h"
#include "app/ui/editor/editor_observer.h"
#include "app/ui/input_chain_element.h"
#include "app/ui/timeline/ani_controls.h"
@@ -73,7 +74,8 @@ class Timeline : public ui::Widget,
public DocObserver,
public EditorObserver,
public InputChainElement,
- public TagProvider {
+ public TagProvider,
+ public Dockable {
public:
using Range = view::Range;
using RealRange = view::RealRange;
@@ -155,6 +157,12 @@ public:
void clearAndInvalidateRange();
+ // Dockable impl
+ int dockableAt() const override
+ {
+ return ui::TOP | ui::BOTTOM | ui::LEFT | ui::RIGHT | ui::EXPANSIVE;
+ }
+
protected:
bool onProcessMessage(ui::Message* msg) override;
void onInitTheme(ui::InitThemeEvent& ev) override;
diff --git a/src/app/ui/toolbar.h b/src/app/ui/toolbar.h
index f534fe7af..d52a2e841 100644
--- a/src/app/ui/toolbar.h
+++ b/src/app/ui/toolbar.h
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2025 Igara Studio S.A.
+// Copyright (C) 2021-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@@ -10,6 +10,7 @@
#pragma once
#include "app/tools/active_tool_observer.h"
+#include "app/ui/dockable.h"
#include "app/ui/skin/skin_part.h"
#include "gfx/point.h"
#include "obs/connection.h"
@@ -32,6 +33,7 @@ class ToolGroup;
// Class to show selected tools for each tool (vertically)
class ToolBar : public ui::Widget,
+ public Dockable,
public tools::ActiveToolObserver {
static ToolBar* m_instance;
@@ -52,6 +54,14 @@ public:
void openTipWindow(tools::ToolGroup* toolGroup, tools::Tool* tool);
void closeTipWindow();
+ // Dockable impl
+ int dockableAt() const override
+ {
+ // TODO add future support to dock the tool bar at the
+ // top/bottom sides
+ return ui::LEFT | ui::RIGHT;
+ }
+
protected:
bool onProcessMessage(ui::Message* msg) override;
void onSizeHint(ui::SizeHintEvent& ev) override;