Merge branch 'dock' into beta (#518, #3485)

This commit is contained in:
David Capello 2025-08-05 16:22:43 -03:00
commit 7434db6882
44 changed files with 2774 additions and 262 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -216,7 +216,7 @@
<part id="tab_normal" x="2" y="112" w1="4" w2="5" w3="5" h1="4" h2="6" h3="2" />
<part id="tab_active" x="16" y="112" w1="4" w2="7" w3="5" h1="4" h2="6" h3="2" />
<part id="tab_bottom_active" x="16" y="124" w1="4" w2="7" w3="5" h1="2" h2="1" h3="2" />
<part id="tab_bottom_normal" x="2" y="124" w="12" h="5" />
<part id="tab_bottom_normal" x="2" y="124" w1="4" w2="5" w3="3" h1="2" h2="1" h3="2" />
<part id="tab_filler" x="0" y="112" w="2" h="12" />
<part id="tab_modified_icon_normal" x="32" y="112" w="5" h="5" />
<part id="tab_modified_icon_active" x="32" y="117" w="5" h="5" />
@ -374,6 +374,7 @@
<part id="icon_close" x="152" y="256" w="7" h="7" />
<part id="icon_search" x="160" y="256" w="8" h="8" />
<part id="icon_user_data" x="168" y="256" w="8" h="8" />
<part id="icon_layout" x="176" y="256" w="5" h="6" />
<part id="icon_pos" x="144" y="264" w="8" h="8" />
<part id="icon_size" x="152" y="264" w="8" h="8" />
<part id="icon_selsize" x="160" y="264" w="8" h="8" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -212,7 +212,7 @@
<part id="tab_normal" x="2" y="112" w1="4" w2="5" w3="5" h1="4" h2="6" h3="2" />
<part id="tab_active" x="16" y="112" w1="4" w2="7" w3="5" h1="4" h2="6" h3="2" />
<part id="tab_bottom_active" x="16" y="124" w1="4" w2="7" w3="5" h1="2" h2="1" h3="2" />
<part id="tab_bottom_normal" x="2" y="124" w="12" h="5" />
<part id="tab_bottom_normal" x="2" y="124" w1="4" w2="5" w3="3" h1="2" h2="1" h3="2" />
<part id="tab_filler" x="0" y="112" w="2" h="12" />
<part id="tab_modified_icon_normal" x="32" y="112" w="5" h="5" />
<part id="tab_modified_icon_active" x="32" y="117" w="5" h="5" />
@ -370,6 +370,7 @@
<part id="icon_close" x="152" y="256" w="7" h="7" />
<part id="icon_search" x="160" y="256" w="8" h="8" />
<part id="icon_user_data" x="168" y="256" w="8" h="8" />
<part id="icon_layout" x="176" y="256" w="5" h="6" />
<part id="icon_pos" x="144" y="264" w="8" h="8" />
<part id="icon_size" x="152" y="264" w="8" h="8" />
<part id="icon_selsize" x="160" y="264" w="8" h="8" />

View File

@ -175,6 +175,7 @@
<param name="popup" value="background" />
</key>
<key command="ShowExtras" shortcut="Ctrl+H" />
<key command="ToggleWorkspaceLayout" shortcut="Shift+W" />
<!-- Tabs -->
<key command="GotoNextTab" shortcut="Ctrl+Tab" />
<key command="GotoPreviousTab" shortcut="Ctrl+Shift+Tab" />
@ -1002,6 +1003,7 @@
</menu>
<menu text="@.view" id="view_menu">
<item command="DuplicateView" text="@.view_duplicate_view" group="view_new" />
<item command="ToggleWorkspaceLayout" text="@.view_workspace_layout" />
<separator />
<item command="ShowExtras" text="@.view_show_extras" />
<menu text="@.view_show" group="view_extras">

View File

@ -167,6 +167,7 @@
<option id="keep_closed_sprite_on_memory_for" type="double" default="15.0" />
<option id="show_full_path" type="bool" default="true" />
<option id="edit_full_path" type="bool" default="false" />
<option id="workspace_layout" type="std::string" />
<option id="timeline_position" type="TimelinePosition" default="TimelinePosition::BOTTOM" />
<option id="timeline_layer_panel_width" type="int" default="100" />
<option id="show_menu_bar" type="bool" default="true" />

View File

@ -488,6 +488,7 @@ TilesetDuplicate = Duplicate Tileset
Undo = Undo
UndoHistory = Undo History
UnlinkCel = Unlink Cel
ToggleWorkspaceLayout = Toggle Workspace Layout
Zoom = Zoom
Zoom_In = Zoom In
Zoom_Out = Zoom Out
@ -1164,6 +1165,7 @@ select_load_from_file = &Load from MSK file
select_save_to_file = &Save to MSK file
view = &View
view_duplicate_view = Duplicate &View
view_workspace_layout = Workspace &Layout
view_show_extras = &Extras
view_show = &Show
view_show_layer_edges = &Layer Edges
@ -1205,6 +1207,14 @@ help_twitter = Twitter
help_enter_license = Enter &License
help_about = &About
[main_window]
layout = Workspace Layout
default_layout = Default
mirrored_default_layout = Mirrored Default
timeline = Timeline
user_layouts = User Layouts
new_layout = New Layout...
[mask_by_color]
title = Select Color
label_color = Color:
@ -1233,6 +1243,20 @@ name = Name:
tileset = Tileset:
default_new_layer_name = New Layer
[new_layout]
title = New Workspace Layout
name = Name:
deleting_layout = Deleting Layout
deleting_layout_confirmation = Are you sure you want to delete the '{}' layout?
restoring_layout = Restoring Layout
restoring_layout_confirmation = Are you sure you want to restore the '{}' layout?
[dock]
left = Dock Left
right = Dock Right
top = Dock Top
bottom = Dock Bottom
[news_listbox]
more = More...
problem_loading = Problems loading news. Please retry.
@ -1849,9 +1873,6 @@ more_options = More Options
[timeline_conf]
position = Position:
left = &Left
right = &Right
bottom = &Bottom
frame_header = Frame Header:
first_frame = First Frame:
thumbnails = Thumbnails

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

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

View File

@ -6,13 +6,7 @@
<hbox>
<vbox>
<separator cell_hspan="2" text="@.position" left="true" horizontal="true" />
<hbox>
<buttonset columns="2" id="position">
<item text="@.left" />
<item text="@.right" />
<item text="@.bottom" hspan="2" />
</buttonset>
</hbox>
<button id="layout" icon="icon_layout" />
</vbox>
<vbox>
<separator text="@.frame_header" left="true" horizontal="true" />

View File

@ -520,6 +520,7 @@ target_sources(app-lib PRIVATE
commands/tileset_mode.cpp
commands/toggle_other_layers_opacity.cpp
commands/toggle_play_option.cpp
commands/toggle_workspace_layout.cpp
console.cpp
context.cpp
context_flags.cpp
@ -609,6 +610,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 +655,9 @@ target_sources(app-lib PRIVATE
ui/input_chain.cpp
ui/keyboard_shortcuts.cpp
ui/layer_frame_comboboxes.cpp
ui/layout.cpp
ui/layouts.cpp
ui/layout_selector.cpp
ui/main_menu_bar.cpp
ui/main_window.cpp
ui/mini_help_button.cpp

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -174,6 +174,7 @@ FOR_EACH_COMMAND(TogglePreview)
FOR_EACH_COMMAND(ToggleRewindOnStop)
FOR_EACH_COMMAND(ToggleTilesMode)
FOR_EACH_COMMAND(ToggleTimelineThumbnails)
FOR_EACH_COMMAND(ToggleWorkspaceLayout)
FOR_EACH_COMMAND(Undo)
FOR_EACH_COMMAND(UndoHistory)
FOR_EACH_COMMAND(UnlinkCel)

View File

@ -0,0 +1,47 @@
// Aseprite
// Copyright (c) 2024 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/app.h"
#include "app/commands/command.h"
#include "app/ui/layout_selector.h"
#include "app/ui/main_window.h"
namespace app {
class ToggleWorkspaceLayoutCommand : public Command {
public:
ToggleWorkspaceLayoutCommand();
protected:
bool onChecked(Context* ctx) override;
void onExecute(Context* ctx) override;
};
ToggleWorkspaceLayoutCommand::ToggleWorkspaceLayoutCommand()
: Command(CommandId::ToggleWorkspaceLayout(), CmdUIOnlyFlag)
{
}
bool ToggleWorkspaceLayoutCommand::onChecked(Context* ctx)
{
return App::instance()->mainWindow()->layoutSelector()->isSelectorVisible();
}
void ToggleWorkspaceLayoutCommand::onExecute(Context* ctx)
{
App::instance()->mainWindow()->layoutSelector()->switchSelectorFromCommand();
}
Command* CommandFactory::createToggleWorkspaceLayoutCommand()
{
return new ToggleWorkspaceLayoutCommand();
}
} // namespace app

View File

@ -72,6 +72,7 @@
#include "ui/menu.h"
#include "ui/message.h"
#include "ui/paint_event.h"
#include "ui/resize_event.h"
#include "ui/separator.h"
#include "ui/splitter.h"
#include "ui/system.h"
@ -138,8 +139,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)
@ -299,7 +300,7 @@ ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
InitTheme.connect([this, fgBox, bgBox] {
auto theme = SkinTheme::get(this);
setBorder(gfx::Border(2 * guiscale(), 0, 0, 0));
setBorder(gfx::Border(0));
setChildSpacing(2 * guiscale());
m_fgColor.resetSizeHint();
@ -644,6 +645,18 @@ void ColorBar::onSizeHint(ui::SizeHintEvent& ev)
Box::onSizeHint(ev);
}
void ColorBar::onResize(ui::ResizeEvent& ev)
{
// Docked at left side
// TODO improve this how this is calculated
if (ev.bounds().x == 0)
setBorder(gfx::Border(2 * guiscale(), 0, 0, 0));
else
setBorder(gfx::Border(0, 0, 2 * guiscale(), 0));
Box::onResize(ev);
}
void ColorBar::onActiveSiteChange(const Site& site)
{
if (m_lastDocument != site.document()) {

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,10 +125,18 @@ 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:
void onSizeHint(ui::SizeHintEvent& ev) override;
void onResize(ui::ResizeEvent& ev) override;
void onAppPaletteChange();
void onFocusPaletteView(ui::Message* msg);
void onFocusTilesView(ui::Message* msg);

View File

@ -53,7 +53,7 @@ ConfigureTimelinePopup::ConfigureTimelinePopup()
m_box = new app::gen::TimelineConf();
addChild(m_box);
m_box->position()->ItemChange.connect([this] { onChangePosition(); });
m_box->layout()->Click.connect([this] { onWorkspaceLayout(); });
m_box->firstFrame()->Change.connect([this] { onChangeFirstFrame(); });
m_box->merge()->Click.connect([this] { onChangeType(); });
m_box->tint()->Click.connect([this] { onChangeType(); });
@ -93,15 +93,6 @@ void ConfigureTimelinePopup::updateWidgetsFromCurrentSettings()
DocumentPreferences& docPref = this->docPref();
base::ScopedValue lockUpdates(m_lockUpdates, true);
auto position = Preferences::instance().general.timelinePosition();
int selItem = 2;
switch (position) {
case gen::TimelinePosition::LEFT: selItem = 0; break;
case gen::TimelinePosition::RIGHT: selItem = 1; break;
case gen::TimelinePosition::BOTTOM: selItem = 2; break;
}
m_box->position()->setSelectedItem(selItem, false);
m_box->firstFrame()->setTextf("%d", docPref.timeline.firstFrame());
switch (docPref.onionskin.type()) {
@ -147,17 +138,10 @@ bool ConfigureTimelinePopup::onProcessMessage(ui::Message* msg)
return PopupWindow::onProcessMessage(msg);
}
void ConfigureTimelinePopup::onChangePosition()
void ConfigureTimelinePopup::onWorkspaceLayout()
{
gen::TimelinePosition newTimelinePos = gen::TimelinePosition::BOTTOM;
int selITem = m_box->position()->selectedItem();
switch (selITem) {
case 0: newTimelinePos = gen::TimelinePosition::LEFT; break;
case 1: newTimelinePos = gen::TimelinePosition::RIGHT; break;
case 2: newTimelinePos = gen::TimelinePosition::BOTTOM; break;
}
Preferences::instance().general.timelinePosition(newTimelinePos);
UIContext::instance()->executeCommand(
Commands::instance()->byId(CommandId::ToggleWorkspaceLayout()));
}
void ConfigureTimelinePopup::onChangeFirstFrame()

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2022-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -33,7 +33,7 @@ public:
protected:
bool onProcessMessage(ui::Message* msg) override;
void onChangePosition();
void onWorkspaceLayout();
void onChangeFirstFrame();
void onChangeType();
void onOpacity();

View File

@ -1962,6 +1962,12 @@ void ContextBar::onInitTheme(ui::InitThemeEvent& ev)
auto theme = SkinTheme::get(this);
gfx::Border border = this->border();
border.bottom(2 * guiscale());
// Docked at the left side
// TODO improve this how this is calculated
if (bounds().x == 0)
border.left(border.left() + 2 * guiscale());
setBorder(border);
setBgColor(theme->colors.workspace());
m_sprayLabel->setStyle(theme->styles.miniLabel());

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,10 @@ public:
// For freehand with dynamics
const tools::DynamicsOptions& getDynamics() const;
// Dockable impl
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
int dockHandleSide() const override { return ui::LEFT; }
// Signals
obs::signal<void()> BrushChange;
obs::signal<void(const FontInfo&, FontEntry::From)> FontChange;

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

@ -0,0 +1,866 @@
// Aseprite
// Copyright (C) 2021-2025 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/app.h"
#include "app/i18n/strings.h"
#include "app/ini_file.h"
#include "app/modules/gfx.h"
#include "app/pref/preferences.h"
#include "app/ui/dockable.h"
#include "app/ui/layout_selector.h"
#include "app/ui/main_window.h"
#include "app/ui/skin/skin_theme.h"
#include "os/system.h"
#include "ui/cursor_type.h"
#include "ui/label.h"
#include "ui/menu.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
}
int side_from_index(int index)
{
switch (index) {
case kTopIndex: return ui::TOP;
case kBottomIndex: return ui::BOTTOM;
case kLeftIndex: return ui::LEFT;
case kRightIndex: return ui::RIGHT;
}
return ui::CENTER; // kCenterIndex
}
} // anonymous namespace
// TODO: Duplicated from main_window.cpp
static constexpr auto kLegacyLayoutMainWindowSection = "layout:main_window";
static constexpr auto kLegacyLayoutTimelineSplitter = "timeline_splitter";
Dock::DropzonePlaceholder::DropzonePlaceholder(Widget* dragWidget, const gfx::Point& mousePosition)
: Widget(kGenericWidget)
{
setExpansive(true);
setSizeHint(dragWidget->sizeHint());
setMinSize(dragWidget->size());
m_mouseOffset = mousePosition - dragWidget->bounds().origin();
const os::SurfaceRef surface = os::System::instance()->makeRgbaSurface(dragWidget->size().w,
dragWidget->size().h);
{
const os::SurfaceLock lock(surface.get());
Paint paint;
paint.color(gfx::rgba(0, 0, 0, 0));
paint.style(os::Paint::Fill);
surface->drawRect(gfx::Rect(0, 0, surface->width(), surface->height()), paint);
}
{
Graphics g(display(), surface, 0, 0);
g.setFont(font());
Paint paint;
paint.color(gfx::rgba(0, 0, 0, 200));
// TODO: This will render any open things, especially the preview editor, need to close or hide
// that for a frame or paint the widget itself to a surface instead of croppping the backLayer.
auto backLayerSurface = display()->backLayer()->surface();
g.drawSurface(backLayerSurface.get(),
dragWidget->bounds(),
gfx::Rect(0, 0, surface->width(), surface->height()),
os::Sampling(),
&paint);
}
m_floatingUILayer = UILayer::Make();
m_floatingUILayer->setSurface(surface);
m_floatingUILayer->setPosition(dragWidget->bounds().origin());
display()->addLayer(m_floatingUILayer);
}
Dock::DropzonePlaceholder::~DropzonePlaceholder()
{
display()->removeLayer(m_floatingUILayer);
}
void Dock::DropzonePlaceholder::setGhostPosition(const gfx::Point& position) const
{
ASSERT(m_floatingUILayer);
display()->dirtyRect(m_floatingUILayer->bounds());
m_floatingUILayer->setPosition(position - m_mouseOffset);
display()->dirtyRect(m_floatingUILayer->bounds());
}
void Dock::DropzonePlaceholder::onPaint(PaintEvent& ev)
{
Graphics* g = ev.graphics();
gfx::Rect bounds = clientBounds();
g->fillRect(bgColor(), bounds);
bounds.shrink(2 * guiscale());
const auto* theme = SkinTheme::get(this);
const gfx::Color color = theme->colors.workspaceText();
g->drawRect(color, bounds);
g->drawLine(color, bounds.center(), bounds.origin());
g->drawLine(color, bounds.center(), bounds.point2());
g->drawLine(color, bounds.center(), bounds.point2() - gfx::Point(bounds.w, 0));
g->drawLine(color, bounds.center(), bounds.origin() + gfx::Point(bounds.w, 0));
g->drawRect(
color,
gfx::Rect(bounds.center() - gfx::Point(2, 2) * guiscale(), gfx::Size(4, 4) * guiscale()));
}
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();
}
void Dock::setCustomizing(bool enable, bool doLayout)
{
m_customizing = enable;
for (int i = 0; i < kSides; ++i) {
auto* child = m_sides[i];
if (!child)
continue;
if (auto* subdock = dynamic_cast<Dock*>(child))
subdock->setCustomizing(enable, false);
}
if (doLayout)
layout();
}
void Dock::resetDocks()
{
for (int i = 0; i < kSides; ++i) {
auto* child = m_sides[i];
if (!child)
continue;
if (auto* subdock = dynamic_cast<Dock*>(child)) {
subdock->resetDocks();
if (subdock->m_autoDelete)
delete subdock;
}
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 {
ASSERT(false); // Docking failure!
}
}
void Dock::dockRelativeTo(ui::Widget* relative,
int side,
ui::Widget* widget,
const gfx::Size& prefSize)
{
ASSERT(relative);
Widget* parent = relative->parent();
ASSERT(parent);
auto* subdock = new Dock;
subdock->m_autoDelete = true;
subdock->m_customizing = m_customizing;
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);
m_sizes[i] = gfx::Size();
break;
}
}
if (parentDock != this && parentDock->children().empty()) {
undock(parentDock);
}
}
else {
parent->removeChild(widget);
}
}
int Dock::whichSideChildIsDocked(const ui::Widget* widget) const
{
for (int i = 0; i < kSides; ++i)
if (m_sides[i] == widget)
return side_from_index(i);
return 0;
}
const gfx::Size Dock::getUserDefinedSizeAtSide(int side) const
{
int i = side_index(side);
// Only EXPANSIVE sides can be user-defined (has a splitter so the
// user can expand or shrink it)
if (m_aligns[i] & EXPANSIVE)
return m_sizes[i];
return gfx::Size();
}
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;
newSubdock->m_autoDelete = true;
newSubdock->m_customizing = m_customizing;
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 fitIn = ev.fitInSize();
gfx::Size sz;
for (int i = 0; i < kSides; ++i) {
auto* widget = m_sides[i];
if (!widget || !widget->isVisible() || widget->isDecorative())
continue;
const int spacing = (m_aligns[i] & EXPANSIVE ? childSpacing() : 0);
const auto hint = (m_aligns[i] & EXPANSIVE ? m_sizes[i] : widget->sizeHint(fitIn));
switch (i) {
case kTopIndex:
case kBottomIndex:
sz.h += hint.h;
fitIn.h = std::max(0, fitIn.h - hint.h);
if (spacing > 0) {
sz.h += spacing;
fitIn.h = std::max(0, fitIn.h - spacing);
}
break;
case kLeftIndex:
case kRightIndex:
sz.w += hint.w;
fitIn.w = std::max(0, fitIn.w - hint.w);
if (spacing > 0) {
sz.w += spacing;
fitIn.w = std::max(0, fitIn.w - spacing);
}
break;
case kCenterIndex:
sz += gfx::Size(std::max(hint.w, std::max(m_sizes[kTopIndex].w, m_sizes[kBottomIndex].w)),
std::max(hint.h, std::max(m_sizes[kLeftIndex].h, m_sizes[kRightIndex].h)));
break;
}
}
sz += border();
ev.setSizeHint(sz);
}
void Dock::onResize(ui::ResizeEvent& ev)
{
gfx::Rect bounds = ev.bounds();
setBoundsQuietly(bounds);
bounds = childrenBounds();
updateDockVisibility();
forEachSide(bounds,
[this](ui::Widget* widget,
const gfx::Rect& widgetBounds,
const gfx::Rect& separator,
const int index) {
gfx::Rect rc = widgetBounds;
auto th = textHeight();
if (isCustomizing()) {
int handleSide = 0;
if (auto* dockable = dynamic_cast<Dockable*>(widget))
handleSide = dockable->dockHandleSide();
switch (handleSide) {
case ui::TOP:
rc.y += th;
rc.h -= th;
break;
case ui::LEFT:
rc.x += th;
rc.w -= th;
break;
}
}
widget->setBounds(rc);
});
}
void Dock::onPaint(ui::PaintEvent& ev)
{
Graphics* g = ev.graphics();
const gfx::Rect& bounds = clientBounds();
g->fillRect(bgColor(), bounds);
if (isCustomizing()) {
forEachSide(bounds,
[this, g](ui::Widget* widget,
const gfx::Rect& widgetBounds,
const gfx::Rect& separator,
const int index) {
gfx::Rect rc = widgetBounds;
auto th = textHeight();
if (isCustomizing()) {
auto* theme = SkinTheme::get(this);
const gfx::Color color = theme->colors.workspaceText();
int handleSide = 0;
if (auto* dockable = dynamic_cast<Dockable*>(widget))
handleSide = dockable->dockHandleSide();
switch (handleSide) {
case ui::TOP:
rc.h = th;
for (int y = rc.y; y + 1 < rc.y2(); y += 2)
g->drawHLine(color,
rc.x + widget->border().left(),
y,
rc.w - widget->border().width());
break;
case ui::LEFT:
rc.w = th;
for (int x = rc.x; x + 1 < rc.x2(); x += 2)
g->drawVLine(color,
x,
rc.y + widget->border().top(),
rc.h - widget->border().height());
break;
}
}
});
}
}
void Dock::onInitTheme(ui::InitThemeEvent& ev)
{
Widget::onInitTheme(ev);
setBorder(gfx::Border(0));
setChildSpacing(4 * ui::guiscale());
for (int i = 0; i < kSides; ++i) {
Widget* widget = m_sides[i];
if (widget)
widget->initTheme();
}
}
bool Dock::onProcessMessage(ui::Message* msg)
{
switch (msg->type()) {
case kMouseDownMessage: {
auto* mouseMessage = static_cast<MouseMessage*>(msg);
const gfx::Point& pos = mouseMessage->position();
if (m_hit.sideIndex >= 0 || m_hit.dockable) {
m_startPos = pos;
if (m_hit.sideIndex >= 0)
m_startSize = m_sizes[m_hit.sideIndex];
captureMouse();
if (m_hit.dockable && !mouseMessage->right()) {
m_dragging = true;
}
return true;
}
break;
}
case kMouseMoveMessage: {
if (hasCapture()) {
const gfx::Point& pos = static_cast<MouseMessage*>(msg)->position();
if (m_dropzonePlaceholder)
m_dropzonePlaceholder->setGhostPosition(pos);
if (m_hit.sideIndex >= 0) {
if (!display()->bounds().contains(pos) ||
(m_hit.widget && m_hit.widget->parent() &&
!m_hit.widget->parent()->bounds().contains(pos)))
break; // Do not handle anything outside bounds.
gfx::Size& sz = m_sizes[m_hit.sideIndex];
gfx::Size minSize(16 * guiscale(), 16 * guiscale());
if (m_hit.widget) {
minSize.w = std::max(m_hit.widget->minSize().w, minSize.w);
minSize.h = std::max(m_hit.widget->minSize().h, minSize.h);
}
switch (m_hit.sideIndex) {
case kTopIndex: sz.h = std::max(m_startSize.h + pos.y - m_startPos.y, minSize.h); break;
case kBottomIndex:
sz.h = std::max(m_startSize.h - pos.y + m_startPos.y, minSize.h);
break;
case kLeftIndex:
sz.w = std::max(m_startSize.w + pos.x - m_startPos.x, minSize.w);
break;
case kRightIndex:
sz.w = std::max(m_startSize.w - pos.x + m_startPos.x, minSize.w);
break;
}
layout();
Resize();
}
else if (m_hit.dockable && m_dragging) {
invalidate();
auto* parentDock = dynamic_cast<Dock*>(m_hit.widget->parent());
ASSERT(parentDock);
if (!parentDock)
break;
if (!m_dropzonePlaceholder)
m_dropzonePlaceholder.reset(new DropzonePlaceholder(m_hit.widget, pos));
auto dockedAt = parentDock->whichSideChildIsDocked(m_hit.widget);
const auto& bounds = parentDock->bounds();
if (!bounds.contains(pos))
break; // Do not handle anything outside the bounds of the dock.
const int kBufferZone =
std::max(12 * guiscale(), std::min(m_hit.widget->size().w, m_hit.widget->size().h));
int newTargetSide = -1;
if (m_hit.dockable->dockableAt() & LEFT && !(dockedAt & LEFT) &&
pos.x < bounds.x + kBufferZone) {
newTargetSide = LEFT;
}
else if (m_hit.dockable->dockableAt() & RIGHT && !(dockedAt & RIGHT) &&
pos.x > (bounds.w - kBufferZone)) {
newTargetSide = RIGHT;
}
else if (m_hit.dockable->dockableAt() & TOP && !(dockedAt & TOP) &&
pos.y < bounds.y + kBufferZone) {
newTargetSide = TOP;
}
else if (m_hit.dockable->dockableAt() & BOTTOM && !(dockedAt & BOTTOM) &&
pos.y > (bounds.h - kBufferZone)) {
newTargetSide = BOTTOM;
}
if (m_hit.targetSide == newTargetSide)
break;
m_hit.targetSide = newTargetSide;
// Always undock the placeholder
if (m_dropzonePlaceholder && m_dropzonePlaceholder->parent()) {
auto* placeholderCurrentDock = dynamic_cast<Dock*>(m_dropzonePlaceholder->parent());
placeholderCurrentDock->undock(m_dropzonePlaceholder.get());
}
if (m_dropzonePlaceholder && m_hit.targetSide != -1) {
parentDock->dock(m_hit.targetSide,
m_dropzonePlaceholder.get(),
m_hit.widget->sizeHint());
}
layout();
}
}
break;
}
case kMouseUpMessage: {
if (hasCapture()) {
releaseMouse();
if (m_dropzonePlaceholder && m_dropzonePlaceholder->parent()) {
// Always undock the dropzone placeholder to avoid dangling sizes.
auto* placeholderCurrentDock = dynamic_cast<Dock*>(m_dropzonePlaceholder->parent());
placeholderCurrentDock->undock(m_dropzonePlaceholder.get());
}
if (m_hit.dockable) {
auto* dockableWidget = dynamic_cast<Widget*>(m_hit.dockable);
auto* widgetDock = dynamic_cast<Dock*>(dockableWidget->parent());
int currentSide = widgetDock->whichSideChildIsDocked(dockableWidget);
assert(dockableWidget && widgetDock);
const auto* mouseMessage = static_cast<MouseMessage*>(msg);
if (mouseMessage->right() && !m_dragging) {
Menu menu;
MenuItem left(Strings::dock_left());
MenuItem right(Strings::dock_right());
MenuItem top(Strings::dock_top());
MenuItem bottom(Strings::dock_bottom());
if (m_hit.dockable->dockableAt() & ui::LEFT) {
menu.addChild(&left);
}
if (m_hit.dockable->dockableAt() & ui::RIGHT) {
menu.addChild(&right);
}
if (m_hit.dockable->dockableAt() & ui::TOP) {
menu.addChild(&top);
}
if (m_hit.dockable->dockableAt() & ui::BOTTOM) {
menu.addChild(&bottom);
}
switch (currentSide) {
case ui::LEFT: left.setEnabled(false); break;
case ui::RIGHT: right.setEnabled(false); break;
case ui::TOP: top.setEnabled(false); break;
case ui::BOTTOM: bottom.setEnabled(false); break;
}
left.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::LEFT); });
right.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::RIGHT); });
top.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::TOP); });
bottom.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::BOTTOM); });
menu.showPopup(mouseMessage->position(), display());
return false;
}
else if (m_hit.targetSide > 0 && m_dragging) {
ASSERT(m_hit.dockable->dockableAt() & m_hit.targetSide);
redockWidget(widgetDock, dockableWidget, m_hit.targetSide);
m_dropzonePlaceholder = nullptr;
m_dragging = false;
m_hit = Hit();
return false;
}
}
m_dropzonePlaceholder = nullptr;
m_dragging = false;
// Call UserResizedDock signal after resizing a Dock splitter
if (m_hit.sideIndex >= 0)
onUserResizedDock();
m_hit = Hit();
}
break;
}
case kSetCursorMessage: {
const gfx::Point& pos = static_cast<MouseMessage*>(msg)->position();
ui::CursorType cursor = ui::kArrowCursor;
if (!hasCapture())
m_hit = calcHit(pos);
if (m_hit.sideIndex >= 0) {
switch (m_hit.sideIndex) {
case kTopIndex:
case kBottomIndex: cursor = ui::kSizeNSCursor; break;
case kLeftIndex:
case kRightIndex: cursor = ui::kSizeWECursor; break;
}
}
else if (m_hit.dockable && m_hit.targetSide == -1) {
cursor = ui::kMoveCursor;
}
ui::set_mouse_cursor(cursor);
return true;
}
}
return Widget::onProcessMessage(msg);
}
void Dock::onUserResizedDock()
{
// Generate the UserResizedDock signal, this can be used to know
// when the user modified the dock configuration to save the new
// layout in a user/preference file.
UserResizedDock();
// Send the same notification for the parent (as probably eh
// MainWindow is listening the signal of just the root dock).
if (auto* parentDock = dynamic_cast<Dock*>(parent())) {
parentDock->onUserResizedDock();
}
}
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* 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();
}
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() || widget->isDecorative()) {
continue;
}
const int spacing = (m_aligns[i] & EXPANSIVE ? childSpacing() : 0);
const gfx::Size sz = (m_aligns[i] & EXPANSIVE ? m_sizes[i] : widget->sizeHint(bounds.size()));
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);
}
}
void Dock::redockWidget(app::Dock* widgetDock, ui::Widget* dockableWidget, const int side)
{
gfx::Size size;
if (dockableWidget->id() == "timeline") {
const gfx::Rect workspaceBounds = widgetDock->bounds();
size.w = 64;
size.h = 64;
const auto timelineSplitterPos =
get_config_double(kLegacyLayoutMainWindowSection, kLegacyLayoutTimelineSplitter, 75.0) /
100.0;
auto pos = gen::TimelinePosition::LEFT;
size.w = (workspaceBounds.w * (1.0 - timelineSplitterPos)) / guiscale();
if (side & RIGHT) {
pos = gen::TimelinePosition::RIGHT;
}
if (side & BOTTOM || side & TOP) {
pos = gen::TimelinePosition::BOTTOM;
size.h = (workspaceBounds.h * (1.0 - timelineSplitterPos)) / guiscale();
}
Preferences::instance().general.timelinePosition(pos);
}
widgetDock->undock(dockableWidget);
widgetDock->dock(side, dockableWidget, size);
App::instance()->mainWindow()->invalidate();
layout();
onUserResizedDock();
}
Dock::Hit Dock::calcHit(const gfx::Point& pos)
{
Hit hit;
forEachSide(childrenBounds(),
[this, pos, &hit](ui::Widget* widget,
const gfx::Rect& widgetBounds,
const gfx::Rect& separator,
const int index) {
if (separator.contains(pos)) {
hit.widget = widget;
hit.sideIndex = index;
}
else if (isCustomizing()) {
auto th = textHeight();
gfx::Rect rc = widgetBounds;
if (auto* dockable = dynamic_cast<Dockable*>(widget)) {
switch (dockable->dockHandleSide()) {
case ui::TOP:
rc.h = th;
if (rc.contains(pos)) {
hit.widget = widget;
hit.dockable = dockable;
}
break;
case ui::LEFT:
rc.w = th;
if (rc.contains(pos)) {
hit.widget = widget;
hit.dockable = dockable;
}
break;
}
}
}
});
return hit;
}
} // namespace app

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

