mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-14 13:21:34 +00:00
Merge f3b7836102c611d5f27440a240655870a730e6a0 into 5739bfe28707c6722fe0e7e8e75a23e6fbf53538
This commit is contained in:
commit
88c2137e17
@ -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">
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
@ -1157,6 +1158,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
|
||||
@ -1198,6 +1200,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:
|
||||
@ -1226,6 +1236,11 @@ name = Name:
|
||||
tileset = Tileset:
|
||||
default_new_layer_name = New Layer
|
||||
|
||||
[new_layout]
|
||||
title = New Workspace Layout
|
||||
name = Name:
|
||||
default_name = User Layout {}
|
||||
|
||||
[news_listbox]
|
||||
more = More...
|
||||
problem_loading = Problems loading news. Please retry.
|
||||
|
@ -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>
|
19
data/widgets/new_layout.xml
Normal file
19
data/widgets/new_layout.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2022 by Igara Studio S.A. -->
|
||||
<gui>
|
||||
<window id="new_layout" text="@.title">
|
||||
<vbox>
|
||||
<hbox>
|
||||
<label text="@.name" />
|
||||
<vbox id="name_placeholder" expansive="true" />
|
||||
</hbox>
|
||||
|
||||
<separator horizontal="true" />
|
||||
|
||||
<hbox homogeneous="true" cell_align="right">
|
||||
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
|
||||
<button text="@general.cancel" closewindow="true" />
|
||||
</hbox>
|
||||
</vbox>
|
||||
</window>
|
||||
</gui>
|
@ -521,6 +521,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
|
||||
@ -607,6 +608,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
|
||||
@ -651,6 +653,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
|
||||
|
@ -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)
|
||||
|
47
src/app/commands/toggle_workspace_layout.cpp
Normal file
47
src/app/commands/toggle_workspace_layout.cpp
Normal 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
|
@ -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()) {
|
||||
|
@ -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);
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2020-2021 Igara Studio S.A.
|
||||
// Copyright (C) 2020-2022 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -53,7 +53,8 @@ ConfigureTimelinePopup::ConfigureTimelinePopup()
|
||||
m_box = new app::gen::TimelineConf();
|
||||
addChild(m_box);
|
||||
|
||||
m_box->position()->ItemChange.connect([this] { onChangePosition(); });
|
||||
m_box->position()->ItemChange.connect(
|
||||
[this] { onChangeTimelinePosition(m_box->position()->selectedItem()); });
|
||||
m_box->firstFrame()->Change.connect([this] { onChangeFirstFrame(); });
|
||||
m_box->merge()->Click.connect([this] { onChangeType(); });
|
||||
m_box->tint()->Click.connect([this] { onChangeType(); });
|
||||
@ -143,12 +144,12 @@ bool ConfigureTimelinePopup::onProcessMessage(ui::Message* msg)
|
||||
return PopupWindow::onProcessMessage(msg);
|
||||
}
|
||||
|
||||
void ConfigureTimelinePopup::onChangePosition()
|
||||
void ConfigureTimelinePopup::onChangeTimelinePosition(int option)
|
||||
{
|
||||
gen::TimelinePosition newTimelinePos = gen::TimelinePosition::BOTTOM;
|
||||
|
||||
int selITem = m_box->position()->selectedItem();
|
||||
switch (selITem) {
|
||||
int selItem = option;
|
||||
switch (selItem) {
|
||||
case 0: newTimelinePos = gen::TimelinePosition::LEFT; break;
|
||||
case 1: newTimelinePos = gen::TimelinePosition::RIGHT; break;
|
||||
case 2: newTimelinePos = gen::TimelinePosition::BOTTOM; break;
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2022 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -30,9 +31,10 @@ class ConfigureTimelinePopup : public ui::PopupWindow {
|
||||
public:
|
||||
ConfigureTimelinePopup();
|
||||
|
||||
static void onChangeTimelinePosition(int option);
|
||||
|
||||
protected:
|
||||
bool onProcessMessage(ui::Message* msg) override;
|
||||
void onChangePosition();
|
||||
void onChangeFirstFrame();
|
||||
void onChangeType();
|
||||
void onOpacity();
|
||||
|
@ -2019,6 +2019,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());
|
||||
|
@ -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();
|
||||
@ -99,6 +101,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;
|
||||
|
693
src/app/ui/dock.cpp
Normal file
693
src/app/ui/dock.cpp
Normal file
@ -0,0 +1,693 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2021-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/dock.h"
|
||||
|
||||
#include "app/ui/dockable.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
#include "ui/cursor_type.h"
|
||||
#include "ui/message.h"
|
||||
#include "ui/paint_event.h"
|
||||
#include "ui/resize_event.h"
|
||||
#include "ui/scale.h"
|
||||
#include "ui/size_hint_event.h"
|
||||
#include "ui/system.h"
|
||||
#include "ui/widget.h"
|
||||
|
||||
namespace app {
|
||||
|
||||
using namespace app::skin;
|
||||
using namespace ui;
|
||||
|
||||
namespace {
|
||||
|
||||
enum { kTopIndex, kBottomIndex, kLeftIndex, kRightIndex, kCenterIndex };
|
||||
|
||||
int side_index(int side)
|
||||
{
|
||||
switch (side) {
|
||||
case ui::TOP: return kTopIndex;
|
||||
case ui::BOTTOM: return kBottomIndex;
|
||||
case ui::LEFT: return kLeftIndex;
|
||||
case ui::RIGHT: return kRightIndex;
|
||||
}
|
||||
return kCenterIndex; // ui::CENTER
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
void DockTabs::onSizeHint(ui::SizeHintEvent& ev)
|
||||
{
|
||||
gfx::Size sz;
|
||||
for (auto child : children()) {
|
||||
if (child->isVisible())
|
||||
sz |= child->sizeHint();
|
||||
}
|
||||
sz.h += textHeight();
|
||||
ev.setSizeHint(sz);
|
||||
}
|
||||
|
||||
void DockTabs::onResize(ui::ResizeEvent& ev)
|
||||
{
|
||||
auto bounds = ev.bounds();
|
||||
setBoundsQuietly(bounds);
|
||||
bounds = childrenBounds();
|
||||
bounds.y += textHeight();
|
||||
bounds.h -= textHeight();
|
||||
|
||||
for (auto child : children()) {
|
||||
child->setBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
void DockTabs::onPaint(ui::PaintEvent& ev)
|
||||
{
|
||||
Graphics* g = ev.graphics();
|
||||
g->fillRect(gfx::rgba(0, 0, 255), clientBounds());
|
||||
}
|
||||
|
||||
Dock::Dock()
|
||||
{
|
||||
for (int i = 0; i < kSides; ++i) {
|
||||
m_sides[i] = nullptr;
|
||||
m_aligns[i] = 0;
|
||||
m_sizes[i] = gfx::Size(0, 0);
|
||||
}
|
||||
|
||||
InitTheme.connect([this] {
|
||||
if (auto p = parent())
|
||||
setBgColor(p->bgColor());
|
||||
});
|
||||
initTheme();
|
||||
}
|
||||
|
||||
void Dock::setCustomizing(bool enable, bool doLayout)
|
||||
{
|
||||
m_customizing = enable;
|
||||
|
||||
for (int i = 0; i < kSides; ++i) {
|
||||
auto child = m_sides[i];
|
||||
if (!child)
|
||||
continue;
|
||||
else 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;
|
||||
else if (auto subdock = dynamic_cast<Dock*>(child)) {
|
||||
subdock->resetDocks();
|
||||
if (subdock->m_autoDelete)
|
||||
delete subdock;
|
||||
}
|
||||
else if (auto tabs = dynamic_cast<DockTabs*>(child)) {
|
||||
for (auto child2 : tabs->children()) {
|
||||
if (auto subdock2 = dynamic_cast<Dock*>(child2)) {
|
||||
subdock2->resetDocks();
|
||||
if (subdock2->m_autoDelete)
|
||||
delete subdock2;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_sides[i] = nullptr;
|
||||
}
|
||||
removeAllChildren();
|
||||
}
|
||||
|
||||
void Dock::dock(int side, ui::Widget* widget, const gfx::Size& prefSize)
|
||||
{
|
||||
ASSERT(widget);
|
||||
|
||||
const int i = side_index(side);
|
||||
if (!m_sides[i]) {
|
||||
setSide(i, widget);
|
||||
addChild(widget);
|
||||
|
||||
if (prefSize != gfx::Size(0, 0))
|
||||
m_sizes[i] = prefSize;
|
||||
}
|
||||
else if (auto subdock = dynamic_cast<Dock*>(m_sides[i])) {
|
||||
subdock->dock(CENTER, widget, prefSize);
|
||||
}
|
||||
else if (auto tabs = dynamic_cast<DockTabs*>(m_sides[i])) {
|
||||
tabs->addChild(widget);
|
||||
}
|
||||
// If this side already contains a widget, we create a DockTabs in
|
||||
// this side.
|
||||
else {
|
||||
auto oldWidget = m_sides[i];
|
||||
auto newTabs = new DockTabs;
|
||||
replaceChild(oldWidget, newTabs);
|
||||
newTabs->addChild(oldWidget);
|
||||
newTabs->addChild(widget);
|
||||
setSide(i, newTabs);
|
||||
}
|
||||
}
|
||||
|
||||
void Dock::dockRelativeTo(ui::Widget* relative,
|
||||
int side,
|
||||
ui::Widget* widget,
|
||||
const gfx::Size& prefSize)
|
||||
{
|
||||
ASSERT(relative);
|
||||
|
||||
Widget* parent = relative->parent();
|
||||
ASSERT(parent);
|
||||
|
||||
Dock* subdock = new Dock;
|
||||
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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (parentDock != this && parentDock->children().empty()) {
|
||||
undock(parentDock);
|
||||
}
|
||||
}
|
||||
else if (auto parentTabs = dynamic_cast<DockTabs*>(parent)) {
|
||||
parentTabs->removeChild(widget);
|
||||
|
||||
if (parentTabs->children().empty()) {
|
||||
undock(parentTabs);
|
||||
}
|
||||
}
|
||||
else {
|
||||
parent->removeChild(widget);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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];
|
||||
else
|
||||
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 sz = border().size();
|
||||
|
||||
if (hasVisibleSide(kLeftIndex))
|
||||
sz.w += m_sides[kLeftIndex]->sizeHint().w + childSpacing();
|
||||
if (hasVisibleSide(kRightIndex))
|
||||
sz.w += m_sides[kRightIndex]->sizeHint().w + childSpacing();
|
||||
if (hasVisibleSide(kTopIndex))
|
||||
sz.h += m_sides[kTopIndex]->sizeHint().h + childSpacing();
|
||||
if (hasVisibleSide(kBottomIndex))
|
||||
sz.h += m_sides[kBottomIndex]->sizeHint().h + childSpacing();
|
||||
if (hasVisibleSide(kCenterIndex))
|
||||
sz += m_sides[kCenterIndex]->sizeHint();
|
||||
|
||||
ev.setSizeHint(sz);
|
||||
}
|
||||
|
||||
void Dock::onResize(ui::ResizeEvent& ev)
|
||||
{
|
||||
auto bounds = ev.bounds();
|
||||
setBoundsQuietly(bounds);
|
||||
bounds = childrenBounds();
|
||||
|
||||
updateDockVisibility();
|
||||
|
||||
forEachSide(bounds,
|
||||
[this](ui::Widget* widget,
|
||||
const gfx::Rect& widgetBounds,
|
||||
const gfx::Rect& separator,
|
||||
const int index) {
|
||||
auto 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();
|
||||
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) {
|
||||
auto rc = widgetBounds;
|
||||
auto th = textHeight();
|
||||
if (isCustomizing()) {
|
||||
auto theme = SkinTheme::get(this);
|
||||
auto 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());
|
||||
}
|
||||
|
||||
bool Dock::onProcessMessage(ui::Message* msg)
|
||||
{
|
||||
switch (msg->type()) {
|
||||
case kMouseDownMessage: {
|
||||
const gfx::Point pos = static_cast<MouseMessage*>(msg)->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)
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case kMouseMoveMessage: {
|
||||
if (hasCapture()) {
|
||||
if (m_hit.sideIndex >= 0) {
|
||||
const gfx::Point pos = static_cast<MouseMessage*>(msg)->position();
|
||||
gfx::Size& sz = m_sizes[m_hit.sideIndex];
|
||||
|
||||
switch (m_hit.sideIndex) {
|
||||
case kTopIndex: sz.h = (m_startSize.h + pos.y - m_startPos.y); break;
|
||||
case kBottomIndex: sz.h = (m_startSize.h - pos.y + m_startPos.y); break;
|
||||
case kLeftIndex: sz.w = (m_startSize.w + pos.x - m_startPos.x); break;
|
||||
case kRightIndex: sz.w = (m_startSize.w - pos.x + m_startPos.x); break;
|
||||
}
|
||||
|
||||
layout();
|
||||
Resize();
|
||||
}
|
||||
else if (m_hit.dockable) {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case kMouseUpMessage: {
|
||||
if (hasCapture()) {
|
||||
releaseMouse();
|
||||
onUserResizedDock();
|
||||
}
|
||||
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) {
|
||||
cursor = ui::kMoveCursor;
|
||||
}
|
||||
|
||||
#if 0
|
||||
m_hit = Hit();
|
||||
forEachSide(
|
||||
childrenBounds(),
|
||||
[this, pos, &cursor](ui::Widget* widget,
|
||||
const gfx::Rect& widgetBounds,
|
||||
const gfx::Rect& separator,
|
||||
const int index) {
|
||||
if (separator.contains(pos)) {
|
||||
m_hit.widget = widget;
|
||||
m_hit.sideIndex = index;
|
||||
|
||||
if (index == kTopIndex || index == kBottomIndex) {
|
||||
cursor = ui::kSizeNSCursor;
|
||||
}
|
||||
else if (index == kLeftIndex || index == kRightIndex) {
|
||||
cursor = ui::kSizeWECursor;
|
||||
}
|
||||
}
|
||||
else if (isCustomizing()) {
|
||||
auto th = textHeight();
|
||||
auto rc = widgetBounds;
|
||||
auto theme = SkinTheme::get(this);
|
||||
auto color = theme->colors.workspaceText();
|
||||
if (auto dockable = dynamic_cast<Dockable*>(widget)) {
|
||||
int handleSide = dockable->dockHandleSide();
|
||||
switch (handleSide) {
|
||||
case ui::TOP:
|
||||
rc.h = th;
|
||||
if (rc.contains(pos)) {
|
||||
cursor = ui::kMoveCursor;
|
||||
m_hit.widget = widget;
|
||||
m_hit.dockable = dockable;
|
||||
}
|
||||
break;
|
||||
case ui::LEFT:
|
||||
rc.w = th;
|
||||
if (rc.contains(pos)) {
|
||||
cursor = ui::kMoveCursor;
|
||||
m_hit.widget = widget;
|
||||
m_hit.dockable = dockable;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
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 tabs = dynamic_cast<DockTabs*>(widget)) {
|
||||
for (auto child : tabs->children()) {
|
||||
if (auto subdock2 = dynamic_cast<Dock*>(widget))
|
||||
align |= subdock2->calcAlign(i);
|
||||
else if (auto dockable = dynamic_cast<Dockable*>(child)) {
|
||||
align = dockable->dockableAt();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (auto dockable2 = dynamic_cast<Dockable*>(widget)) {
|
||||
align = dockable2->dockableAt();
|
||||
}
|
||||
return align;
|
||||
}
|
||||
|
||||
void Dock::updateDockVisibility()
|
||||
{
|
||||
bool visible = false;
|
||||
setVisible(true);
|
||||
for (int i = 0; i < kSides; ++i) {
|
||||
Widget* widget = m_sides[i];
|
||||
if (!widget)
|
||||
continue;
|
||||
|
||||
if (auto subdock = dynamic_cast<Dock*>(widget)) {
|
||||
subdock->updateDockVisibility();
|
||||
}
|
||||
else if (auto tabs = dynamic_cast<DockTabs*>(widget)) {
|
||||
bool visible2 = false;
|
||||
for (auto child : tabs->children()) {
|
||||
if (auto subdock2 = dynamic_cast<Dock*>(widget)) {
|
||||
subdock2->updateDockVisibility();
|
||||
}
|
||||
if (child->isVisible()) {
|
||||
visible2 = true;
|
||||
}
|
||||
}
|
||||
tabs->setVisible(visible2);
|
||||
if (visible2)
|
||||
visible = true;
|
||||
}
|
||||
|
||||
if (widget->isVisible()) {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
setVisible(visible);
|
||||
}
|
||||
|
||||
void Dock::forEachSide(gfx::Rect bounds,
|
||||
std::function<void(ui::Widget* widget,
|
||||
const gfx::Rect& widgetBounds,
|
||||
const gfx::Rect& separator,
|
||||
const int index)> f)
|
||||
{
|
||||
for (int i = 0; i < kSides; ++i) {
|
||||
auto widget = m_sides[i];
|
||||
if (!widget || !widget->isVisible()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int spacing = (m_aligns[i] & EXPANSIVE ? childSpacing() : 0);
|
||||
|
||||
const gfx::Size sz = (m_aligns[i] & EXPANSIVE ? m_sizes[i] : widget->sizeHint());
|
||||
|
||||
gfx::Rect rc, separator;
|
||||
switch (i) {
|
||||
case kTopIndex:
|
||||
rc = gfx::Rect(bounds.x, bounds.y, bounds.w, sz.h);
|
||||
bounds.y += rc.h;
|
||||
bounds.h -= rc.h;
|
||||
|
||||
if (spacing > 0) {
|
||||
separator = gfx::Rect(bounds.x, bounds.y, bounds.w, spacing);
|
||||
bounds.y += spacing;
|
||||
bounds.h -= spacing;
|
||||
}
|
||||
break;
|
||||
case kBottomIndex:
|
||||
rc = gfx::Rect(bounds.x, bounds.y2() - sz.h, bounds.w, sz.h);
|
||||
bounds.h -= rc.h;
|
||||
|
||||
if (spacing > 0) {
|
||||
separator = gfx::Rect(bounds.x, bounds.y2() - spacing, bounds.w, spacing);
|
||||
bounds.h -= spacing;
|
||||
}
|
||||
break;
|
||||
case kLeftIndex:
|
||||
rc = gfx::Rect(bounds.x, bounds.y, sz.w, bounds.h);
|
||||
bounds.x += rc.w;
|
||||
bounds.w -= rc.w;
|
||||
|
||||
if (spacing > 0) {
|
||||
separator = gfx::Rect(bounds.x, bounds.y, spacing, bounds.h);
|
||||
bounds.x += spacing;
|
||||
bounds.w -= spacing;
|
||||
}
|
||||
break;
|
||||
case kRightIndex:
|
||||
rc = gfx::Rect(bounds.x2() - sz.w, bounds.y, sz.w, bounds.h);
|
||||
bounds.w -= rc.w;
|
||||
|
||||
if (spacing > 0) {
|
||||
separator = gfx::Rect(bounds.x2() - spacing, bounds.y, spacing, bounds.h);
|
||||
bounds.w -= spacing;
|
||||
}
|
||||
break;
|
||||
case kCenterIndex: rc = bounds; break;
|
||||
}
|
||||
|
||||
f(widget, rc, separator, i);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
auto rc = widgetBounds;
|
||||
auto theme = SkinTheme::get(this);
|
||||
if (auto dockable = dynamic_cast<Dockable*>(widget)) {
|
||||
int handleSide = dockable->dockHandleSide();
|
||||
switch (handleSide) {
|
||||
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
|
114
src/app/ui/dock.h
Normal file
114
src/app/ui/dock.h
Normal file
@ -0,0 +1,114 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2021-2024 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifndef APP_UI_DOCK_H_INCLUDED
|
||||
#define APP_UI_DOCK_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "gfx/rect.h"
|
||||
#include "gfx/size.h"
|
||||
#include "ui/widget.h"
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace app {
|
||||
|
||||
class Dockable;
|
||||
|
||||
class DockTabs : public ui::Widget {
|
||||
public:
|
||||
protected:
|
||||
void onSizeHint(ui::SizeHintEvent& ev) override;
|
||||
void onResize(ui::ResizeEvent& ev) override;
|
||||
void onPaint(ui::PaintEvent& ev) override;
|
||||
};
|
||||
|
||||
class Dock : public ui::Widget {
|
||||
public:
|
||||
static constexpr const int kSides = 5;
|
||||
|
||||
Dock();
|
||||
|
||||
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;
|
||||
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(const int i, ui::Widget* newWidget);
|
||||
int calcAlign(const int i);
|
||||
void updateDockVisibility();
|
||||
void forEachSide(gfx::Rect bounds,
|
||||
std::function<void(ui::Widget* widget,
|
||||
const gfx::Rect& widgetBounds,
|
||||
const gfx::Rect& separator,
|
||||
const int index)> f);
|
||||
|
||||
bool hasVisibleSide(const int i) const { return (m_sides[i] && m_sides[i]->isVisible()); }
|
||||
|
||||
struct Hit {
|
||||
ui::Widget* widget = nullptr;
|
||||
Dockable* dockable = nullptr;
|
||||
int sideIndex = -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;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif
|
37
src/app/ui/dockable.h
Normal file
37
src/app/ui/dockable.h
Normal 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
|
193
src/app/ui/layout.cpp
Normal file
193
src/app/ui/layout.cpp
Normal file
@ -0,0 +1,193 @@
|
||||
// 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:
|
||||
// Empty side attribute
|
||||
break;
|
||||
}
|
||||
|
||||
XMLElement* childElem = elem->InsertNewChildElement("");
|
||||
|
||||
if (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 (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) {
|
||||
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)
|
||||
{
|
||||
auto layout = std::make_shared<Layout>();
|
||||
if (auto name = layoutElem->Attribute("name")) {
|
||||
layout->m_id = name;
|
||||
layout->m_name = name;
|
||||
}
|
||||
layout->m_elem = layoutElem->DeepClone(&layout->m_dummyDoc)->ToElement();
|
||||
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("name", name.c_str());
|
||||
save_dock_layout(layout->m_elem, dock);
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
bool Layout::matchId(const std::string& id) const
|
||||
{
|
||||
if (m_id == id)
|
||||
return true;
|
||||
else if ((m_id.empty() && id == kDefault) || (m_id == kDefault && id.empty()))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Layout::loadLayout(Dock* dock) const
|
||||
{
|
||||
if (!m_elem)
|
||||
return false;
|
||||
|
||||
XMLElement* elem = m_elem->FirstChildElement();
|
||||
while (elem) {
|
||||
load_dock_layout(elem, dock);
|
||||
elem = elem->NextSiblingElement();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace app
|
46
src/app/ui/layout.h
Normal file
46
src/app/ui/layout.h
Normal file
@ -0,0 +1,46 @@
|
||||
// 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 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(const std::string& id) const;
|
||||
bool loadLayout(Dock* dock) const;
|
||||
|
||||
private:
|
||||
std::string m_id;
|
||||
std::string m_name;
|
||||
tinyxml2::XMLDocument m_dummyDoc;
|
||||
tinyxml2::XMLElement* m_elem = nullptr;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif
|
417
src/app/ui/layout_selector.cpp
Normal file
417
src/app/ui/layout_selector.cpp
Normal file
@ -0,0 +1,417 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2021-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_selector.h"
|
||||
|
||||
#include "app/app.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/match_words.h"
|
||||
#include "app/ui/button_set.h"
|
||||
#include "app/ui/configure_timeline_popup.h"
|
||||
#include "app/ui/main_window.h"
|
||||
#include "app/ui/separator_in_view.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
#include "ui/entry.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 Similar ButtonSet to the one in timeline_conf.xml
|
||||
class TimelineButtons : public ButtonSet {
|
||||
public:
|
||||
TimelineButtons() : ButtonSet(2)
|
||||
{
|
||||
addItem(Strings::timeline_conf_left())->processMnemonicFromText();
|
||||
addItem(Strings::timeline_conf_right())->processMnemonicFromText();
|
||||
addItem(Strings::timeline_conf_bottom(), 2)->processMnemonicFromText();
|
||||
|
||||
auto& timelinePosOption = Preferences::instance().general.timelinePosition;
|
||||
|
||||
setSelectedButtonFromTimelinePosition(timelinePosOption());
|
||||
m_timelinePosConn = timelinePosOption.AfterChange.connect(
|
||||
[this](gen::TimelinePosition position) { setSelectedButtonFromTimelinePosition(position); });
|
||||
|
||||
InitTheme.connect([this] {
|
||||
auto theme = skin::SkinTheme::get(this);
|
||||
setStyle(theme->styles.separatorInView());
|
||||
});
|
||||
initTheme();
|
||||
}
|
||||
|
||||
private:
|
||||
void setSelectedButtonFromTimelinePosition(gen::TimelinePosition pos)
|
||||
{
|
||||
int selItem = 0;
|
||||
switch (pos) {
|
||||
case gen::TimelinePosition::LEFT: selItem = 0; break;
|
||||
case gen::TimelinePosition::RIGHT: selItem = 1; break;
|
||||
case gen::TimelinePosition::BOTTOM: selItem = 2; break;
|
||||
}
|
||||
setSelectedItem(selItem, false);
|
||||
}
|
||||
|
||||
void onItemChange(Item* item) override
|
||||
{
|
||||
ButtonSet::onItemChange(item);
|
||||
ConfigureTimelinePopup::onChangeTimelinePosition(selectedItem());
|
||||
|
||||
// Show the timeline
|
||||
App::instance()->mainWindow()->setTimelineVisibility(true);
|
||||
}
|
||||
|
||||
obs::scoped_connection m_timelinePosConn;
|
||||
};
|
||||
|
||||
// TODO this combobox is similar to FileSelector::CustomFileNameEntry
|
||||
// and GotoFrameCommand::TagsEntry
|
||||
class LayoutsEntry : public ComboBox {
|
||||
public:
|
||||
LayoutsEntry(Layouts& layouts) : m_layouts(layouts)
|
||||
{
|
||||
setEditable(true);
|
||||
getEntryWidget()->Change.connect(&LayoutsEntry::onEntryChange, this);
|
||||
fill(true);
|
||||
}
|
||||
|
||||
private:
|
||||
void fill(bool all)
|
||||
{
|
||||
deleteAllItems();
|
||||
|
||||
MatchWords match(getEntryWidget()->text());
|
||||
|
||||
bool matchAny = false;
|
||||
for (auto& layout : m_layouts) {
|
||||
if (match(layout->name())) {
|
||||
matchAny = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (auto& layout : m_layouts) {
|
||||
if (all || !matchAny || match(layout->name()))
|
||||
addItem(layout->name());
|
||||
}
|
||||
}
|
||||
|
||||
void onEntryChange()
|
||||
{
|
||||
closeListBox();
|
||||
fill(false);
|
||||
if (getItemCount() > 0)
|
||||
openListBox();
|
||||
}
|
||||
|
||||
Layouts& m_layouts;
|
||||
};
|
||||
|
||||
}; // namespace
|
||||
|
||||
class LayoutSelector::LayoutItem : public ListItem {
|
||||
public:
|
||||
enum LayoutOption {
|
||||
DEFAULT,
|
||||
MIRRORED_DEFAULT,
|
||||
USER_DEFINED,
|
||||
NEW_LAYOUT,
|
||||
};
|
||||
|
||||
LayoutItem(LayoutSelector* selector,
|
||||
const LayoutOption option,
|
||||
const std::string& text,
|
||||
const LayoutPtr layout)
|
||||
: ListItem(text)
|
||||
, m_option(option)
|
||||
, m_selector(selector)
|
||||
, m_layout(layout)
|
||||
{
|
||||
}
|
||||
|
||||
std::string getLayoutId() const
|
||||
{
|
||||
if (m_layout)
|
||||
return m_layout->id();
|
||||
else
|
||||
return std::string();
|
||||
}
|
||||
|
||||
bool matchId(const std::string& id) const { return (m_layout && m_layout->matchId(id)); }
|
||||
|
||||
const LayoutPtr& layout() const { return m_layout; }
|
||||
|
||||
void setLayout(const LayoutPtr& layout) { m_layout = layout; }
|
||||
|
||||
void selectImmediately()
|
||||
{
|
||||
MainWindow* win = App::instance()->mainWindow();
|
||||
|
||||
if (m_layout)
|
||||
m_selector->m_activeLayoutId = m_layout->id();
|
||||
|
||||
switch (m_option) {
|
||||
case LayoutOption::DEFAULT: win->setDefaultLayout(); break;
|
||||
case LayoutOption::MIRRORED_DEFAULT: win->setMirroredDefaultLayout(); break;
|
||||
}
|
||||
// Even Default & Mirrored Default can have a customized layout
|
||||
// (customized default layout).
|
||||
if (m_layout)
|
||||
win->loadUserLayout(m_layout.get());
|
||||
}
|
||||
|
||||
void selectAfterClose()
|
||||
{
|
||||
MainWindow* win = App::instance()->mainWindow();
|
||||
|
||||
switch (m_option) {
|
||||
case LayoutOption::NEW_LAYOUT: {
|
||||
// Select the "Layout" separator (it's like selecting nothing)
|
||||
// TODO improve the ComboBox to select a real "nothing" (with
|
||||
// a placeholder text)
|
||||
m_selector->m_comboBox.setSelectedItemIndex(0);
|
||||
|
||||
gen::NewLayout window;
|
||||
LayoutsEntry name(m_selector->m_layouts);
|
||||
name.getEntryWidget()->setMaxTextLength(128);
|
||||
name.setFocusMagnet(true);
|
||||
name.setValue(Strings::new_layout_default_name(m_selector->m_layouts.size() + 1));
|
||||
|
||||
window.namePlaceholder()->addChild(&name);
|
||||
window.openWindowInForeground();
|
||||
if (window.closer() == window.ok()) {
|
||||
auto layout =
|
||||
Layout::MakeFromDock(name.getValue(), name.getValue(), win->customizableDock());
|
||||
|
||||
m_selector->addLayout(layout);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
LayoutOption m_option;
|
||||
LayoutSelector* m_selector;
|
||||
LayoutPtr m_layout;
|
||||
};
|
||||
|
||||
void LayoutSelector::LayoutComboBox::onChange()
|
||||
{
|
||||
ComboBox::onChange();
|
||||
if (auto item = dynamic_cast<LayoutItem*>(getSelectedItem())) {
|
||||
item->selectImmediately();
|
||||
m_selected = item;
|
||||
}
|
||||
}
|
||||
|
||||
void LayoutSelector::LayoutComboBox::onCloseListBox()
|
||||
{
|
||||
ComboBox::onCloseListBox();
|
||||
if (m_selected) {
|
||||
m_selected->selectAfterClose();
|
||||
m_selected = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
LayoutSelector::LayoutSelector(TooltipManager* tooltipManager)
|
||||
: m_button(SkinTheme::instance()->parts.iconUserData())
|
||||
{
|
||||
m_activeLayoutId = Preferences::instance().general.workspaceLayout();
|
||||
|
||||
m_button.Click.connect([this]() { switchSelector(); });
|
||||
|
||||
m_comboBox.setVisible(false);
|
||||
|
||||
addChild(&m_comboBox);
|
||||
addChild(&m_button);
|
||||
|
||||
setupTooltips(tooltipManager);
|
||||
|
||||
InitTheme.connect([this] {
|
||||
noBorderNoChildSpacing();
|
||||
m_comboBox.noBorderNoChildSpacing();
|
||||
m_button.noBorderNoChildSpacing();
|
||||
});
|
||||
initTheme();
|
||||
}
|
||||
|
||||
LayoutSelector::~LayoutSelector()
|
||||
{
|
||||
Preferences::instance().general.workspaceLayout(m_activeLayoutId);
|
||||
|
||||
stopAnimation();
|
||||
}
|
||||
|
||||
LayoutPtr LayoutSelector::activeLayout()
|
||||
{
|
||||
return m_layouts.getById(m_activeLayoutId);
|
||||
}
|
||||
|
||||
void LayoutSelector::addLayout(const LayoutPtr& layout)
|
||||
{
|
||||
bool added = m_layouts.addLayout(layout);
|
||||
if (added) {
|
||||
auto item = new LayoutItem(this, LayoutItem::USER_DEFINED, layout->name(), layout);
|
||||
m_comboBox.insertItem(m_comboBox.getItemCount() - 1, // Above the "New Layout" item
|
||||
item);
|
||||
|
||||
m_comboBox.setSelectedItem(item);
|
||||
}
|
||||
else {
|
||||
for (auto item : m_comboBox) {
|
||||
if (auto layoutItem = dynamic_cast<LayoutItem*>(item)) {
|
||||
if (layoutItem->layout() && layoutItem->layout()->name() == layout->name()) {
|
||||
layoutItem->setLayout(layout);
|
||||
m_comboBox.setSelectedItem(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LayoutSelector::updateActiveLayout(const LayoutPtr& newLayout)
|
||||
{
|
||||
bool result = m_layouts.addLayout(newLayout);
|
||||
|
||||
// It means that the layout wasn't added, but replaced, when we
|
||||
// update a layout it must be existent in the m_layouts collection.
|
||||
ASSERT(result == false);
|
||||
}
|
||||
|
||||
void LayoutSelector::onAnimationFrame()
|
||||
{
|
||||
switch (animation()) {
|
||||
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) {
|
||||
m_comboBox.addItem(new SeparatorInView(Strings::main_window_layout(), HORIZONTAL));
|
||||
m_comboBox.addItem(new LayoutItem(this,
|
||||
LayoutItem::DEFAULT,
|
||||
Strings::main_window_default_layout(),
|
||||
m_layouts.getById(Layout::kDefault)));
|
||||
m_comboBox.addItem(new LayoutItem(this,
|
||||
LayoutItem::MIRRORED_DEFAULT,
|
||||
Strings::main_window_mirrored_default_layout(),
|
||||
m_layouts.getById(Layout::kMirroredDefault)));
|
||||
m_comboBox.addItem(new SeparatorInView(Strings::main_window_timeline(), HORIZONTAL));
|
||||
m_comboBox.addItem(new TimelineButtons());
|
||||
m_comboBox.addItem(new SeparatorInView(Strings::main_window_user_layouts(), HORIZONTAL));
|
||||
for (const auto& layout : m_layouts) {
|
||||
m_comboBox.addItem(new LayoutItem(this, LayoutItem::USER_DEFINED, layout->name(), layout));
|
||||
}
|
||||
m_comboBox.addItem(
|
||||
new LayoutItem(this, LayoutItem::NEW_LAYOUT, Strings::main_window_new_layout(), nullptr));
|
||||
}
|
||||
|
||||
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::setupTooltips(TooltipManager* tooltipManager)
|
||||
{
|
||||
tooltipManager->addTooltipFor(&m_button, Strings::main_window_layout(), TOP);
|
||||
}
|
||||
|
||||
LayoutSelector::LayoutItem* LayoutSelector::getItemByLayoutId(const std::string& id)
|
||||
{
|
||||
for (auto child : m_comboBox) {
|
||||
if (auto item = dynamic_cast<LayoutItem*>(child)) {
|
||||
if (item->matchId(id))
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace app
|
79
src/app/ui/layout_selector.h
Normal file
79
src/app/ui/layout_selector.h
Normal file
@ -0,0 +1,79 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2021-2024 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::HBox,
|
||||
public ui::AnimatedWidget,
|
||||
public Dockable {
|
||||
enum Ani : int {
|
||||
ANI_NONE,
|
||||
ANI_EXPANDING,
|
||||
ANI_COLLAPSING,
|
||||
};
|
||||
|
||||
class LayoutItem;
|
||||
|
||||
class LayoutComboBox : public ui::ComboBox {
|
||||
private:
|
||||
void onChange() override;
|
||||
void onCloseListBox() override;
|
||||
LayoutItem* m_selected = nullptr;
|
||||
};
|
||||
|
||||
public:
|
||||
LayoutSelector(ui::TooltipManager* tooltipManager);
|
||||
~LayoutSelector();
|
||||
|
||||
LayoutPtr activeLayout();
|
||||
std::string activeLayoutId() const { return m_activeLayoutId; }
|
||||
|
||||
void addLayout(const LayoutPtr& layout);
|
||||
void updateActiveLayout(const LayoutPtr& layout);
|
||||
void switchSelector();
|
||||
void switchSelectorFromCommand();
|
||||
bool isSelectorVisible() const;
|
||||
|
||||
// Dockable impl
|
||||
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
|
||||
|
||||
private:
|
||||
void setupTooltips(ui::TooltipManager* tooltipManager);
|
||||
LayoutItem* getItemByLayoutId(const std::string& id);
|
||||
void onAnimationFrame() override;
|
||||
void onAnimationStop(int animation) override;
|
||||
|
||||
std::string m_activeLayoutId;
|
||||
LayoutComboBox m_comboBox;
|
||||
IconButton m_button;
|
||||
gfx::Size m_startSize;
|
||||
gfx::Size m_endSize;
|
||||
Layouts m_layouts;
|
||||
bool m_switchComboBoxAfterAni = false;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif
|
101
src/app/ui/layouts.cpp
Normal file
101
src/app/ui/layouts.cpp
Normal file
@ -0,0 +1,101 @@
|
||||
// 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()
|
||||
{
|
||||
if (!m_userLayoutsFilename.empty())
|
||||
save(m_userLayoutsFilename);
|
||||
}
|
||||
|
||||
LayoutPtr Layouts::getById(const std::string& id) const
|
||||
{
|
||||
auto it = std::find_if(m_layouts.begin(), m_layouts.end(), [&id](const LayoutPtr& l) {
|
||||
return l->matchId(id);
|
||||
});
|
||||
return (it != m_layouts.end() ? *it : nullptr);
|
||||
}
|
||||
|
||||
bool Layouts::addLayout(const LayoutPtr& layout)
|
||||
{
|
||||
auto it = std::find_if(m_layouts.begin(), m_layouts.end(), [layout](const LayoutPtr& l) {
|
||||
return l->matchId(layout->id());
|
||||
});
|
||||
if (it != m_layouts.end()) {
|
||||
*it = layout; // Replace existent layout
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
m_layouts.push_back(layout);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void Layouts::load(const std::string& fn)
|
||||
{
|
||||
XMLDocumentRef doc = app::open_xml(fn);
|
||||
XMLHandle handle(doc.get());
|
||||
XMLElement* layoutElem =
|
||||
handle.FirstChildElement("layouts").FirstChildElement("layout").ToElement();
|
||||
|
||||
while (layoutElem) {
|
||||
m_layouts.push_back(Layout::MakeFromXmlElement(layoutElem));
|
||||
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("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
|
48
src/app/ui/layouts.h
Normal file
48
src/app/ui/layouts.h
Normal file
@ -0,0 +1,48 @@
|
||||
// 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);
|
||||
|
||||
// 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
|
@ -9,17 +9,22 @@
|
||||
#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 reload();
|
||||
|
||||
// Dockable impl
|
||||
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
|
||||
|
||||
private:
|
||||
obs::scoped_connection m_extKeys;
|
||||
obs::scoped_connection m_extScripts;
|
||||
|
@ -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 const char* kLegacyLayoutMainWindowSection = "layout:main_window";
|
||||
static const char* kLegacyLayoutTimelineSplitter = "timeline_splitter";
|
||||
static 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,8 @@ 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_layoutSelector = std::make_unique<LayoutSelector>(m_tooltipManager);
|
||||
|
||||
// Register commands to load menus+shortcuts for these commands
|
||||
Editor::registerCommands();
|
||||
@ -117,20 +128,20 @@ 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_notifications = std::make_unique<Notifications>();
|
||||
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,40 @@ 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_customizableDockPlaceholder->InitTheme.connect([this] {
|
||||
auto theme = static_cast<skin::SkinTheme*>(this->theme());
|
||||
m_customizableDock->setBgColor(theme->colors.workspace());
|
||||
});
|
||||
m_customizableDockPlaceholder->initTheme();
|
||||
|
||||
m_dock->top()->right()->dock(ui::RIGHT, m_notifications.get());
|
||||
m_dock->top()->right()->dock(ui::CENTER, m_layoutSelector.get());
|
||||
m_dock->top()->dock(ui::BOTTOM, m_tabsBar.get());
|
||||
m_dock->top()->dock(ui::CENTER, m_menuBar.get());
|
||||
m_dock->dock(ui::CENTER, m_customizableDockPlaceholder.get());
|
||||
|
||||
// After the user resizes the dock we save the updated layout
|
||||
m_saveDockLayoutConn = m_customizableDock->UserResizedDock.connect(
|
||||
[this] { saveActiveLayout(); });
|
||||
|
||||
setDefaultLayout();
|
||||
if (LayoutPtr layout = m_layoutSelector->activeLayout())
|
||||
loadUserLayout(layout.get());
|
||||
|
||||
// Reconfigure workspace when the timeline position is changed.
|
||||
auto& pref = Preferences::instance();
|
||||
@ -179,42 +209,47 @@ void MainWindow::initialize()
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
delete m_scalePanic;
|
||||
m_timelineResizeConn.disconnect();
|
||||
m_colorBarResizeConn.disconnect();
|
||||
m_saveDockLayoutConn.disconnect();
|
||||
|
||||
m_dock->resetDocks();
|
||||
m_customizableDock->resetDocks();
|
||||
|
||||
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 +267,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 +289,7 @@ void MainWindow::showNotification(INotificationDelegate* del)
|
||||
{
|
||||
m_notifications->addLink(del);
|
||||
m_notifications->setVisible(true);
|
||||
m_notifications->parent()->layout();
|
||||
layout();
|
||||
}
|
||||
|
||||
void MainWindow::showHomeOnOpen()
|
||||
@ -270,20 +305,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 +333,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 +353,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
|
||||
}
|
||||
@ -360,6 +395,66 @@ void MainWindow::popTimeline()
|
||||
setTimelineVisibility(true);
|
||||
}
|
||||
|
||||
void MainWindow::setDefaultLayout()
|
||||
{
|
||||
m_timelineResizeConn.disconnect();
|
||||
m_colorBarResizeConn.disconnect();
|
||||
|
||||
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());
|
||||
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::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))
|
||||
setDefaultLayout();
|
||||
|
||||
this->layout();
|
||||
}
|
||||
|
||||
void MainWindow::setCustomizeDock(bool enable)
|
||||
{
|
||||
m_customizableDock->setCustomizing(enable);
|
||||
}
|
||||
|
||||
void MainWindow::dataRecoverySessionsAreReady()
|
||||
{
|
||||
getHomeView()->dataRecoverySessionsAreReady();
|
||||
@ -375,24 +470,15 @@ bool MainWindow::onProcessMessage(ui::Message* msg)
|
||||
|
||||
void MainWindow::onInitTheme(ui::InitThemeEvent& ev)
|
||||
{
|
||||
app::gen::MainWindow::onInitTheme(ev);
|
||||
ui::Window::onInitTheme(ev);
|
||||
noBorderNoChildSpacing();
|
||||
if (m_previewEditor)
|
||||
m_previewEditor->initTheme();
|
||||
}
|
||||
|
||||
void MainWindow::onSaveLayout(SaveLayoutEvent& ev)
|
||||
{
|
||||
// Invert the timeline splitter position before we save the setting.
|
||||
if (Preferences::instance().general.timelinePosition() == gen::TimelinePosition::LEFT) {
|
||||
timelineSplitter()->setPosition(100 - timelineSplitter()->getPosition());
|
||||
}
|
||||
|
||||
Window::onSaveLayout(ev);
|
||||
}
|
||||
|
||||
void MainWindow::onResize(ui::ResizeEvent& ev)
|
||||
{
|
||||
app::gen::MainWindow::onResize(ev);
|
||||
ui::Window::onResize(ev);
|
||||
|
||||
os::Window* nativeWindow = (display() ? display()->nativeWindow() : nullptr);
|
||||
if (nativeWindow && nativeWindow->screen()) {
|
||||
@ -405,7 +491,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 +508,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)
|
||||
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 +600,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,63 +680,76 @@ 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 isDoc = (getDocView() != nullptr);
|
||||
|
||||
if (os::System::instance()->menus() == nullptr || pref.general.showMenuBar()) {
|
||||
m_menuBar->resetMaxSize();
|
||||
if (!m_menuBar->parent())
|
||||
m_dock->top()->dock(CENTER, m_menuBar.get());
|
||||
}
|
||||
else {
|
||||
m_menuBar->setMaxSize(gfx::Size(0, 0));
|
||||
if (m_menuBar->parent())
|
||||
m_dock->undock(m_menuBar.get());
|
||||
}
|
||||
|
||||
m_menuBar->setVisible(normal);
|
||||
m_notifications->setVisible(normal && m_notifications->hasNotifications());
|
||||
m_tabsBar->setVisible(normal);
|
||||
colorBarPlaceholder()->setVisible(normal && isDoc);
|
||||
|
||||
// TODO set visibility of color bar widgets
|
||||
m_colorBar->setVisible(normal && isDoc);
|
||||
m_colorBarResizeConn = m_customizableDock->Resize.connect(
|
||||
[this] { saveColorBarConfiguration(); });
|
||||
|
||||
m_toolBar->setVisible(normal && isDoc);
|
||||
m_statusBar->setVisible(normal);
|
||||
m_contextBar->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode));
|
||||
|
||||
// Configure timeline
|
||||
{
|
||||
const gfx::Rect workspaceBounds = m_customizableDock->center()->center()->bounds();
|
||||
// Get legacy timeline position and splitter position
|
||||
auto timelinePosition = pref.general.timelinePosition();
|
||||
bool invertWidgets = false;
|
||||
int align = VERTICAL;
|
||||
auto timelineSplitterPos =
|
||||
get_config_double(kLegacyLayoutMainWindowSection, kLegacyLayoutTimelineSplitter, 75.0) /
|
||||
100.0;
|
||||
int side = ui::BOTTOM;
|
||||
|
||||
m_customizableDock->undock(m_timeline.get());
|
||||
|
||||
int w, h;
|
||||
w = h = 64;
|
||||
|
||||
switch (timelinePosition) {
|
||||
case gen::TimelinePosition::LEFT:
|
||||
align = HORIZONTAL;
|
||||
invertWidgets = true;
|
||||
side = ui::LEFT;
|
||||
w = (workspaceBounds.w * (1.0 - timelineSplitterPos)) / guiscale();
|
||||
break;
|
||||
case gen::TimelinePosition::RIGHT:
|
||||
side = ui::RIGHT;
|
||||
w = (workspaceBounds.w * (1.0 - timelineSplitterPos)) / guiscale();
|
||||
break;
|
||||
case gen::TimelinePosition::BOTTOM:
|
||||
side = ui::BOTTOM;
|
||||
h = (workspaceBounds.h * (1.0 - timelineSplitterPos)) / guiscale();
|
||||
break;
|
||||
case gen::TimelinePosition::RIGHT: align = HORIZONTAL; break;
|
||||
case gen::TimelinePosition::BOTTOM: break;
|
||||
}
|
||||
|
||||
timelineSplitter()->setAlign(align);
|
||||
timelinePlaceholder()->setVisible(
|
||||
isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode) &&
|
||||
pref.general.visibleTimeline());
|
||||
// Listen to resizing changes in the dock that contains the
|
||||
// timeline (so we save the new splitter position)
|
||||
m_timelineResizeConn = m_customizableDock->center()->center()->Resize.connect(
|
||||
[this] { saveTimelineConfiguration(); });
|
||||
|
||||
bool invertSplitterPos = false;
|
||||
if (invertWidgets) {
|
||||
if (timelineSplitter()->firstChild() == workspacePlaceholder() &&
|
||||
timelineSplitter()->lastChild() == timelinePlaceholder()) {
|
||||
timelineSplitter()->removeChild(workspacePlaceholder());
|
||||
timelineSplitter()->addChild(workspacePlaceholder());
|
||||
invertSplitterPos = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (timelineSplitter()->firstChild() == timelinePlaceholder() &&
|
||||
timelineSplitter()->lastChild() == workspacePlaceholder()) {
|
||||
timelineSplitter()->removeChild(timelinePlaceholder());
|
||||
timelineSplitter()->addChild(timelinePlaceholder());
|
||||
invertSplitterPos = true;
|
||||
}
|
||||
}
|
||||
if (invertSplitterPos)
|
||||
timelineSplitter()->setPosition(100 - timelineSplitter()->getPosition());
|
||||
m_customizableDock->center()->center()->dock(side,
|
||||
m_timeline.get(),
|
||||
gfx::Size(w * guiscale(), h * guiscale()));
|
||||
|
||||
m_timeline->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode) &&
|
||||
pref.general.visibleTimeline());
|
||||
}
|
||||
|
||||
if (m_contextBar->isVisible()) {
|
||||
@ -652,7 +757,45 @@ void MainWindow::configureWorkspaceLayout()
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
@ -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);
|
||||
const Dock* customizableDock() const { 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,35 @@ 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<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<Notifications> m_notifications;
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
@ -9,6 +9,7 @@
|
||||
#define APP_UI_NOTIFICATIONS_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "app/ui/dockable.h"
|
||||
#include "ui/button.h"
|
||||
#include "ui/menu.h"
|
||||
|
||||
@ -19,13 +20,17 @@ class Style;
|
||||
namespace app {
|
||||
class INotificationDelegate;
|
||||
|
||||
class Notifications : public ui::Button {
|
||||
class Notifications : public ui::Button,
|
||||
public Dockable {
|
||||
public:
|
||||
Notifications();
|
||||
|
||||
void addLink(INotificationDelegate* del);
|
||||
bool hasNotifications() const { return m_popup.hasChildren(); }
|
||||
|
||||
// Dockable impl
|
||||
int dockableAt() const override { return ui::TOP | ui::BOTTOM | ui::LEFT | ui::RIGHT; }
|
||||
|
||||
protected:
|
||||
void onSizeHint(ui::SizeHintEvent& ev) override;
|
||||
void onPaint(ui::PaintEvent& ev) override;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
@ -72,7 +73,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;
|
||||
@ -152,6 +154,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;
|
||||
|
@ -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)
|
||||
@ -326,10 +320,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::onPaint(ui::PaintEvent& 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();
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2021-2022 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -9,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"
|
||||
@ -16,6 +18,7 @@
|
||||
#include "ui/widget.h"
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
namespace ui {
|
||||
class CloseEvent;
|
||||
@ -31,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;
|
||||
|
||||
@ -51,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;
|
||||
@ -60,6 +72,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,
|
||||
@ -93,12 +106,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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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))) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user