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;