@ -0,0 +1,129 @@
// Aseprite
// Copyright (C) 2021-2025 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 "app/ui/dockable.h"
#include "gfx/rect.h"
#include "gfx/size.h"
#include "ui/widget.h"
#include <array>
#include <functional>
#include <string>
#include <vector>
namespace app {
class Dockable;
class Dock : public ui::Widget {
public:
static constexpr const int kSides = 5;
class DropzonePlaceholder final : public Widget,
public Dockable {
public:
DropzonePlaceholder(Widget* dragWidget, const gfx::Point& mousePosition);
~DropzonePlaceholder() override;
void setGhostPosition(const gfx::Point& position) const;
private:
void onPaint(ui::PaintEvent& ev) override;
int dockHandleSide() const override { return 0; }
gfx::Point m_mouseOffset;
ui::UILayerRef m_floatingUILayer;
};
Dock();
bool isCustomizing() const { return m_customizing; }
void setCustomizing(bool enable, bool doLayout = true);
void resetDocks();
// 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); }
// Functions useful to query/save the dock layout.
int whichSideChildIsDocked(const ui::Widget* widget) const;
const gfx::Size getUserDefinedSizeAtSide(int side) const;
obs::signal<void()> Resize;
obs::signal<void()> UserResizedDock;
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;
void onUserResizedDock();
private:
void setSide(int i, ui::Widget* newWidget);
int calcAlign(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);
bool hasVisibleSide(const int i) const { return (m_sides[i] && m_sides[i]->isVisible()); }
void redockWidget(app::Dock* widgetDock, ui::Widget* dockableWidget, const int side);
struct Hit {
ui::Widget* widget = nullptr;
Dockable* dockable = nullptr;
int sideIndex = -1;
int targetSide = -1;
};
Hit calcHit(const gfx::Point& pos);
std::array<Widget*, kSides> m_sides;
std::array<int, kSides> m_aligns;
std::array<gfx::Size, kSides> m_sizes;
bool m_autoDelete = false;
// Use to drag-and-drop stuff (splitters and dockable widgets)
Hit m_hit;
// Used to resize sizes splitters.
gfx::Size m_startSize;
gfx::Point m_startPos;
// True when we paint/can drag-and-drop dockable widgets from handles.
bool m_customizing = false;
// True when we're dragging a widget to attempt to dock it somewhere else.
bool m_dragging = false;
std::unique_ptr<DropzonePlaceholder> m_dropzonePlaceholder;
};
} // namespace app
#endif

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

