Compare commits

...

59 Commits

Author SHA1 Message Date
David Capello fceed86a96
Merge 334739524a into 6d89a6bc15 2025-07-29 18:01:00 +00:00
David Capello 334739524a Restore the "Position" section in Timeline configuration
We're going to keep this section for a couple of versions as old users
get used to the new Workspace Layout customization.

The "Position" section will contain the layout icon so we can switch
the Workspace Layout combobox from there too,
2025-07-29 14:58:53 -03:00
David Capello 49da8e1ae1 Close layout combobox after clicking the reset/delete layout button
Without this, the combobox is still visible and clicking the button
again will crash the program.
2025-07-29 14:58:53 -03:00
David Capello 047e331b40 Remove base field from "New Layout" dialog 2025-07-29 14:58:53 -03:00
David Capello 0754a82023 Fix loading modified default layouts at the very beginning 2025-07-29 14:58:53 -03:00
David Capello 8f3b15276f Fix some strings
From https://github.com/aseprite/aseprite/pull/3485#pullrequestreview-2824195211
2025-07-29 14:58:53 -03:00
David Capello 763ebba6a2 Fix crashes resetting the default layout + remove some hacks
With this patch we've also simplified some hacks handling the
populateComboBox() call, deferring the deletion of widgets/items when
we are inside an event generated by those items.
2025-07-29 14:58:53 -03:00
David Capello dcbf444aaf Fix several layout saving/loading issues
* Better support to load legacy timeline information: we have to
  estimate workspace bounds)
* Auto-save layouts after resizing a splitter/dock
* Fix resetting expansive widgets inside docks after switching tabs
* Load mirrored layout correctly if it was the last selected layout
2025-07-29 14:58:53 -03:00
David Capello 47fc3b0391 Fix switch/break style 2025-07-29 14:58:53 -03:00
David Capello d0656ddd98 Move LayoutSelector::setActiveLayoutId() impl to .cpp file 2025-07-29 14:58:53 -03:00
David Capello 4dd20d2b85 Minor changes (format, remove unused/unnecessary vars/keywords) 2025-07-29 14:58:53 -03:00
David Capello cb6b512d25 Change notification flag location inside the layout selector widget
This improves the look of the flag as another icon above the tabs
bottom and at the right of the layout selector icon.
2025-07-29 14:58:53 -03:00
David Capello 5b57abd810 [theme] New layout selector icon + bg color fixes 2025-07-29 14:58:53 -03:00
David Capello f69e94c6e3 Improve layout selector widget location when menu bar is visible/hidden 2025-07-29 14:58:53 -03:00
David Capello f1759b069a Remove unused member variable from ui::Dock 2025-07-29 14:58:53 -03:00
Christian Kaiser 88e3c2a48c Polish layout handling
@dacap's notes: A description of the included changes:

* Improve UX auto-saving layouts when docks are modified, and new 'X'
  icon to delete layouts (or reset the 'Default' layout).
* Remove old timeline position controls (Left/Right/Bottom buttons)
  from the Timeline configuration and from the layout selector
* Add support to drag and drop docks to other sides with real-time
  feedback using a semi-transparent UILayer
* Add a context menu w/right-click to dock the widget at the supported
  sides without drag-and-drop

Some review comments in https://github.com/dacap/aseprite/pull/2
2025-07-29 14:58:53 -03:00
David Capello d9415fdf1b Show handles in dockable areas to drag-and-drop them 2025-07-29 14:58:53 -03:00
David Capello d0457cc1f4 Fix bug where the ToolBar popup wasn't being opened
This was a new issue with combination of
9b90159d1b, probably we should close the
popup when the window is resized (instead of onSizeHint()).
2025-07-29 14:58:53 -03:00
David Capello 84c35a63c2 Update saving/loading dock layouts w/tinyxml2 library 2025-07-29 14:58:52 -03:00
David Capello 00b0ba93d1 Save layout changes immediately when docks are resized
This patch includes:

* The layout is updated immediately when docks are resized (before
  this the layout was like a snapshot of the configuration when the
  layout was created or overwritten with the "New Layout" option)
* Saving the active layout used in
  preferences.general.workspace_layout so we can restore it after
  restarting Aseprite
* Change "UI Layout" to "Workspace Layout"
* Some strings moved to en.ini file for i18n
* Fixed a crash on MainWindow::onActiveViewChange() when the
  application was being closed
2025-07-29 14:58:52 -03:00
David Capello eebd7c2666 Improve borders of context & color bars for both sides (left/right) 2025-07-29 14:58:52 -03:00
David Capello e6c1fbf560 Add View > Workspace Layout option to switch the layout w/keys 2025-07-29 14:58:52 -03:00
David Capello 90fae86c9d Add possibility to overwrite existent layouts 2025-07-29 14:58:52 -03:00
David Capello ec41e9fe66 Include StatusBar in the set of customizable widgets in the layout 2025-07-29 14:58:52 -03:00
David Capello 6b0a1028fb Save/Load user defined layouts in new user.aseprite-layouts file
And now we store the TiXmlElement for each Layout, instead of
converting from/to text back and forth.
2025-07-29 14:58:52 -03:00
David Capello dc51ca25e0 Add option to save/restore user-defined layouts on memory
This happens only in memory at the moment (layouts are not saved in
disk yet), and the customization is quite simple (only size of
splitters, timeline position). But in the future we should be able to
dock elements in any place.
2025-07-29 14:58:52 -03:00
David Capello 381d9e663a Set the initial timeline position in the LayoutSelector correctly 2025-07-29 14:58:52 -03:00
David Capello e2fcbc86df Show the timeline when we set its position from the LayoutSelector 2025-07-29 14:58:52 -03:00
David Capello b0683f7914 Improve the layout selector UI
Changes:
* Now we use the "user data" icon as the button to expand the layouts
  combobox
* Added a tooltip to this icon
* Added buttons to configure the Timeline position in the same
  combobox
* Fixed some bugs in Dock using space for hidden widgets
2025-07-29 14:58:52 -03:00
David Capello 47c8e5eae3 Save/restore color bar splitter position correctly 2025-07-29 14:58:52 -03:00
David Capello bba8b00bd2 Save/restore timeline splitter position correctly 2025-07-29 14:58:52 -03:00
David Capello fc57df4305 Fix popups & tooltips direction when ToolBar is docked at the left side 2025-07-29 14:58:52 -03:00
David Capello 7ca6f53eb6 Fix std::clamp() max bound in TipWindow::pointAt() 2025-07-29 14:58:52 -03:00
David Capello 1f75d5444d Use std::unique_ptr in ToolBar members 2025-07-29 14:58:52 -03:00
David Capello 4858a5103e Fix memory leaks in MainWindow
Temporary/created subdocks must be deleted automatically, and children
that are not part of the window hierarchy must be deleted explicitly
now (using some std::unique_ptrs).
2025-07-29 14:58:52 -03:00
David Capello fa21d87ba8 Add Dock widget, initial & basic version of dockable elements (#518)
Some missing features so far:

1) Restore old layout configuration (color bar split pos, timeline
   pos, etc.) and migrate to new Dock layout
