Add Dock widget, initial & basic version of dockable elements (#518)

Some missing features so far:

1) Restore old layout configuration (color bar split pos, timeline
   pos, etc.) and migrate to new Dock layout
2) Load/saving Dock layout
3) Create & customize current layoout (drag-and-drop widgets, etc.)
This commit is contained in:
David Capello 2021-10-13 10:50:42 -03:00 committed by David Capello
parent cef92c1a38
commit fa21d87ba8
18 changed files with 942 additions and 109 deletions

View File

@ -1,30 +0,0 @@
<!-- Aseprite -->
<!-- Copyright (C) 2001-2017 by David Capello -->
<gui>
<window id="main_window" noborders="true" desktop="true">
<vbox noborders="true" expansive="true">
<hbox noborders="true" id="menu_bar_placeholder" />
<hbox noborders="true" id="tabs_placeholder" />
<splitter id="color_bar_splitter"
horizontal="true" expansive="true"
by="pixel"
style="workspace_splitter">
<vbox noborders="true" id="color_bar_placeholder" />
<vbox noborders="true" expansive="true">
<vbox noborders="true" id="context_bar_placeholder" />
<hbox noborders="true" expansive="true">
<splitter id="timeline_splitter"
vertical="true" expansive="true"
by="percetage" position="100"
style="workspace_splitter">
<hbox noborders="true" id="workspace_placeholder" expansive="true" />
<vbox noborders="true" id="timeline_placeholder" expansive="true" />
</splitter>
<vbox noborders="true" id="tool_bar_placeholder" />
</hbox>
</vbox>
</splitter>
<hbox noborders="true" id="status_bar_placeholder" />
</vbox>
</window>
</gui>

View File

@ -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

View File

@ -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)

View File

@ -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<void()> ChangeSelection;
protected:

View File

@ -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<ui::HBox>,
public obs::observable<ContextBarObserver>,
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<void()> BrushChange;
obs::signal<void(const FontInfo&, FontEntry::From)> FontChange;

476
src/app/ui/dock.cpp Normal file
View File

@ -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<Dock*>(child)) {
subdock->reset();
}
else if (auto tabs = dynamic_cast<DockTabs*>(child)) {
for (auto child2 : tabs->children()) {
if (auto subdock2 = dynamic_cast<Dock*>(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<Dock*>(m_sides[i])) {
subdock->dock(CENTER, widget, prefSize);
}
else if (auto tabs = dynamic_cast<DockTabs*>(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<Dock*>(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<Dock*>(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<DockTabs*>(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<Dock*>(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<MouseMessage*>(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<MouseMessage*>(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<MouseMessage*>(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<Dock*>(widget)) {
align = subdock->calcAlign(i);
}
else if (auto tabs = dynamic_cast<DockTabs*>(widget)) {
for (auto child : tabs->children()) {
if (auto subdock2 = dynamic_cast<Dock*>(widget))
align |= subdock2->calcAlign(i);
else if (auto dockable = dynamic_cast<Dockable*>(child)) {
align = dockable->dockableAt();
}
}
}
else if (auto dockable2 = dynamic_cast<Dockable*>(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<Dock*>(widget)) {
subdock->updateDockVisibility();
}
else if (auto tabs = dynamic_cast<DockTabs*>(widget)) {
bool visible2 = false;
for (auto child : tabs->children()) {
if (auto subdock2 = dynamic_cast<Dock*>(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<void(ui::Widget* widget,
const gfx::Rect& widgetBounds,
const gfx::Rect& separator,
const int index)> 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

86
src/app/ui/dock.h Normal file
View File

@ -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 <array>
#include <functional>
#include <string>
#include <vector>
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<void(ui::Widget* widget,
const gfx::Rect& widgetBounds,
const gfx::Rect& separator,
const int index)> f);
std::array<Widget*, kSides> m_sides;
std::array<int, kSides> m_aligns;
std::array<gfx::Size, kSides> 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

33
src/app/ui/dockable.h Normal file
View File

@ -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

View File

@ -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<LayoutItem*>(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

View File

@ -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 <memory>
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

View File

@ -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;

View File

@ -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<skin::SkinTheme*>(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

View File

@ -12,7 +12,7 @@
#include "app/ui/tabs.h"
#include "ui/window.h"
#include "main_window.xml.h"
#include <memory>
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;

View File

@ -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;

View File

@ -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<ui::HBox>,
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;

View File

@ -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;

View File

@ -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;

View File

@ -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;