@ -0,0 +1,37 @@
// 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;
}
// Returns the preferred side where the dock handle to move the
// widget should be.
virtual int dockHandleSide() const { return ui::TOP; }
};
} // namespace app
#endif

View File

@ -36,10 +36,10 @@ void IconButton::setIcon(const skin::SkinPartPtr& part)
void IconButton::onInitTheme(InitThemeEvent& ev)
{
Button::onInitTheme(ev);
auto theme = SkinTheme::get(this);
setBgColor(theme->colors.menuitemNormalFace());
Button::onInitTheme(ev);
}
void IconButton::onSizeHint(SizeHintEvent& ev)

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

@ -0,0 +1,222 @@
// Aseprite
// Copyright (C) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/ui/layout.h"
#include "app/app.h"
#include "app/ui/color_bar.h"
#include "app/ui/context_bar.h"
#include "app/ui/dock.h"
#include "app/ui/main_window.h"
#include "app/ui/status_bar.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui/toolbar.h"
#include "app/ui/workspace.h"
#include "app/xml_document.h"
#include "app/xml_exception.h"
#include "base/convert_to.h"
#include "ui/widget.h"
#include <cstring>
#include <sstream>
namespace app {
using namespace tinyxml2;
static void save_dock_layout(XMLElement* elem, const Dock* dock)
{
for (const auto* child : dock->children()) {
const int side = dock->whichSideChildIsDocked(child);
const gfx::Size size = dock->getUserDefinedSizeAtSide(side);
std::string sideStr;
switch (side) {
case ui::LEFT: sideStr = "left"; break;
case ui::RIGHT: sideStr = "right"; break;
case ui::TOP: sideStr = "top"; break;
case ui::BOTTOM: sideStr = "bottom"; break;
case ui::CENTER:
default:
// Empty side attribute
break;
}
XMLElement* childElem = elem->InsertNewChildElement("");
if (const auto* subdock = dynamic_cast<const Dock*>(child)) {
childElem->SetValue("dock");
if (!sideStr.empty())
childElem->SetAttribute("side", sideStr.c_str());
save_dock_layout(childElem, subdock);
}
else {
// Set the widget ID as the element name, e.g. <timeline />,
// <colorbar />, etc.
childElem->SetValue(child->id().c_str());
if (!sideStr.empty())
childElem->SetAttribute("side", sideStr.c_str());
if (size.w)
childElem->SetAttribute("width", size.w);
if (size.h)
childElem->SetAttribute("height", size.h);
}
}
}
static void load_dock_layout(const XMLElement* elem, Dock* dock)
{
const char* elemNameStr = elem->Value();
if (!elemNameStr) {
ASSERT(false); // Impossible?
return;
}
const std::string elemName = elemNameStr;
MainWindow* win = App::instance()->mainWindow();
ASSERT(win);
ui::Widget* widget = nullptr;
Dock* subdock = nullptr;
int side = ui::CENTER;
if (const auto* sideStr = elem->Attribute("side")) {
if (std::strcmp(sideStr, "left") == 0)
side = ui::LEFT;
if (std::strcmp(sideStr, "right") == 0)
side = ui::RIGHT;
if (std::strcmp(sideStr, "top") == 0)
side = ui::TOP;
if (std::strcmp(sideStr, "bottom") == 0)
side = ui::BOTTOM;
}
const char* widthStr = elem->Attribute("width");
const char* heightStr = elem->Attribute("height");
gfx::Size size;
if (widthStr)
size.w = base::convert_to<int>(std::string(widthStr));
if (heightStr)
size.h = base::convert_to<int>(std::string(heightStr));
if (elemName == "colorbar") {
widget = win->colorBar();
}
else if (elemName == "contextbar") {
widget = win->getContextBar();
}
else if (elemName == "timeline") {
widget = win->getTimeline();
}
else if (elemName == "toolbar") {
widget = win->toolBar();
}
else if (elemName == "workspace") {
widget = win->getWorkspace();
}
else if (elemName == "statusbar") {
widget = win->statusBar();
}
else if (elemName == "dock") {
subdock = dock->subdock(side);
}
if (subdock) {
const auto* childElem = elem->FirstChildElement();
while (childElem) {
load_dock_layout(childElem, subdock);
childElem = childElem->NextSiblingElement();
}
}
else {
dock->dock(side, widget, size);
}
}
// static
LayoutPtr Layout::MakeFromXmlElement(const XMLElement* layoutElem)
{
const char* name = layoutElem->Attribute("name");
const char* id = layoutElem->Attribute("id");
if (id == nullptr || name == nullptr) {
LOG(WARNING, "Invalid XML layout provided\n");
return nullptr;
}
auto layout = std::make_shared<Layout>();
layout->m_id = id;
layout->m_name = name;
layout->m_elem = layoutElem->DeepClone(&layout->m_dummyDoc)->ToElement();
ASSERT(!layout->m_name.empty() && !layout->m_id.empty());
if (layout->m_elem->ChildElementCount() == 0) // TODO: More error checking here.
return nullptr;
if (layout->m_name.empty() || layout->m_id.empty())
return nullptr;
return layout;
}
// static
LayoutPtr Layout::MakeFromDock(const std::string& id, const std::string& name, const Dock* dock)
{
auto layout = std::make_shared<Layout>();
layout->m_id = id;
layout->m_name = name;
layout->m_elem = layout->m_dummyDoc.NewElement("layout");
layout->m_elem->SetAttribute("id", id.c_str());
layout->m_elem->SetAttribute("name", name.c_str());
save_dock_layout(layout->m_elem, dock);
return layout;
}
bool Layout::matchId(const std::string_view id) const
{
if (m_id == id)
return true;
if ((m_id.empty() && id == kDefault) || (m_id == kDefault && id.empty()))
return true;
return false;
}
bool Layout::loadLayout(Dock* dock) const
{
if (!m_elem)
return false;
XMLElement* elem = m_elem->FirstChildElement();
while (elem) {
load_dock_layout(elem, dock);
elem = elem->NextSiblingElement();
}
return true;
}
bool Layout::isValidName(const std::string_view name)
{
if (name.empty())
return false;
if (name[0] == '_')
return false;
if (name.length() > 128)
return false;
return true;
}
} // namespace app

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