2) Load/saving Dock layout
3) Create & customize current layoout (drag-and-drop widgets, etc.)
2025-07-29 14:58:52 -03:00
David Capello cef92c1a38 Add .plist files for macOS
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
We don't have an Aseprite.app target in cmake files yet, but we might
add it in a near future.
2025-07-28 16:18:19 -03:00
Christian Kaiser 22e72ab5cb [win] Fix includeDesktopDir returning the default path
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
Uses SHGFP_TYPE_CURRENT which returns the Desktop that the user has configured instead of the default, fixes Windows 11's OneDrive Desktop folder.
2025-07-28 10:47:53 -03:00
Christian Kaiser 80fa065bd5 [lua] Add sprite.undoHistory
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-25 13:58:52 -03:00
David Capello de1ccb24dd [win] Don't drop text when IME dialog composition is accepted w/Enter
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
With #5230, now that we can show the IME dialog on Windows, when we
are selecting a specific word/composition in the IME dialog, if we
press Enter we'll receive that Enter onKeyUp(). It's better if we
process the Enter key onKeyDown() (as the IME enter key is not
received in that case).
2025-07-25 09:19:50 -03:00
David Capello 7d91c4b9d9 [win] Fix dead keys on Windows 2025-07-24 17:45:46 -03:00
Cerallin 6d89a6bc15 fix entry
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-24 12:50:32 -03:00
Cerallin d4e97b5a96 Add const method Entry::caretPosOnScreen()
This method is for Entry::setTextInput() and IME positioning.
2025-07-24 12:50:32 -03:00
Cerallin 205b18dc0f Make Entry::getCharBoxBounds() a const method 2025-07-24 12:50:32 -03:00
David Capello 2ba051b59b Fix crash deselecting moved pixels when using certain extensions (fix #5280)
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
Although the issue refers to deselecting MovingPixelsState, the same
crash could happen when canceling/finishing WritingTextState or
MovingSelectionState. This fixes the crash for all these states.
2025-07-23 19:01:37 -03:00
Christian Kaiser 3fcb000eb1 Fix slice transformations not updating editors
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-22 02:11:07 -03:00
David Capello af9dc3c817 Fix brush boundaries accumulation switching brush type only (fix #5281)
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-07-21 17:28:11 -03:00
David Capello 250dfdc86a Convert the brush generation counter into an atomic var 2025-07-21 17:27:23 -03:00
David Capello c904c41b39 Don't show stroke/fill option for theme fonts
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-18 18:48:35 -03:00
David Capello 9e941e9a8b [win32] Fix listing hidden files on Windows (related to #5269 / #3079) 2025-07-18 18:37:44 -03:00
Liebranca bbab4d5875 Add 'Show hidden' check to file selector 2025-07-18 18:26:35 -03:00
David Capello 5c4daff128 Add options to stroke/fill text (fix #5271)
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-14 23:17:25 -03:00
David Capello 11a7b061ff Remove unused var 2025-07-14 20:24:32 -03:00
David Capello 283bedf77e Add pinned/recent folders to export file popup menus
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
Related to https://community.aseprite.org/t/25920
2025-07-10 18:55:28 -03:00
David Capello 2eeb6f04a7 Fix "buttononly" bool attribute for <filename> widget 2025-07-10 18:13:10 -03:00
David Capello 706d0b8a7a Add possibility to select export file names w/"one click"
It's not with one click, it requires dragging the mouse but it's
better than two clicks.

Related to https://community.aseprite.org/t/25920
2025-07-10 18:12:38 -03:00
David Capello 2f3a7f5dec Update to new laf API: setTranslateDeadKeys -> setTextInput
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-07-10 16:06:03 -03:00
David Capello d5de74b715 Update laf module
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
2025-07-10 08:52:01 -03:00
David Capello 2d87a7b184 [ui] Fix drawing cursor at the end of the Entry/Combobox field
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
Regression introduced in 1a6a39700e
2025-07-07 22:21:58 -03:00
81 changed files with 3443 additions and 428 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

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

View File

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

View File

@ -167,6 +167,7 @@
<option id="keep_closed_sprite_on_memory_for" type="double" default="15.0" />
<option id="show_full_path" type="bool" default="true" />
<option id="edit_full_path" type="bool" default="false" />
<option id="workspace_layout" type="std::string" />
<option id="timeline_position" type="TimelinePosition" default="TimelinePosition::BOTTOM" />
<option id="timeline_layer_panel_width" type="int" default="100" />
<option id="show_menu_bar" type="bool" default="true" />
@ -356,6 +357,7 @@
<section id="file_selector">
<option id="current_folder" type="std::string" default="&quot;&lt;empty&gt;&quot;" />
<option id="zoom" type="double" default="1.0" />
<option id="show_hidden" type="bool" default="false" />
</section>
<section id="text_tool">
<option id="font_face" type="std::string" />

View File

@ -488,6 +488,7 @@ TilesetDuplicate = Duplicate Tileset
Undo = Undo
UndoHistory = Undo History
UnlinkCel = Unlink Cel
ToggleWorkspaceLayout = Toggle Workspace Layout
Zoom = Zoom
Zoom_In = Zoom In
Zoom_Out = Zoom Out
@ -765,6 +766,7 @@ pinned_folders = Pinned Folders
recent_folders = Recent Folders
all_formats = All formats
all_files = All files
show_hidden = Show hidden
[filters]
selected_cels = Selected
@ -1163,6 +1165,7 @@ select_load_from_file = &Load from MSK file
select_save_to_file = &Save to MSK file
view = &View
view_duplicate_view = Duplicate &View
view_workspace_layout = Workspace &Layout
view_show_extras = &Extras
view_show = &Show
view_show_layer_edges = &Layer Edges
@ -1204,6 +1207,14 @@ help_twitter = Twitter
help_enter_license = Enter &License
help_about = &About
[main_window]
layout = Workspace Layout
default_layout = Default
mirrored_default_layout = Mirrored Default
timeline = Timeline
user_layouts = User Layouts
new_layout = New Layout...
[mask_by_color]
title = Select Color
label_color = Color:
@ -1232,6 +1243,20 @@ name = Name:
tileset = Tileset:
default_new_layer_name = New Layer
[new_layout]
title = New Workspace Layout
name = Name:
deleting_layout = Deleting Layout
deleting_layout_confirmation = Are you sure you want to delete the '{}' layout?
restoring_layout = Restoring Layout
restoring_layout_confirmation = Are you sure you want to restore the '{}' layout?
[dock]
left = Dock Left
right = Dock Right
top = Dock Top
bottom = Dock Bottom
[news_listbox]
more = More...
problem_loading = Problems loading news. Please retry.
@ -1834,11 +1859,20 @@ pixel_scale = Pixel Scale
with_vars = Use CSS3 Variables
generate_html = Generate Sample HTML File
[shape]
fill = Fill
stroke = Stroke
stroke_width = Stroke Width
[text_tool]
font_family = Font Family
font_size = Font Size
bold = Bold
italic = Italic
more_options = More Options
[timeline_conf]
position = Position:
left = &Left
right = &Right
bottom = &Bottom
frame_header = Frame Header:
first_frame = First Frame:
thumbnails = Thumbnails

View File

@ -23,6 +23,7 @@
<combobox id="location" expansive="true" />
<button text="" id="refresh_button" style="refresh_button"
tooltip="@.refresh_button_tooltip" tooltip_dir="bottom" />
<check id="show_hidden_check" text="@.show_hidden" />
</box>
<vbox id="file_view_placeholder" expansive="true" />
<grid columns="2">

View File

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

View File

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

View File

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

2
laf

@ -1 +1 @@
Subproject commit 7c01959d7f0f2662a48eaa98e9b94336c4e45fda
Subproject commit 8ec4b553f1618f7a4b47cdcf4cfc2663266111ac

View File

@ -180,8 +180,8 @@ if(ENABLE_ASEPRITE_EXE)
if(WIN32)
set(main_resources
main/resources_win32.rc
main/settings.manifest)
main/win/resources_win32.rc
main/win/settings.manifest)
endif()
add_executable(${main_target}

View File

@ -520,6 +520,7 @@ target_sources(app-lib PRIVATE
commands/tileset_mode.cpp
commands/toggle_other_layers_opacity.cpp
commands/toggle_play_option.cpp
commands/toggle_workspace_layout.cpp
console.cpp
context.cpp
context_flags.cpp
@ -609,6 +610,7 @@ target_sources(app-lib PRIVATE
ui/context_bar.cpp
ui/dithering_selector.cpp
ui/doc_view.cpp
ui/dock.cpp
ui/drop_down_button.cpp
ui/dynamics_popup.cpp
ui/editor/brush_preview.cpp
@ -653,6 +655,9 @@ target_sources(app-lib PRIVATE
ui/input_chain.cpp
ui/keyboard_shortcuts.cpp
ui/layer_frame_comboboxes.cpp
ui/layout.cpp
ui/layouts.cpp
ui/layout_selector.cpp
ui/main_menu_bar.cpp
ui/main_window.cpp
ui/mini_help_button.cpp

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -10,6 +10,7 @@
#endif
#include "app/app.h"
#include "app/color_utils.h"
#include "app/commands/command.h"
#include "app/console.h"
#include "app/context.h"
@ -88,10 +89,10 @@ void PasteTextCommand::onExecute(Context* ctx)
std::string text = window.userText()->text();
app::Color color = window.fontColor()->getColor();
doc::ImageRef image = render_text(
fontInfo,
text,
gfx::rgba(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
ui::Paint paint = window.fontFace()->paint();
paint.color(color_utils::color_for_ui(color));
doc::ImageRef image = render_text(fontInfo, text, paint);
if (image) {
Sprite* sprite = editor->sprite();
if (image->pixelFormat() != sprite->pixelFormat()) {

View File

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

View File

@ -0,0 +1,47 @@
// Aseprite
// Copyright (c) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/app.h"
#include "app/commands/command.h"
#include "app/ui/layout_selector.h"
#include "app/ui/main_window.h"
namespace app {
class ToggleWorkspaceLayoutCommand : public Command {
public:
ToggleWorkspaceLayoutCommand();
protected:
bool onChecked(Context* ctx) override;
void onExecute(Context* ctx) override;
};
ToggleWorkspaceLayoutCommand::ToggleWorkspaceLayoutCommand()
: Command(CommandId::ToggleWorkspaceLayout(), CmdUIOnlyFlag)
{
}
bool ToggleWorkspaceLayoutCommand::onChecked(Context* ctx)
{
return App::instance()->mainWindow()->layoutSelector()->isSelectorVisible();
}
void ToggleWorkspaceLayoutCommand::onExecute(Context* ctx)
{
App::instance()->mainWindow()->layoutSelector()->switchSelectorFromCommand();
}
Command* CommandFactory::createToggleWorkspaceLayoutCommand()
{
return new ToggleWorkspaceLayoutCommand();
}
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -82,6 +82,9 @@ public:
unsigned int m_version;
bool m_removed;
mutable bool m_is_folder;
#ifdef _WIN32
bool m_isHidden = false;
#endif
std::atomic<double> m_thumbnailProgress;
std::atomic<os::Surface*> m_thumbnail;
#ifdef _WIN32
@ -266,7 +269,7 @@ IFileItem* FileSystemModule::getRootFileItem()
fileitem->m_pidl = pidl;
fileitem->m_fullpidl = pidl;
SFGAOF attrib = SFGAO_FOLDER;
SFGAOF attrib = SFGAO_FOLDER | SFGAO_HIDDEN;
shl_idesktop->GetAttributesOf(1, (LPCITEMIDLIST*)&pidl, &attrib);
update_by_pidl(fileitem, attrib);
@ -357,7 +360,7 @@ bool FileItem::isHidden() const
ASSERT(m_displayname != NOTINITIALIZED);
#ifdef _WIN32
return false;
return m_isHidden;
#else
return m_displayname[0] == '.';
#endif
@ -462,7 +465,7 @@ const FileItemList& FileItem::children()
// Get the interface to enumerate subitems
hr = pFolder->EnumObjects(
reinterpret_cast<HWND>(os::System::instance()->defaultWindow()->nativeHandle()),
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS,
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN,
&pEnum);
if (hr == S_OK && pEnum) {
@ -473,10 +476,9 @@ const FileItemList& FileItem::children()
while (pEnum->Next(256, itempidl, &fetched) == S_OK && fetched > 0) {
// Request the SFGAO_FOLDER attribute to know what of the
// item is file or a folder
for (c = 0; c < fetched; ++c) {
attribs[c] = SFGAO_FOLDER;
pFolder->GetAttributesOf(1, (LPCITEMIDLIST*)itempidl, attribs + c);
}
for (c = 0; c < fetched; ++c)
attribs[c] = SFGAO_FOLDER | SFGAO_HIDDEN;
pFolder->GetAttributesOf(fetched, (LPCITEMIDLIST*)itempidl, attribs);
// Generate the FileItems
for (c = 0; c < fetched; ++c) {
@ -755,6 +757,9 @@ static void update_by_pidl(FileItem* fileitem, SFGAOF attrib)
// Is it a folder?
fileitem->m_is_folder = calc_is_folder(fileitem->m_filename, attrib);
#if _WIN32
fileitem->m_isHidden = (attrib & SFGAO_HIDDEN ? true : false);
#endif
// Get the name to display

View File

@ -213,7 +213,7 @@ void ResourceFinder::includeDesktopDir(const char* filename)
#ifdef _WIN32
std::vector<wchar_t> buf(MAX_PATH);
HRESULT hr = SHGetFolderPath(NULL, CSIDL_DESKTOPDIRECTORY, NULL, SHGFP_TYPE_DEFAULT, &buf[0]);
HRESULT hr = SHGetFolderPath(NULL, CSIDL_DESKTOP, NULL, SHGFP_TYPE_CURRENT, &buf[0]);
if (hr == S_OK) {
addPath(base::join_path(base::to_utf8(&buf[0]), filename));
}

View File

@ -13,6 +13,8 @@
#include "app/console.h"
#include "app/context.h"
#include "app/context_observer.h"
#include "app/doc.h"
#include "app/doc_undo.h"
#include "app/script/docobj.h"
#include "app/script/engine.h"
#include "app/script/luacpp.h"

View File

@ -64,6 +64,7 @@
#include "doc/tag.h"
#include "doc/tileset.h"
#include "doc/tilesets.h"
#include "undo/undo_state.h"
#include <algorithm>
@ -1029,6 +1030,42 @@ int Sprite_set_useLayerUuids(lua_State* L)
return 0;
}
int Sprite_get_undoHistory(lua_State* L)
{
const auto* sprite = get_docobj<Sprite>(L, 1);
const auto* doc = static_cast<Doc*>(sprite->document());
const auto* history = doc->undoHistory();
if (!history) {
lua_pushnil(L);
return 1;
}
const undo::UndoState* currentState = history->currentState();
const undo::UndoState* s = history->firstState();
const bool canRedo = history->canRedo();
bool pastCurrent = !currentState && canRedo;
int undoSteps = 0;
int redoSteps = 0;
while (s) {
if (pastCurrent && canRedo)
redoSteps++;
else if (currentState || !canRedo)
undoSteps++;
if (s == currentState || !currentState)
pastCurrent = true;
s = s->next();
}
lua_newtable(L);
setfield_integer(L, "undoSteps", undoSteps);
setfield_integer(L, "redoSteps", redoSteps);
return 1;
}
const luaL_Reg Sprite_methods[] = {
{ "__eq", Sprite_eq },
{ "resize", Sprite_resize },
@ -1094,6 +1131,7 @@ const Property Sprite_properties[] = {
{ "events", Sprite_get_events, nullptr },
{ "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin },
{ "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids },
{ "undoHistory", Sprite_get_undoHistory, nullptr },
{ nullptr, nullptr, nullptr }
};

View File

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

View File

@ -17,6 +17,7 @@
#include "app/tileset_mode.h"
#include "app/ui/button_set.h"
#include "app/ui/color_button.h"
#include "app/ui/dockable.h"
#include "app/ui/input_chain_element.h"
#include "app/ui/palette_view.h"
#include "app/ui/tile_button.h"
@ -50,7 +51,8 @@ class ColorBar : public ui::Box,
public PaletteViewDelegate,
public ContextObserver,
public DocObserver,
public InputChainElement {
public InputChainElement,
public Dockable {
static ColorBar* m_instance;
public:
@ -65,7 +67,7 @@ public:
static ColorBar* instance() { return m_instance; }
ColorBar(int align, ui::TooltipManager* tooltipManager);
ColorBar(ui::TooltipManager* tooltipManager);
~ColorBar();
void setPixelFormat(doc::PixelFormat pixelFormat);
@ -123,10 +125,18 @@ public:
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;
// Dockable impl
int dockableAt() const override
{
// TODO split the ColorBar in different dockable widgets
return ui::LEFT | ui::RIGHT | ui::EXPANSIVE;
}
obs::signal<void()> ChangeSelection;
protected:
void onSizeHint(ui::SizeHintEvent& ev) override;
void onResize(ui::ResizeEvent& ev) override;
void onAppPaletteChange();
void onFocusPaletteView(ui::Message* msg);
void onFocusTilesView(ui::Message* msg);

View File

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

View File

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

View File

@ -1857,7 +1857,7 @@ private:
class ContextBar::FontSelector : public FontEntry {
public:
FontSelector(ContextBar* contextBar)
FontSelector(ContextBar* contextBar) : FontEntry(true) // With stroke and fill options
{
// Load the font from the preferences
setInfo(FontInfo::getFromPreferences(), FontEntry::From::Init);
@ -1962,6 +1962,12 @@ void ContextBar::onInitTheme(ui::InitThemeEvent& ev)
auto theme = SkinTheme::get(this);
gfx::Border border = this->border();
border.bottom(2 * guiscale());
// Docked at the left side
// TODO improve this how this is calculated
if (bounds().x == 0)
border.left(border.left() + 2 * guiscale());
setBorder(border);
setBgColor(theme->colors.workspace());
m_sprayLabel->setStyle(theme->styles.miniLabel());
@ -2559,6 +2565,11 @@ FontInfo ContextBar::fontInfo() const
return m_fontSelector->info();
}
FontEntry* ContextBar::fontEntry()
{
return m_fontSelector;
}
render::DitheringMatrix ContextBar::ditheringMatrix()
{
return m_ditheringSelector->ditheringMatrix();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -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();
@ -90,6 +92,7 @@ public:
// For text tool
FontInfo fontInfo() const;
FontEntry* fontEntry();
// For gradients
render::DitheringMatrix ditheringMatrix();
@ -99,6 +102,10 @@ public:
// For freehand with dynamics
const tools::DynamicsOptions& getDynamics() const;
// Dockable impl
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
int dockHandleSide() const override { return ui::LEFT; }
// Signals
obs::signal<void()> BrushChange;
obs::signal<void(const FontInfo&, FontEntry::From)> FontChange;

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

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

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

@ -0,0 +1,129 @@
// Aseprite
// Copyright (C) 2021-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_UI_DOCK_H_INCLUDED
#define APP_UI_DOCK_H_INCLUDED
#pragma once
#include "app/ui/dockable.h"
#include "gfx/rect.h"
#include "gfx/size.h"
#include "ui/widget.h"
#include <array>
#include <functional>
#include <string>
#include <vector>
namespace app {
class Dockable;
class Dock : public ui::Widget {
public:
static constexpr const int kSides = 5;
class DropzonePlaceholder final : public Widget,
public Dockable {
public:
DropzonePlaceholder(Widget* dragWidget, const gfx::Point& mousePosition);
~DropzonePlaceholder() override;
void setGhostPosition(const gfx::Point& position) const;
private:
void onPaint(ui::PaintEvent& ev) override;
int dockHandleSide() const override { return 0; }
gfx::Point m_mouseOffset;
ui::UILayerRef m_floatingUILayer;
};
Dock();
bool isCustomizing() const { return m_customizing; }
void setCustomizing(bool enable, bool doLayout = true);
void resetDocks();
// side = ui::LEFT, or ui::RIGHT, etc.
void dock(int side, ui::Widget* widget, const gfx::Size& prefSize = gfx::Size());
void dockRelativeTo(ui::Widget* relative,
int side,
ui::Widget* widget,
const gfx::Size& prefSize = gfx::Size());
void undock(ui::Widget* widget);
Dock* subdock(int side);
Dock* top() { return subdock(ui::TOP); }
Dock* bottom() { return subdock(ui::BOTTOM); }
Dock* left() { return subdock(ui::LEFT); }
Dock* right() { return subdock(ui::RIGHT); }
Dock* center() { return subdock(ui::CENTER); }
// Functions useful to query/save the dock layout.
int whichSideChildIsDocked(const ui::Widget* widget) const;
const gfx::Size getUserDefinedSizeAtSide(int side) const;
obs::signal<void()> Resize;
obs::signal<void()> UserResizedDock;
protected:
void onSizeHint(ui::SizeHintEvent& ev) override;
void onResize(ui::ResizeEvent& ev) override;
void onPaint(ui::PaintEvent& ev) override;
void onInitTheme(ui::InitThemeEvent& ev) override;
bool onProcessMessage(ui::Message* msg) override;
void onUserResizedDock();
private:
void setSide(int i, ui::Widget* newWidget);
int calcAlign(int i);
void updateDockVisibility();
void forEachSide(gfx::Rect bounds,
std::function<void(ui::Widget* widget,
const gfx::Rect& widgetBounds,
const gfx::Rect& separator,
const int index)> f);
bool hasVisibleSide(const int i) const { return (m_sides[i] && m_sides[i]->isVisible()); }
void redockWidget(app::Dock* widgetDock, ui::Widget* dockableWidget, const int side);
struct Hit {
ui::Widget* widget = nullptr;
Dockable* dockable = nullptr;
int sideIndex = -1;
int targetSide = -1;
};
Hit calcHit(const gfx::Point& pos);
std::array<Widget*, kSides> m_sides;
std::array<int, kSides> m_aligns;
std::array<gfx::Size, kSides> m_sizes;
bool m_autoDelete = false;
// Use to drag-and-drop stuff (splitters and dockable widgets)
Hit m_hit;
// Used to resize sizes splitters.
gfx::Size m_startSize;
gfx::Point m_startPos;
// True when we paint/can drag-and-drop dockable widgets from handles.
bool m_customizing = false;
// True when we're dragging a widget to attempt to dock it somewhere else.
bool m_dragging = false;
std::unique_ptr<DropzonePlaceholder> m_dropzonePlaceholder;
};
} // namespace app
#endif

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

@ -0,0 +1,37 @@
// Aseprite
// Copyright (C) 2021 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_UI_DOCKABLE_H_INCLUDED
#define APP_UI_DOCKABLE_H_INCLUDED
#pragma once
#include "ui/base.h"
namespace app {
class Dockable {
public:
virtual ~Dockable() {}
// LEFT = can be docked at the left side
// TOP = can be docked at the top
// RIGHT = can be docked at the right side
// BOTTOM = can be docked at the bottom
// CENTER = can be docked at the center
// EXPANSIVE = can be resized (e.g. add a splitter when docked at sides)
virtual int dockableAt() const
{
return ui::LEFT | ui::TOP | ui::RIGHT | ui::BOTTOM | ui::CENTER | ui::EXPANSIVE;
}
// Returns the preferred side where the dock handle to move the
// widget should be.
virtual int dockHandleSide() const { return ui::TOP; }
};
} // namespace app
#endif

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -457,6 +457,8 @@ void BrushPreview::show(const gfx::Point& screenPos)
// Here we re-use the cached surface
if (!cached && m_uiLayer->surface()) {
m_uiLayer->surface()->clear();
gfx::Rect layerBounds = m_uiLayer->surface()->bounds();
ui::Graphics g(display, m_uiLayer->surface(), 0, 0);

View File

@ -58,6 +58,7 @@
#include "app/util/tile_flags_utils.h"
#include "base/chrono.h"
#include "base/convert_to.h"
#include "base/scoped_value.h"
#include "doc/doc.h"
#include "doc/mask_boundaries.h"
#include "doc/slice.h"
@ -266,6 +267,23 @@ void Editor::setStateInternal(const EditorStatePtr& newState)
{
m_brushPreview.hide();
// Some onLeaveState impls (like the ones from MovingPixelsState,
// WritingTextState, MovingSelectionState) might generate a
// Tx/Transaction::commit(), which will add a new undo state,
// triggering a sprite change scripting event
// (SpriteEvents::onAddUndoState). This event could be handled by an
// extension and that extension might want to save the current
// sprite (e.g. calling Sprite_saveCopyAs, the kind of extension
// that takes snapshots after each sprite change). That will be a
// new Context::executeCommand() for the save command, generating a
// BeforeCommandExecution signal, getting back to onLeaveState
// again. In that case, we just ignore the reentry as the first
// onLeaveState should handle everything (to avoid an stack
// overflow/infinite recursion).
if (m_leavingState)
return;
base::ScopedValue leaving(m_leavingState, true);
// Fire before change state event, set the state, and fire after
// change state event.
EditorState::LeaveAction leaveAction = m_state->onLeaveState(this, newState.get());

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -458,6 +458,13 @@ private:
DocView* m_docView;
// Special flag to avoid re-entering a new state when we are leaving
// the current one. This avoids an infinite onLeaveState() recursion
// in some special cases when an extension (third-party code)
// creates a new sprite change in the same sprite change scripting
// event.
bool m_leavingState = false;
// Last known mouse position received by this editor when the
// mouse button was pressed. Used for auto-scrolling. To get the
// current mouse position on the editor you can use

View File

@ -428,8 +428,8 @@ bool MovingSliceState::onMouseMove(Editor* editor, MouseMessage* msg)
if (editor->slicesTransforms())
drawExtraCel();
// Redraw the editor.
editor->invalidate();
// Notify changes
m_site.document()->notifyGeneralUpdate();
// Use StandbyState implementation
return StandbyState::onMouseMove(editor, msg);

View File

@ -15,6 +15,7 @@
#include "app/commands/command.h"
#include "app/extra_cel.h"
#include "app/fonts/font_info.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "app/site.h"
#include "app/tx.h"
@ -43,12 +44,42 @@
#include "os/skia/skia_surface.h"
#endif
#include <cmath>
namespace app {
using namespace ui;
// Get ui::Paint to render text from context bar options / preferences
static ui::Paint get_paint_for_text()
{
ui::Paint paint;
if (auto* app = App::instance()) {
if (auto* ctxBar = app->contextBar())
paint = ctxBar->fontEntry()->paint();
}
paint.color(color_utils::color_for_ui(Preferences::instance().colorBar.fgColor()));
return paint;
}
static gfx::RectF calc_blob_bounds(const text::TextBlobRef& blob)
{
gfx::RectF bounds = get_text_blob_required_bounds(blob);
ui::Paint paint = get_paint_for_text();
if (paint.style() == ui::Paint::Style::Stroke ||
paint.style() == ui::Paint::Style::StrokeAndFill) {
bounds.enlarge(std::ceil(paint.strokeWidth()));
}
return bounds;
}
class WritingTextState::TextEditor : public Entry {
public:
enum TextPreview {
Intermediate, // With selection preview / user interface
Final, // Final to be rendered in the cel
};
TextEditor(Editor* editor, const Site& site, const gfx::Rect& bounds)
: Entry(4096, "")
, m_editor(editor)
@ -61,7 +92,7 @@ public:
setPersistSelection(true);
createExtraCel(site, bounds);
renderExtraCelBase();
renderExtraCel(TextPreview::Intermediate);
FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
if (auto font = Fonts::instance()->fontFromInfo(fontInfo))
@ -76,36 +107,37 @@ public:
// Returns the extra cel with the text rendered (but without the
// selected text highlighted).
ExtraCelRef extraCel()
ExtraCelRef extraCel(const TextPreview textPreview)
{
renderExtraCelBase();
renderExtraCelText(false);
renderExtraCel(textPreview);
return m_extraCel;
}
void setExtraCelBounds(const gfx::Rect& bounds)
void setExtraCelBounds(const gfx::RectF& bounds)
{
doc::Image* extraImg = m_extraCel->image();
if (!extraImg || bounds.w != extraImg->width() || bounds.h != extraImg->height()) {
if (!extraImg || std::ceil(bounds.w) != extraImg->width() ||
std::ceil(bounds.h) != extraImg->height()) {
createExtraCel(m_editor->getSite(), bounds);
}
else {
m_baseBounds = bounds;
m_extraCel->cel()->setBounds(bounds);
}
renderExtraCelBase();
renderExtraCelText(true);
renderExtraCel(TextPreview::Intermediate);
}
obs::signal<void(const gfx::Size&)> NewRequiredBounds;
obs::signal<void(const gfx::RectF&)> NewRequiredBounds;
private:
void createExtraCel(const Site& site, const gfx::Rect& bounds)
{
m_baseBounds = bounds;
m_extraCel->create(ExtraCel::Purpose::TextPreview,
site.tilemapMode(),
site.sprite(),
bounds,
bounds.size(),
gfx::Size(std::ceil(bounds.w), std::ceil(bounds.h)),
site.frame(),
255);
@ -176,7 +208,7 @@ private:
// Notify that we could make the text editor bigger to show this
// text blob.
NewRequiredBounds(get_text_blob_required_size(blob));
NewRequiredBounds(calc_blob_bounds(blob));
}
void onPaint(PaintEvent& ev) override
@ -205,8 +237,7 @@ private:
}
// Render extra cel with text + selected text
renderExtraCelBase();
renderExtraCelText(true);
renderExtraCel(TextPreview::Intermediate);
m_doc->setExtraCel(m_extraCel);
// Paint caret
@ -227,76 +258,80 @@ private:
}
}
void renderExtraCelBase()
void renderExtraCel(const TextPreview textPreview)
{
doc::Image* extraImg = m_extraCel->image();
ASSERT(extraImg);
if (!extraImg)
return;
const doc::Cel* extraCel = m_extraCel->cel();
extraImg->clear(extraImg->maskColor());
text::TextBlobRef blob = textBlob();
doc::ImageRef blobImage;
gfx::RectF bounds;
if (blob) {
const ui::Paint paint = get_paint_for_text();
bounds = calc_blob_bounds(blob);
blobImage = render_text_blob(blob, bounds, get_paint_for_text());
if (!blobImage)
return;
// Invert selected range in the image
if (textPreview == TextPreview::Intermediate) {
Range range;
getEntryThemeInfo(nullptr, nullptr, nullptr, &range);
if (!range.isEmpty()) {
gfx::RectF selectedBounds = getCharBoxBounds(range.from) | getCharBoxBounds(range.to - 1);
if (!selectedBounds.isEmpty()) {
selectedBounds.offset(-bounds.origin());
#ifdef LAF_SKIA
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(blobImage.get());
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
os::Paint paint2 = paint;
paint2.blendMode(os::BlendMode::Xor);
paint2.style(os::Paint::Style::Fill);
surface->drawRect(selectedBounds, paint2);
#endif // LAF_SKIA
}
}
}
}
doc::Cel* extraCel = m_extraCel->cel();
ASSERT(extraCel);
if (!extraCel)
return;
extraImg->clear(extraImg->maskColor());
extraCel->setPosition(m_baseBounds.x + bounds.x, m_baseBounds.y + bounds.y);
render::Render().renderLayer(extraImg,
m_editor->layer(),
m_editor->frame(),
gfx::Clip(0, 0, extraCel->bounds()),
doc::BlendMode::SRC);
}
void renderExtraCelText(const bool withSelection)
{
const auto textColor = color_utils::color_for_image(Preferences::instance().colorBar.fgColor(),
IMAGE_RGB);
text::TextBlobRef blob = textBlob();
if (!blob)
return;
doc::ImageRef image = render_text_blob(blob, textColor);
if (!image)
return;
// Invert selected range in the image
if (withSelection) {
Range range;
getEntryThemeInfo(nullptr, nullptr, nullptr, &range);
if (!range.isEmpty()) {
gfx::RectF selectedBounds = getCharBoxBounds(range.from) | getCharBoxBounds(range.to - 1);
if (!selectedBounds.isEmpty()) {
#ifdef LAF_SKIA
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(image.get());
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
os::Paint paint;
paint.blendMode(os::BlendMode::Xor);
paint.color(textColor);
surface->drawRect(selectedBounds, paint);
#endif // LAF_SKIA
}
}
if (blobImage) {
doc::blend_image(extraImg,
blobImage.get(),
gfx::Clip(blobImage->bounds().size()),
m_doc->sprite()->palette(m_editor->frame()),
255,
doc::BlendMode::NORMAL);
}
doc::Image* extraImg = m_extraCel->image();
ASSERT(extraImg);
if (!extraImg)
return;
doc::blend_image(extraImg,
image.get(),
gfx::Clip(image->bounds().size()),
m_doc->sprite()->palette(m_editor->frame()),
255,
doc::BlendMode::NORMAL);
}
Editor* m_editor;
Doc* m_doc;
ExtraCelRef m_extraCel;
// Initial bounds for the entry field. This can be modified later to
// render the text in case some initial letter/glyph needs some
// extra room at the left side.
gfx::Rect m_baseBounds;
};
WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
@ -312,10 +347,10 @@ WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
m_fontChangeConn =
App::instance()->contextBar()->FontChange.connect(&WritingTextState::onFontChange, this);
m_entry->NewRequiredBounds.connect([this](const gfx::Size& blobSize) {
if (m_bounds.w < blobSize.w || m_bounds.h < blobSize.h) {
m_bounds.w = std::max(m_bounds.w, blobSize.w);
m_bounds.h = std::max(m_bounds.h, blobSize.h);
m_entry->NewRequiredBounds.connect([this](const gfx::RectF& blobBounds) {
if (m_bounds.w < blobBounds.w || m_bounds.h < blobBounds.h) {
m_bounds.w = std::max(m_bounds.w, blobBounds.w);
m_bounds.h = std::max(m_bounds.h, blobBounds.h);
m_entry->setExtraCelBounds(m_bounds);
m_entry->setBounds(calcEntryBounds());
}
@ -388,11 +423,11 @@ void WritingTextState::onCommitMouseMove(Editor* editor, const gfx::PointF& spri
if (!m_movingBounds)
return;
gfx::Point delta(spritePos - m_cursorStart);
gfx::PointF delta(spritePos - m_cursorStart);
if (delta.x == 0 && delta.y == 0)
return;
m_bounds.setOrigin(gfx::Point(delta + m_boundsOrigin));
m_bounds.setOrigin(delta + m_boundsOrigin);
m_entry->setExtraCelBounds(m_bounds);
m_entry->setBounds(calcEntryBounds());
}
@ -408,12 +443,7 @@ bool WritingTextState::onSetCursor(Editor* editor, const gfx::Point& mouseScreen
return true;
}
bool WritingTextState::onKeyDown(Editor*, KeyMessage*)
{
return false;
}
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
bool WritingTextState::onKeyDown(Editor*, KeyMessage* msg)
{
// Cancel loop pressing Esc key
if (msg->scancode() == ui::kKeyEsc) {
@ -422,7 +452,17 @@ bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
// Drop text pressing Enter key
else if (msg->scancode() == ui::kKeyEnter) {
drop();
return true;
}
return false;
}
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
{
// Note: We cannot process kKeyEnter key here to drop the text as it
// could be received after the Enter key is pressed in the IME
// dialog to accept the composition (not to accept the text). So we
// process kKeyEnter in onKeyDown().
return true;
}
@ -475,8 +515,8 @@ EditorState::LeaveAction WritingTextState::onLeaveState(Editor* editor, EditorSt
// Paints the text in the active layer/sprite creating an
// undoable transaction.
Site site = m_editor->getSite();
ExtraCelRef extraCel = m_entry->extraCel();
Tx tx(site.document(), "Text Tool");
ExtraCelRef extraCel = m_entry->extraCel(TextEditor::Final);
Tx tx(site.document(), Strings::tools_text());
ExpandCelCanvas expand(site, site.layer(), TiledMode::NONE, tx, ExpandCelCanvas::None);
expand.validateDestCanvas(gfx::Region(extraCel->cel()->bounds()));
@ -527,7 +567,7 @@ void WritingTextState::onFontChange(const FontInfo& fontInfo, FontEntry::From fr
// This is useful to show changes to the anti-alias option
// immediately.
auto dummy = m_entry->extraCel();
auto dummy = m_entry->extraCel(TextEditor::Intermediate);
if (fromField == FontEntry::From::Popup) {
if (m_entry)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (c) 2022-2024 Igara Studio S.A.
// Copyright (c) 2022-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -59,7 +59,7 @@ private:
DelayedMouseMove m_delayedMouseMove;
Editor* m_editor;
gfx::Rect m_bounds;
gfx::RectF m_bounds;
std::unique_ptr<TextEditor> m_entry;
// True if the text was discarded.
@ -71,7 +71,7 @@ private:
bool m_mouseMoveReceived = false;
bool m_movingBounds = false;
gfx::PointF m_cursorStart;
gfx::Point m_boundsOrigin;
gfx::PointF m_boundsOrigin;
obs::scoped_connection m_beforeCmdConn;
obs::scoped_connection m_fontChangeConn;

View File

@ -45,6 +45,7 @@ FileList::FileList()
, m_multiselect(false)
, m_zoom(1.0)
, m_itemsPerRow(0)
, m_showHidden(Preferences::instance().fileSelector.showHidden())
{
setFocusStop(true);
setDoubleBuffered(true);
@ -173,6 +174,14 @@ void FileList::animateToZoom(const double zoom)
startAnimation(ANI_ZOOM, 10);
}
void FileList::setShowHidden(const bool show)
{
m_showHidden = show;
m_req_valid = false;
m_selected = nullptr;
regenerateList();
}
bool FileList::onProcessMessage(Message* msg)
{
switch (msg->type()) {
@ -825,7 +834,7 @@ void FileList::regenerateList()
for (FileItemList::iterator it = m_list.begin(); it != m_list.end();) {
IFileItem* fileitem = *it;
if (fileitem->isHidden())
if (fileitem->isHidden() && !m_showHidden)
it = m_list.erase(it);
else if (!fileitem->isFolder() && !fileitem->hasExtension(m_exts)) {
it = m_list.erase(it);

View File

@ -54,6 +54,7 @@ public:
double zoom() const { return m_zoom; }
void setZoom(const double zoom);
void animateToZoom(const double zoom);
void setShowHidden(const bool show);
obs::signal<void()> FileSelected;
obs::signal<void()> FileAccepted;
@ -137,6 +138,7 @@ private:
double m_toZoom;
int m_itemsPerRow;
bool m_showHidden;
};
} // namespace app

View File

@ -316,6 +316,7 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
for (auto child : viewType()->children())
child->setFocusStop(false);
showHiddenCheck()->setSelected(Preferences::instance().fileSelector.showHidden());
m_fileList = new FileList();
m_fileList->setId("fileview");
m_fileName->setAssociatedFileList(m_fileList);
@ -334,6 +335,10 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
viewType()->ItemChange.connect([this] { onChangeViewType(); });
location()->CloseListBox.connect([this] { onLocationCloseListBox(); });
fileType()->Change.connect([this] { onFileTypeChange(); });
showHiddenCheck()->Click.connect([this] {
Preferences::instance().fileSelector.showHidden(showHiddenCheck()->isSelected());
m_fileList->setShowHidden(showHiddenCheck()->isSelected());
});
m_fileList->FileSelected.connect([this] { onFileListFileSelected(); });
m_fileList->FileAccepted.connect([this] { onFileListFileAccepted(); });
m_fileList->CurrentFolderChanged.connect([this] { onFileListCurrentFolderChanged(); });

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -10,8 +10,10 @@
#include "app/ui/filename_field.h"
#include "app/app.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "app/recent_files.h"
#include "app/ui/skin/skin_theme.h"
#include "base/fs.h"
#include "ui/box.h"
@ -24,6 +26,11 @@ namespace app {
using namespace ui;
FilenameField::FilenameButton::FilenameButton(const std::string& text) : ButtonSet(1)
{
addItem(text);
}
FilenameField::FilenameField(const Type type, const std::string& pathAndFilename)
: m_entry(type == EntryAndButton ? new ui::Entry(1024, "") : nullptr)
, m_button(type == EntryAndButton ? Strings::select_file_browse() : Strings::select_file_text())
@ -46,7 +53,10 @@ FilenameField::FilenameField(const Type type, const std::string& pathAndFilename
if (m_entry)
m_entry->Change.connect([this] { setFilename(updatedFilename()); });
m_button.Click.connect([this] { onBrowse(); });
m_button.ItemChange.connect([this](ButtonSet::Item* item) {
m_button.setSelectedItem(nullptr);
onBrowse();
});
initTheme();
m_editFullPathChangeConn = Preferences::instance().general.editFullPath.AfterChange.connect(
@ -94,7 +104,6 @@ void FilenameField::onSetEditFullPath()
void FilenameField::onBrowse()
{
const gfx::Rect bounds = m_button.bounds();
m_button.setSelected(false);
ui::Menu menu;
ui::MenuItem choose(Strings::select_file_choose());
@ -107,6 +116,11 @@ void FilenameField::onBrowse()
menu.addChild(&relative);
menu.addChild(&absolute);
if (auto* recent = App::instance()->recentFiles()) {
addFoldersToMenu(&menu, recent->pinnedFolders(), Strings::file_selector_pinned_folders());
addFoldersToMenu(&menu, recent->recentFolders(), Strings::file_selector_recent_folders());
}
choose.Click.connect([this] {
std::string fn = SelectOutputFile();
if (!fn.empty()) {
@ -120,6 +134,21 @@ void FilenameField::onBrowse()
menu.showPopup(gfx::Point(bounds.x, bounds.y2()), display());
}
void FilenameField::addFoldersToMenu(ui::Menu* menu,
const base::paths& folders,
const std::string& separatorTitle)
{
if (folders.empty())
return;
menu->addChild(new ui::Separator(separatorTitle, ui::HORIZONTAL));
for (const std::string& folder : folders) {
MenuItem* folderItem = new MenuItem(folder);
folderItem->Click.connect([this, folder] { setFilename(base::join_path(folder, m_file)); });
menu->addChild(folderItem);
}
}
void FilenameField::setFilename(const std::string& pathAndFilename)
{
const std::string spritePath = base::get_file_path(m_docFilename);
@ -164,11 +193,6 @@ void FilenameField::onInitTheme(ui::InitThemeEvent& ev)
{
HBox::onInitTheme(ev);
setChildSpacing(0);
auto theme = skin::SkinTheme::get(this);
ui::Style* style = theme->styles.miniButton();
if (style)
m_button.setStyle(style);
}
void FilenameField::onUpdateText()
@ -181,9 +205,9 @@ void FilenameField::updateWidgets()
if (m_entry)
m_entry->setText(displayedFilename());
else if (m_file.empty())
m_button.setText(Strings::select_file_text());
m_button.getItem(0)->setText(Strings::select_file_text());
else
m_button.setText(displayedFilename());
m_button.getItem(0)->setText(displayedFilename());
Change();
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -8,14 +8,19 @@
#define APP_UI_FILENAME_FIELD_H_INCLUDED
#pragma once
#include "app/ui/button_set.h"
#include "base/paths.h"
#include "obs/connection.h"
#include "obs/signal.h"
#include "ui/box.h"
#include "ui/button.h"
#include "ui/entry.h"
#include <string>
namespace ui {
class Menu;
}
namespace app {
class FilenameField : public ui::HBox {
@ -44,17 +49,25 @@ protected:
void onSetEditFullPath();
private:
class FilenameButton : public ButtonSet {
public:
FilenameButton(const std::string& text);
};
void setEditFullPath(const bool on);
void updateWidgets();
void onBrowse();
std::string updatedFilename() const;
void addFoldersToMenu(ui::Menu* menu,
const base::paths& folders,
const std::string& separatorTitle);
std::string m_path;
std::string m_pathBase;
std::string m_file;
std::string m_docFilename;
ui::Entry* m_entry;
ui::Button m_button;
FilenameButton m_button;
bool m_editFullPath;
bool m_askOverwrite;

View File

@ -12,10 +12,12 @@
#include "app/app.h"
#include "app/console.h"
#include "app/i18n/strings.h"
#include "app/recent_files.h"
#include "app/ui/font_popup.h"
#include "app/ui/skin/skin_theme.h"
#include "base/contains.h"
#include "base/convert_to.h"
#include "base/scoped_value.h"
#include "fmt/format.h"
#include "ui/display.h"
@ -262,22 +264,91 @@ void FontEntry::FontSize::onEntryChange()
Change();
}
FontEntry::FontStyle::FontStyle() : ButtonSet(3, true)
FontEntry::FontStyle::FontStyle(ui::TooltipManager* tooltips) : ButtonSet(3, true)
{
addItem("B");
addItem("I");
addItem("...");
setMultiMode(MultiMode::Set);
tooltips->addTooltipFor(getItem(0), Strings::text_tool_bold(), BOTTOM);
tooltips->addTooltipFor(getItem(1), Strings::text_tool_italic(), BOTTOM);
tooltips->addTooltipFor(getItem(2), Strings::text_tool_more_options(), BOTTOM);
}
FontEntry::FontEntry()
FontEntry::FontStroke::FontStroke(ui::TooltipManager* tooltips) : m_fill(2)
{
auto* theme = skin::SkinTheme::get(this);
m_fill.addItem(theme->parts.toolFilledRectangle(), theme->styles.contextBarButton());
m_fill.addItem(theme->parts.toolRectangle(), theme->styles.contextBarButton());
m_fill.setSelectedItem(0);
m_fill.ItemChange.connect([this] { Change(); });
m_stroke.setText("0");
m_stroke.setSuffix("pt");
m_stroke.ValueChange.connect([this] { Change(); });
addChild(&m_fill);
addChild(&m_stroke);
tooltips->addTooltipFor(m_fill.getItem(0), Strings::shape_fill(), BOTTOM);
tooltips->addTooltipFor(m_fill.getItem(1), Strings::shape_stroke(), BOTTOM);
tooltips->addTooltipFor(&m_stroke, Strings::shape_stroke_width(), BOTTOM);
}
bool FontEntry::FontStroke::fill() const
{
return const_cast<FontStroke*>(this)->m_fill.getItem(0)->isSelected();
}
float FontEntry::FontStroke::stroke() const
{
return m_stroke.textDouble();
}
FontEntry::FontStroke::WidthEntry::WidthEntry() : ui::IntEntry(0, 100, this)
{
}
void FontEntry::FontStroke::WidthEntry::onValueChange()
{
ui::IntEntry::onValueChange();
ValueChange();
}
bool FontEntry::FontStroke::WidthEntry::onAcceptUnicodeChar(int unicodeChar)
{
return (IntEntry::onAcceptUnicodeChar(unicodeChar) || unicodeChar == '.');
}
std::string FontEntry::FontStroke::WidthEntry::onGetTextFromValue(int value)
{
return fmt::format("{:.1f}", value / 10.0);
}
int FontEntry::FontStroke::WidthEntry::onGetValueFromText(const std::string& text)
{
return int(10.0 * base::convert_to<double>(text));
}
FontEntry::FontEntry(const bool withStrokeAndFill)
: m_style(&m_tooltips)
, m_stroke(withStrokeAndFill ? std::make_unique<FontStroke>(&m_tooltips) : nullptr)
{
m_face.setExpansive(true);
m_size.setExpansive(false);
m_style.setExpansive(false);
addChild(&m_tooltips);
addChild(&m_face);
addChild(&m_size);
addChild(&m_style);
if (m_stroke)
addChild(m_stroke.get());
m_tooltips.addTooltipFor(&m_face, Strings::text_tool_font_family(), BOTTOM);
m_tooltips.addTooltipFor(m_size.getEntryWidget(), Strings::text_tool_font_size(), BOTTOM);
m_face.setMinSize(gfx::Size(128 * guiscale(), 0));
@ -299,6 +370,8 @@ FontEntry::FontEntry()
});
m_style.ItemChange.connect(&FontEntry::onStyleItemClick, this);
if (m_stroke)
m_stroke->Change.connect(&FontEntry::onStrokeChange, this);
}
// Defined here as FontPopup type is not fully defined in the header
@ -327,6 +400,29 @@ void FontEntry::setInfo(const FontInfo& info, const From fromField)
FontChange(m_info, fromField);
}
ui::Paint FontEntry::paint()
{
ui::Paint paint;
ui::Paint::Style style = ui::Paint::Fill;
if (m_stroke) {
const float stroke = m_stroke->stroke();
if (m_stroke->fill()) {
if (stroke > 0.0f) {
style = ui::Paint::StrokeAndFill;
paint.strokeWidth(stroke);
}
}
else {
style = ui::Paint::Stroke;
paint.strokeWidth(stroke);
}
}
paint.style(style);
return paint;
}
void FontEntry::onStyleItemClick(ButtonSet::Item* item)
{
text::FontStyle style = m_info.style();
@ -404,4 +500,9 @@ void FontEntry::onStyleItemClick(ButtonSet::Item* item)
}
}
void FontEntry::onStrokeChange()
{
FontChange(m_info, From::Paint);
}
} // namespace app

View File

@ -14,7 +14,11 @@
#include "ui/box.h"
#include "ui/button.h"
#include "ui/combobox.h"
#include "ui/int_entry.h"
#include "ui/paint.h"
#include "ui/tooltips.h"
#include <memory>
#include <string>
namespace app {
@ -30,18 +34,22 @@ public:
Flags,
Hinting,
Popup,
Paint,
};
FontEntry();
FontEntry(bool withStrokeAndFill);
~FontEntry();
FontInfo info() { return m_info; }
void setInfo(const FontInfo& info, From from);
ui::Paint paint();
obs::signal<void(const FontInfo&, From)> FontChange;
private:
void onStyleItemClick(ButtonSet::Item* item);
void onStrokeChange();
class FontFace : public SearchEntry {
public:
@ -73,13 +81,40 @@ private:
class FontStyle : public ButtonSet {
public:
FontStyle();
FontStyle(ui::TooltipManager* tooltips);
};
class FontStroke : public HBox {
public:
FontStroke(ui::TooltipManager* tooltips);
bool fill() const;
float stroke() const;
obs::signal<void()> Change;
private:
class WidthEntry : public ui::IntEntry,
public ui::SliderDelegate {
public:
WidthEntry();
obs::signal<void()> ValueChange;
private:
void onValueChange() override;
bool onAcceptUnicodeChar(int unicodeChar) override;
// SliderDelegate impl
std::string onGetTextFromValue(int value) override;
int onGetValueFromText(const std::string& text) override;
};
ButtonSet m_fill;
WidthEntry m_stroke;
};
ui::TooltipManager m_tooltips;
FontInfo m_info;
FontFace m_face;
FontSize m_size;
FontStyle m_style;
std::unique_ptr<FontStroke> m_stroke;
bool m_lockFace = false;
};

View File

@ -194,7 +194,11 @@ private:
if (!blob)
return;
doc::ImageRef image = render_text_blob(blob, gfx::rgba(0, 0, 0));
ui::Paint paint;
paint.color(gfx::rgba(0, 0, 0));
paint.style(ui::Paint::Fill);
const gfx::RectF textBounds = get_text_blob_required_bounds(blob);
doc::ImageRef image = render_text_blob(blob, textBounds, paint);
if (!image)
return;

View File

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

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

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

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

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

View File

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

View File

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

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

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

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

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

View File

@ -9,18 +9,23 @@
#define APP_UI_MAIN_MENU_BAR_H_INCLUDED
#pragma once
#include "app/ui/dockable.h"
#include "obs/connection.h"
#include "ui/menu.h"
namespace app {
class MainMenuBar : public ui::MenuBar {
class MainMenuBar : public ui::MenuBar,
public Dockable {
public:
MainMenuBar();
void queueReload();
void reload();
// Dockable impl
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
private:
obs::scoped_connection m_extKeys;
obs::scoped_connection m_extScripts;

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2023 Igara Studio S.A.
// Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -9,6 +9,7 @@
#define APP_UI_NOTIFICATIONS_H_INCLUDED
#pragma once
#include "app/ui/dockable.h"
#include "ui/button.h"
#include "ui/menu.h"
@ -19,20 +20,24 @@ class Style;
namespace app {
class INotificationDelegate;
class Notifications : public ui::Button {
class Notifications : public ui::Button,
public Dockable {
public:
Notifications();
void addLink(INotificationDelegate* del);
bool hasNotifications() const { return m_popup.hasChildren(); }
// Dockable impl
int dockableAt() const override { return ui::TOP | ui::BOTTOM | ui::LEFT | ui::RIGHT; }
protected:
void onInitTheme(ui::InitThemeEvent& ev) override;
void onSizeHint(ui::SizeHintEvent& ev) override;
void onPaint(ui::PaintEvent& ev) override;
void onClick() override;
private:
ui::Style* m_flagStyle;
bool m_red;
ui::Menu m_popup;
};

View File

@ -1483,6 +1483,8 @@ void SkinTheme::drawEntryText(ui::Graphics* g, ui::Entry* widget)
// Draw caret at the end of the text
if (!delegate.caretDrawn()) {
bounds.x += delegate.textBounds().w;
gfx::Rect charBounds(bounds.x + widget->bounds().x,
bounds.y + widget->bounds().y,
0,

View File

@ -13,6 +13,7 @@
#include "app/context_observer.h"
#include "app/tools/active_tool_observer.h"
#include "app/ui/doc_observer_widget.h"
#include "app/ui/dockable.h"
#include "base/time.h"
#include "doc/tile.h"
#include "ui/base.h"
@ -44,7 +45,8 @@ class Tool;
}
class StatusBar : public DocObserverWidget<ui::HBox>,
public tools::ActiveToolObserver {
public tools::ActiveToolObserver,
public Dockable {
static StatusBar* m_instance;
public:
@ -72,6 +74,10 @@ public:
void showBackupIcon(BackupIcon icon);
// Dockable impl
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
int dockHandleSide() const override { return ui::LEFT; }
protected:
void onInitTheme(ui::InitThemeEvent& ev) override;
void onResize(ui::ResizeEvent& ev) override;

View File

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

View File

@ -9,6 +9,7 @@
#define APP_UI_TABS_H_INCLUDED
#pragma once
#include "app/ui/dockable.h"
#include "base/ref.h"
#include "text/fwd.h"
#include "ui/animated_widget.h"
@ -118,7 +119,8 @@ public:
// Tabs control. Used to show opened documents.
class Tabs : public ui::Widget,
public ui::AnimatedWidget {
public ui::AnimatedWidget,
public Dockable {
struct Tab {
TabView* view;
std::string text;
@ -181,6 +183,9 @@ public:
void removeDropViewPreview();
int getDropTabIndex() const { return m_dropNewIndex; }
// Dockable impl
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
protected:
bool onProcessMessage(ui::Message* msg) override;
void onInitTheme(ui::InitThemeEvent& ev) override;

View File

@ -13,6 +13,7 @@
#include "app/docs_observer.h"
#include "app/loop_tag.h"
#include "app/pref/preferences.h"
#include "app/ui/dockable.h"
#include "app/ui/editor/editor_observer.h"
#include "app/ui/input_chain_element.h"
#include "app/ui/timeline/ani_controls.h"
@ -73,7 +74,8 @@ class Timeline : public ui::Widget,
public DocObserver,
public EditorObserver,
public InputChainElement,
public TagProvider {
public TagProvider,
public Dockable {
public:
using Range = view::Range;
using RealRange = view::RealRange;
@ -155,6 +157,12 @@ public:
void clearAndInvalidateRange();
// Dockable impl
int dockableAt() const override
{
return ui::TOP | ui::BOTTOM | ui::LEFT | ui::RIGHT | ui::EXPANSIVE;
}
protected:
bool onProcessMessage(ui::Message* msg) override;
void onInitTheme(ui::InitThemeEvent& ev) override;

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2021-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -10,6 +10,7 @@
#pragma once
#include "app/tools/active_tool_observer.h"
#include "app/ui/dockable.h"
#include "app/ui/skin/skin_part.h"
#include "gfx/point.h"
#include "obs/connection.h"
@ -17,6 +18,7 @@
#include "ui/widget.h"
#include <map>
#include <memory>
namespace ui {
class CloseEvent;
@ -32,6 +34,7 @@ class ToolGroup;
// Class to show selected tools for each tool (vertically)
class ToolBar : public ui::Widget,
public Dockable,
public tools::ActiveToolObserver {
static ToolBar* m_instance;
@ -52,6 +55,14 @@ public:
void openTipWindow(tools::ToolGroup* toolGroup, tools::Tool* tool);
void closeTipWindow();
// Dockable impl
int dockableAt() const override
{
// TODO add future support to dock the tool bar at the
// top/bottom sides
return ui::LEFT | ui::RIGHT;
}
protected:
bool onProcessMessage(ui::Message* msg) override;
void onSizeHint(ui::SizeHintEvent& ev) override;
@ -62,6 +73,7 @@ protected:
private:
enum class GroupType { Regular, Overflow };
bool isDockedAtLeftSide() const;
int getToolGroupIndex(tools::ToolGroup* group);
void openPopupWindow(GroupType group_type,
int group_index = 0,
@ -95,12 +107,12 @@ private:
bool m_openedRecently;
// Window displayed to show a tool-group
ui::PopupWindow* m_popupWindow;
std::unique_ptr<ui::PopupWindow> m_popupWindow;
class ToolStrip;
ToolStrip* m_currentStrip;
ToolStrip* m_currentStrip = nullptr;
// Tool-tip window
ui::TipWindow* m_tipWindow;
std::unique_ptr<ui::TipWindow> m_tipWindow;
ui::Timer m_tipTimer;
bool m_tipOpened;

View File

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

View File

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

View File

@ -60,7 +60,7 @@ private:
} // anonymous namespace
gfx::Size get_text_blob_required_size(const text::TextBlobRef& blob)
gfx::RectF get_text_blob_required_bounds(const text::TextBlobRef& blob)
{
ASSERT(blob != nullptr);
@ -74,32 +74,28 @@ gfx::Size get_text_blob_required_size(const text::TextBlobRef& blob)
bounds.w = 1;
if (bounds.h < 1)
bounds.h = 1;
return gfx::Size(std::ceil(bounds.w), std::ceil(bounds.h));
return bounds;
}
doc::ImageRef render_text_blob(const text::TextBlobRef& blob, gfx::Color color)
doc::ImageRef render_text_blob(const text::TextBlobRef& blob,
const gfx::RectF& textBounds,
const ui::Paint& paint)
{
ASSERT(blob != nullptr);
os::Paint paint;
// TODO offer Stroke, StrokeAndFill, and Fill styles
paint.style(os::Paint::Fill);
paint.color(color);
gfx::Size blobSize = get_text_blob_required_size(blob);
doc::ImageRef image(doc::Image::create(doc::IMAGE_RGB, blobSize.w, blobSize.h));
doc::ImageRef image(
doc::Image::create(doc::IMAGE_RGB, std::ceil(textBounds.w), std::ceil(textBounds.h)));
#ifdef LAF_SKIA
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(image.get());
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
text::draw_text(surface.get(), blob, gfx::PointF(0, 0), &paint);
text::draw_text(surface.get(), blob, -textBounds.origin(), &paint);
#endif // LAF_SKIA
return image;
}
doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, gfx::Color color)
doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, const ui::Paint& paint)
{
Fonts* fonts = Fonts::instance();
ASSERT(fonts);
@ -113,10 +109,6 @@ doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, gfx
const text::FontMgrRef fontMgr = fonts->fontMgr();
ASSERT(fontMgr);
os::Paint paint;
paint.style(os::Paint::StrokeAndFill);
paint.color(color);
// We have to measure all text runs which might use different
// fonts (e.g. if the given font is not enough to shape other code
// points/languages).

View File

@ -11,25 +11,28 @@
#include "doc/image_ref.h"
#include "gfx/color.h"
#include "gfx/rect.h"
#include "text/text_blob.h"
#include "ui/paint.h"
#include <string>
namespace app {
class Color;
class FontInfo;
namespace skin {
class SkinTheme;
}
// Returns the exact bounds that are required to draw this TextBlob,
// i.e. the image size that will be required in render_text_blob().
gfx::Size get_text_blob_required_size(const text::TextBlobRef& blob);
// Returns the exact bounds that are required to draw this TextBlob in
// the origin point (0, 0), i.e. the image size that will be required
// in render_text_blob().
gfx::RectF get_text_blob_required_bounds(const text::TextBlobRef& blob);
doc::ImageRef render_text_blob(const text::TextBlobRef& blob, gfx::Color color);
doc::ImageRef render_text_blob(const text::TextBlobRef& blob,
const gfx::RectF& textBounds,
const ui::Paint& paint);
doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, gfx::Color color);
doc::ImageRef render_text(const FontInfo& fontInfo,
const std::string& text,
const ui::Paint& paint);
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -257,11 +257,9 @@ Widget* WidgetLoader::convertXmlElementToWidget(const XMLElement* elem,
((ExprEntry*)widget)->setDecimals(strtol(decimals, nullptr, 10));
}
if (elem_name == "filename") {
const char* button_only = elem->Attribute("button_only");
const app::FilenameField::Type type = ((button_only != nullptr &&
strtol(button_only, nullptr, 10) == 1) ?
app::FilenameField::Type::ButtonOnly :
app::FilenameField::Type::EntryAndButton);
const bool buttononly = bool_attr(elem, "buttononly", false);
const app::FilenameField::Type type = (buttononly ? app::FilenameField::Type::ButtonOnly :
app::FilenameField::Type::EntryAndButton);
widget = new app::FilenameField(type, "");
}
@ -534,7 +532,7 @@ Widget* WidgetLoader::convertXmlElementToWidget(const XMLElement* elem,
}
else if (elem_name == "font") {
if (!widget)
widget = new FontEntry;
widget = new FontEntry(false);
}
// Was the widget created?

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -20,11 +20,12 @@
#include "doc/primitives.h"
#include <algorithm>
#include <atomic>
#include <cmath>
namespace doc {
static int generation = 0;
static std::atomic<int> g_generation = 0;
Brush::Brush()
{
@ -300,7 +301,7 @@ void Brush::setCenter(const gfx::Point& center)
// Cleans the brush's data (image and region).
void Brush::clean()
{
m_gen = ++generation;
m_gen = ++g_generation;
m_image.reset();
m_maskBitmap.reset();
m_backupImage.reset();

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

86
src/main/osx/Info.plist Normal file
View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>aseprite</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>Document.icns</string>
<key>CFBundleTypeName</key>
<string>Aseprite Sprite</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>ase</string>
<string>bmp</string>
<string>flc</string>
<string>fli</string>
<string>gif</string>
<string>ico</string>
<string>jpeg</string>
<string>jpg</string>
<string>pcx</string>
<string>png</string>
<string>tga</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>Document.icns</string>
<key>CFBundleTypeName</key>
<string>Aseprite Sprite</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>aseprite-extension</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>Extension.icns</string>
<key>CFBundleTypeName</key>
<string>Aseprite Extension</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
</array>
<key>CFBundleDisplayName</key>
<string>Aseprite</string>
<key>CFBundleExecutable</key>
<string>aseprite</string>
<key>CFBundleIdentifier</key>
<string>org.aseprite.Aseprite</string>
<key>CFBundleName</key>
<string>Aseprite</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>1.3</string>
<key>CFBundleVersion</key>
<string>1.3</string>
<key>CFBundleIconFile</key>
<string>Aseprite.icns</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.graphics-design</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2001-2025, Igara Studio S.A.
All rights reserved.</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSRequiresAquaSystemAppearance</key>
<false/>
</dict>
</plist>

View File

@ -106,7 +106,6 @@ void Box::onResize(ResizeEvent& ev)
continue; \
\
int size = 0; \
int sizeDiff = 0; \
\
if (align() & HOMOGENEOUS) { \
if (i < visibleChildren - 1) \

View File

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

View File

@ -128,6 +128,15 @@ int Entry::lastCaretPos() const
return int(m_boxes.size() - 1);
}
gfx::Point Entry::caretPosOnScreen() const
{
const gfx::Point caretPos = getCharBoxBounds(m_caret).point2();
const os::Window* nativeWindow = display()->nativeWindow();
const gfx::Point pos = nativeWindow->pointToScreen(caretPos + bounds().origin());
return pos;
}
void Entry::setCaretPos(const int pos)
{
gfx::Size caretSize = theme()->getEntryCaretSize(this);
@ -160,6 +169,8 @@ void Entry::setCaretPos(const int pos)
startTimer();
m_state = true;
os::System::instance()->setTextInput(true, caretPosOnScreen());
invalidate();
}
@ -251,7 +262,7 @@ gfx::Rect Entry::getEntryTextBounds() const
return onGetEntryTextBounds();
}
gfx::Rect Entry::getCharBoxBounds(const int i)
gfx::Rect Entry::getCharBoxBounds(const int i) const
{
ASSERT(i >= 0 && i < int(m_boxes.size()));
if (i >= 0 && i < int(m_boxes.size()))
@ -288,8 +299,9 @@ bool Entry::onProcessMessage(Message* msg)
}
// Start processing dead keys
if (m_translate_dead_keys)
os::System::instance()->setTranslateDeadKeys(true);
if (m_translate_dead_keys) {
os::System::instance()->setTextInput(true, caretPosOnScreen());
}
break;
case kFocusLeaveMessage:
@ -304,7 +316,7 @@ bool Entry::onProcessMessage(Message* msg)
// Stop processing dead keys
if (m_translate_dead_keys)
os::System::instance()->setTranslateDeadKeys(false);
os::System::instance()->setTextInput(false);
break;
case kKeyDownMessage:

View File

@ -43,6 +43,7 @@ public:
int caretPos() const { return m_caret; }
int lastCaretPos() const;
gfx::Point caretPosOnScreen() const;
void setCaretPos(int pos);
void setCaretToEnd();
@ -76,7 +77,7 @@ public:
obs::signal<void()> Change;
protected:
gfx::Rect getCharBoxBounds(int i);
gfx::Rect getCharBoxBounds(int i) const;
// Events
bool onProcessMessage(Message* msg) override;

View File

@ -115,8 +115,8 @@ bool IntEntry::onProcessMessage(Message* msg)
case kKeyDownMessage:
if (hasFocus() && !isReadOnly()) {
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
int chr = keymsg->unicodeChar();
if (chr >= 32 && (chr < '0' || chr > '9')) {
const int chr = keymsg->unicodeChar();
if (chr >= 32 && !onAcceptUnicodeChar(chr)) {
// "Eat" all keys that aren't number
return true;
}
@ -166,6 +166,11 @@ void IntEntry::onValueChange()
// Do nothing
}
bool IntEntry::onAcceptUnicodeChar(const int unicodeChar)
{
return (unicodeChar >= '0' && unicodeChar <= '9');
}
void IntEntry::openPopup()
{
m_slider->setValue(getValue());

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2022-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -36,6 +36,7 @@ protected:
// New events
virtual void onValueChange();
virtual bool onAcceptUnicodeChar(int unicodeChar);
int m_min;
int m_max;

View File

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

View File

@ -228,3 +228,69 @@ do
c = app.open(fn)
assert(c.tileManagementPlugin == nil)
end
-- Undo History
function test_undo_history()
local sprite = Sprite(1, 1)
assert(sprite.undoHistory.undoSteps == 0)
assert(sprite.undoHistory.redoSteps == 0)
sprite:resize(10, 10)
assert(sprite.undoHistory.undoSteps == 1)
assert(sprite.undoHistory.redoSteps == 0)
sprite:resize(10, 15)
assert(sprite.undoHistory.undoSteps == 2)
assert(sprite.undoHistory.redoSteps == 0)
sprite:resize(10, 30)
assert(sprite.undoHistory.undoSteps == 3)
assert(sprite.undoHistory.redoSteps == 0)
app.undo()
assert(sprite.undoHistory.undoSteps == 2)
assert(sprite.undoHistory.redoSteps == 1)
app.undo()
assert(sprite.undoHistory.undoSteps == 1)
assert(sprite.undoHistory.redoSteps == 2)
app.redo()
assert(sprite.undoHistory.undoSteps == 2)
assert(sprite.undoHistory.redoSteps == 1)
app.undo()
app.undo()
assert(sprite.undoHistory.undoSteps == 0)
assert(sprite.undoHistory.redoSteps == 3)
sprite:resize(10, 30)
if (app.preferences.undo.allow_nonlinear_history) then
assert(sprite.undoHistory.undoSteps == 4)
assert(sprite.undoHistory.redoSteps == 0)
else
assert(sprite.undoHistory.undoSteps == 1)
assert(sprite.undoHistory.redoSteps == 0)
end
end
do
local prevSetting = app.preferences.undo.allow_nonlinear_history
app.preferences.undo.allow_nonlinear_history = true
test_undo_history()
app.preferences.undo.allow_nonlinear_history = prevSetting
end
do
local prevSetting = app.preferences.undo.allow_nonlinear_history
app.preferences.undo.allow_nonlinear_history = false
test_undo_history()
app.preferences.undo.allow_nonlinear_history = prevSetting
end