@ -0,0 +1,54 @@
// Aseprite
// Copyright (C) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_UI_LAYOUT_H_INCLUDED
#define APP_UI_LAYOUT_H_INCLUDED
#pragma once
#include <memory>
#include <string>
#include "tinyxml2.h"
namespace app {
class Dock;
class Layout;
using LayoutPtr = std::shared_ptr<Layout>;
class Layout final {
public:
static constexpr const char* kDefault = "_default_";
static constexpr const char* kMirroredDefault = "_mirrored_default_";
static constexpr const char* kDefaultOriginal = "_default_original_";
static constexpr const char* kMirroredDefaultOriginal = "_mirrored_default_original_";
static LayoutPtr MakeFromXmlElement(const tinyxml2::XMLElement* layoutElem);
static LayoutPtr MakeFromDock(const std::string& id, const std::string& name, const Dock* dock);
const std::string& id() const { return m_id; }
const std::string& name() const { return m_name; }
const tinyxml2::XMLElement* xmlElement() const { return m_elem; }
bool matchId(std::string_view id) const;
bool loadLayout(Dock* dock) const;
bool isDefault() const { return m_id == kDefault || m_id == kMirroredDefault; }
// Validates that the given name is short and doesn't begin with a "_" (reserved for _defaults)
static bool isValidName(std::string_view name);
private:
std::string m_id;
std::string m_name;
tinyxml2::XMLDocument m_dummyDoc;
tinyxml2::XMLElement* m_elem = nullptr;
};
} // namespace app
#endif

View File

@ -0,0 +1,570 @@
// Aseprite
// Copyright (C) 2021-2025 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/i18n/strings.h"
#include "app/match_words.h"
#include "app/pref/preferences.h"
#include "app/ui/main_window.h"
#include "app/ui/separator_in_view.h"
#include "app/ui/skin/skin_theme.h"
#include "fmt/printf.h"
#include "ui/alert.h"
#include "ui/app_state.h"
#include "ui/entry.h"
#include "ui/label.h"
#include "ui/listitem.h"
#include "ui/tooltips.h"
#include "ui/window.h"
#include "new_layout.xml.h"
#define ANI_TICKS 2
namespace app {
using namespace app::skin;
using namespace ui;
namespace {
// TODO this combobox is similar to FileSelector::CustomFileNameEntry
// and GotoFrameCommand::TagsEntry
class LayoutsEntry final : public ComboBox {
public:
explicit LayoutsEntry(Layouts& layouts) : m_layouts(layouts)
{
setEditable(true);
getEntryWidget()->Change.connect(&LayoutsEntry::onEntryChange, this);
fill(true);
}
private:
void fill(bool all)
{
deleteAllItems();
const MatchWords match(getEntryWidget()->text());
bool matchAny = false;
for (const auto& layout : m_layouts) {
if (layout->isDefault())
continue; // Ignore custom defaults.
if (match(layout->name())) {
matchAny = true;
break;
}
}
for (const auto& layout : m_layouts) {
if (layout->isDefault())
continue;
if (all || !matchAny || match(layout->name()))
addItem(layout->name());
}
}
void onEntryChange() override
{
closeListBox();
fill(false);
if (getItemCount() > 0 && !empty())
openListBox();
}
Layouts& m_layouts;
};
}; // namespace
class LayoutSelector::LayoutItem final : public ListItem {
public:
enum LayoutOption : uint8_t {
DEFAULT,
MIRRORED_DEFAULT,
USER_DEFINED,
NEW_LAYOUT,
};
LayoutItem(LayoutSelector* selector,
const LayoutOption option,
const std::string& text,
const std::string& layoutId = "")
: ListItem(text)
, m_option(option)
, m_selector(selector)
, m_layoutId(layoutId)
{
m_hbox.setTransparent(true);
addChild(&m_hbox);
auto* filler = new BoxFiller();
filler->setTransparent(true);
m_hbox.addChild(filler);
if (option == USER_DEFINED ||
((option == DEFAULT || option == MIRRORED_DEFAULT) && !layoutId.empty())) {
addActionButton();
}
}
// Separated from the constructor so we can add it on the fly when modifying Default/Mirrored
void addActionButton(const std::string& newLayoutId = "")
{
if (!newLayoutId.empty())
m_layoutId = newLayoutId;
ASSERT(!m_layoutId.empty());
// TODO: Custom icons for each one would be nice here.
m_actionButton = new IconButton(SkinTheme::instance()->parts.iconClose());
const int th = m_actionButton->textHeight();
m_actionButton->setSizeHint(gfx::Size(th, th));
m_actionButton->setTransparent(true);
m_actionButton->InitTheme.connect(
[this] { m_actionButton->setBgColor(gfx::rgba(0, 0, 0, 0)); });
if (m_option == USER_DEFINED) {
m_actionConn = m_actionButton->Click.connect([this] {
MainWindow* win = App::instance()->mainWindow();
win->layoutSelector()->closeComboBox();
const auto alert = Alert::create(Strings::new_layout_deleting_layout());
alert->addLabel(Strings::new_layout_deleting_layout_confirmation(text()), LEFT);
alert->addButton(Strings::general_ok());
alert->addButton(Strings::general_cancel());
if (alert->show() == 1) {
if (m_layoutId == m_selector->activeLayoutId()) {
m_selector->setActiveLayoutId(Layout::kDefault);
App::instance()->mainWindow()->setDefaultLayout();
}
m_selector->removeLayout(m_layoutId);
}
});
}
else {
m_actionConn = m_actionButton->Click.connect([this] {
MainWindow* win = App::instance()->mainWindow();
win->layoutSelector()->closeComboBox();
const auto alert = Alert::create(Strings::new_layout_restoring_layout());
alert->addLabel(
Strings::new_layout_restoring_layout_confirmation(text().substr(0, text().size() - 1)),
LEFT);
alert->addButton(Strings::general_ok());
alert->addButton(Strings::general_cancel());
if (alert->show() == 1) {
if (m_layoutId == Layout::kDefault) {
App::instance()->mainWindow()->setDefaultLayout();
}
else {
App::instance()->mainWindow()->setMirroredDefaultLayout();
}
m_selector->setActiveLayoutId(m_layoutId);
m_selector->removeLayout(m_layoutId);
}
});
}
m_hbox.addChild(m_actionButton);
}
std::string_view getLayoutId() const { return m_layoutId; }
void selectImmediately() const
{
MainWindow* win = App::instance()->mainWindow();
switch (m_option) {
case DEFAULT: {
if (const auto& defaultLayout = win->layoutSelector()->m_layouts.getById(
Layout::kDefault)) {
win->loadUserLayout(defaultLayout.get());
}
else {
win->setDefaultLayout();
}
m_selector->setActiveLayoutId(Layout::kDefault);
break;
}
case MIRRORED_DEFAULT: {
if (const auto& mirroredLayout = win->layoutSelector()->m_layouts.getById(
Layout::kMirroredDefault)) {
win->loadUserLayout(mirroredLayout.get());
}
else {
win->setMirroredDefaultLayout();
}
m_selector->setActiveLayoutId(Layout::kMirroredDefault);
break;
}
case USER_DEFINED: {
const auto selectedLayout = m_selector->m_layouts.getById(m_layoutId);
ASSERT(!m_layoutId.empty());
ASSERT(selectedLayout);
m_selector->setActiveLayoutId(m_layoutId);
win->loadUserLayout(selectedLayout.get());
break;
}
}
}
void selectAfterClose() const
{
if (m_option != NEW_LAYOUT)
return;
//
// Adding a NEW_LAYOUT
//
MainWindow* win = App::instance()->mainWindow();
gen::NewLayout window;
window.name()->Change.connect([&] {
bool valid = Layout::isValidName(window.name()->text()) &&
m_selector->m_layouts.getById(window.name()->text()) == nullptr;
window.ok()->setEnabled(valid);
});
window.openWindowInForeground();
if (window.closer() == window.ok()) {
const auto layout =
Layout::MakeFromDock(window.name()->text(), window.name()->text(), win->customizableDock());
m_selector->addLayout(layout);
m_selector->m_layouts.saveUserLayouts();
m_selector->setActiveLayoutId(layout->id());
win->loadUserLayout(layout.get());
}
else {
// Ensure we go back to having the layout we were at selected.
m_selector->populateComboBox();
}
}
private:
LayoutOption m_option;
LayoutSelector* m_selector = nullptr;
std::string m_layoutId;
HBox m_hbox;
IconButton* m_actionButton = nullptr;
obs::scoped_connection m_actionConn;
};
void LayoutSelector::LayoutComboBox::onChange()
{
ComboBox::onChange();
if (m_lockChange)
return;
if (auto* item = dynamic_cast<LayoutItem*>(getSelectedItem())) {
item->selectImmediately();
m_selected = item;
}
}
void LayoutSelector::LayoutComboBox::onCloseListBox()
{
ComboBox::onCloseListBox();
if (m_lockChange)
return;
if (m_selected) {
m_selected->selectAfterClose();
m_selected = nullptr;
}
}
LayoutSelector::LayoutSelector(TooltipManager* tooltipManager, Widget* notifications)
: m_button(SkinTheme::instance()->parts.iconLayout())
, m_notifications(notifications)
{
setActiveLayoutId(Preferences::instance().general.workspaceLayout());
m_button.Click.connect([this]() { switchSelector(); });
m_comboBox.setVisible(false);
m_top.setExpansive(true);
addChild(&m_top);
addChild(&m_center);
addChild(&m_bottom);
m_center.addChild(&m_comboBox);
m_center.addChild(&m_button);
m_center.addChild(m_notifications);
setupTooltips(tooltipManager);
initTheme();
}
LayoutSelector::~LayoutSelector()
{
m_center.removeChild(m_notifications);
Preferences::instance().general.workspaceLayout(m_activeLayoutId);
if (!is_app_state_closing())
stopAnimation();
}
LayoutPtr LayoutSelector::activeLayout() const
{
return m_layouts.getById(m_activeLayoutId);
}
void LayoutSelector::addLayout(const LayoutPtr& layout)
{
m_layouts.addLayout(layout);
populateComboBox();
}
void LayoutSelector::removeLayout(const LayoutPtr& layout)
{
m_layouts.removeLayout(layout);
m_layouts.saveUserLayouts();
populateComboBox();
}
void LayoutSelector::removeLayout(const std::string& layoutId)
{
auto layout = m_layouts.getById(layoutId);
ASSERT(layout);
removeLayout(layout);
}
void LayoutSelector::updateActiveLayout(const LayoutPtr& newLayout)
{
bool added = m_layouts.addLayout(newLayout);
setActiveLayoutId(newLayout->id());
m_layouts.saveUserLayouts();
if (added && newLayout->isDefault()) {
// Mark it with an asterisk if we're editing a default layout.
populateComboBox();
}
}
void LayoutSelector::onInitTheme(ui::InitThemeEvent& ev)
{
VBox::onInitTheme(ev);
auto* theme = SkinTheme::get(this);
setBgColor(theme->colors.windowFace());
noBorderNoChildSpacing();
m_top.noBorderNoChildSpacing();
m_center.noBorderNoChildSpacing();
m_bottom.noBorderNoChildSpacing();
m_comboBox.noBorderNoChildSpacing();
m_button.noBorderNoChildSpacing();
m_bottom.setStyle(theme->styles.tabBottom());
m_bottom.setMinSize(gfx::Size(0, theme->dimensions.tabsBottomHeight()));
}
void LayoutSelector::onAnimationFrame()
{
switch (animation()) {
case ANI_NONE: break;
case ANI_EXPANDING:
case ANI_COLLAPSING: {
const double t = animationTime();
m_comboBox.setSizeHint(gfx::Size(int(inbetween(m_startSize.w, m_endSize.w, t)),
int(inbetween(m_startSize.h, m_endSize.h, t))));
break;
}
}
if (auto* win = window())
win->layout();
}
void LayoutSelector::onAnimationStop(int animation)
{
switch (animation) {
case ANI_EXPANDING:
m_comboBox.setSizeHint(m_endSize);
if (m_switchComboBoxAfterAni) {
m_switchComboBoxAfterAni = false;
m_comboBox.openListBox();
}
break;
case ANI_COLLAPSING:
m_comboBox.setVisible(false);
m_comboBox.setSizeHint(m_endSize);
if (m_switchComboBoxAfterAni) {
m_switchComboBoxAfterAni = false;
m_comboBox.closeListBox();
}
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) {
populateComboBox();
}
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);
}
if (auto* item = getItemByLayoutId(m_activeLayoutId))
m_comboBox.setSelectedItem(item);
m_comboBox.setSizeHint(m_startSize);
startAnimation((expand ? ANI_EXPANDING : ANI_COLLAPSING), ANI_TICKS);
MainWindow* win = App::instance()->mainWindow();
win->setCustomizeDock(expand);
}
void LayoutSelector::switchSelectorFromCommand()
{
m_switchComboBoxAfterAni = true;
switchSelector();
}
bool LayoutSelector::isSelectorVisible() const
{
return (m_comboBox.isVisible());
}
void LayoutSelector::closeComboBox()
{
m_comboBox.closeListBox();
}
void LayoutSelector::setupTooltips(TooltipManager* tooltipManager)
{
tooltipManager->addTooltipFor(&m_button, Strings::main_window_layout(), TOP);
}
void LayoutSelector::setActiveLayoutId(const std::string& layoutId)
{
if (layoutId.empty()) {
m_activeLayoutId = Layout::kDefault;
return;
}
if (layoutId == m_activeLayoutId)
return;
m_activeLayoutId = layoutId;
for (auto* item : m_comboBox.items()) {
if (auto* layoutItem = dynamic_cast<LayoutItem*>(item)) {
if (layoutItem->getLayoutId() == layoutId) {
m_comboBox.setSelectedItem(item);
break;
}
}
}
}
void LayoutSelector::populateComboBox()
{
// Disable combobox onChange() event processing when we are
// re-creating the combobox items. This avoids calling
// LayoutSelector::LayoutItem::selectImmediately() which could
// delete docks that generate this same event, e.g. resizing a dock
// can generate a UserResizedDock which might call this
// populateComboBox() function.
m_comboBox.setLockChange(true);
// Defer deletion of current items because we can be inside one of
// these item callbacks.
auto itemsCopy = m_comboBox.items();
for (auto* item : itemsCopy) {
m_comboBox.removeItem(item);
item->deferDelete();
}
m_comboBox.addItem(new SeparatorInView(Strings::main_window_layout(), HORIZONTAL));
m_comboBox.addItem(
new LayoutItem(this, LayoutItem::DEFAULT, Strings::main_window_default_layout()));
m_comboBox.addItem(new LayoutItem(this,
LayoutItem::MIRRORED_DEFAULT,
Strings::main_window_mirrored_default_layout()));
m_comboBox.addItem(new SeparatorInView(Strings::main_window_user_layouts(), HORIZONTAL));
for (const auto& layout : m_layouts) {
LayoutItem* item;
if (layout->isDefault()) {
item = dynamic_cast<LayoutItem*>(
m_comboBox.getItem(layout->id() == Layout::kDefault ? 1 : 2));
// Indicate we've modified this with an asterisk.
item->setText(item->text() + "*");
item->addActionButton(layout->id());
}
else {
item = new LayoutItem(this, LayoutItem::USER_DEFINED, layout->name(), layout->id());
m_comboBox.addItem(item);
}
if (layout->id() == m_activeLayoutId)
m_comboBox.setSelectedItem(item);
}
m_comboBox.addItem(
new LayoutItem(this, LayoutItem::NEW_LAYOUT, Strings::main_window_new_layout(), ""));
if (m_activeLayoutId == Layout::kDefault)
m_comboBox.setSelectedItemIndex(1);
if (m_activeLayoutId == Layout::kMirroredDefault)
m_comboBox.setSelectedItemIndex(2);
m_comboBox.getEntryWidget()->deselectText();
m_comboBox.setLockChange(false);
}
LayoutSelector::LayoutItem* LayoutSelector::getItemByLayoutId(const std::string& id)
{
for (auto* child : m_comboBox) {
if (auto* item = dynamic_cast<LayoutItem*>(child)) {
if (item->getLayoutId() == id)
return item;
}
}
return nullptr;
}
} // namespace app

View File

@ -0,0 +1,94 @@
// Aseprite
// Copyright (C) 2021-2025 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 "app/ui/icon_button.h"
#include "app/ui/layout.h"
#include "app/ui/layouts.h"
#include "ui/animated_widget.h"
#include "ui/box.h"
#include "ui/combobox.h"
#include <memory>
#include <vector>
namespace ui {
class TooltipManager;
}
namespace app {
class LayoutSelector : public ui::VBox,
public ui::AnimatedWidget,
public Dockable {
enum Ani : int {
ANI_NONE,
ANI_EXPANDING,
ANI_COLLAPSING,
};
class LayoutItem;
class LayoutComboBox : public ui::ComboBox {
public:
void setLockChange(bool state) { m_lockChange = state; }
private:
void onChange() override;
void onCloseListBox() override;
LayoutItem* m_selected = nullptr;
bool m_lockChange = false;
};
public:
LayoutSelector(ui::TooltipManager* tooltipManager, ui::Widget* notifications);
~LayoutSelector();
LayoutPtr activeLayout() const;
const std::string& activeLayoutId() const { return m_activeLayoutId; }
void addLayout(const LayoutPtr& layout);
void removeLayout(const LayoutPtr& layout);
void removeLayout(const std::string& layoutId);
void updateActiveLayout(const LayoutPtr& layout);
void switchSelector();
void switchSelectorFromCommand();
bool isSelectorVisible() const;
void closeComboBox();
// Dockable impl
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
protected:
void onInitTheme(ui::InitThemeEvent& ev) override;
private:
void setupTooltips(ui::TooltipManager* tooltipManager);
void setActiveLayoutId(const std::string& layoutId);
void populateComboBox();
LayoutItem* getItemByLayoutId(const std::string& id);
void onAnimationFrame() override;
void onAnimationStop(int animation) override;
std::string m_activeLayoutId;
ui::HBox m_top, m_center, m_bottom;
LayoutComboBox m_comboBox;
IconButton m_button;
Widget* m_notifications = nullptr;
gfx::Size m_startSize;
gfx::Size m_endSize;
Layouts m_layouts;
bool m_switchComboBoxAfterAni = false;
};
} // namespace app
#endif

146
src/app/ui/layouts.cpp Normal file
View File

@ -0,0 +1,146 @@
// Aseprite
// Copyright (c) 2022-2024 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/layouts.h"
#include "app/resource_finder.h"
#include "app/xml_document.h"
#include "app/xml_exception.h"
#include "base/fs.h"
#include <algorithm>
#include <fstream>
namespace app {
using namespace tinyxml2;
Layouts::Layouts()
{
try {
std::string fn = m_userLayoutsFilename = UserLayoutsFilename();
if (base::is_file(fn))
load(fn);
}
catch (const std::exception& ex) {
LOG(ERROR, "LAY: Error loading user layouts: %s\n", ex.what());
}
}
Layouts::~Layouts()
{
try {
saveUserLayouts();
}
catch (const std::exception& ex) {
LOG(ERROR, "LAY: Error saving user layouts on exit: %s\n", ex.what());
}
}
LayoutPtr Layouts::getById(const std::string& id) const
{
auto it = std::find_if(m_layouts.begin(), m_layouts.end(), [&id](const LayoutPtr& l) {
return l->matchId(id);
});
return (it != m_layouts.end() ? *it : nullptr);
}
bool Layouts::addLayout(const LayoutPtr& layout)
{
ASSERT(layout);
const auto it = std::find_if(m_layouts.begin(), m_layouts.end(), [layout](const LayoutPtr& l) {
return l->matchId(layout->id());
});
if (it != m_layouts.end()) {
*it = layout; // Replace existent layout
return false;
}
m_layouts.push_back(layout);
return true;
}
void Layouts::removeLayout(const LayoutPtr& layout)
{
if (m_layouts.size() <= 1) {
m_layouts.clear();
return;
}
ASSERT(layout);
const auto it = std::find_if(m_layouts.begin(), m_layouts.end(), [layout](const LayoutPtr& l) {
return l->matchId(layout->id());
});
m_layouts.erase(it);
}
void Layouts::saveUserLayouts()
{
if (m_userLayoutsFilename.empty())
return;
save(m_userLayoutsFilename);
// TODO: We probably have too much I/O here, but it's the easiest way to keep the XML and
// internal representations synced up.
reload();
}
void Layouts::reload()
{
if (m_userLayoutsFilename.empty())
return;
m_layouts.clear();
load(m_userLayoutsFilename);
}
void Layouts::load(const std::string& fn)
{
const XMLDocumentRef doc = app::open_xml(fn);
XMLHandle handle(doc.get());
XMLElement* layoutElem =
handle.FirstChildElement("layouts").FirstChildElement("layout").ToElement();
while (layoutElem) {
if (auto layout = Layout::MakeFromXmlElement(layoutElem)) {
m_layouts.push_back(layout);
}
layoutElem = layoutElem->NextSiblingElement();
}
}
void Layouts::save(const std::string& fn) const
{
auto doc = std::make_unique<XMLDocument>();
XMLElement* layoutsElem = doc->NewElement("layouts");
for (const auto& layout : m_layouts) {
layoutsElem->InsertEndChild(layout->xmlElement()->DeepClone(doc.get()));
}
doc->InsertEndChild(doc->NewDeclaration(R"(xml version="1.0" encoding="utf-8")"));
doc->InsertEndChild(layoutsElem);
save_xml(doc.get(), fn);
}
// static
std::string Layouts::UserLayoutsFilename()
{
ResourceFinder rf;
rf.includeUserDir("user.aseprite-layouts");
return rf.getFirstOrCreateDefault();
}
} // namespace app

52
src/app/ui/layouts.h Normal file
View File

@ -0,0 +1,52 @@
// Aseprite
// Copyright (c) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_UI_LAYOUTS_H_INCLUDED
#define APP_UI_LAYOUTS_H_INCLUDED
#pragma once
#include "app/ui/layout.h"
#include <string>
#include <vector>
namespace app {
class Layouts {
public:
Layouts();
~Layouts();
size_t size() const { return m_layouts.size(); }
LayoutPtr getById(const std::string& id) const;
// Returns true if the layout is added, or false if it was
// replaced.
bool addLayout(const LayoutPtr& layout);
void removeLayout(const LayoutPtr& layout);
void saveUserLayouts();
void reload();
// To iterate layouts
using List = std::vector<LayoutPtr>;
using iterator = List::iterator;
iterator begin() { return m_layouts.begin(); }
iterator end() { return m_layouts.end(); }
private:
void load(const std::string& fn);
void save(const std::string& fn) const;
static std::string UserLayoutsFilename();
List m_layouts;
std::string m_userLayoutsFilename;
};
} // 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

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -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"
@ -42,6 +44,7 @@
#include "os/event.h"
#include "os/event_queue.h"
#include "os/system.h"
#include "ui/app_state.h"
#include "ui/drag_event.h"
#include "ui/message.h"
#include "ui/splitter.h"
@ -57,6 +60,10 @@ namespace app {
using namespace ui;
static constexpr const char* kLegacyLayoutMainWindowSection = "layout:main_window";
static constexpr const char* kLegacyLayoutTimelineSplitter = "timeline_splitter";
static constexpr const char* kLegacyLayoutColorBarSplitter = "color_bar_splitter";
class ScreenScalePanic : public INotificationDelegate {
public:
std::string notificationText() override { return "Reset Scale!"; }
@ -83,7 +90,11 @@ public:
};
MainWindow::MainWindow()
: m_mode(NormalMode)
: ui::Window(ui::Window::DesktopWindow)
, m_tooltipManager(new TooltipManager)
, m_dock(new Dock)
, m_customizableDock(new Dock)
, m_mode(NormalMode)
, m_homeView(nullptr)
, m_scalePanic(nullptr)
, m_browserView(nullptr)
@ -105,8 +116,9 @@ MainWindow::MainWindow()
// Refer to https://github.com/aseprite/aseprite/issues/3914
void MainWindow::initialize()
{
m_tooltipManager = new TooltipManager();
m_menuBar = new MainMenuBar();
m_menuBar = std::make_unique<MainMenuBar>();
m_notifications = std::make_unique<Notifications>();
m_layoutSelector = std::make_unique<LayoutSelector>(m_tooltipManager, m_notifications.get());
// Register commands to load menus+shortcuts for these commands
Editor::registerCommands();
@ -117,20 +129,19 @@ void MainWindow::initialize()
// Setup the main menubar
m_menuBar->setMenu(AppMenus::instance()->getRootMenu());
m_notifications = new Notifications();
m_statusBar = new StatusBar(m_tooltipManager);
m_toolBar = new ToolBar();
m_tabsBar = new WorkspaceTabs(this);
m_workspace = new Workspace();
m_previewEditor = new PreviewEditorWindow();
m_colorBar = new ColorBar(colorBarPlaceholder()->align(), m_tooltipManager);
m_contextBar = new ContextBar(m_tooltipManager, m_colorBar);
m_statusBar = std::make_unique<StatusBar>(m_tooltipManager);
m_toolBar = std::make_unique<ToolBar>();
m_tabsBar = std::make_unique<WorkspaceTabs>(this);
m_workspace = std::make_unique<Workspace>();
m_previewEditor = std::make_unique<PreviewEditorWindow>();
m_colorBar = std::make_unique<ColorBar>(m_tooltipManager);
m_contextBar = std::make_unique<ContextBar>(m_tooltipManager, m_colorBar.get());
// The timeline (AniControls) tooltips will use the keyboard
// shortcuts loaded above.
m_timeline = new Timeline(m_tooltipManager);
m_timeline = std::make_unique<Timeline>(m_tooltipManager);
m_workspace->setTabsBar(m_tabsBar);
m_workspace->setTabsBar(m_tabsBar.get());
m_workspace->BeforeViewChanged.connect(&MainWindow::onBeforeViewChange, this);
m_workspace->ActiveViewChanged.connect(&MainWindow::onActiveViewChange, this);
@ -146,21 +157,31 @@ void MainWindow::initialize()
m_workspace->setExpansive(true);
m_notifications->setVisible(false);
// IDs to create UI layouts from a Dock (see app::Layout
// constructor).
m_colorBar->setId("colorbar");
m_contextBar->setId("contextbar");
m_statusBar->setId("statusbar");
m_timeline->setId("timeline");
m_toolBar->setId("toolbar");
m_workspace->setId("workspace");
// 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);
m_customizableDockPlaceholder = std::make_unique<Widget>();
m_customizableDockPlaceholder->addChild(m_customizableDock);
m_dock->top()->dock(ui::RIGHT, m_layoutSelector.get());
m_dock->top()->center()->dock(ui::BOTTOM, m_tabsBar.get());
m_dock->top()->center()->dock(ui::CENTER, m_menuBar.get());
m_dock->dock(ui::CENTER, m_customizableDockPlaceholder.get());
// After the user resizes the dock we save the updated layout
m_saveDockLayoutConn = m_customizableDock->UserResizedDock.connect(&MainWindow::saveActiveLayout,
this);
// Reconfigure workspace when the timeline position is changed.
auto& pref = Preferences::instance();
@ -172,49 +193,60 @@ void MainWindow::initialize()
AppMenus::instance()->rebuildRecentList();
// When the language is change, we reload the menu bar strings and
// When the language is changed, we reload the menu bar strings and
// relayout the whole main window.
Strings::instance()->LanguageChange.connect([this] { onLanguageChange(); });
initTheme();
}
MainWindow::~MainWindow()
{
delete m_scalePanic;
m_timelineResizeConn.disconnect();
m_colorBarResizeConn.disconnect();
m_saveDockLayoutConn.disconnect();
m_dock->resetDocks();
m_customizableDock->resetDocks();
// Leaving them in can cause crashes when cleaning up.
m_dock = nullptr;
m_customizableDock = nullptr;
m_layoutSelector.reset();
m_scalePanic.reset();
#ifdef ENABLE_SCRIPTING
if (m_devConsoleView) {
if (m_devConsoleView->parent() && m_workspace)
m_workspace->removeView(m_devConsoleView);
delete m_devConsoleView;
m_workspace->removeView(m_devConsoleView.get());
m_devConsoleView.reset();
}
#endif
if (m_browserView) {
if (m_browserView->parent() && m_workspace)
m_workspace->removeView(m_browserView);
delete m_browserView;
m_workspace->removeView(m_browserView.get());
m_browserView.reset();
}
if (m_homeView) {
if (m_homeView->parent() && m_workspace)
m_workspace->removeView(m_homeView);
delete m_homeView;
m_workspace->removeView(m_homeView.get());
m_homeView.reset();
}
if (m_contextBar)
delete m_contextBar;
if (m_previewEditor)
delete m_previewEditor;
m_contextBar.reset();
m_previewEditor.reset();
// Destroy the workspace first so ~Editor can dettach slots from
// ColorBar. TODO this is a terrible hack for slot/signal stuff,
// connections should be handle in a better/safer way.
if (m_workspace)
delete m_workspace;
m_workspace.reset();
// Remove the root-menu from the menu-bar (because the rootmenu
// module should destroy it).
if (m_menuBar)
m_menuBar->setMenu(NULL);
m_menuBar->setMenu(nullptr);
}
void MainWindow::onLanguageChange()
@ -232,8 +264,8 @@ DocView* MainWindow::getDocView()
HomeView* MainWindow::getHomeView()
{
if (!m_homeView)
m_homeView = new HomeView;
return m_homeView;
m_homeView = std::make_unique<HomeView>();
return m_homeView.get();
}
#ifdef ENABLE_UPDATER
@ -254,7 +286,7 @@ void MainWindow::showNotification(INotificationDelegate* del)
{
m_notifications->addLink(del);
m_notifications->setVisible(true);
m_notifications->parent()->layout();
layout();
}
void MainWindow::showHomeOnOpen()
@ -270,20 +302,20 @@ void MainWindow::showHomeOnOpen()
// Show "Home" tab in the first position, and select it only if
// there is no other view selected.
m_workspace->addView(m_homeView, 0);
m_workspace->addView(m_homeView.get(), 0);
if (selectedTab)
m_tabsBar->selectTab(selectedTab);
else
m_tabsBar->selectTab(m_homeView);
m_tabsBar->selectTab(m_homeView.get());
}
}
void MainWindow::showHome()
{
if (!getHomeView()->parent()) {
m_workspace->addView(m_homeView, 0);
m_workspace->addView(m_homeView.get(), 0);
}
m_tabsBar->selectTab(m_homeView);
m_tabsBar->selectTab(m_homeView.get());
}
void MainWindow::showDefaultStatusBar()
@ -298,19 +330,19 @@ void MainWindow::showDefaultStatusBar()
bool MainWindow::isHomeSelected() const
{
return (m_homeView && m_workspace->activeView() == m_homeView);
return (m_homeView && m_workspace->activeView() == m_homeView.get());
}
void MainWindow::showBrowser(const std::string& filename, const std::string& section)
{
if (!m_browserView)
m_browserView = new BrowserView;
m_browserView = std::make_unique<BrowserView>();
m_browserView->loadFile(filename, section);
if (!m_browserView->parent()) {
m_workspace->addView(m_browserView);
m_tabsBar->selectTab(m_browserView);
m_workspace->addView(m_browserView.get());
m_tabsBar->selectTab(m_browserView.get());
}
}
@ -318,11 +350,11 @@ void MainWindow::showDevConsole()
{
#ifdef ENABLE_SCRIPTING
if (!m_devConsoleView)
m_devConsoleView = new DevConsoleView;
m_devConsoleView = std::make_unique<DevConsoleView>();
if (!m_devConsoleView->parent()) {
m_workspace->addView(m_devConsoleView);
m_tabsBar->selectTab(m_devConsoleView);
m_workspace->addView(m_devConsoleView.get());
m_tabsBar->selectTab(m_devConsoleView.get());
}
#endif
}
@ -351,15 +383,109 @@ void MainWindow::setTimelineVisibility(bool visible)
void MainWindow::popTimeline()
{
Preferences& preferences = Preferences::instance();
if (!preferences.general.autoshowTimeline())
if (!Preferences::instance().general.autoshowTimeline())
return;
if (!getTimelineVisibility())
setTimelineVisibility(true);
}
void MainWindow::setDefaultLayout()
{
m_timelineResizeConn.disconnect();
m_colorBarResizeConn.disconnect();
const auto colorBarWidth = get_config_double(kLegacyLayoutMainWindowSection,
kLegacyLayoutColorBarSplitter,
m_colorBar->sizeHint().w);
m_customizableDock->resetDocks();
m_customizableDock->dock(ui::LEFT, m_colorBar.get(), gfx::Size(colorBarWidth, 0));
m_customizableDock->dock(ui::BOTTOM, m_statusBar.get());
m_customizableDock->center()->dock(ui::TOP, m_contextBar.get());
m_customizableDock->center()->dock(ui::RIGHT, m_toolBar.get());
const auto timelineSplitterPos =
get_config_double(kLegacyLayoutMainWindowSection, kLegacyLayoutTimelineSplitter, 75.0) / 100.0;
const auto timelinePos = Preferences::instance().general.timelinePosition();
// We calculate a estimate of the workspace bounds (as we don't yet
// know its size, because we're just constructing the dock where the
// workspace will be inside).
const int kLegacySplitterSeparation = 3 * ui::guiscale();
auto workspaceBounds = bounds();
workspaceBounds.w -= colorBarWidth + m_toolBar->sizeHint().w + 2 * kLegacySplitterSeparation;
workspaceBounds.h -= m_menuBar->sizeHint().h + m_tabsBar->sizeHint().h +
m_contextBar->sizeHint().h + m_statusBar->sizeHint().h;
int timelineSide;
gfx::Size timelineSize(75, 75);
switch (timelinePos) {
case gen::TimelinePosition::LEFT:
timelineSide = ui::LEFT;
timelineSize.w = (workspaceBounds.w * (1.0 - timelineSplitterPos));
break;
case gen::TimelinePosition::RIGHT:
timelineSide = ui::RIGHT;
timelineSize.w = (workspaceBounds.w * (1.0 - timelineSplitterPos));
break;
default:
case gen::TimelinePosition::BOTTOM:
timelineSide = ui::BOTTOM;
timelineSize.h = (workspaceBounds.h * (1.0 - timelineSplitterPos));
break;
}
// Timeline config
m_customizableDock->center()->center()->dock(timelineSide,
m_timeline.get(),
timelineSize.createUnion(gfx::Size(64, 64)));
m_customizableDock->center()->center()->dock(ui::CENTER, m_workspace.get());
configureWorkspaceLayout();
}
void MainWindow::setMirroredDefaultLayout()
{
m_timelineResizeConn.disconnect();
m_colorBarResizeConn.disconnect();
auto colorBarWidth = get_config_double(kLegacyLayoutMainWindowSection,
kLegacyLayoutColorBarSplitter,
m_colorBar->sizeHint().w);
m_customizableDock->resetDocks();
m_customizableDock->dock(ui::RIGHT, m_colorBar.get(), gfx::Size(colorBarWidth, 0));
m_customizableDock->dock(ui::BOTTOM, m_statusBar.get());
m_customizableDock->center()->dock(ui::TOP, m_contextBar.get());
m_customizableDock->center()->dock(ui::LEFT, m_toolBar.get());
m_customizableDock->center()->center()->dock(ui::BOTTOM,
m_timeline.get(),
gfx::Size(64 * guiscale(), 64 * guiscale()));
m_customizableDock->center()->center()->dock(ui::CENTER, m_workspace.get());
configureWorkspaceLayout();
}
void MainWindow::loadUserLayout(const Layout* layout)
{
m_timelineResizeConn.disconnect();
m_colorBarResizeConn.disconnect();
m_customizableDock->resetDocks();
if (!layout->loadLayout(m_customizableDock)) {
LOG(WARNING, "Layout %s failed to load, resetting to default.\n", layout->id().c_str());
setDefaultLayout();
}
this->layout();
}
void MainWindow::setCustomizeDock(bool enable)
{
m_customizableDock->setCustomizing(enable);
}
void MainWindow::dataRecoverySessionsAreReady()
{
getHomeView()->dataRecoverySessionsAreReady();
@ -375,24 +501,37 @@ 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);
auto* theme = static_cast<skin::SkinTheme*>(this->theme());
m_dock->setBgColor(theme->colors.windowFace());
m_customizableDock->setBgColor(theme->colors.workspace());
}
void MainWindow::onResize(ui::ResizeEvent& ev)
{
app::gen::MainWindow::onResize(ev);
ui::Window::onResize(ev);
// Load default or user-selected layout after the first resize event
// is received.
if (m_firstResize) {
m_firstResize = false;
// If the layout is defined in the user layouts file, we loaded it
// (it can be a modified default/mirrored layout).
if (LayoutPtr layout = m_layoutSelector->activeLayout()) {
loadUserLayout(layout.get());
}
else if (m_layoutSelector->activeLayoutId() == Layout::kMirroredDefault) {
setMirroredDefaultLayout();
}
else {
setDefaultLayout();
}
}
os::Window* nativeWindow = (display() ? display()->nativeWindow() : nullptr);
if (nativeWindow && nativeWindow->screen()) {
@ -405,7 +544,8 @@ void MainWindow::onResize(ui::ResizeEvent& ev)
if ((scale > 2) && (!m_scalePanic)) {
const gfx::Size wa = nativeWindow->screen()->workarea().size();
if ((wa.w / scale < 256 || wa.h / scale < 256)) {
showNotification(m_scalePanic = new ScreenScalePanic);
m_scalePanic = std::make_unique<ScreenScalePanic>();
showNotification(m_scalePanic.get());
}
}
}
@ -421,6 +561,11 @@ void MainWindow::onBeforeViewChange()
// inform to the UIContext that the current view has changed.
void MainWindow::onActiveViewChange()
{
// If we are closing the app, we just ignore all view changes (as
// docs will be destroyed and views closed).
if (get_app_state() != AppState::kNormal || !m_dock)
return;
// First we have to configure the MainWindow layout (e.g. show
// Timeline if needed) as UIContext::setActiveView() will configure
// several widgets (calling updateUsingEditor() functions) using the
@ -508,7 +653,7 @@ void MainWindow::onContextMenuTab(Tabs* tabs, TabView* tabView)
WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
ASSERT(view);
if (view)
view->onTabPopup(m_workspace);
view->onTabPopup(m_workspace.get());
}
void MainWindow::onTabsContainerDoubleClicked(Tabs* tabs)
@ -588,71 +733,80 @@ DropTabResult MainWindow::onDropTab(Tabs* tabs,
void MainWindow::configureWorkspaceLayout()
{
// First layout to get the bounds of some widgets
layout();
const auto& pref = Preferences::instance();
bool normal = (m_mode == NormalMode);
bool showMenu = normal;
bool isDoc = (getDocView() != nullptr);
if (os::System::instance()->menus() == nullptr || pref.general.showMenuBar()) {
m_menuBar->resetMaxSize();
}
else {
m_menuBar->setMaxSize(gfx::Size(0, 0));
}
if (os::System::instance()->menus() && !pref.general.showMenuBar())
showMenu = false;
m_menuBar->setVisible(normal);
m_menuBar->setVisible(showMenu);
m_notifications->setVisible(normal && m_notifications->hasNotifications());
m_tabsBar->setVisible(normal);
colorBarPlaceholder()->setVisible(normal && isDoc);
m_colorBar->setVisible(normal && isDoc);
m_colorBarResizeConn = m_customizableDock->Resize.connect(&MainWindow::saveColorBarConfiguration,
this);
m_toolBar->setVisible(normal && isDoc);
m_statusBar->setVisible(normal);
m_contextBar->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode));
// Configure timeline
{
auto timelinePosition = pref.general.timelinePosition();
bool invertWidgets = false;
int align = VERTICAL;
switch (timelinePosition) {
case gen::TimelinePosition::LEFT:
align = HORIZONTAL;
invertWidgets = true;
break;
case gen::TimelinePosition::RIGHT: align = HORIZONTAL; break;
case gen::TimelinePosition::BOTTOM: break;
if (m_timeline && m_timeline->parent()) {
m_timelineResizeConn = dynamic_cast<Dock*>(m_timeline->parent())
->Resize.connect(&MainWindow::saveTimelineConfiguration, this);
}
timelineSplitter()->setAlign(align);
timelinePlaceholder()->setVisible(
isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode) &&
m_timeline->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode) &&
pref.general.visibleTimeline());
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());
}
if (m_contextBar->isVisible()) {
m_contextBar->updateForActiveTool();
}
layout();
invalidate();
}
void MainWindow::saveTimelineConfiguration()
{
const auto& pref = Preferences::instance();
const gfx::Rect timelineBounds = m_timeline->bounds();
const gfx::Rect workspaceBounds = m_customizableDock->center()->center()->bounds();
auto timelinePosition = pref.general.timelinePosition();
double timelineSplitterPos = 0.75;
switch (timelinePosition) {
case gen::TimelinePosition::LEFT:
case gen::TimelinePosition::RIGHT:
timelineSplitterPos = 1.0 - double(timelineBounds.w) / workspaceBounds.w;
break;
case gen::TimelinePosition::BOTTOM:
timelineSplitterPos = 1.0 - double(timelineBounds.h) / workspaceBounds.h;
break;
}
set_config_double(kLegacyLayoutMainWindowSection,
kLegacyLayoutTimelineSplitter,
std::clamp(timelineSplitterPos * 100.0, 1.0, 99.0));
}
void MainWindow::saveColorBarConfiguration()
{
set_config_double(kLegacyLayoutMainWindowSection,
kLegacyLayoutColorBarSplitter,
m_colorBar->bounds().w);
}
void MainWindow::saveActiveLayout()
{
ASSERT(m_layoutSelector);
auto id = m_layoutSelector->activeLayoutId();
auto layout = Layout::MakeFromDock(id, id, m_customizableDock);
m_layoutSelector->updateActiveLayout(layout);
}
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -10,9 +10,10 @@
#pragma once
#include "app/ui/tabs.h"
#include "obs/connection.h"
#include "ui/window.h"
#include "main_window.xml.h"
#include <memory>
namespace ui {
class Splitter;
@ -30,13 +31,17 @@ class ColorBar;
class ContextBar;
class DevConsoleView;
class DocView;
class Dock;
class HomeView;
class INotificationDelegate;
class Layout;
class LayoutSelector;
class MainMenuBar;
class Notifications;
class PreviewEditorWindow;
class StatusBar;
class Timeline;
class ToolBar;
class Workspace;
class WorkspaceTabs;
@ -44,7 +49,7 @@ namespace crash {
class DataRecovery;
}
class MainWindow : public app::gen::MainWindow,
class MainWindow : public ui::Window,
public TabsDelegate {
public:
enum Mode { NormalMode, ContextBarAndTimelineMode, EditorOnlyMode };
@ -52,13 +57,17 @@ public:
MainWindow();
~MainWindow();
MainMenuBar* getMenuBar() { return m_menuBar; }
ContextBar* getContextBar() { return m_contextBar; }
StatusBar* statusBar() { return m_statusBar; }
WorkspaceTabs* getTabsBar() { return m_tabsBar; }
Timeline* getTimeline() { return m_timeline; }
Workspace* getWorkspace() { return m_workspace; }
PreviewEditorWindow* getPreviewEditor() { return m_previewEditor; }
// TODO refactor: remove the get prefix from these functions
MainMenuBar* getMenuBar() { return m_menuBar.get(); }
LayoutSelector* layoutSelector() { return m_layoutSelector.get(); }
ContextBar* getContextBar() { return m_contextBar.get(); }
StatusBar* statusBar() { return m_statusBar.get(); }
WorkspaceTabs* getTabsBar() { return m_tabsBar.get(); }
Timeline* getTimeline() { return m_timeline.get(); }
Workspace* getWorkspace() { return m_workspace.get(); }
ColorBar* colorBar() { return m_colorBar.get(); }
ToolBar* toolBar() { return m_toolBar.get(); }
PreviewEditorWindow* getPreviewEditor() { return m_previewEditor.get(); }
#ifdef ENABLE_UPDATER
CheckUpdateDelegate* getCheckUpdateDelegate();
#endif
@ -83,6 +92,12 @@ public:
void setTimelineVisibility(bool visible);
void popTimeline();
void setDefaultLayout();
void setMirroredDefaultLayout();
void loadUserLayout(const Layout* layout);
Dock* customizableDock() { return m_customizableDock; }
void setCustomizeDock(bool enable);
// When crash::DataRecovery finish to search for sessions, this
// function is called.
void dataRecoverySessionsAreReady();
@ -109,7 +124,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();
@ -121,25 +135,36 @@ private:
DocView* getDocView();
HomeView* getHomeView();
void configureWorkspaceLayout();
void saveTimelineConfiguration();
void saveColorBarConfiguration();
void saveActiveLayout();
ui::TooltipManager* m_tooltipManager;
MainMenuBar* m_menuBar;
StatusBar* m_statusBar;
ColorBar* m_colorBar;
ContextBar* m_contextBar;
ui::Widget* m_toolBar;
WorkspaceTabs* m_tabsBar;
Dock* m_dock;
Dock* m_customizableDock;
std::unique_ptr<Widget> m_customizableDockPlaceholder;
std::unique_ptr<MainMenuBar> m_menuBar;
std::unique_ptr<Notifications> m_notifications;
std::unique_ptr<LayoutSelector> m_layoutSelector;
std::unique_ptr<StatusBar> m_statusBar;
std::unique_ptr<ColorBar> m_colorBar;
std::unique_ptr<ContextBar> m_contextBar;
std::unique_ptr<ToolBar> m_toolBar;
std::unique_ptr<WorkspaceTabs> m_tabsBar;
Mode m_mode;
Timeline* m_timeline;
Workspace* m_workspace;
PreviewEditorWindow* m_previewEditor;
HomeView* m_homeView;
Notifications* m_notifications;
INotificationDelegate* m_scalePanic;
BrowserView* m_browserView;
std::unique_ptr<Timeline> m_timeline;
std::unique_ptr<Workspace> m_workspace;
std::unique_ptr<PreviewEditorWindow> m_previewEditor;
std::unique_ptr<HomeView> m_homeView;
std::unique_ptr<INotificationDelegate> m_scalePanic;
std::unique_ptr<BrowserView> m_browserView;
#ifdef ENABLE_SCRIPTING
DevConsoleView* m_devConsoleView;
std::unique_ptr<DevConsoleView> m_devConsoleView;
#endif
obs::scoped_connection m_timelineResizeConn;
obs::scoped_connection m_colorBarResizeConn;
obs::scoped_connection m_saveDockLayoutConn;
bool m_firstResize = true;
};
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2023 Igara Studio S.A.
// Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -20,6 +20,7 @@
namespace app {
using namespace app::skin;
using namespace ui;
class NotificationItem : public MenuItem {
@ -39,10 +40,7 @@ private:
INotificationDelegate* m_delegate;
};
Notifications::Notifications()
: Button("")
, m_flagStyle(skin::SkinTheme::get(this)->styles.flag())
, m_red(false)
Notifications::Notifications() : Button(""), m_red(false)
{
}
@ -52,13 +50,22 @@ void Notifications::addLink(INotificationDelegate* del)
m_red = true;
}
void Notifications::onInitTheme(InitThemeEvent& ev)
{
Button::onInitTheme(ev);
m_popup.initTheme();
}
void Notifications::onSizeHint(SizeHintEvent& ev)
{
ev.setSizeHint(gfx::Size(16, 10) * guiscale()); // TODO hard-coded flag size
auto* theme = SkinTheme::get(this);
auto hint = theme->calcSizeHint(this, theme->styles.flag());
ev.setSizeHint(hint);
}
void Notifications::onPaint(PaintEvent& ev)
{
auto* theme = SkinTheme::get(this);
Graphics* g = ev.graphics();
PaintWidgetPartInfo info;
@ -69,7 +76,7 @@ void Notifications::onPaint(PaintEvent& ev)
if (isSelected())
info.styleFlags |= ui::Style::Layer::kSelected;
theme()->paintWidgetPart(g, m_flagStyle, clientBounds(), info);
theme->paintWidgetPart(g, theme->styles.flag(), clientBounds(), info);
}
void Notifications::onClick()

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2023 Igara Studio S.A.
// Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -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,20 +20,24 @@ 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 onInitTheme(ui::InitThemeEvent& ev) override;
void onSizeHint(ui::SizeHintEvent& ev) override;
void onPaint(ui::PaintEvent& ev) override;
void onClick() override;
private:
ui::Style* m_flagStyle;
bool m_red;
ui::Menu m_popup;
};

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,10 @@ public:
void showBackupIcon(BackupIcon icon);
// Dockable impl
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
int dockHandleSide() const override { return ui::LEFT; }
protected:
void onInitTheme(ui::InitThemeEvent& ev) override;
void onResize(ui::ResizeEvent& ev) override;

View File

@ -73,7 +73,8 @@ Tabs::~Tabs()
m_addedTab.reset();
m_removedTab.reset();
// Stop animation
// Stop animation, can cause issues with docks when stopping during close.
if (!is_app_state_closing())
stopAnimation();
// Remove all tabs

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

@ -94,9 +94,6 @@ ToolBar::ToolBar() : Widget(kGenericWidget), m_openedRecently(false), m_tipTimer
m_hotTool = NULL;
m_hotIndex = NoneIndex;
m_openOnHot = false;
m_popupWindow = NULL;
m_currentStrip = NULL;
m_tipWindow = NULL;
m_tipOpened = false;
m_minHeight = 0;
@ -112,9 +109,6 @@ ToolBar::ToolBar() : Widget(kGenericWidget), m_openedRecently(false), m_tipTimer
ToolBar::~ToolBar()
{
App::instance()->activeToolManager()->remove_observer(this);
delete m_popupWindow;
delete m_tipWindow;
}
bool ToolBar::isToolVisible(Tool* tool)
@ -314,10 +308,14 @@ void ToolBar::onSizeHint(SizeHintEvent& ev)
iconsize.h += border().height();
ev.setSizeHint(iconsize);
#if 0 // The Dock widget will ask for sizeHint() of this widget when
// we open the popup, so we cannot close the recently closed
// popup.
if (m_popupWindow) {
closePopupWindow();
closeTipWindow();
}
#endif
}
void ToolBar::onResize(ui::ResizeEvent& ev)
@ -401,6 +399,11 @@ void ToolBar::onVisible(bool visible)
}
}
bool ToolBar::isDockedAtLeftSide() const
{
return bounds().center().x < window()->bounds().center().x;
}
int ToolBar::getToolGroupIndex(ToolGroup* group)
{
ToolBox* toolbox = App::instance()->toolBox();
@ -465,7 +468,7 @@ void ToolBar::openPopupWindow(GroupType group_type, int group_index, tools::Tool
// In case this tool contains more than just one tool, show the popup window
m_openOnHot = true;
m_popupWindow = new TransparentPopupWindow(
m_popupWindow = std::make_unique<TransparentPopupWindow>(
PopupWindow::ClickBehavior::CloseOnClickOutsideHotRegion);
m_closeConn = m_popupWindow->Close.connect([this] { onClosePopup(); });
m_openedRecently = true;
@ -474,19 +477,23 @@ void ToolBar::openPopupWindow(GroupType group_type, int group_index, tools::Tool
m_currentStrip = toolstrip;
m_popupWindow->addChild(toolstrip);
const int borderWidth = border().width();
Rect rc = getToolGroupBounds(group_index);
int w = 0;
int w = borderWidth;
for (const auto* tool : tools) {
(void)tool;
w += bounds().w - border().width() - 1 * guiscale();
w += bounds().w - borderWidth - 1 * guiscale();
}
rc.x -= w;
if (isDockedAtLeftSide())
rc.x = bounds().x2() - borderWidth;
else
rc.x -= w - borderWidth;
rc.w = w;
// Set hotregion of popup window
m_popupWindow->setAutoRemap(false);
ui::fit_bounds(display(), m_popupWindow, rc);
ui::fit_bounds(display(), m_popupWindow.get(), rc);
m_popupWindow->setBounds(rc);
Region rgn(m_popupWindow->boundsOnScreen().enlarge(16 * guiscale()));
@ -500,8 +507,7 @@ void ToolBar::closePopupWindow()
{
if (m_popupWindow) {
m_popupWindow->closeWindow(nullptr);
delete m_popupWindow;
m_popupWindow = nullptr;
m_popupWindow.reset();
}
}
@ -542,7 +548,7 @@ Point ToolBar::getToolPositionInGroup(const Tool* tool) const
const auto& tools = m_currentStrip->tools();
const int nth = std::find(tools.begin(), tools.end(), tool) - tools.begin();
return Point(iconsize.w / 2 + nth * (iconsize.w - 1 * guiscale()), iconsize.h);
return Point(iconsize.w / 2 + nth * (iconsize.w - border().right()), iconsize.h);
}
void ToolBar::openTipWindow(ToolGroup* tool_group, Tool* tool)
@ -591,15 +597,31 @@ void ToolBar::openTipWindow(int group_index, Tool* tool)
else
return;
m_tipWindow = new TipWindow(tooltip);
m_tipWindow = std::make_unique<TipWindow>(tooltip);
m_tipWindow->remapWindow();
Rect toolrc = getToolGroupBounds(group_index);
Point arrow = (tool ? getToolPositionInGroup(tool) : Point(0, 0));
if (tool && m_popupWindow && m_popupWindow->isVisible())
toolrc.x += arrow.x - m_popupWindow->bounds().w;
m_tipWindow->pointAt(TOP | RIGHT, toolrc, ui::Manager::getDefault()->display());
int pointAt = TOP;
if (isDockedAtLeftSide()) {
pointAt |= LEFT;
toolrc.x -= border().width();
}
else {
pointAt |= RIGHT;
toolrc.x += border().width();
}
// Tooltip for subtools (tools inside groups)
if (tool && m_popupWindow && m_popupWindow->isVisible()) {
if (isDockedAtLeftSide())
toolrc.x += arrow.x + bounds().w - border().width();
else
toolrc.x += arrow.x - m_popupWindow->bounds().w + border().width();
}
m_tipWindow->pointAt(pointAt, toolrc, ui::Manager::getDefault()->display());
if (m_tipOpened)
m_tipWindow->openWindow();
@ -613,8 +635,7 @@ void ToolBar::closeTipWindow()
if (m_tipWindow) {
m_tipWindow->closeWindow(NULL);
delete m_tipWindow;
m_tipWindow = NULL;
m_tipWindow.reset();
}
}
@ -651,7 +672,7 @@ void ToolBar::onClosePopup()
m_openOnHot = false;
m_hotTool = NULL;
m_hotIndex = NoneIndex;
m_currentStrip = NULL;
m_currentStrip = nullptr;
invalidate();
}

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"
@ -17,6 +18,7 @@
#include "ui/widget.h"
#include <map>
#include <memory>
namespace ui {
class CloseEvent;
@ -32,6 +34,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 +55,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;
@ -62,6 +73,7 @@ protected:
private:
enum class GroupType { Regular, Overflow };
bool isDockedAtLeftSide() const;
int getToolGroupIndex(tools::ToolGroup* group);
void openPopupWindow(GroupType group_type,
int group_index = 0,
@ -95,12 +107,12 @@ private:
bool m_openedRecently;
// Window displayed to show a tool-group
ui::PopupWindow* m_popupWindow;
std::unique_ptr<ui::PopupWindow> m_popupWindow;
class ToolStrip;
ToolStrip* m_currentStrip;
ToolStrip* m_currentStrip = nullptr;
// Tool-tip window
ui::TipWindow* m_tipWindow;
std::unique_ptr<ui::TipWindow> m_tipWindow;
ui::Timer m_tipTimer;
bool m_tipOpened;

View File

@ -19,7 +19,8 @@ namespace app {
class WorkspaceTabs;
class Workspace : public ui::Widget,
public app::InputChainElement {
public app::InputChainElement,
public Dockable {
public:
typedef WorkspaceViews::iterator iterator;
@ -75,6 +76,11 @@ public:
WorkspacePanel* mainPanel() { return &m_mainPanel; }
// Dockable impl
int dockableAt() const override { return 0; }
int dockHandleSide() const override { return 0; } // No handles
// Signals
obs::signal<void()> BeforeViewChanged;
obs::signal<void()> ActiveViewChanged;

View File

@ -20,6 +20,10 @@ public:
WorkspaceTabs(TabsDelegate* tabsDelegate);
~WorkspaceTabs();
// Dockable impl
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
int dockHandleSide() const override { return ui::LEFT; }
WorkspacePanel* panel() const { return m_panel; }
void setPanel(WorkspacePanel* panel);

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -38,6 +38,8 @@ public:
Items::iterator begin() { return m_items.begin(); }
Items::iterator end() { return m_items.end(); }
bool empty() const { return m_items.empty(); }
const Items& items() { return m_items; }
void setEditable(bool state);
void setClickOpen(bool state);

View File

@ -218,16 +218,16 @@ bool TipWindow::pointAt(int arrowAlign, const gfx::Rect& target, const ui::Displ
if (get_multiple_displays()) {
const gfx::Rect waBounds = nativeParentWindow->screen()->workarea();
gfx::Point pt = nativeParentWindow->pointToScreen(gfx::Point(x, y));
pt.x = std::clamp(pt.x, waBounds.x, waBounds.x2() - w);
pt.y = std::clamp(pt.y, waBounds.y, waBounds.y2() - h);
pt.x = std::clamp(pt.x, waBounds.x, std::max(waBounds.x, waBounds.x2() - w));
pt.y = std::clamp(pt.y, waBounds.y, std::max(waBounds.y, waBounds.y2() - h));
pt = nativeParentWindow->pointFromScreen(pt);
x = pt.x;
y = pt.y;
}
else {
const gfx::Rect displayBounds = display->bounds();
x = std::clamp(x, displayBounds.x, displayBounds.x2() - w);
y = std::clamp(y, displayBounds.y, displayBounds.y2() - h);
x = std::clamp(x, displayBounds.x, std::max(displayBounds.x, displayBounds.x2() - w));
y = std::clamp(y, displayBounds.y, std::max(displayBounds.y, displayBounds.y2() - h));
}
if (m_target.intersects(gfx::Rect(x, y, w, h))) {