mirror of https://github.com/aseprite/aseprite.git
Compare commits
58 Commits
b7a9bec119
...
334739524a
| Author | SHA1 | Date |
|---|---|---|
|
|
334739524a | |
|
|
49da8e1ae1 | |
|
|
047e331b40 | |
|
|
0754a82023 | |
|
|
8f3b15276f | |
|
|
763ebba6a2 | |
|
|
dcbf444aaf | |
|
|
47fc3b0391 | |
|
|
d0656ddd98 | |
|
|
4dd20d2b85 | |
|
|
cb6b512d25 | |
|
|
5b57abd810 | |
|
|
f69e94c6e3 | |
|
|
f1759b069a | |
|
|
88e3c2a48c | |
|
|
d9415fdf1b | |
|
|
d0457cc1f4 | |
|
|
84c35a63c2 | |
|
|
00b0ba93d1 | |
|
|
eebd7c2666 | |
|
|
e6c1fbf560 | |
|
|
90fae86c9d | |
|
|
ec41e9fe66 | |
|
|
6b0a1028fb | |
|
|
dc51ca25e0 | |
|
|
381d9e663a | |
|
|
e2fcbc86df | |
|
|
b0683f7914 | |
|
|
47c8e5eae3 | |
|
|
bba8b00bd2 | |
|
|
fc57df4305 | |
|
|
7ca6f53eb6 | |
|
|
1f75d5444d | |
|
|
4858a5103e | |
|
|
fa21d87ba8 | |
|
|
cef92c1a38 | |
|
|
22e72ab5cb | |
|
|
80fa065bd5 | |
|
|
de1ccb24dd | |
|
|
7d91c4b9d9 | |
|
|
6d89a6bc15 | |
|
|
d4e97b5a96 | |
|
|
205b18dc0f | |
|
|
2ba051b59b | |
|
|
3fcb000eb1 | |
|
|
af9dc3c817 | |
|
|
250dfdc86a | |
|
|
c904c41b39 | |
|
|
9e941e9a8b | |
|
|
bbab4d5875 | |
|
|
5c4daff128 | |
|
|
11a7b061ff | |
|
|
283bedf77e | |
|
|
2eeb6f04a7 | |
|
|
706d0b8a7a | |
|
|
2f3a7f5dec | |
|
|
d5de74b715 | |
|
|
2d87a7b184 |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
|
@ -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_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_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_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_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_normal" x="32" y="112" w="5" h="5" />
|
||||||
<part id="tab_modified_icon_active" x="32" y="117" 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_close" x="152" y="256" w="7" h="7" />
|
||||||
<part id="icon_search" x="160" y="256" w="8" h="8" />
|
<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_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_pos" x="144" y="264" w="8" h="8" />
|
||||||
<part id="icon_size" x="152" 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" />
|
<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 |
|
|
@ -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_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_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_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_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_normal" x="32" y="112" w="5" h="5" />
|
||||||
<part id="tab_modified_icon_active" x="32" y="117" 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_close" x="152" y="256" w="7" h="7" />
|
||||||
<part id="icon_search" x="160" y="256" w="8" h="8" />
|
<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_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_pos" x="144" y="264" w="8" h="8" />
|
||||||
<part id="icon_size" x="152" 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" />
|
<part id="icon_selsize" x="160" y="264" w="8" h="8" />
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,7 @@
|
||||||
<param name="popup" value="background" />
|
<param name="popup" value="background" />
|
||||||
</key>
|
</key>
|
||||||
<key command="ShowExtras" shortcut="Ctrl+H" />
|
<key command="ShowExtras" shortcut="Ctrl+H" />
|
||||||
|
<key command="ToggleWorkspaceLayout" shortcut="Shift+W" />
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<key command="GotoNextTab" shortcut="Ctrl+Tab" />
|
<key command="GotoNextTab" shortcut="Ctrl+Tab" />
|
||||||
<key command="GotoPreviousTab" shortcut="Ctrl+Shift+Tab" />
|
<key command="GotoPreviousTab" shortcut="Ctrl+Shift+Tab" />
|
||||||
|
|
@ -1002,6 +1003,7 @@
|
||||||
</menu>
|
</menu>
|
||||||
<menu text="@.view" id="view_menu">
|
<menu text="@.view" id="view_menu">
|
||||||
<item command="DuplicateView" text="@.view_duplicate_view" group="view_new" />
|
<item command="DuplicateView" text="@.view_duplicate_view" group="view_new" />
|
||||||
|
<item command="ToggleWorkspaceLayout" text="@.view_workspace_layout" />
|
||||||
<separator />
|
<separator />
|
||||||
<item command="ShowExtras" text="@.view_show_extras" />
|
<item command="ShowExtras" text="@.view_show_extras" />
|
||||||
<menu text="@.view_show" group="view_extras">
|
<menu text="@.view_show" group="view_extras">
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,7 @@
|
||||||
<option id="keep_closed_sprite_on_memory_for" type="double" default="15.0" />
|
<option id="keep_closed_sprite_on_memory_for" type="double" default="15.0" />
|
||||||
<option id="show_full_path" type="bool" default="true" />
|
<option id="show_full_path" type="bool" default="true" />
|
||||||
<option id="edit_full_path" type="bool" default="false" />
|
<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_position" type="TimelinePosition" default="TimelinePosition::BOTTOM" />
|
||||||
<option id="timeline_layer_panel_width" type="int" default="100" />
|
<option id="timeline_layer_panel_width" type="int" default="100" />
|
||||||
<option id="show_menu_bar" type="bool" default="true" />
|
<option id="show_menu_bar" type="bool" default="true" />
|
||||||
|
|
@ -356,6 +357,7 @@
|
||||||
<section id="file_selector">
|
<section id="file_selector">
|
||||||
<option id="current_folder" type="std::string" default=""<empty>"" />
|
<option id="current_folder" type="std::string" default=""<empty>"" />
|
||||||
<option id="zoom" type="double" default="1.0" />
|
<option id="zoom" type="double" default="1.0" />
|
||||||
|
<option id="show_hidden" type="bool" default="false" />
|
||||||
</section>
|
</section>
|
||||||
<section id="text_tool">
|
<section id="text_tool">
|
||||||
<option id="font_face" type="std::string" />
|
<option id="font_face" type="std::string" />
|
||||||
|
|
|
||||||
|
|
@ -488,6 +488,7 @@ TilesetDuplicate = Duplicate Tileset
|
||||||
Undo = Undo
|
Undo = Undo
|
||||||
UndoHistory = Undo History
|
UndoHistory = Undo History
|
||||||
UnlinkCel = Unlink Cel
|
UnlinkCel = Unlink Cel
|
||||||
|
ToggleWorkspaceLayout = Toggle Workspace Layout
|
||||||
Zoom = Zoom
|
Zoom = Zoom
|
||||||
Zoom_In = Zoom In
|
Zoom_In = Zoom In
|
||||||
Zoom_Out = Zoom Out
|
Zoom_Out = Zoom Out
|
||||||
|
|
@ -765,6 +766,7 @@ pinned_folders = Pinned Folders
|
||||||
recent_folders = Recent Folders
|
recent_folders = Recent Folders
|
||||||
all_formats = All formats
|
all_formats = All formats
|
||||||
all_files = All files
|
all_files = All files
|
||||||
|
show_hidden = Show hidden
|
||||||
|
|
||||||
[filters]
|
[filters]
|
||||||
selected_cels = Selected
|
selected_cels = Selected
|
||||||
|
|
@ -1163,6 +1165,7 @@ select_load_from_file = &Load from MSK file
|
||||||
select_save_to_file = &Save to MSK file
|
select_save_to_file = &Save to MSK file
|
||||||
view = &View
|
view = &View
|
||||||
view_duplicate_view = Duplicate &View
|
view_duplicate_view = Duplicate &View
|
||||||
|
view_workspace_layout = Workspace &Layout
|
||||||
view_show_extras = &Extras
|
view_show_extras = &Extras
|
||||||
view_show = &Show
|
view_show = &Show
|
||||||
view_show_layer_edges = &Layer Edges
|
view_show_layer_edges = &Layer Edges
|
||||||
|
|
@ -1204,6 +1207,14 @@ help_twitter = Twitter
|
||||||
help_enter_license = Enter &License
|
help_enter_license = Enter &License
|
||||||
help_about = &About
|
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]
|
[mask_by_color]
|
||||||
title = Select Color
|
title = Select Color
|
||||||
label_color = Color:
|
label_color = Color:
|
||||||
|
|
@ -1232,6 +1243,20 @@ name = Name:
|
||||||
tileset = Tileset:
|
tileset = Tileset:
|
||||||
default_new_layer_name = New Layer
|
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]
|
[news_listbox]
|
||||||
more = More...
|
more = More...
|
||||||
problem_loading = Problems loading news. Please retry.
|
problem_loading = Problems loading news. Please retry.
|
||||||
|
|
@ -1834,11 +1859,20 @@ pixel_scale = Pixel Scale
|
||||||
with_vars = Use CSS3 Variables
|
with_vars = Use CSS3 Variables
|
||||||
generate_html = Generate Sample HTML File
|
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]
|
[timeline_conf]
|
||||||
position = Position:
|
position = Position:
|
||||||
left = &Left
|
|
||||||
right = &Right
|
|
||||||
bottom = &Bottom
|
|
||||||
frame_header = Frame Header:
|
frame_header = Frame Header:
|
||||||
first_frame = First Frame:
|
first_frame = First Frame:
|
||||||
thumbnails = Thumbnails
|
thumbnails = Thumbnails
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
<combobox id="location" expansive="true" />
|
<combobox id="location" expansive="true" />
|
||||||
<button text="" id="refresh_button" style="refresh_button"
|
<button text="" id="refresh_button" style="refresh_button"
|
||||||
tooltip="@.refresh_button_tooltip" tooltip_dir="bottom" />
|
tooltip="@.refresh_button_tooltip" tooltip_dir="bottom" />
|
||||||
|
<check id="show_hidden_check" text="@.show_hidden" />
|
||||||
</box>
|
</box>
|
||||||
<vbox id="file_view_placeholder" expansive="true" />
|
<vbox id="file_view_placeholder" expansive="true" />
|
||||||
<grid columns="2">
|
<grid columns="2">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -6,13 +6,7 @@
|
||||||
<hbox>
|
<hbox>
|
||||||
<vbox>
|
<vbox>
|
||||||
<separator cell_hspan="2" text="@.position" left="true" horizontal="true" />
|
<separator cell_hspan="2" text="@.position" left="true" horizontal="true" />
|
||||||
<hbox>
|
<button id="layout" icon="icon_layout" />
|
||||||
<buttonset columns="2" id="position">
|
|
||||||
<item text="@.left" />
|
|
||||||
<item text="@.right" />
|
|
||||||
<item text="@.bottom" hspan="2" />
|
|
||||||
</buttonset>
|
|
||||||
</hbox>
|
|
||||||
</vbox>
|
</vbox>
|
||||||
<vbox>
|
<vbox>
|
||||||
<separator text="@.frame_header" left="true" horizontal="true" />
|
<separator text="@.frame_header" left="true" horizontal="true" />
|
||||||
|
|
|
||||||
2
laf
2
laf
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7c01959d7f0f2662a48eaa98e9b94336c4e45fda
|
Subproject commit 8ec4b553f1618f7a4b47cdcf4cfc2663266111ac
|
||||||
|
|
@ -180,8 +180,8 @@ if(ENABLE_ASEPRITE_EXE)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set(main_resources
|
set(main_resources
|
||||||
main/resources_win32.rc
|
main/win/resources_win32.rc
|
||||||
main/settings.manifest)
|
main/win/settings.manifest)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_executable(${main_target}
|
add_executable(${main_target}
|
||||||
|
|
|
||||||
|
|
@ -520,6 +520,7 @@ target_sources(app-lib PRIVATE
|
||||||
commands/tileset_mode.cpp
|
commands/tileset_mode.cpp
|
||||||
commands/toggle_other_layers_opacity.cpp
|
commands/toggle_other_layers_opacity.cpp
|
||||||
commands/toggle_play_option.cpp
|
commands/toggle_play_option.cpp
|
||||||
|
commands/toggle_workspace_layout.cpp
|
||||||
console.cpp
|
console.cpp
|
||||||
context.cpp
|
context.cpp
|
||||||
context_flags.cpp
|
context_flags.cpp
|
||||||
|
|
@ -609,6 +610,7 @@ target_sources(app-lib PRIVATE
|
||||||
ui/context_bar.cpp
|
ui/context_bar.cpp
|
||||||
ui/dithering_selector.cpp
|
ui/dithering_selector.cpp
|
||||||
ui/doc_view.cpp
|
ui/doc_view.cpp
|
||||||
|
ui/dock.cpp
|
||||||
ui/drop_down_button.cpp
|
ui/drop_down_button.cpp
|
||||||
ui/dynamics_popup.cpp
|
ui/dynamics_popup.cpp
|
||||||
ui/editor/brush_preview.cpp
|
ui/editor/brush_preview.cpp
|
||||||
|
|
@ -653,6 +655,9 @@ target_sources(app-lib PRIVATE
|
||||||
ui/input_chain.cpp
|
ui/input_chain.cpp
|
||||||
ui/keyboard_shortcuts.cpp
|
ui/keyboard_shortcuts.cpp
|
||||||
ui/layer_frame_comboboxes.cpp
|
ui/layer_frame_comboboxes.cpp
|
||||||
|
ui/layout.cpp
|
||||||
|
ui/layouts.cpp
|
||||||
|
ui/layout_selector.cpp
|
||||||
ui/main_menu_bar.cpp
|
ui/main_menu_bar.cpp
|
||||||
ui/main_window.cpp
|
ui/main_window.cpp
|
||||||
ui/mini_help_button.cpp
|
ui/mini_help_button.cpp
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "app/app.h"
|
#include "app/app.h"
|
||||||
|
#include "app/color_utils.h"
|
||||||
#include "app/commands/command.h"
|
#include "app/commands/command.h"
|
||||||
#include "app/console.h"
|
#include "app/console.h"
|
||||||
#include "app/context.h"
|
#include "app/context.h"
|
||||||
|
|
@ -88,10 +89,10 @@ void PasteTextCommand::onExecute(Context* ctx)
|
||||||
std::string text = window.userText()->text();
|
std::string text = window.userText()->text();
|
||||||
app::Color color = window.fontColor()->getColor();
|
app::Color color = window.fontColor()->getColor();
|
||||||
|
|
||||||
doc::ImageRef image = render_text(
|
ui::Paint paint = window.fontFace()->paint();
|
||||||
fontInfo,
|
paint.color(color_utils::color_for_ui(color));
|
||||||
text,
|
|
||||||
gfx::rgba(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
|
doc::ImageRef image = render_text(fontInfo, text, paint);
|
||||||
if (image) {
|
if (image) {
|
||||||
Sprite* sprite = editor->sprite();
|
Sprite* sprite = editor->sprite();
|
||||||
if (image->pixelFormat() != sprite->pixelFormat()) {
|
if (image->pixelFormat() != sprite->pixelFormat()) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2023 Igara Studio S.A.
|
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -174,6 +174,7 @@ FOR_EACH_COMMAND(TogglePreview)
|
||||||
FOR_EACH_COMMAND(ToggleRewindOnStop)
|
FOR_EACH_COMMAND(ToggleRewindOnStop)
|
||||||
FOR_EACH_COMMAND(ToggleTilesMode)
|
FOR_EACH_COMMAND(ToggleTilesMode)
|
||||||
FOR_EACH_COMMAND(ToggleTimelineThumbnails)
|
FOR_EACH_COMMAND(ToggleTimelineThumbnails)
|
||||||
|
FOR_EACH_COMMAND(ToggleWorkspaceLayout)
|
||||||
FOR_EACH_COMMAND(Undo)
|
FOR_EACH_COMMAND(Undo)
|
||||||
FOR_EACH_COMMAND(UndoHistory)
|
FOR_EACH_COMMAND(UndoHistory)
|
||||||
FOR_EACH_COMMAND(UnlinkCel)
|
FOR_EACH_COMMAND(UnlinkCel)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -82,6 +82,9 @@ public:
|
||||||
unsigned int m_version;
|
unsigned int m_version;
|
||||||
bool m_removed;
|
bool m_removed;
|
||||||
mutable bool m_is_folder;
|
mutable bool m_is_folder;
|
||||||
|
#ifdef _WIN32
|
||||||
|
bool m_isHidden = false;
|
||||||
|
#endif
|
||||||
std::atomic<double> m_thumbnailProgress;
|
std::atomic<double> m_thumbnailProgress;
|
||||||
std::atomic<os::Surface*> m_thumbnail;
|
std::atomic<os::Surface*> m_thumbnail;
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
@ -266,7 +269,7 @@ IFileItem* FileSystemModule::getRootFileItem()
|
||||||
fileitem->m_pidl = pidl;
|
fileitem->m_pidl = pidl;
|
||||||
fileitem->m_fullpidl = pidl;
|
fileitem->m_fullpidl = pidl;
|
||||||
|
|
||||||
SFGAOF attrib = SFGAO_FOLDER;
|
SFGAOF attrib = SFGAO_FOLDER | SFGAO_HIDDEN;
|
||||||
shl_idesktop->GetAttributesOf(1, (LPCITEMIDLIST*)&pidl, &attrib);
|
shl_idesktop->GetAttributesOf(1, (LPCITEMIDLIST*)&pidl, &attrib);
|
||||||
|
|
||||||
update_by_pidl(fileitem, attrib);
|
update_by_pidl(fileitem, attrib);
|
||||||
|
|
@ -357,7 +360,7 @@ bool FileItem::isHidden() const
|
||||||
ASSERT(m_displayname != NOTINITIALIZED);
|
ASSERT(m_displayname != NOTINITIALIZED);
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
return false;
|
return m_isHidden;
|
||||||
#else
|
#else
|
||||||
return m_displayname[0] == '.';
|
return m_displayname[0] == '.';
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -462,7 +465,7 @@ const FileItemList& FileItem::children()
|
||||||
// Get the interface to enumerate subitems
|
// Get the interface to enumerate subitems
|
||||||
hr = pFolder->EnumObjects(
|
hr = pFolder->EnumObjects(
|
||||||
reinterpret_cast<HWND>(os::System::instance()->defaultWindow()->nativeHandle()),
|
reinterpret_cast<HWND>(os::System::instance()->defaultWindow()->nativeHandle()),
|
||||||
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS,
|
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN,
|
||||||
&pEnum);
|
&pEnum);
|
||||||
|
|
||||||
if (hr == S_OK && pEnum) {
|
if (hr == S_OK && pEnum) {
|
||||||
|
|
@ -473,10 +476,9 @@ const FileItemList& FileItem::children()
|
||||||
while (pEnum->Next(256, itempidl, &fetched) == S_OK && fetched > 0) {
|
while (pEnum->Next(256, itempidl, &fetched) == S_OK && fetched > 0) {
|
||||||
// Request the SFGAO_FOLDER attribute to know what of the
|
// Request the SFGAO_FOLDER attribute to know what of the
|
||||||
// item is file or a folder
|
// item is file or a folder
|
||||||
for (c = 0; c < fetched; ++c) {
|
for (c = 0; c < fetched; ++c)
|
||||||
attribs[c] = SFGAO_FOLDER;
|
attribs[c] = SFGAO_FOLDER | SFGAO_HIDDEN;
|
||||||
pFolder->GetAttributesOf(1, (LPCITEMIDLIST*)itempidl, attribs + c);
|
pFolder->GetAttributesOf(fetched, (LPCITEMIDLIST*)itempidl, attribs);
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the FileItems
|
// Generate the FileItems
|
||||||
for (c = 0; c < fetched; ++c) {
|
for (c = 0; c < fetched; ++c) {
|
||||||
|
|
@ -755,6 +757,9 @@ static void update_by_pidl(FileItem* fileitem, SFGAOF attrib)
|
||||||
// Is it a folder?
|
// Is it a folder?
|
||||||
|
|
||||||
fileitem->m_is_folder = calc_is_folder(fileitem->m_filename, attrib);
|
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
|
// Get the name to display
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ void ResourceFinder::includeDesktopDir(const char* filename)
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
||||||
std::vector<wchar_t> buf(MAX_PATH);
|
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) {
|
if (hr == S_OK) {
|
||||||
addPath(base::join_path(base::to_utf8(&buf[0]), filename));
|
addPath(base::join_path(base::to_utf8(&buf[0]), filename));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
#include "app/console.h"
|
#include "app/console.h"
|
||||||
#include "app/context.h"
|
#include "app/context.h"
|
||||||
#include "app/context_observer.h"
|
#include "app/context_observer.h"
|
||||||
|
#include "app/doc.h"
|
||||||
|
#include "app/doc_undo.h"
|
||||||
#include "app/script/docobj.h"
|
#include "app/script/docobj.h"
|
||||||
#include "app/script/engine.h"
|
#include "app/script/engine.h"
|
||||||
#include "app/script/luacpp.h"
|
#include "app/script/luacpp.h"
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
#include "doc/tag.h"
|
#include "doc/tag.h"
|
||||||
#include "doc/tileset.h"
|
#include "doc/tileset.h"
|
||||||
#include "doc/tilesets.h"
|
#include "doc/tilesets.h"
|
||||||
|
#include "undo/undo_state.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
@ -1029,6 +1030,42 @@ int Sprite_set_useLayerUuids(lua_State* L)
|
||||||
return 0;
|
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[] = {
|
const luaL_Reg Sprite_methods[] = {
|
||||||
{ "__eq", Sprite_eq },
|
{ "__eq", Sprite_eq },
|
||||||
{ "resize", Sprite_resize },
|
{ "resize", Sprite_resize },
|
||||||
|
|
@ -1094,6 +1131,7 @@ const Property Sprite_properties[] = {
|
||||||
{ "events", Sprite_get_events, nullptr },
|
{ "events", Sprite_get_events, nullptr },
|
||||||
{ "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin },
|
{ "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin },
|
||||||
{ "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids },
|
{ "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids },
|
||||||
|
{ "undoHistory", Sprite_get_undoHistory, nullptr },
|
||||||
{ nullptr, nullptr, nullptr }
|
{ nullptr, nullptr, nullptr }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@
|
||||||
#include "ui/menu.h"
|
#include "ui/menu.h"
|
||||||
#include "ui/message.h"
|
#include "ui/message.h"
|
||||||
#include "ui/paint_event.h"
|
#include "ui/paint_event.h"
|
||||||
|
#include "ui/resize_event.h"
|
||||||
#include "ui/separator.h"
|
#include "ui/separator.h"
|
||||||
#include "ui/splitter.h"
|
#include "ui/splitter.h"
|
||||||
#include "ui/system.h"
|
#include "ui/system.h"
|
||||||
|
|
@ -138,8 +139,8 @@ void ColorBar::ScrollableView::onInitTheme(InitThemeEvent& ev)
|
||||||
|
|
||||||
ColorBar* ColorBar::m_instance = NULL;
|
ColorBar* ColorBar::m_instance = NULL;
|
||||||
|
|
||||||
ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
|
ColorBar::ColorBar(TooltipManager* tooltipManager)
|
||||||
: Box(align)
|
: Box(VERTICAL)
|
||||||
, m_editPal(1)
|
, m_editPal(1)
|
||||||
, m_buttons(int(PalButton::MAX))
|
, m_buttons(int(PalButton::MAX))
|
||||||
, m_tilesButton(1)
|
, m_tilesButton(1)
|
||||||
|
|
@ -299,7 +300,7 @@ ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
|
||||||
InitTheme.connect([this, fgBox, bgBox] {
|
InitTheme.connect([this, fgBox, bgBox] {
|
||||||
auto theme = SkinTheme::get(this);
|
auto theme = SkinTheme::get(this);
|
||||||
|
|
||||||
setBorder(gfx::Border(2 * guiscale(), 0, 0, 0));
|
setBorder(gfx::Border(0));
|
||||||
setChildSpacing(2 * guiscale());
|
setChildSpacing(2 * guiscale());
|
||||||
|
|
||||||
m_fgColor.resetSizeHint();
|
m_fgColor.resetSizeHint();
|
||||||
|
|
@ -644,6 +645,18 @@ void ColorBar::onSizeHint(ui::SizeHintEvent& ev)
|
||||||
Box::onSizeHint(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)
|
void ColorBar::onActiveSiteChange(const Site& site)
|
||||||
{
|
{
|
||||||
if (m_lastDocument != site.document()) {
|
if (m_lastDocument != site.document()) {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
#include "app/tileset_mode.h"
|
#include "app/tileset_mode.h"
|
||||||
#include "app/ui/button_set.h"
|
#include "app/ui/button_set.h"
|
||||||
#include "app/ui/color_button.h"
|
#include "app/ui/color_button.h"
|
||||||
|
#include "app/ui/dockable.h"
|
||||||
#include "app/ui/input_chain_element.h"
|
#include "app/ui/input_chain_element.h"
|
||||||
#include "app/ui/palette_view.h"
|
#include "app/ui/palette_view.h"
|
||||||
#include "app/ui/tile_button.h"
|
#include "app/ui/tile_button.h"
|
||||||
|
|
@ -50,7 +51,8 @@ class ColorBar : public ui::Box,
|
||||||
public PaletteViewDelegate,
|
public PaletteViewDelegate,
|
||||||
public ContextObserver,
|
public ContextObserver,
|
||||||
public DocObserver,
|
public DocObserver,
|
||||||
public InputChainElement {
|
public InputChainElement,
|
||||||
|
public Dockable {
|
||||||
static ColorBar* m_instance;
|
static ColorBar* m_instance;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
@ -65,7 +67,7 @@ public:
|
||||||
|
|
||||||
static ColorBar* instance() { return m_instance; }
|
static ColorBar* instance() { return m_instance; }
|
||||||
|
|
||||||
ColorBar(int align, ui::TooltipManager* tooltipManager);
|
ColorBar(ui::TooltipManager* tooltipManager);
|
||||||
~ColorBar();
|
~ColorBar();
|
||||||
|
|
||||||
void setPixelFormat(doc::PixelFormat pixelFormat);
|
void setPixelFormat(doc::PixelFormat pixelFormat);
|
||||||
|
|
@ -123,10 +125,18 @@ public:
|
||||||
bool onClear(Context* ctx) override;
|
bool onClear(Context* ctx) override;
|
||||||
void onCancel(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;
|
obs::signal<void()> ChangeSelection;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void onSizeHint(ui::SizeHintEvent& ev) override;
|
void onSizeHint(ui::SizeHintEvent& ev) override;
|
||||||
|
void onResize(ui::ResizeEvent& ev) override;
|
||||||
void onAppPaletteChange();
|
void onAppPaletteChange();
|
||||||
void onFocusPaletteView(ui::Message* msg);
|
void onFocusPaletteView(ui::Message* msg);
|
||||||
void onFocusTilesView(ui::Message* msg);
|
void onFocusTilesView(ui::Message* msg);
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ ConfigureTimelinePopup::ConfigureTimelinePopup()
|
||||||
m_box = new app::gen::TimelineConf();
|
m_box = new app::gen::TimelineConf();
|
||||||
addChild(m_box);
|
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->firstFrame()->Change.connect([this] { onChangeFirstFrame(); });
|
||||||
m_box->merge()->Click.connect([this] { onChangeType(); });
|
m_box->merge()->Click.connect([this] { onChangeType(); });
|
||||||
m_box->tint()->Click.connect([this] { onChangeType(); });
|
m_box->tint()->Click.connect([this] { onChangeType(); });
|
||||||
|
|
@ -93,15 +93,6 @@ void ConfigureTimelinePopup::updateWidgetsFromCurrentSettings()
|
||||||
DocumentPreferences& docPref = this->docPref();
|
DocumentPreferences& docPref = this->docPref();
|
||||||
base::ScopedValue lockUpdates(m_lockUpdates, true);
|
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());
|
m_box->firstFrame()->setTextf("%d", docPref.timeline.firstFrame());
|
||||||
|
|
||||||
switch (docPref.onionskin.type()) {
|
switch (docPref.onionskin.type()) {
|
||||||
|
|
@ -147,17 +138,10 @@ bool ConfigureTimelinePopup::onProcessMessage(ui::Message* msg)
|
||||||
return PopupWindow::onProcessMessage(msg);
|
return PopupWindow::onProcessMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureTimelinePopup::onChangePosition()
|
void ConfigureTimelinePopup::onWorkspaceLayout()
|
||||||
{
|
{
|
||||||
gen::TimelinePosition newTimelinePos = gen::TimelinePosition::BOTTOM;
|
UIContext::instance()->executeCommand(
|
||||||
|
Commands::instance()->byId(CommandId::ToggleWorkspaceLayout()));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureTimelinePopup::onChangeFirstFrame()
|
void ConfigureTimelinePopup::onChangeFirstFrame()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2025 Igara Studio S.A.
|
// Copyright (C) 2022-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -33,7 +33,7 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool onProcessMessage(ui::Message* msg) override;
|
bool onProcessMessage(ui::Message* msg) override;
|
||||||
void onChangePosition();
|
void onWorkspaceLayout();
|
||||||
void onChangeFirstFrame();
|
void onChangeFirstFrame();
|
||||||
void onChangeType();
|
void onChangeType();
|
||||||
void onOpacity();
|
void onOpacity();
|
||||||
|
|
|
||||||
|
|
@ -1857,7 +1857,7 @@ private:
|
||||||
|
|
||||||
class ContextBar::FontSelector : public FontEntry {
|
class ContextBar::FontSelector : public FontEntry {
|
||||||
public:
|
public:
|
||||||
FontSelector(ContextBar* contextBar)
|
FontSelector(ContextBar* contextBar) : FontEntry(true) // With stroke and fill options
|
||||||
{
|
{
|
||||||
// Load the font from the preferences
|
// Load the font from the preferences
|
||||||
setInfo(FontInfo::getFromPreferences(), FontEntry::From::Init);
|
setInfo(FontInfo::getFromPreferences(), FontEntry::From::Init);
|
||||||
|
|
@ -1962,6 +1962,12 @@ void ContextBar::onInitTheme(ui::InitThemeEvent& ev)
|
||||||
auto theme = SkinTheme::get(this);
|
auto theme = SkinTheme::get(this);
|
||||||
gfx::Border border = this->border();
|
gfx::Border border = this->border();
|
||||||
border.bottom(2 * guiscale());
|
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);
|
setBorder(border);
|
||||||
setBgColor(theme->colors.workspace());
|
setBgColor(theme->colors.workspace());
|
||||||
m_sprayLabel->setStyle(theme->styles.miniLabel());
|
m_sprayLabel->setStyle(theme->styles.miniLabel());
|
||||||
|
|
@ -2559,6 +2565,11 @@ FontInfo ContextBar::fontInfo() const
|
||||||
return m_fontSelector->info();
|
return m_fontSelector->info();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FontEntry* ContextBar::fontEntry()
|
||||||
|
{
|
||||||
|
return m_fontSelector;
|
||||||
|
}
|
||||||
|
|
||||||
render::DitheringMatrix ContextBar::ditheringMatrix()
|
render::DitheringMatrix ContextBar::ditheringMatrix()
|
||||||
{
|
{
|
||||||
return m_ditheringSelector->ditheringMatrix();
|
return m_ditheringSelector->ditheringMatrix();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
#include "app/tools/tool_loop_modifiers.h"
|
#include "app/tools/tool_loop_modifiers.h"
|
||||||
#include "app/ui/context_bar_observer.h"
|
#include "app/ui/context_bar_observer.h"
|
||||||
#include "app/ui/doc_observer_widget.h"
|
#include "app/ui/doc_observer_widget.h"
|
||||||
|
#include "app/ui/dockable.h"
|
||||||
#include "app/ui/font_entry.h"
|
#include "app/ui/font_entry.h"
|
||||||
#include "doc/brush.h"
|
#include "doc/brush.h"
|
||||||
#include "obs/connection.h"
|
#include "obs/connection.h"
|
||||||
|
|
@ -60,7 +61,8 @@ class Transformation;
|
||||||
|
|
||||||
class ContextBar : public DocObserverWidget<ui::HBox>,
|
class ContextBar : public DocObserverWidget<ui::HBox>,
|
||||||
public obs::observable<ContextBarObserver>,
|
public obs::observable<ContextBarObserver>,
|
||||||
public tools::ActiveToolObserver {
|
public tools::ActiveToolObserver,
|
||||||
|
public Dockable {
|
||||||
public:
|
public:
|
||||||
ContextBar(ui::TooltipManager* tooltipManager, ColorBar* colorBar);
|
ContextBar(ui::TooltipManager* tooltipManager, ColorBar* colorBar);
|
||||||
~ContextBar();
|
~ContextBar();
|
||||||
|
|
@ -90,6 +92,7 @@ public:
|
||||||
|
|
||||||
// For text tool
|
// For text tool
|
||||||
FontInfo fontInfo() const;
|
FontInfo fontInfo() const;
|
||||||
|
FontEntry* fontEntry();
|
||||||
|
|
||||||
// For gradients
|
// For gradients
|
||||||
render::DitheringMatrix ditheringMatrix();
|
render::DitheringMatrix ditheringMatrix();
|
||||||
|
|
@ -99,6 +102,10 @@ public:
|
||||||
// For freehand with dynamics
|
// For freehand with dynamics
|
||||||
const tools::DynamicsOptions& getDynamics() const;
|
const tools::DynamicsOptions& getDynamics() const;
|
||||||
|
|
||||||
|
// Dockable impl
|
||||||
|
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
|
||||||
|
int dockHandleSide() const override { return ui::LEFT; }
|
||||||
|
|
||||||
// Signals
|
// Signals
|
||||||
obs::signal<void()> BrushChange;
|
obs::signal<void()> BrushChange;
|
||||||
obs::signal<void(const FontInfo&, FontEntry::From)> FontChange;
|
obs::signal<void(const FontInfo&, FontEntry::From)> FontChange;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// 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
|
// Here we re-use the cached surface
|
||||||
if (!cached && m_uiLayer->surface()) {
|
if (!cached && m_uiLayer->surface()) {
|
||||||
|
m_uiLayer->surface()->clear();
|
||||||
|
|
||||||
gfx::Rect layerBounds = m_uiLayer->surface()->bounds();
|
gfx::Rect layerBounds = m_uiLayer->surface()->bounds();
|
||||||
ui::Graphics g(display, m_uiLayer->surface(), 0, 0);
|
ui::Graphics g(display, m_uiLayer->surface(), 0, 0);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@
|
||||||
#include "app/util/tile_flags_utils.h"
|
#include "app/util/tile_flags_utils.h"
|
||||||
#include "base/chrono.h"
|
#include "base/chrono.h"
|
||||||
#include "base/convert_to.h"
|
#include "base/convert_to.h"
|
||||||
|
#include "base/scoped_value.h"
|
||||||
#include "doc/doc.h"
|
#include "doc/doc.h"
|
||||||
#include "doc/mask_boundaries.h"
|
#include "doc/mask_boundaries.h"
|
||||||
#include "doc/slice.h"
|
#include "doc/slice.h"
|
||||||
|
|
@ -266,6 +267,23 @@ void Editor::setStateInternal(const EditorStatePtr& newState)
|
||||||
{
|
{
|
||||||
m_brushPreview.hide();
|
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
|
// Fire before change state event, set the state, and fire after
|
||||||
// change state event.
|
// change state event.
|
||||||
EditorState::LeaveAction leaveAction = m_state->onLeaveState(this, newState.get());
|
EditorState::LeaveAction leaveAction = m_state->onLeaveState(this, newState.get());
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -458,6 +458,13 @@ private:
|
||||||
|
|
||||||
DocView* m_docView;
|
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
|
// Last known mouse position received by this editor when the
|
||||||
// mouse button was pressed. Used for auto-scrolling. To get the
|
// mouse button was pressed. Used for auto-scrolling. To get the
|
||||||
// current mouse position on the editor you can use
|
// current mouse position on the editor you can use
|
||||||
|
|
|
||||||
|
|
@ -428,8 +428,8 @@ bool MovingSliceState::onMouseMove(Editor* editor, MouseMessage* msg)
|
||||||
if (editor->slicesTransforms())
|
if (editor->slicesTransforms())
|
||||||
drawExtraCel();
|
drawExtraCel();
|
||||||
|
|
||||||
// Redraw the editor.
|
// Notify changes
|
||||||
editor->invalidate();
|
m_site.document()->notifyGeneralUpdate();
|
||||||
|
|
||||||
// Use StandbyState implementation
|
// Use StandbyState implementation
|
||||||
return StandbyState::onMouseMove(editor, msg);
|
return StandbyState::onMouseMove(editor, msg);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
#include "app/commands/command.h"
|
#include "app/commands/command.h"
|
||||||
#include "app/extra_cel.h"
|
#include "app/extra_cel.h"
|
||||||
#include "app/fonts/font_info.h"
|
#include "app/fonts/font_info.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
#include "app/pref/preferences.h"
|
#include "app/pref/preferences.h"
|
||||||
#include "app/site.h"
|
#include "app/site.h"
|
||||||
#include "app/tx.h"
|
#include "app/tx.h"
|
||||||
|
|
@ -43,12 +44,42 @@
|
||||||
#include "os/skia/skia_surface.h"
|
#include "os/skia/skia_surface.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
using namespace ui;
|
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 {
|
class WritingTextState::TextEditor : public Entry {
|
||||||
public:
|
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)
|
TextEditor(Editor* editor, const Site& site, const gfx::Rect& bounds)
|
||||||
: Entry(4096, "")
|
: Entry(4096, "")
|
||||||
, m_editor(editor)
|
, m_editor(editor)
|
||||||
|
|
@ -61,7 +92,7 @@ public:
|
||||||
setPersistSelection(true);
|
setPersistSelection(true);
|
||||||
|
|
||||||
createExtraCel(site, bounds);
|
createExtraCel(site, bounds);
|
||||||
renderExtraCelBase();
|
renderExtraCel(TextPreview::Intermediate);
|
||||||
|
|
||||||
FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
|
FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
|
||||||
if (auto font = Fonts::instance()->fontFromInfo(fontInfo))
|
if (auto font = Fonts::instance()->fontFromInfo(fontInfo))
|
||||||
|
|
@ -76,36 +107,37 @@ public:
|
||||||
|
|
||||||
// Returns the extra cel with the text rendered (but without the
|
// Returns the extra cel with the text rendered (but without the
|
||||||
// selected text highlighted).
|
// selected text highlighted).
|
||||||
ExtraCelRef extraCel()
|
ExtraCelRef extraCel(const TextPreview textPreview)
|
||||||
{
|
{
|
||||||
renderExtraCelBase();
|
renderExtraCel(textPreview);
|
||||||
renderExtraCelText(false);
|
|
||||||
return m_extraCel;
|
return m_extraCel;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setExtraCelBounds(const gfx::Rect& bounds)
|
void setExtraCelBounds(const gfx::RectF& bounds)
|
||||||
{
|
{
|
||||||
doc::Image* extraImg = m_extraCel->image();
|
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);
|
createExtraCel(m_editor->getSite(), bounds);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
m_baseBounds = bounds;
|
||||||
m_extraCel->cel()->setBounds(bounds);
|
m_extraCel->cel()->setBounds(bounds);
|
||||||
}
|
}
|
||||||
renderExtraCelBase();
|
renderExtraCel(TextPreview::Intermediate);
|
||||||
renderExtraCelText(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
obs::signal<void(const gfx::Size&)> NewRequiredBounds;
|
obs::signal<void(const gfx::RectF&)> NewRequiredBounds;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void createExtraCel(const Site& site, const gfx::Rect& bounds)
|
void createExtraCel(const Site& site, const gfx::Rect& bounds)
|
||||||
{
|
{
|
||||||
|
m_baseBounds = bounds;
|
||||||
m_extraCel->create(ExtraCel::Purpose::TextPreview,
|
m_extraCel->create(ExtraCel::Purpose::TextPreview,
|
||||||
site.tilemapMode(),
|
site.tilemapMode(),
|
||||||
site.sprite(),
|
site.sprite(),
|
||||||
bounds,
|
bounds,
|
||||||
bounds.size(),
|
gfx::Size(std::ceil(bounds.w), std::ceil(bounds.h)),
|
||||||
site.frame(),
|
site.frame(),
|
||||||
255);
|
255);
|
||||||
|
|
||||||
|
|
@ -176,7 +208,7 @@ private:
|
||||||
|
|
||||||
// Notify that we could make the text editor bigger to show this
|
// Notify that we could make the text editor bigger to show this
|
||||||
// text blob.
|
// text blob.
|
||||||
NewRequiredBounds(get_text_blob_required_size(blob));
|
NewRequiredBounds(calc_blob_bounds(blob));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onPaint(PaintEvent& ev) override
|
void onPaint(PaintEvent& ev) override
|
||||||
|
|
@ -205,8 +237,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render extra cel with text + selected text
|
// Render extra cel with text + selected text
|
||||||
renderExtraCelBase();
|
renderExtraCel(TextPreview::Intermediate);
|
||||||
renderExtraCelText(true);
|
|
||||||
m_doc->setExtraCel(m_extraCel);
|
m_doc->setExtraCel(m_extraCel);
|
||||||
|
|
||||||
// Paint caret
|
// Paint caret
|
||||||
|
|
@ -227,76 +258,80 @@ private:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void renderExtraCelBase()
|
void renderExtraCel(const TextPreview textPreview)
|
||||||
{
|
{
|
||||||
doc::Image* extraImg = m_extraCel->image();
|
doc::Image* extraImg = m_extraCel->image();
|
||||||
ASSERT(extraImg);
|
ASSERT(extraImg);
|
||||||
if (!extraImg)
|
if (!extraImg)
|
||||||
return;
|
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);
|
ASSERT(extraCel);
|
||||||
if (!extraCel)
|
if (!extraCel)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
extraImg->clear(extraImg->maskColor());
|
extraCel->setPosition(m_baseBounds.x + bounds.x, m_baseBounds.y + bounds.y);
|
||||||
|
|
||||||
render::Render().renderLayer(extraImg,
|
render::Render().renderLayer(extraImg,
|
||||||
m_editor->layer(),
|
m_editor->layer(),
|
||||||
m_editor->frame(),
|
m_editor->frame(),
|
||||||
gfx::Clip(0, 0, extraCel->bounds()),
|
gfx::Clip(0, 0, extraCel->bounds()),
|
||||||
doc::BlendMode::SRC);
|
doc::BlendMode::SRC);
|
||||||
}
|
|
||||||
|
|
||||||
void renderExtraCelText(const bool withSelection)
|
if (blobImage) {
|
||||||
{
|
doc::blend_image(extraImg,
|
||||||
const auto textColor = color_utils::color_for_image(Preferences::instance().colorBar.fgColor(),
|
blobImage.get(),
|
||||||
IMAGE_RGB);
|
gfx::Clip(blobImage->bounds().size()),
|
||||||
|
m_doc->sprite()->palette(m_editor->frame()),
|
||||||
text::TextBlobRef blob = textBlob();
|
255,
|
||||||
if (!blob)
|
doc::BlendMode::NORMAL);
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
Editor* m_editor;
|
||||||
Doc* m_doc;
|
Doc* m_doc;
|
||||||
ExtraCelRef m_extraCel;
|
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)
|
WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
|
||||||
|
|
@ -312,10 +347,10 @@ WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
|
||||||
m_fontChangeConn =
|
m_fontChangeConn =
|
||||||
App::instance()->contextBar()->FontChange.connect(&WritingTextState::onFontChange, this);
|
App::instance()->contextBar()->FontChange.connect(&WritingTextState::onFontChange, this);
|
||||||
|
|
||||||
m_entry->NewRequiredBounds.connect([this](const gfx::Size& blobSize) {
|
m_entry->NewRequiredBounds.connect([this](const gfx::RectF& blobBounds) {
|
||||||
if (m_bounds.w < blobSize.w || m_bounds.h < blobSize.h) {
|
if (m_bounds.w < blobBounds.w || m_bounds.h < blobBounds.h) {
|
||||||
m_bounds.w = std::max(m_bounds.w, blobSize.w);
|
m_bounds.w = std::max(m_bounds.w, blobBounds.w);
|
||||||
m_bounds.h = std::max(m_bounds.h, blobSize.h);
|
m_bounds.h = std::max(m_bounds.h, blobBounds.h);
|
||||||
m_entry->setExtraCelBounds(m_bounds);
|
m_entry->setExtraCelBounds(m_bounds);
|
||||||
m_entry->setBounds(calcEntryBounds());
|
m_entry->setBounds(calcEntryBounds());
|
||||||
}
|
}
|
||||||
|
|
@ -388,11 +423,11 @@ void WritingTextState::onCommitMouseMove(Editor* editor, const gfx::PointF& spri
|
||||||
if (!m_movingBounds)
|
if (!m_movingBounds)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
gfx::Point delta(spritePos - m_cursorStart);
|
gfx::PointF delta(spritePos - m_cursorStart);
|
||||||
if (delta.x == 0 && delta.y == 0)
|
if (delta.x == 0 && delta.y == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_bounds.setOrigin(gfx::Point(delta + m_boundsOrigin));
|
m_bounds.setOrigin(delta + m_boundsOrigin);
|
||||||
m_entry->setExtraCelBounds(m_bounds);
|
m_entry->setExtraCelBounds(m_bounds);
|
||||||
m_entry->setBounds(calcEntryBounds());
|
m_entry->setBounds(calcEntryBounds());
|
||||||
}
|
}
|
||||||
|
|
@ -408,12 +443,7 @@ bool WritingTextState::onSetCursor(Editor* editor, const gfx::Point& mouseScreen
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WritingTextState::onKeyDown(Editor*, KeyMessage*)
|
bool WritingTextState::onKeyDown(Editor*, KeyMessage* msg)
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
|
|
||||||
{
|
{
|
||||||
// Cancel loop pressing Esc key
|
// Cancel loop pressing Esc key
|
||||||
if (msg->scancode() == ui::kKeyEsc) {
|
if (msg->scancode() == ui::kKeyEsc) {
|
||||||
|
|
@ -422,7 +452,17 @@ bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
|
||||||
// Drop text pressing Enter key
|
// Drop text pressing Enter key
|
||||||
else if (msg->scancode() == ui::kKeyEnter) {
|
else if (msg->scancode() == ui::kKeyEnter) {
|
||||||
drop();
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -475,8 +515,8 @@ EditorState::LeaveAction WritingTextState::onLeaveState(Editor* editor, EditorSt
|
||||||
// Paints the text in the active layer/sprite creating an
|
// Paints the text in the active layer/sprite creating an
|
||||||
// undoable transaction.
|
// undoable transaction.
|
||||||
Site site = m_editor->getSite();
|
Site site = m_editor->getSite();
|
||||||
ExtraCelRef extraCel = m_entry->extraCel();
|
ExtraCelRef extraCel = m_entry->extraCel(TextEditor::Final);
|
||||||
Tx tx(site.document(), "Text Tool");
|
Tx tx(site.document(), Strings::tools_text());
|
||||||
ExpandCelCanvas expand(site, site.layer(), TiledMode::NONE, tx, ExpandCelCanvas::None);
|
ExpandCelCanvas expand(site, site.layer(), TiledMode::NONE, tx, ExpandCelCanvas::None);
|
||||||
|
|
||||||
expand.validateDestCanvas(gfx::Region(extraCel->cel()->bounds()));
|
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
|
// This is useful to show changes to the anti-alias option
|
||||||
// immediately.
|
// immediately.
|
||||||
auto dummy = m_entry->extraCel();
|
auto dummy = m_entry->extraCel(TextEditor::Intermediate);
|
||||||
|
|
||||||
if (fromField == FontEntry::From::Popup) {
|
if (fromField == FontEntry::From::Popup) {
|
||||||
if (m_entry)
|
if (m_entry)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// 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
|
// This program is distributed under the terms of
|
||||||
// the End-User License Agreement for Aseprite.
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
@ -59,7 +59,7 @@ private:
|
||||||
|
|
||||||
DelayedMouseMove m_delayedMouseMove;
|
DelayedMouseMove m_delayedMouseMove;
|
||||||
Editor* m_editor;
|
Editor* m_editor;
|
||||||
gfx::Rect m_bounds;
|
gfx::RectF m_bounds;
|
||||||
std::unique_ptr<TextEditor> m_entry;
|
std::unique_ptr<TextEditor> m_entry;
|
||||||
|
|
||||||
// True if the text was discarded.
|
// True if the text was discarded.
|
||||||
|
|
@ -71,7 +71,7 @@ private:
|
||||||
bool m_mouseMoveReceived = false;
|
bool m_mouseMoveReceived = false;
|
||||||
bool m_movingBounds = false;
|
bool m_movingBounds = false;
|
||||||
gfx::PointF m_cursorStart;
|
gfx::PointF m_cursorStart;
|
||||||
gfx::Point m_boundsOrigin;
|
gfx::PointF m_boundsOrigin;
|
||||||
|
|
||||||
obs::scoped_connection m_beforeCmdConn;
|
obs::scoped_connection m_beforeCmdConn;
|
||||||
obs::scoped_connection m_fontChangeConn;
|
obs::scoped_connection m_fontChangeConn;
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ FileList::FileList()
|
||||||
, m_multiselect(false)
|
, m_multiselect(false)
|
||||||
, m_zoom(1.0)
|
, m_zoom(1.0)
|
||||||
, m_itemsPerRow(0)
|
, m_itemsPerRow(0)
|
||||||
|
, m_showHidden(Preferences::instance().fileSelector.showHidden())
|
||||||
{
|
{
|
||||||
setFocusStop(true);
|
setFocusStop(true);
|
||||||
setDoubleBuffered(true);
|
setDoubleBuffered(true);
|
||||||
|
|
@ -173,6 +174,14 @@ void FileList::animateToZoom(const double zoom)
|
||||||
startAnimation(ANI_ZOOM, 10);
|
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)
|
bool FileList::onProcessMessage(Message* msg)
|
||||||
{
|
{
|
||||||
switch (msg->type()) {
|
switch (msg->type()) {
|
||||||
|
|
@ -825,7 +834,7 @@ void FileList::regenerateList()
|
||||||
for (FileItemList::iterator it = m_list.begin(); it != m_list.end();) {
|
for (FileItemList::iterator it = m_list.begin(); it != m_list.end();) {
|
||||||
IFileItem* fileitem = *it;
|
IFileItem* fileitem = *it;
|
||||||
|
|
||||||
if (fileitem->isHidden())
|
if (fileitem->isHidden() && !m_showHidden)
|
||||||
it = m_list.erase(it);
|
it = m_list.erase(it);
|
||||||
else if (!fileitem->isFolder() && !fileitem->hasExtension(m_exts)) {
|
else if (!fileitem->isFolder() && !fileitem->hasExtension(m_exts)) {
|
||||||
it = m_list.erase(it);
|
it = m_list.erase(it);
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ public:
|
||||||
double zoom() const { return m_zoom; }
|
double zoom() const { return m_zoom; }
|
||||||
void setZoom(const double zoom);
|
void setZoom(const double zoom);
|
||||||
void animateToZoom(const double zoom);
|
void animateToZoom(const double zoom);
|
||||||
|
void setShowHidden(const bool show);
|
||||||
|
|
||||||
obs::signal<void()> FileSelected;
|
obs::signal<void()> FileSelected;
|
||||||
obs::signal<void()> FileAccepted;
|
obs::signal<void()> FileAccepted;
|
||||||
|
|
@ -137,6 +138,7 @@ private:
|
||||||
double m_toZoom;
|
double m_toZoom;
|
||||||
|
|
||||||
int m_itemsPerRow;
|
int m_itemsPerRow;
|
||||||
|
bool m_showHidden;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,7 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
|
||||||
for (auto child : viewType()->children())
|
for (auto child : viewType()->children())
|
||||||
child->setFocusStop(false);
|
child->setFocusStop(false);
|
||||||
|
|
||||||
|
showHiddenCheck()->setSelected(Preferences::instance().fileSelector.showHidden());
|
||||||
m_fileList = new FileList();
|
m_fileList = new FileList();
|
||||||
m_fileList->setId("fileview");
|
m_fileList->setId("fileview");
|
||||||
m_fileName->setAssociatedFileList(m_fileList);
|
m_fileName->setAssociatedFileList(m_fileList);
|
||||||
|
|
@ -334,6 +335,10 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
|
||||||
viewType()->ItemChange.connect([this] { onChangeViewType(); });
|
viewType()->ItemChange.connect([this] { onChangeViewType(); });
|
||||||
location()->CloseListBox.connect([this] { onLocationCloseListBox(); });
|
location()->CloseListBox.connect([this] { onLocationCloseListBox(); });
|
||||||
fileType()->Change.connect([this] { onFileTypeChange(); });
|
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->FileSelected.connect([this] { onFileListFileSelected(); });
|
||||||
m_fileList->FileAccepted.connect([this] { onFileListFileAccepted(); });
|
m_fileList->FileAccepted.connect([this] { onFileListFileAccepted(); });
|
||||||
m_fileList->CurrentFolderChanged.connect([this] { onFileListCurrentFolderChanged(); });
|
m_fileList->CurrentFolderChanged.connect([this] { onFileListCurrentFolderChanged(); });
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// 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
|
// This program is distributed under the terms of
|
||||||
// the End-User License Agreement for Aseprite.
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
@ -10,8 +10,10 @@
|
||||||
|
|
||||||
#include "app/ui/filename_field.h"
|
#include "app/ui/filename_field.h"
|
||||||
|
|
||||||
|
#include "app/app.h"
|
||||||
#include "app/i18n/strings.h"
|
#include "app/i18n/strings.h"
|
||||||
#include "app/pref/preferences.h"
|
#include "app/pref/preferences.h"
|
||||||
|
#include "app/recent_files.h"
|
||||||
#include "app/ui/skin/skin_theme.h"
|
#include "app/ui/skin/skin_theme.h"
|
||||||
#include "base/fs.h"
|
#include "base/fs.h"
|
||||||
#include "ui/box.h"
|
#include "ui/box.h"
|
||||||
|
|
@ -24,6 +26,11 @@ namespace app {
|
||||||
|
|
||||||
using namespace ui;
|
using namespace ui;
|
||||||
|
|
||||||
|
FilenameField::FilenameButton::FilenameButton(const std::string& text) : ButtonSet(1)
|
||||||
|
{
|
||||||
|
addItem(text);
|
||||||
|
}
|
||||||
|
|
||||||
FilenameField::FilenameField(const Type type, const std::string& pathAndFilename)
|
FilenameField::FilenameField(const Type type, const std::string& pathAndFilename)
|
||||||
: m_entry(type == EntryAndButton ? new ui::Entry(1024, "") : nullptr)
|
: m_entry(type == EntryAndButton ? new ui::Entry(1024, "") : nullptr)
|
||||||
, m_button(type == EntryAndButton ? Strings::select_file_browse() : Strings::select_file_text())
|
, 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)
|
if (m_entry)
|
||||||
m_entry->Change.connect([this] { setFilename(updatedFilename()); });
|
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();
|
initTheme();
|
||||||
|
|
||||||
m_editFullPathChangeConn = Preferences::instance().general.editFullPath.AfterChange.connect(
|
m_editFullPathChangeConn = Preferences::instance().general.editFullPath.AfterChange.connect(
|
||||||
|
|
@ -94,7 +104,6 @@ void FilenameField::onSetEditFullPath()
|
||||||
void FilenameField::onBrowse()
|
void FilenameField::onBrowse()
|
||||||
{
|
{
|
||||||
const gfx::Rect bounds = m_button.bounds();
|
const gfx::Rect bounds = m_button.bounds();
|
||||||
m_button.setSelected(false);
|
|
||||||
|
|
||||||
ui::Menu menu;
|
ui::Menu menu;
|
||||||
ui::MenuItem choose(Strings::select_file_choose());
|
ui::MenuItem choose(Strings::select_file_choose());
|
||||||
|
|
@ -107,6 +116,11 @@ void FilenameField::onBrowse()
|
||||||
menu.addChild(&relative);
|
menu.addChild(&relative);
|
||||||
menu.addChild(&absolute);
|
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] {
|
choose.Click.connect([this] {
|
||||||
std::string fn = SelectOutputFile();
|
std::string fn = SelectOutputFile();
|
||||||
if (!fn.empty()) {
|
if (!fn.empty()) {
|
||||||
|
|
@ -120,6 +134,21 @@ void FilenameField::onBrowse()
|
||||||
menu.showPopup(gfx::Point(bounds.x, bounds.y2()), display());
|
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)
|
void FilenameField::setFilename(const std::string& pathAndFilename)
|
||||||
{
|
{
|
||||||
const std::string spritePath = base::get_file_path(m_docFilename);
|
const std::string spritePath = base::get_file_path(m_docFilename);
|
||||||
|
|
@ -164,11 +193,6 @@ void FilenameField::onInitTheme(ui::InitThemeEvent& ev)
|
||||||
{
|
{
|
||||||
HBox::onInitTheme(ev);
|
HBox::onInitTheme(ev);
|
||||||
setChildSpacing(0);
|
setChildSpacing(0);
|
||||||
|
|
||||||
auto theme = skin::SkinTheme::get(this);
|
|
||||||
ui::Style* style = theme->styles.miniButton();
|
|
||||||
if (style)
|
|
||||||
m_button.setStyle(style);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FilenameField::onUpdateText()
|
void FilenameField::onUpdateText()
|
||||||
|
|
@ -181,9 +205,9 @@ void FilenameField::updateWidgets()
|
||||||
if (m_entry)
|
if (m_entry)
|
||||||
m_entry->setText(displayedFilename());
|
m_entry->setText(displayedFilename());
|
||||||
else if (m_file.empty())
|
else if (m_file.empty())
|
||||||
m_button.setText(Strings::select_file_text());
|
m_button.getItem(0)->setText(Strings::select_file_text());
|
||||||
else
|
else
|
||||||
m_button.setText(displayedFilename());
|
m_button.getItem(0)->setText(displayedFilename());
|
||||||
|
|
||||||
Change();
|
Change();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
// the End-User License Agreement for Aseprite.
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
@ -8,14 +8,19 @@
|
||||||
#define APP_UI_FILENAME_FIELD_H_INCLUDED
|
#define APP_UI_FILENAME_FIELD_H_INCLUDED
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "app/ui/button_set.h"
|
||||||
|
#include "base/paths.h"
|
||||||
#include "obs/connection.h"
|
#include "obs/connection.h"
|
||||||
#include "obs/signal.h"
|
#include "obs/signal.h"
|
||||||
#include "ui/box.h"
|
#include "ui/box.h"
|
||||||
#include "ui/button.h"
|
|
||||||
#include "ui/entry.h"
|
#include "ui/entry.h"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
namespace ui {
|
||||||
|
class Menu;
|
||||||
|
}
|
||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
class FilenameField : public ui::HBox {
|
class FilenameField : public ui::HBox {
|
||||||
|
|
@ -44,17 +49,25 @@ protected:
|
||||||
void onSetEditFullPath();
|
void onSetEditFullPath();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
class FilenameButton : public ButtonSet {
|
||||||
|
public:
|
||||||
|
FilenameButton(const std::string& text);
|
||||||
|
};
|
||||||
|
|
||||||
void setEditFullPath(const bool on);
|
void setEditFullPath(const bool on);
|
||||||
void updateWidgets();
|
void updateWidgets();
|
||||||
void onBrowse();
|
void onBrowse();
|
||||||
std::string updatedFilename() const;
|
std::string updatedFilename() const;
|
||||||
|
void addFoldersToMenu(ui::Menu* menu,
|
||||||
|
const base::paths& folders,
|
||||||
|
const std::string& separatorTitle);
|
||||||
|
|
||||||
std::string m_path;
|
std::string m_path;
|
||||||
std::string m_pathBase;
|
std::string m_pathBase;
|
||||||
std::string m_file;
|
std::string m_file;
|
||||||
std::string m_docFilename;
|
std::string m_docFilename;
|
||||||
ui::Entry* m_entry;
|
ui::Entry* m_entry;
|
||||||
ui::Button m_button;
|
FilenameButton m_button;
|
||||||
bool m_editFullPath;
|
bool m_editFullPath;
|
||||||
bool m_askOverwrite;
|
bool m_askOverwrite;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,12 @@
|
||||||
|
|
||||||
#include "app/app.h"
|
#include "app/app.h"
|
||||||
#include "app/console.h"
|
#include "app/console.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
#include "app/recent_files.h"
|
#include "app/recent_files.h"
|
||||||
#include "app/ui/font_popup.h"
|
#include "app/ui/font_popup.h"
|
||||||
#include "app/ui/skin/skin_theme.h"
|
#include "app/ui/skin/skin_theme.h"
|
||||||
#include "base/contains.h"
|
#include "base/contains.h"
|
||||||
|
#include "base/convert_to.h"
|
||||||
#include "base/scoped_value.h"
|
#include "base/scoped_value.h"
|
||||||
#include "fmt/format.h"
|
#include "fmt/format.h"
|
||||||
#include "ui/display.h"
|
#include "ui/display.h"
|
||||||
|
|
@ -262,22 +264,91 @@ void FontEntry::FontSize::onEntryChange()
|
||||||
Change();
|
Change();
|
||||||
}
|
}
|
||||||
|
|
||||||
FontEntry::FontStyle::FontStyle() : ButtonSet(3, true)
|
FontEntry::FontStyle::FontStyle(ui::TooltipManager* tooltips) : ButtonSet(3, true)
|
||||||
{
|
{
|
||||||
addItem("B");
|
addItem("B");
|
||||||
addItem("I");
|
addItem("I");
|
||||||
addItem("...");
|
addItem("...");
|
||||||
setMultiMode(MultiMode::Set);
|
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_face.setExpansive(true);
|
||||||
m_size.setExpansive(false);
|
m_size.setExpansive(false);
|
||||||
m_style.setExpansive(false);
|
m_style.setExpansive(false);
|
||||||
|
|
||||||
|
addChild(&m_tooltips);
|
||||||
addChild(&m_face);
|
addChild(&m_face);
|
||||||
addChild(&m_size);
|
addChild(&m_size);
|
||||||
addChild(&m_style);
|
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));
|
m_face.setMinSize(gfx::Size(128 * guiscale(), 0));
|
||||||
|
|
||||||
|
|
@ -299,6 +370,8 @@ FontEntry::FontEntry()
|
||||||
});
|
});
|
||||||
|
|
||||||
m_style.ItemChange.connect(&FontEntry::onStyleItemClick, this);
|
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
|
// 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);
|
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)
|
void FontEntry::onStyleItemClick(ButtonSet::Item* item)
|
||||||
{
|
{
|
||||||
text::FontStyle style = m_info.style();
|
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
|
} // namespace app
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,11 @@
|
||||||
#include "ui/box.h"
|
#include "ui/box.h"
|
||||||
#include "ui/button.h"
|
#include "ui/button.h"
|
||||||
#include "ui/combobox.h"
|
#include "ui/combobox.h"
|
||||||
|
#include "ui/int_entry.h"
|
||||||
|
#include "ui/paint.h"
|
||||||
|
#include "ui/tooltips.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
@ -30,18 +34,22 @@ public:
|
||||||
Flags,
|
Flags,
|
||||||
Hinting,
|
Hinting,
|
||||||
Popup,
|
Popup,
|
||||||
|
Paint,
|
||||||
};
|
};
|
||||||
|
|
||||||
FontEntry();
|
FontEntry(bool withStrokeAndFill);
|
||||||
~FontEntry();
|
~FontEntry();
|
||||||
|
|
||||||
FontInfo info() { return m_info; }
|
FontInfo info() { return m_info; }
|
||||||
void setInfo(const FontInfo& info, From from);
|
void setInfo(const FontInfo& info, From from);
|
||||||
|
|
||||||
|
ui::Paint paint();
|
||||||
|
|
||||||
obs::signal<void(const FontInfo&, From)> FontChange;
|
obs::signal<void(const FontInfo&, From)> FontChange;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void onStyleItemClick(ButtonSet::Item* item);
|
void onStyleItemClick(ButtonSet::Item* item);
|
||||||
|
void onStrokeChange();
|
||||||
|
|
||||||
class FontFace : public SearchEntry {
|
class FontFace : public SearchEntry {
|
||||||
public:
|
public:
|
||||||
|
|
@ -73,13 +81,40 @@ private:
|
||||||
|
|
||||||
class FontStyle : public ButtonSet {
|
class FontStyle : public ButtonSet {
|
||||||
public:
|
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;
|
FontInfo m_info;
|
||||||
FontFace m_face;
|
FontFace m_face;
|
||||||
FontSize m_size;
|
FontSize m_size;
|
||||||
FontStyle m_style;
|
FontStyle m_style;
|
||||||
|
std::unique_ptr<FontStroke> m_stroke;
|
||||||
bool m_lockFace = false;
|
bool m_lockFace = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,11 @@ private:
|
||||||
if (!blob)
|
if (!blob)
|
||||||
return;
|
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)
|
if (!image)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,10 @@ void IconButton::setIcon(const skin::SkinPartPtr& part)
|
||||||
|
|
||||||
void IconButton::onInitTheme(InitThemeEvent& ev)
|
void IconButton::onInitTheme(InitThemeEvent& ev)
|
||||||
{
|
{
|
||||||
Button::onInitTheme(ev);
|
|
||||||
|
|
||||||
auto theme = SkinTheme::get(this);
|
auto theme = SkinTheme::get(this);
|
||||||
setBgColor(theme->colors.menuitemNormalFace());
|
setBgColor(theme->colors.menuitemNormalFace());
|
||||||
|
|
||||||
|
Button::onInitTheme(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
void IconButton::onSizeHint(SizeHintEvent& ev)
|
void IconButton::onSizeHint(SizeHintEvent& ev)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -9,18 +9,23 @@
|
||||||
#define APP_UI_MAIN_MENU_BAR_H_INCLUDED
|
#define APP_UI_MAIN_MENU_BAR_H_INCLUDED
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "app/ui/dockable.h"
|
||||||
#include "obs/connection.h"
|
#include "obs/connection.h"
|
||||||
#include "ui/menu.h"
|
#include "ui/menu.h"
|
||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
class MainMenuBar : public ui::MenuBar {
|
class MainMenuBar : public ui::MenuBar,
|
||||||
|
public Dockable {
|
||||||
public:
|
public:
|
||||||
MainMenuBar();
|
MainMenuBar();
|
||||||
|
|
||||||
void queueReload();
|
void queueReload();
|
||||||
void reload();
|
void reload();
|
||||||
|
|
||||||
|
// Dockable impl
|
||||||
|
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
obs::scoped_connection m_extKeys;
|
obs::scoped_connection m_extKeys;
|
||||||
obs::scoped_connection m_extScripts;
|
obs::scoped_connection m_extScripts;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -24,9 +24,11 @@
|
||||||
#include "app/ui/color_bar.h"
|
#include "app/ui/color_bar.h"
|
||||||
#include "app/ui/context_bar.h"
|
#include "app/ui/context_bar.h"
|
||||||
#include "app/ui/doc_view.h"
|
#include "app/ui/doc_view.h"
|
||||||
|
#include "app/ui/dock.h"
|
||||||
#include "app/ui/editor/editor.h"
|
#include "app/ui/editor/editor.h"
|
||||||
#include "app/ui/editor/editor_view.h"
|
#include "app/ui/editor/editor_view.h"
|
||||||
#include "app/ui/home_view.h"
|
#include "app/ui/home_view.h"
|
||||||
|
#include "app/ui/layout_selector.h"
|
||||||
#include "app/ui/main_menu_bar.h"
|
#include "app/ui/main_menu_bar.h"
|
||||||
#include "app/ui/notifications.h"
|
#include "app/ui/notifications.h"
|
||||||
#include "app/ui/preview_editor.h"
|
#include "app/ui/preview_editor.h"
|
||||||
|
|
@ -42,6 +44,7 @@
|
||||||
#include "os/event.h"
|
#include "os/event.h"
|
||||||
#include "os/event_queue.h"
|
#include "os/event_queue.h"
|
||||||
#include "os/system.h"
|
#include "os/system.h"
|
||||||
|
#include "ui/app_state.h"
|
||||||
#include "ui/drag_event.h"
|
#include "ui/drag_event.h"
|
||||||
#include "ui/message.h"
|
#include "ui/message.h"
|
||||||
#include "ui/splitter.h"
|
#include "ui/splitter.h"
|
||||||
|
|
@ -57,6 +60,10 @@ namespace app {
|
||||||
|
|
||||||
using namespace ui;
|
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 {
|
class ScreenScalePanic : public INotificationDelegate {
|
||||||
public:
|
public:
|
||||||
std::string notificationText() override { return "Reset Scale!"; }
|
std::string notificationText() override { return "Reset Scale!"; }
|
||||||
|
|
@ -83,7 +90,11 @@ public:
|
||||||
};
|
};
|
||||||
|
|
||||||
MainWindow::MainWindow()
|
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_homeView(nullptr)
|
||||||
, m_scalePanic(nullptr)
|
, m_scalePanic(nullptr)
|
||||||
, m_browserView(nullptr)
|
, m_browserView(nullptr)
|
||||||
|
|
@ -105,8 +116,9 @@ MainWindow::MainWindow()
|
||||||
// Refer to https://github.com/aseprite/aseprite/issues/3914
|
// Refer to https://github.com/aseprite/aseprite/issues/3914
|
||||||
void MainWindow::initialize()
|
void MainWindow::initialize()
|
||||||
{
|
{
|
||||||
m_tooltipManager = new TooltipManager();
|
m_menuBar = std::make_unique<MainMenuBar>();
|
||||||
m_menuBar = new 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
|
// Register commands to load menus+shortcuts for these commands
|
||||||
Editor::registerCommands();
|
Editor::registerCommands();
|
||||||
|
|
@ -117,20 +129,19 @@ void MainWindow::initialize()
|
||||||
// Setup the main menubar
|
// Setup the main menubar
|
||||||
m_menuBar->setMenu(AppMenus::instance()->getRootMenu());
|
m_menuBar->setMenu(AppMenus::instance()->getRootMenu());
|
||||||
|
|
||||||
m_notifications = new Notifications();
|
m_statusBar = std::make_unique<StatusBar>(m_tooltipManager);
|
||||||
m_statusBar = new StatusBar(m_tooltipManager);
|
m_toolBar = std::make_unique<ToolBar>();
|
||||||
m_toolBar = new ToolBar();
|
m_tabsBar = std::make_unique<WorkspaceTabs>(this);
|
||||||
m_tabsBar = new WorkspaceTabs(this);
|
m_workspace = std::make_unique<Workspace>();
|
||||||
m_workspace = new Workspace();
|
m_previewEditor = std::make_unique<PreviewEditorWindow>();
|
||||||
m_previewEditor = new PreviewEditorWindow();
|
m_colorBar = std::make_unique<ColorBar>(m_tooltipManager);
|
||||||
m_colorBar = new ColorBar(colorBarPlaceholder()->align(), m_tooltipManager);
|
m_contextBar = std::make_unique<ContextBar>(m_tooltipManager, m_colorBar.get());
|
||||||
m_contextBar = new ContextBar(m_tooltipManager, m_colorBar);
|
|
||||||
|
|
||||||
// The timeline (AniControls) tooltips will use the keyboard
|
// The timeline (AniControls) tooltips will use the keyboard
|
||||||
// shortcuts loaded above.
|
// 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->BeforeViewChanged.connect(&MainWindow::onBeforeViewChange, this);
|
||||||
m_workspace->ActiveViewChanged.connect(&MainWindow::onActiveViewChange, this);
|
m_workspace->ActiveViewChanged.connect(&MainWindow::onActiveViewChange, this);
|
||||||
|
|
||||||
|
|
@ -146,21 +157,31 @@ void MainWindow::initialize()
|
||||||
m_workspace->setExpansive(true);
|
m_workspace->setExpansive(true);
|
||||||
m_notifications->setVisible(false);
|
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
|
// Add the widgets in the boxes
|
||||||
addChild(m_tooltipManager);
|
addChild(m_tooltipManager);
|
||||||
menuBarPlaceholder()->addChild(m_menuBar);
|
addChild(m_dock);
|
||||||
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);
|
|
||||||
|
|
||||||
// Default splitter positions
|
m_customizableDockPlaceholder = std::make_unique<Widget>();
|
||||||
colorBarSplitter()->setPosition(m_colorBar->sizeHint().w);
|
m_customizableDockPlaceholder->addChild(m_customizableDock);
|
||||||
timelineSplitter()->setPosition(75);
|
|
||||||
|
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.
|
// Reconfigure workspace when the timeline position is changed.
|
||||||
auto& pref = Preferences::instance();
|
auto& pref = Preferences::instance();
|
||||||
|
|
@ -172,49 +193,60 @@ void MainWindow::initialize()
|
||||||
|
|
||||||
AppMenus::instance()->rebuildRecentList();
|
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.
|
// relayout the whole main window.
|
||||||
Strings::instance()->LanguageChange.connect([this] { onLanguageChange(); });
|
Strings::instance()->LanguageChange.connect([this] { onLanguageChange(); });
|
||||||
|
|
||||||
|
initTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::~MainWindow()
|
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
|
#ifdef ENABLE_SCRIPTING
|
||||||
if (m_devConsoleView) {
|
if (m_devConsoleView) {
|
||||||
if (m_devConsoleView->parent() && m_workspace)
|
if (m_devConsoleView->parent() && m_workspace)
|
||||||
m_workspace->removeView(m_devConsoleView);
|
m_workspace->removeView(m_devConsoleView.get());
|
||||||
delete m_devConsoleView;
|
m_devConsoleView.reset();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (m_browserView) {
|
if (m_browserView) {
|
||||||
if (m_browserView->parent() && m_workspace)
|
if (m_browserView->parent() && m_workspace)
|
||||||
m_workspace->removeView(m_browserView);
|
m_workspace->removeView(m_browserView.get());
|
||||||
delete m_browserView;
|
m_browserView.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_homeView) {
|
if (m_homeView) {
|
||||||
if (m_homeView->parent() && m_workspace)
|
if (m_homeView->parent() && m_workspace)
|
||||||
m_workspace->removeView(m_homeView);
|
m_workspace->removeView(m_homeView.get());
|
||||||
delete m_homeView;
|
m_homeView.reset();
|
||||||
}
|
}
|
||||||
if (m_contextBar)
|
m_contextBar.reset();
|
||||||
delete m_contextBar;
|
m_previewEditor.reset();
|
||||||
if (m_previewEditor)
|
|
||||||
delete m_previewEditor;
|
|
||||||
|
|
||||||
// Destroy the workspace first so ~Editor can dettach slots from
|
// Destroy the workspace first so ~Editor can dettach slots from
|
||||||
// ColorBar. TODO this is a terrible hack for slot/signal stuff,
|
// ColorBar. TODO this is a terrible hack for slot/signal stuff,
|
||||||
// connections should be handle in a better/safer way.
|
// connections should be handle in a better/safer way.
|
||||||
if (m_workspace)
|
m_workspace.reset();
|
||||||
delete m_workspace;
|
|
||||||
|
|
||||||
// Remove the root-menu from the menu-bar (because the rootmenu
|
// Remove the root-menu from the menu-bar (because the rootmenu
|
||||||
// module should destroy it).
|
// module should destroy it).
|
||||||
if (m_menuBar)
|
if (m_menuBar)
|
||||||
m_menuBar->setMenu(NULL);
|
m_menuBar->setMenu(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onLanguageChange()
|
void MainWindow::onLanguageChange()
|
||||||
|
|
@ -232,8 +264,8 @@ DocView* MainWindow::getDocView()
|
||||||
HomeView* MainWindow::getHomeView()
|
HomeView* MainWindow::getHomeView()
|
||||||
{
|
{
|
||||||
if (!m_homeView)
|
if (!m_homeView)
|
||||||
m_homeView = new HomeView;
|
m_homeView = std::make_unique<HomeView>();
|
||||||
return m_homeView;
|
return m_homeView.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ENABLE_UPDATER
|
#ifdef ENABLE_UPDATER
|
||||||
|
|
@ -254,7 +286,7 @@ void MainWindow::showNotification(INotificationDelegate* del)
|
||||||
{
|
{
|
||||||
m_notifications->addLink(del);
|
m_notifications->addLink(del);
|
||||||
m_notifications->setVisible(true);
|
m_notifications->setVisible(true);
|
||||||
m_notifications->parent()->layout();
|
layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::showHomeOnOpen()
|
void MainWindow::showHomeOnOpen()
|
||||||
|
|
@ -270,20 +302,20 @@ void MainWindow::showHomeOnOpen()
|
||||||
|
|
||||||
// Show "Home" tab in the first position, and select it only if
|
// Show "Home" tab in the first position, and select it only if
|
||||||
// there is no other view selected.
|
// there is no other view selected.
|
||||||
m_workspace->addView(m_homeView, 0);
|
m_workspace->addView(m_homeView.get(), 0);
|
||||||
if (selectedTab)
|
if (selectedTab)
|
||||||
m_tabsBar->selectTab(selectedTab);
|
m_tabsBar->selectTab(selectedTab);
|
||||||
else
|
else
|
||||||
m_tabsBar->selectTab(m_homeView);
|
m_tabsBar->selectTab(m_homeView.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::showHome()
|
void MainWindow::showHome()
|
||||||
{
|
{
|
||||||
if (!getHomeView()->parent()) {
|
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()
|
void MainWindow::showDefaultStatusBar()
|
||||||
|
|
@ -298,19 +330,19 @@ void MainWindow::showDefaultStatusBar()
|
||||||
|
|
||||||
bool MainWindow::isHomeSelected() const
|
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)
|
void MainWindow::showBrowser(const std::string& filename, const std::string& section)
|
||||||
{
|
{
|
||||||
if (!m_browserView)
|
if (!m_browserView)
|
||||||
m_browserView = new BrowserView;
|
m_browserView = std::make_unique<BrowserView>();
|
||||||
|
|
||||||
m_browserView->loadFile(filename, section);
|
m_browserView->loadFile(filename, section);
|
||||||
|
|
||||||
if (!m_browserView->parent()) {
|
if (!m_browserView->parent()) {
|
||||||
m_workspace->addView(m_browserView);
|
m_workspace->addView(m_browserView.get());
|
||||||
m_tabsBar->selectTab(m_browserView);
|
m_tabsBar->selectTab(m_browserView.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,11 +350,11 @@ void MainWindow::showDevConsole()
|
||||||
{
|
{
|
||||||
#ifdef ENABLE_SCRIPTING
|
#ifdef ENABLE_SCRIPTING
|
||||||
if (!m_devConsoleView)
|
if (!m_devConsoleView)
|
||||||
m_devConsoleView = new DevConsoleView;
|
m_devConsoleView = std::make_unique<DevConsoleView>();
|
||||||
|
|
||||||
if (!m_devConsoleView->parent()) {
|
if (!m_devConsoleView->parent()) {
|
||||||
m_workspace->addView(m_devConsoleView);
|
m_workspace->addView(m_devConsoleView.get());
|
||||||
m_tabsBar->selectTab(m_devConsoleView);
|
m_tabsBar->selectTab(m_devConsoleView.get());
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
@ -351,15 +383,109 @@ void MainWindow::setTimelineVisibility(bool visible)
|
||||||
|
|
||||||
void MainWindow::popTimeline()
|
void MainWindow::popTimeline()
|
||||||
{
|
{
|
||||||
Preferences& preferences = Preferences::instance();
|
if (!Preferences::instance().general.autoshowTimeline())
|
||||||
|
|
||||||
if (!preferences.general.autoshowTimeline())
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!getTimelineVisibility())
|
if (!getTimelineVisibility())
|
||||||
setTimelineVisibility(true);
|
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()
|
void MainWindow::dataRecoverySessionsAreReady()
|
||||||
{
|
{
|
||||||
getHomeView()->dataRecoverySessionsAreReady();
|
getHomeView()->dataRecoverySessionsAreReady();
|
||||||
|
|
@ -375,24 +501,37 @@ bool MainWindow::onProcessMessage(ui::Message* msg)
|
||||||
|
|
||||||
void MainWindow::onInitTheme(ui::InitThemeEvent& ev)
|
void MainWindow::onInitTheme(ui::InitThemeEvent& ev)
|
||||||
{
|
{
|
||||||
app::gen::MainWindow::onInitTheme(ev);
|
ui::Window::onInitTheme(ev);
|
||||||
|
noBorderNoChildSpacing();
|
||||||
if (m_previewEditor)
|
if (m_previewEditor)
|
||||||
m_previewEditor->initTheme();
|
m_previewEditor->initTheme();
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::onSaveLayout(SaveLayoutEvent& ev)
|
auto* theme = static_cast<skin::SkinTheme*>(this->theme());
|
||||||
{
|
m_dock->setBgColor(theme->colors.windowFace());
|
||||||
// Invert the timeline splitter position before we save the setting.
|
m_customizableDock->setBgColor(theme->colors.workspace());
|
||||||
if (Preferences::instance().general.timelinePosition() == gen::TimelinePosition::LEFT) {
|
|
||||||
timelineSplitter()->setPosition(100 - timelineSplitter()->getPosition());
|
|
||||||
}
|
|
||||||
|
|
||||||
Window::onSaveLayout(ev);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onResize(ui::ResizeEvent& ev)
|
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);
|
os::Window* nativeWindow = (display() ? display()->nativeWindow() : nullptr);
|
||||||
if (nativeWindow && nativeWindow->screen()) {
|
if (nativeWindow && nativeWindow->screen()) {
|
||||||
|
|
@ -405,7 +544,8 @@ void MainWindow::onResize(ui::ResizeEvent& ev)
|
||||||
if ((scale > 2) && (!m_scalePanic)) {
|
if ((scale > 2) && (!m_scalePanic)) {
|
||||||
const gfx::Size wa = nativeWindow->screen()->workarea().size();
|
const gfx::Size wa = nativeWindow->screen()->workarea().size();
|
||||||
if ((wa.w / scale < 256 || wa.h / scale < 256)) {
|
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.
|
// inform to the UIContext that the current view has changed.
|
||||||
void MainWindow::onActiveViewChange()
|
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
|
// First we have to configure the MainWindow layout (e.g. show
|
||||||
// Timeline if needed) as UIContext::setActiveView() will configure
|
// Timeline if needed) as UIContext::setActiveView() will configure
|
||||||
// several widgets (calling updateUsingEditor() functions) using the
|
// several widgets (calling updateUsingEditor() functions) using the
|
||||||
|
|
@ -508,7 +653,7 @@ void MainWindow::onContextMenuTab(Tabs* tabs, TabView* tabView)
|
||||||
WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
|
WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
|
||||||
ASSERT(view);
|
ASSERT(view);
|
||||||
if (view)
|
if (view)
|
||||||
view->onTabPopup(m_workspace);
|
view->onTabPopup(m_workspace.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onTabsContainerDoubleClicked(Tabs* tabs)
|
void MainWindow::onTabsContainerDoubleClicked(Tabs* tabs)
|
||||||
|
|
@ -588,71 +733,80 @@ DropTabResult MainWindow::onDropTab(Tabs* tabs,
|
||||||
|
|
||||||
void MainWindow::configureWorkspaceLayout()
|
void MainWindow::configureWorkspaceLayout()
|
||||||
{
|
{
|
||||||
|
// First layout to get the bounds of some widgets
|
||||||
|
layout();
|
||||||
|
|
||||||
const auto& pref = Preferences::instance();
|
const auto& pref = Preferences::instance();
|
||||||
bool normal = (m_mode == NormalMode);
|
bool normal = (m_mode == NormalMode);
|
||||||
|
bool showMenu = normal;
|
||||||
bool isDoc = (getDocView() != nullptr);
|
bool isDoc = (getDocView() != nullptr);
|
||||||
|
|
||||||
if (os::System::instance()->menus() == nullptr || pref.general.showMenuBar()) {
|
if (os::System::instance()->menus() && !pref.general.showMenuBar())
|
||||||
m_menuBar->resetMaxSize();
|
showMenu = false;
|
||||||
}
|
|
||||||
else {
|
|
||||||
m_menuBar->setMaxSize(gfx::Size(0, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_menuBar->setVisible(normal);
|
m_menuBar->setVisible(showMenu);
|
||||||
m_notifications->setVisible(normal && m_notifications->hasNotifications());
|
m_notifications->setVisible(normal && m_notifications->hasNotifications());
|
||||||
m_tabsBar->setVisible(normal);
|
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_toolBar->setVisible(normal && isDoc);
|
||||||
m_statusBar->setVisible(normal);
|
m_statusBar->setVisible(normal);
|
||||||
m_contextBar->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode));
|
m_contextBar->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode));
|
||||||
|
|
||||||
// Configure timeline
|
// Configure timeline
|
||||||
{
|
if (m_timeline && m_timeline->parent()) {
|
||||||
auto timelinePosition = pref.general.timelinePosition();
|
m_timelineResizeConn = dynamic_cast<Dock*>(m_timeline->parent())
|
||||||
bool invertWidgets = false;
|
->Resize.connect(&MainWindow::saveTimelineConfiguration, this);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_timeline->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode) &&
|
||||||
|
pref.general.visibleTimeline());
|
||||||
|
|
||||||
if (m_contextBar->isVisible()) {
|
if (m_contextBar->isVisible()) {
|
||||||
m_contextBar->updateForActiveTool();
|
m_contextBar->updateForActiveTool();
|
||||||
}
|
}
|
||||||
|
|
||||||
layout();
|
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
|
} // namespace app
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2023 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -10,9 +10,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "app/ui/tabs.h"
|
#include "app/ui/tabs.h"
|
||||||
|
#include "obs/connection.h"
|
||||||
#include "ui/window.h"
|
#include "ui/window.h"
|
||||||
|
|
||||||
#include "main_window.xml.h"
|
#include <memory>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
class Splitter;
|
class Splitter;
|
||||||
|
|
@ -30,13 +31,17 @@ class ColorBar;
|
||||||
class ContextBar;
|
class ContextBar;
|
||||||
class DevConsoleView;
|
class DevConsoleView;
|
||||||
class DocView;
|
class DocView;
|
||||||
|
class Dock;
|
||||||
class HomeView;
|
class HomeView;
|
||||||
class INotificationDelegate;
|
class INotificationDelegate;
|
||||||
|
class Layout;
|
||||||
|
class LayoutSelector;
|
||||||
class MainMenuBar;
|
class MainMenuBar;
|
||||||
class Notifications;
|
class Notifications;
|
||||||
class PreviewEditorWindow;
|
class PreviewEditorWindow;
|
||||||
class StatusBar;
|
class StatusBar;
|
||||||
class Timeline;
|
class Timeline;
|
||||||
|
class ToolBar;
|
||||||
class Workspace;
|
class Workspace;
|
||||||
class WorkspaceTabs;
|
class WorkspaceTabs;
|
||||||
|
|
||||||
|
|
@ -44,7 +49,7 @@ namespace crash {
|
||||||
class DataRecovery;
|
class DataRecovery;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainWindow : public app::gen::MainWindow,
|
class MainWindow : public ui::Window,
|
||||||
public TabsDelegate {
|
public TabsDelegate {
|
||||||
public:
|
public:
|
||||||
enum Mode { NormalMode, ContextBarAndTimelineMode, EditorOnlyMode };
|
enum Mode { NormalMode, ContextBarAndTimelineMode, EditorOnlyMode };
|
||||||
|
|
@ -52,13 +57,17 @@ public:
|
||||||
MainWindow();
|
MainWindow();
|
||||||
~MainWindow();
|
~MainWindow();
|
||||||
|
|
||||||
MainMenuBar* getMenuBar() { return m_menuBar; }
|
// TODO refactor: remove the get prefix from these functions
|
||||||
ContextBar* getContextBar() { return m_contextBar; }
|
MainMenuBar* getMenuBar() { return m_menuBar.get(); }
|
||||||
StatusBar* statusBar() { return m_statusBar; }
|
LayoutSelector* layoutSelector() { return m_layoutSelector.get(); }
|
||||||
WorkspaceTabs* getTabsBar() { return m_tabsBar; }
|
ContextBar* getContextBar() { return m_contextBar.get(); }
|
||||||
Timeline* getTimeline() { return m_timeline; }
|
StatusBar* statusBar() { return m_statusBar.get(); }
|
||||||
Workspace* getWorkspace() { return m_workspace; }
|
WorkspaceTabs* getTabsBar() { return m_tabsBar.get(); }
|
||||||
PreviewEditorWindow* getPreviewEditor() { return m_previewEditor; }
|
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
|
#ifdef ENABLE_UPDATER
|
||||||
CheckUpdateDelegate* getCheckUpdateDelegate();
|
CheckUpdateDelegate* getCheckUpdateDelegate();
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -83,6 +92,12 @@ public:
|
||||||
void setTimelineVisibility(bool visible);
|
void setTimelineVisibility(bool visible);
|
||||||
void popTimeline();
|
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
|
// When crash::DataRecovery finish to search for sessions, this
|
||||||
// function is called.
|
// function is called.
|
||||||
void dataRecoverySessionsAreReady();
|
void dataRecoverySessionsAreReady();
|
||||||
|
|
@ -109,7 +124,6 @@ public:
|
||||||
protected:
|
protected:
|
||||||
bool onProcessMessage(ui::Message* msg) override;
|
bool onProcessMessage(ui::Message* msg) override;
|
||||||
void onInitTheme(ui::InitThemeEvent& ev) override;
|
void onInitTheme(ui::InitThemeEvent& ev) override;
|
||||||
void onSaveLayout(ui::SaveLayoutEvent& ev) override;
|
|
||||||
void onResize(ui::ResizeEvent& ev) override;
|
void onResize(ui::ResizeEvent& ev) override;
|
||||||
void onBeforeViewChange();
|
void onBeforeViewChange();
|
||||||
void onActiveViewChange();
|
void onActiveViewChange();
|
||||||
|
|
@ -121,25 +135,36 @@ private:
|
||||||
DocView* getDocView();
|
DocView* getDocView();
|
||||||
HomeView* getHomeView();
|
HomeView* getHomeView();
|
||||||
void configureWorkspaceLayout();
|
void configureWorkspaceLayout();
|
||||||
|
void saveTimelineConfiguration();
|
||||||
|
void saveColorBarConfiguration();
|
||||||
|
void saveActiveLayout();
|
||||||
|
|
||||||
ui::TooltipManager* m_tooltipManager;
|
ui::TooltipManager* m_tooltipManager;
|
||||||
MainMenuBar* m_menuBar;
|
Dock* m_dock;
|
||||||
StatusBar* m_statusBar;
|
Dock* m_customizableDock;
|
||||||
ColorBar* m_colorBar;
|
std::unique_ptr<Widget> m_customizableDockPlaceholder;
|
||||||
ContextBar* m_contextBar;
|
std::unique_ptr<MainMenuBar> m_menuBar;
|
||||||
ui::Widget* m_toolBar;
|
std::unique_ptr<Notifications> m_notifications;
|
||||||
WorkspaceTabs* m_tabsBar;
|
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;
|
Mode m_mode;
|
||||||
Timeline* m_timeline;
|
std::unique_ptr<Timeline> m_timeline;
|
||||||
Workspace* m_workspace;
|
std::unique_ptr<Workspace> m_workspace;
|
||||||
PreviewEditorWindow* m_previewEditor;
|
std::unique_ptr<PreviewEditorWindow> m_previewEditor;
|
||||||
HomeView* m_homeView;
|
std::unique_ptr<HomeView> m_homeView;
|
||||||
Notifications* m_notifications;
|
std::unique_ptr<INotificationDelegate> m_scalePanic;
|
||||||
INotificationDelegate* m_scalePanic;
|
std::unique_ptr<BrowserView> m_browserView;
|
||||||
BrowserView* m_browserView;
|
|
||||||
#ifdef ENABLE_SCRIPTING
|
#ifdef ENABLE_SCRIPTING
|
||||||
DevConsoleView* m_devConsoleView;
|
std::unique_ptr<DevConsoleView> m_devConsoleView;
|
||||||
#endif
|
#endif
|
||||||
|
obs::scoped_connection m_timelineResizeConn;
|
||||||
|
obs::scoped_connection m_colorBarResizeConn;
|
||||||
|
obs::scoped_connection m_saveDockLayoutConn;
|
||||||
|
bool m_firstResize = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2020-2023 Igara Studio S.A.
|
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
|
using namespace app::skin;
|
||||||
using namespace ui;
|
using namespace ui;
|
||||||
|
|
||||||
class NotificationItem : public MenuItem {
|
class NotificationItem : public MenuItem {
|
||||||
|
|
@ -39,10 +40,7 @@ private:
|
||||||
INotificationDelegate* m_delegate;
|
INotificationDelegate* m_delegate;
|
||||||
};
|
};
|
||||||
|
|
||||||
Notifications::Notifications()
|
Notifications::Notifications() : Button(""), m_red(false)
|
||||||
: Button("")
|
|
||||||
, m_flagStyle(skin::SkinTheme::get(this)->styles.flag())
|
|
||||||
, m_red(false)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,13 +50,22 @@ void Notifications::addLink(INotificationDelegate* del)
|
||||||
m_red = true;
|
m_red = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Notifications::onInitTheme(InitThemeEvent& ev)
|
||||||
|
{
|
||||||
|
Button::onInitTheme(ev);
|
||||||
|
m_popup.initTheme();
|
||||||
|
}
|
||||||
|
|
||||||
void Notifications::onSizeHint(SizeHintEvent& ev)
|
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)
|
void Notifications::onPaint(PaintEvent& ev)
|
||||||
{
|
{
|
||||||
|
auto* theme = SkinTheme::get(this);
|
||||||
Graphics* g = ev.graphics();
|
Graphics* g = ev.graphics();
|
||||||
|
|
||||||
PaintWidgetPartInfo info;
|
PaintWidgetPartInfo info;
|
||||||
|
|
@ -69,7 +76,7 @@ void Notifications::onPaint(PaintEvent& ev)
|
||||||
if (isSelected())
|
if (isSelected())
|
||||||
info.styleFlags |= ui::Style::Layer::kSelected;
|
info.styleFlags |= ui::Style::Layer::kSelected;
|
||||||
|
|
||||||
theme()->paintWidgetPart(g, m_flagStyle, clientBounds(), info);
|
theme->paintWidgetPart(g, theme->styles.flag(), clientBounds(), info);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Notifications::onClick()
|
void Notifications::onClick()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2020-2023 Igara Studio S.A.
|
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#define APP_UI_NOTIFICATIONS_H_INCLUDED
|
#define APP_UI_NOTIFICATIONS_H_INCLUDED
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "app/ui/dockable.h"
|
||||||
#include "ui/button.h"
|
#include "ui/button.h"
|
||||||
#include "ui/menu.h"
|
#include "ui/menu.h"
|
||||||
|
|
||||||
|
|
@ -19,20 +20,24 @@ class Style;
|
||||||
namespace app {
|
namespace app {
|
||||||
class INotificationDelegate;
|
class INotificationDelegate;
|
||||||
|
|
||||||
class Notifications : public ui::Button {
|
class Notifications : public ui::Button,
|
||||||
|
public Dockable {
|
||||||
public:
|
public:
|
||||||
Notifications();
|
Notifications();
|
||||||
|
|
||||||
void addLink(INotificationDelegate* del);
|
void addLink(INotificationDelegate* del);
|
||||||
bool hasNotifications() const { return m_popup.hasChildren(); }
|
bool hasNotifications() const { return m_popup.hasChildren(); }
|
||||||
|
|
||||||
|
// Dockable impl
|
||||||
|
int dockableAt() const override { return ui::TOP | ui::BOTTOM | ui::LEFT | ui::RIGHT; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void onInitTheme(ui::InitThemeEvent& ev) override;
|
||||||
void onSizeHint(ui::SizeHintEvent& ev) override;
|
void onSizeHint(ui::SizeHintEvent& ev) override;
|
||||||
void onPaint(ui::PaintEvent& ev) override;
|
void onPaint(ui::PaintEvent& ev) override;
|
||||||
void onClick() override;
|
void onClick() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ui::Style* m_flagStyle;
|
|
||||||
bool m_red;
|
bool m_red;
|
||||||
ui::Menu m_popup;
|
ui::Menu m_popup;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1483,6 +1483,8 @@ void SkinTheme::drawEntryText(ui::Graphics* g, ui::Entry* widget)
|
||||||
|
|
||||||
// Draw caret at the end of the text
|
// Draw caret at the end of the text
|
||||||
if (!delegate.caretDrawn()) {
|
if (!delegate.caretDrawn()) {
|
||||||
|
bounds.x += delegate.textBounds().w;
|
||||||
|
|
||||||
gfx::Rect charBounds(bounds.x + widget->bounds().x,
|
gfx::Rect charBounds(bounds.x + widget->bounds().x,
|
||||||
bounds.y + widget->bounds().y,
|
bounds.y + widget->bounds().y,
|
||||||
0,
|
0,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include "app/context_observer.h"
|
#include "app/context_observer.h"
|
||||||
#include "app/tools/active_tool_observer.h"
|
#include "app/tools/active_tool_observer.h"
|
||||||
#include "app/ui/doc_observer_widget.h"
|
#include "app/ui/doc_observer_widget.h"
|
||||||
|
#include "app/ui/dockable.h"
|
||||||
#include "base/time.h"
|
#include "base/time.h"
|
||||||
#include "doc/tile.h"
|
#include "doc/tile.h"
|
||||||
#include "ui/base.h"
|
#include "ui/base.h"
|
||||||
|
|
@ -44,7 +45,8 @@ class Tool;
|
||||||
}
|
}
|
||||||
|
|
||||||
class StatusBar : public DocObserverWidget<ui::HBox>,
|
class StatusBar : public DocObserverWidget<ui::HBox>,
|
||||||
public tools::ActiveToolObserver {
|
public tools::ActiveToolObserver,
|
||||||
|
public Dockable {
|
||||||
static StatusBar* m_instance;
|
static StatusBar* m_instance;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
@ -72,6 +74,10 @@ public:
|
||||||
|
|
||||||
void showBackupIcon(BackupIcon icon);
|
void showBackupIcon(BackupIcon icon);
|
||||||
|
|
||||||
|
// Dockable impl
|
||||||
|
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
|
||||||
|
int dockHandleSide() const override { return ui::LEFT; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void onInitTheme(ui::InitThemeEvent& ev) override;
|
void onInitTheme(ui::InitThemeEvent& ev) override;
|
||||||
void onResize(ui::ResizeEvent& ev) override;
|
void onResize(ui::ResizeEvent& ev) override;
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,9 @@ Tabs::~Tabs()
|
||||||
m_addedTab.reset();
|
m_addedTab.reset();
|
||||||
m_removedTab.reset();
|
m_removedTab.reset();
|
||||||
|
|
||||||
// Stop animation
|
// Stop animation, can cause issues with docks when stopping during close.
|
||||||
stopAnimation();
|
if (!is_app_state_closing())
|
||||||
|
stopAnimation();
|
||||||
|
|
||||||
// Remove all tabs
|
// Remove all tabs
|
||||||
m_list.clear();
|
m_list.clear();
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#define APP_UI_TABS_H_INCLUDED
|
#define APP_UI_TABS_H_INCLUDED
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "app/ui/dockable.h"
|
||||||
#include "base/ref.h"
|
#include "base/ref.h"
|
||||||
#include "text/fwd.h"
|
#include "text/fwd.h"
|
||||||
#include "ui/animated_widget.h"
|
#include "ui/animated_widget.h"
|
||||||
|
|
@ -118,7 +119,8 @@ public:
|
||||||
|
|
||||||
// Tabs control. Used to show opened documents.
|
// Tabs control. Used to show opened documents.
|
||||||
class Tabs : public ui::Widget,
|
class Tabs : public ui::Widget,
|
||||||
public ui::AnimatedWidget {
|
public ui::AnimatedWidget,
|
||||||
|
public Dockable {
|
||||||
struct Tab {
|
struct Tab {
|
||||||
TabView* view;
|
TabView* view;
|
||||||
std::string text;
|
std::string text;
|
||||||
|
|
@ -181,6 +183,9 @@ public:
|
||||||
void removeDropViewPreview();
|
void removeDropViewPreview();
|
||||||
int getDropTabIndex() const { return m_dropNewIndex; }
|
int getDropTabIndex() const { return m_dropNewIndex; }
|
||||||
|
|
||||||
|
// Dockable impl
|
||||||
|
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool onProcessMessage(ui::Message* msg) override;
|
bool onProcessMessage(ui::Message* msg) override;
|
||||||
void onInitTheme(ui::InitThemeEvent& ev) override;
|
void onInitTheme(ui::InitThemeEvent& ev) override;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include "app/docs_observer.h"
|
#include "app/docs_observer.h"
|
||||||
#include "app/loop_tag.h"
|
#include "app/loop_tag.h"
|
||||||
#include "app/pref/preferences.h"
|
#include "app/pref/preferences.h"
|
||||||
|
#include "app/ui/dockable.h"
|
||||||
#include "app/ui/editor/editor_observer.h"
|
#include "app/ui/editor/editor_observer.h"
|
||||||
#include "app/ui/input_chain_element.h"
|
#include "app/ui/input_chain_element.h"
|
||||||
#include "app/ui/timeline/ani_controls.h"
|
#include "app/ui/timeline/ani_controls.h"
|
||||||
|
|
@ -73,7 +74,8 @@ class Timeline : public ui::Widget,
|
||||||
public DocObserver,
|
public DocObserver,
|
||||||
public EditorObserver,
|
public EditorObserver,
|
||||||
public InputChainElement,
|
public InputChainElement,
|
||||||
public TagProvider {
|
public TagProvider,
|
||||||
|
public Dockable {
|
||||||
public:
|
public:
|
||||||
using Range = view::Range;
|
using Range = view::Range;
|
||||||
using RealRange = view::RealRange;
|
using RealRange = view::RealRange;
|
||||||
|
|
@ -155,6 +157,12 @@ public:
|
||||||
|
|
||||||
void clearAndInvalidateRange();
|
void clearAndInvalidateRange();
|
||||||
|
|
||||||
|
// Dockable impl
|
||||||
|
int dockableAt() const override
|
||||||
|
{
|
||||||
|
return ui::TOP | ui::BOTTOM | ui::LEFT | ui::RIGHT | ui::EXPANSIVE;
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool onProcessMessage(ui::Message* msg) override;
|
bool onProcessMessage(ui::Message* msg) override;
|
||||||
void onInitTheme(ui::InitThemeEvent& ev) override;
|
void onInitTheme(ui::InitThemeEvent& ev) override;
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,6 @@ ToolBar::ToolBar() : Widget(kGenericWidget), m_openedRecently(false), m_tipTimer
|
||||||
m_hotTool = NULL;
|
m_hotTool = NULL;
|
||||||
m_hotIndex = NoneIndex;
|
m_hotIndex = NoneIndex;
|
||||||
m_openOnHot = false;
|
m_openOnHot = false;
|
||||||
m_popupWindow = NULL;
|
|
||||||
m_currentStrip = NULL;
|
|
||||||
m_tipWindow = NULL;
|
|
||||||
m_tipOpened = false;
|
m_tipOpened = false;
|
||||||
m_minHeight = 0;
|
m_minHeight = 0;
|
||||||
|
|
||||||
|
|
@ -112,9 +109,6 @@ ToolBar::ToolBar() : Widget(kGenericWidget), m_openedRecently(false), m_tipTimer
|
||||||
ToolBar::~ToolBar()
|
ToolBar::~ToolBar()
|
||||||
{
|
{
|
||||||
App::instance()->activeToolManager()->remove_observer(this);
|
App::instance()->activeToolManager()->remove_observer(this);
|
||||||
|
|
||||||
delete m_popupWindow;
|
|
||||||
delete m_tipWindow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ToolBar::isToolVisible(Tool* tool)
|
bool ToolBar::isToolVisible(Tool* tool)
|
||||||
|
|
@ -314,10 +308,14 @@ void ToolBar::onSizeHint(SizeHintEvent& ev)
|
||||||
iconsize.h += border().height();
|
iconsize.h += border().height();
|
||||||
ev.setSizeHint(iconsize);
|
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) {
|
if (m_popupWindow) {
|
||||||
closePopupWindow();
|
closePopupWindow();
|
||||||
closeTipWindow();
|
closeTipWindow();
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToolBar::onResize(ui::ResizeEvent& ev)
|
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)
|
int ToolBar::getToolGroupIndex(ToolGroup* group)
|
||||||
{
|
{
|
||||||
ToolBox* toolbox = App::instance()->toolBox();
|
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
|
// In case this tool contains more than just one tool, show the popup window
|
||||||
m_openOnHot = true;
|
m_openOnHot = true;
|
||||||
m_popupWindow = new TransparentPopupWindow(
|
m_popupWindow = std::make_unique<TransparentPopupWindow>(
|
||||||
PopupWindow::ClickBehavior::CloseOnClickOutsideHotRegion);
|
PopupWindow::ClickBehavior::CloseOnClickOutsideHotRegion);
|
||||||
m_closeConn = m_popupWindow->Close.connect([this] { onClosePopup(); });
|
m_closeConn = m_popupWindow->Close.connect([this] { onClosePopup(); });
|
||||||
m_openedRecently = true;
|
m_openedRecently = true;
|
||||||
|
|
@ -474,19 +477,23 @@ void ToolBar::openPopupWindow(GroupType group_type, int group_index, tools::Tool
|
||||||
m_currentStrip = toolstrip;
|
m_currentStrip = toolstrip;
|
||||||
m_popupWindow->addChild(toolstrip);
|
m_popupWindow->addChild(toolstrip);
|
||||||
|
|
||||||
|
const int borderWidth = border().width();
|
||||||
Rect rc = getToolGroupBounds(group_index);
|
Rect rc = getToolGroupBounds(group_index);
|
||||||
int w = 0;
|
int w = borderWidth;
|
||||||
for (const auto* tool : tools) {
|
for (const auto* tool : tools) {
|
||||||
(void)tool;
|
(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;
|
rc.w = w;
|
||||||
|
|
||||||
// Set hotregion of popup window
|
// Set hotregion of popup window
|
||||||
m_popupWindow->setAutoRemap(false);
|
m_popupWindow->setAutoRemap(false);
|
||||||
ui::fit_bounds(display(), m_popupWindow, rc);
|
ui::fit_bounds(display(), m_popupWindow.get(), rc);
|
||||||
m_popupWindow->setBounds(rc);
|
m_popupWindow->setBounds(rc);
|
||||||
|
|
||||||
Region rgn(m_popupWindow->boundsOnScreen().enlarge(16 * guiscale()));
|
Region rgn(m_popupWindow->boundsOnScreen().enlarge(16 * guiscale()));
|
||||||
|
|
@ -500,8 +507,7 @@ void ToolBar::closePopupWindow()
|
||||||
{
|
{
|
||||||
if (m_popupWindow) {
|
if (m_popupWindow) {
|
||||||
m_popupWindow->closeWindow(nullptr);
|
m_popupWindow->closeWindow(nullptr);
|
||||||
delete m_popupWindow;
|
m_popupWindow.reset();
|
||||||
m_popupWindow = nullptr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -542,7 +548,7 @@ Point ToolBar::getToolPositionInGroup(const Tool* tool) const
|
||||||
const auto& tools = m_currentStrip->tools();
|
const auto& tools = m_currentStrip->tools();
|
||||||
const int nth = std::find(tools.begin(), tools.end(), tool) - tools.begin();
|
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)
|
void ToolBar::openTipWindow(ToolGroup* tool_group, Tool* tool)
|
||||||
|
|
@ -591,15 +597,31 @@ void ToolBar::openTipWindow(int group_index, Tool* tool)
|
||||||
else
|
else
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_tipWindow = new TipWindow(tooltip);
|
m_tipWindow = std::make_unique<TipWindow>(tooltip);
|
||||||
m_tipWindow->remapWindow();
|
m_tipWindow->remapWindow();
|
||||||
|
|
||||||
Rect toolrc = getToolGroupBounds(group_index);
|
Rect toolrc = getToolGroupBounds(group_index);
|
||||||
Point arrow = (tool ? getToolPositionInGroup(tool) : Point(0, 0));
|
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)
|
if (m_tipOpened)
|
||||||
m_tipWindow->openWindow();
|
m_tipWindow->openWindow();
|
||||||
|
|
@ -613,8 +635,7 @@ void ToolBar::closeTipWindow()
|
||||||
|
|
||||||
if (m_tipWindow) {
|
if (m_tipWindow) {
|
||||||
m_tipWindow->closeWindow(NULL);
|
m_tipWindow->closeWindow(NULL);
|
||||||
delete m_tipWindow;
|
m_tipWindow.reset();
|
||||||
m_tipWindow = NULL;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -651,7 +672,7 @@ void ToolBar::onClosePopup()
|
||||||
m_openOnHot = false;
|
m_openOnHot = false;
|
||||||
m_hotTool = NULL;
|
m_hotTool = NULL;
|
||||||
m_hotIndex = NoneIndex;
|
m_hotIndex = NoneIndex;
|
||||||
m_currentStrip = NULL;
|
m_currentStrip = nullptr;
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2025 Igara Studio S.A.
|
// Copyright (C) 2021-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "app/tools/active_tool_observer.h"
|
#include "app/tools/active_tool_observer.h"
|
||||||
|
#include "app/ui/dockable.h"
|
||||||
#include "app/ui/skin/skin_part.h"
|
#include "app/ui/skin/skin_part.h"
|
||||||
#include "gfx/point.h"
|
#include "gfx/point.h"
|
||||||
#include "obs/connection.h"
|
#include "obs/connection.h"
|
||||||
|
|
@ -17,6 +18,7 @@
|
||||||
#include "ui/widget.h"
|
#include "ui/widget.h"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
class CloseEvent;
|
class CloseEvent;
|
||||||
|
|
@ -32,6 +34,7 @@ class ToolGroup;
|
||||||
|
|
||||||
// Class to show selected tools for each tool (vertically)
|
// Class to show selected tools for each tool (vertically)
|
||||||
class ToolBar : public ui::Widget,
|
class ToolBar : public ui::Widget,
|
||||||
|
public Dockable,
|
||||||
public tools::ActiveToolObserver {
|
public tools::ActiveToolObserver {
|
||||||
static ToolBar* m_instance;
|
static ToolBar* m_instance;
|
||||||
|
|
||||||
|
|
@ -52,6 +55,14 @@ public:
|
||||||
void openTipWindow(tools::ToolGroup* toolGroup, tools::Tool* tool);
|
void openTipWindow(tools::ToolGroup* toolGroup, tools::Tool* tool);
|
||||||
void closeTipWindow();
|
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:
|
protected:
|
||||||
bool onProcessMessage(ui::Message* msg) override;
|
bool onProcessMessage(ui::Message* msg) override;
|
||||||
void onSizeHint(ui::SizeHintEvent& ev) override;
|
void onSizeHint(ui::SizeHintEvent& ev) override;
|
||||||
|
|
@ -62,6 +73,7 @@ protected:
|
||||||
private:
|
private:
|
||||||
enum class GroupType { Regular, Overflow };
|
enum class GroupType { Regular, Overflow };
|
||||||
|
|
||||||
|
bool isDockedAtLeftSide() const;
|
||||||
int getToolGroupIndex(tools::ToolGroup* group);
|
int getToolGroupIndex(tools::ToolGroup* group);
|
||||||
void openPopupWindow(GroupType group_type,
|
void openPopupWindow(GroupType group_type,
|
||||||
int group_index = 0,
|
int group_index = 0,
|
||||||
|
|
@ -95,12 +107,12 @@ private:
|
||||||
bool m_openedRecently;
|
bool m_openedRecently;
|
||||||
|
|
||||||
// Window displayed to show a tool-group
|
// Window displayed to show a tool-group
|
||||||
ui::PopupWindow* m_popupWindow;
|
std::unique_ptr<ui::PopupWindow> m_popupWindow;
|
||||||
class ToolStrip;
|
class ToolStrip;
|
||||||
ToolStrip* m_currentStrip;
|
ToolStrip* m_currentStrip = nullptr;
|
||||||
|
|
||||||
// Tool-tip window
|
// Tool-tip window
|
||||||
ui::TipWindow* m_tipWindow;
|
std::unique_ptr<ui::TipWindow> m_tipWindow;
|
||||||
|
|
||||||
ui::Timer m_tipTimer;
|
ui::Timer m_tipTimer;
|
||||||
bool m_tipOpened;
|
bool m_tipOpened;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ namespace app {
|
||||||
class WorkspaceTabs;
|
class WorkspaceTabs;
|
||||||
|
|
||||||
class Workspace : public ui::Widget,
|
class Workspace : public ui::Widget,
|
||||||
public app::InputChainElement {
|
public app::InputChainElement,
|
||||||
|
public Dockable {
|
||||||
public:
|
public:
|
||||||
typedef WorkspaceViews::iterator iterator;
|
typedef WorkspaceViews::iterator iterator;
|
||||||
|
|
||||||
|
|
@ -75,6 +76,11 @@ public:
|
||||||
|
|
||||||
WorkspacePanel* mainPanel() { return &m_mainPanel; }
|
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()> BeforeViewChanged;
|
||||||
obs::signal<void()> ActiveViewChanged;
|
obs::signal<void()> ActiveViewChanged;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ public:
|
||||||
WorkspaceTabs(TabsDelegate* tabsDelegate);
|
WorkspaceTabs(TabsDelegate* tabsDelegate);
|
||||||
~WorkspaceTabs();
|
~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; }
|
WorkspacePanel* panel() const { return m_panel; }
|
||||||
void setPanel(WorkspacePanel* panel);
|
void setPanel(WorkspacePanel* panel);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ private:
|
||||||
|
|
||||||
} // anonymous namespace
|
} // 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);
|
ASSERT(blob != nullptr);
|
||||||
|
|
||||||
|
|
@ -74,32 +74,28 @@ gfx::Size get_text_blob_required_size(const text::TextBlobRef& blob)
|
||||||
bounds.w = 1;
|
bounds.w = 1;
|
||||||
if (bounds.h < 1)
|
if (bounds.h < 1)
|
||||||
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);
|
ASSERT(blob != nullptr);
|
||||||
|
|
||||||
os::Paint paint;
|
doc::ImageRef image(
|
||||||
// TODO offer Stroke, StrokeAndFill, and Fill styles
|
doc::Image::create(doc::IMAGE_RGB, std::ceil(textBounds.w), std::ceil(textBounds.h)));
|
||||||
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));
|
|
||||||
|
|
||||||
#ifdef LAF_SKIA
|
#ifdef LAF_SKIA
|
||||||
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(image.get());
|
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(image.get());
|
||||||
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
|
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
|
#endif // LAF_SKIA
|
||||||
|
|
||||||
return image;
|
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();
|
Fonts* fonts = Fonts::instance();
|
||||||
ASSERT(fonts);
|
ASSERT(fonts);
|
||||||
|
|
@ -113,10 +109,6 @@ doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, gfx
|
||||||
const text::FontMgrRef fontMgr = fonts->fontMgr();
|
const text::FontMgrRef fontMgr = fonts->fontMgr();
|
||||||
ASSERT(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
|
// 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
|
// fonts (e.g. if the given font is not enough to shape other code
|
||||||
// points/languages).
|
// points/languages).
|
||||||
|
|
|
||||||
|
|
@ -11,25 +11,28 @@
|
||||||
|
|
||||||
#include "doc/image_ref.h"
|
#include "doc/image_ref.h"
|
||||||
#include "gfx/color.h"
|
#include "gfx/color.h"
|
||||||
|
#include "gfx/rect.h"
|
||||||
#include "text/text_blob.h"
|
#include "text/text_blob.h"
|
||||||
|
#include "ui/paint.h"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
class Color;
|
|
||||||
class FontInfo;
|
class FontInfo;
|
||||||
namespace skin {
|
|
||||||
class SkinTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the exact bounds that are required to draw this TextBlob,
|
// Returns the exact bounds that are required to draw this TextBlob in
|
||||||
// i.e. the image size that will be required in render_text_blob().
|
// the origin point (0, 0), i.e. the image size that will be required
|
||||||
gfx::Size get_text_blob_required_size(const text::TextBlobRef& blob);
|
// 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
|
} // namespace app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// 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));
|
((ExprEntry*)widget)->setDecimals(strtol(decimals, nullptr, 10));
|
||||||
}
|
}
|
||||||
if (elem_name == "filename") {
|
if (elem_name == "filename") {
|
||||||
const char* button_only = elem->Attribute("button_only");
|
const bool buttononly = bool_attr(elem, "buttononly", false);
|
||||||
const app::FilenameField::Type type = ((button_only != nullptr &&
|
const app::FilenameField::Type type = (buttononly ? app::FilenameField::Type::ButtonOnly :
|
||||||
strtol(button_only, nullptr, 10) == 1) ?
|
app::FilenameField::Type::EntryAndButton);
|
||||||
app::FilenameField::Type::ButtonOnly :
|
|
||||||
app::FilenameField::Type::EntryAndButton);
|
|
||||||
|
|
||||||
widget = new app::FilenameField(type, "");
|
widget = new app::FilenameField(type, "");
|
||||||
}
|
}
|
||||||
|
|
@ -534,7 +532,7 @@ Widget* WidgetLoader::convertXmlElementToWidget(const XMLElement* elem,
|
||||||
}
|
}
|
||||||
else if (elem_name == "font") {
|
else if (elem_name == "font") {
|
||||||
if (!widget)
|
if (!widget)
|
||||||
widget = new FontEntry;
|
widget = new FontEntry(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Was the widget created?
|
// Was the widget created?
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite Document Library
|
// 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
|
// Copyright (C) 2001-2016 David Capello
|
||||||
//
|
//
|
||||||
// This file is released under the terms of the MIT license.
|
// This file is released under the terms of the MIT license.
|
||||||
|
|
@ -20,11 +20,12 @@
|
||||||
#include "doc/primitives.h"
|
#include "doc/primitives.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
namespace doc {
|
namespace doc {
|
||||||
|
|
||||||
static int generation = 0;
|
static std::atomic<int> g_generation = 0;
|
||||||
|
|
||||||
Brush::Brush()
|
Brush::Brush()
|
||||||
{
|
{
|
||||||
|
|
@ -300,7 +301,7 @@ void Brush::setCenter(const gfx::Point& center)
|
||||||
// Cleans the brush's data (image and region).
|
// Cleans the brush's data (image and region).
|
||||||
void Brush::clean()
|
void Brush::clean()
|
||||||
{
|
{
|
||||||
m_gen = ++generation;
|
m_gen = ++g_generation;
|
||||||
m_image.reset();
|
m_image.reset();
|
||||||
m_maskBitmap.reset();
|
m_maskBitmap.reset();
|
||||||
m_backupImage.reset();
|
m_backupImage.reset();
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -106,7 +106,6 @@ void Box::onResize(ResizeEvent& ev)
|
||||||
continue; \
|
continue; \
|
||||||
\
|
\
|
||||||
int size = 0; \
|
int size = 0; \
|
||||||
int sizeDiff = 0; \
|
|
||||||
\
|
\
|
||||||
if (align() & HOMOGENEOUS) { \
|
if (align() & HOMOGENEOUS) { \
|
||||||
if (i < visibleChildren - 1) \
|
if (i < visibleChildren - 1) \
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite UI Library
|
// 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
|
// Copyright (C) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This file is released under the terms of the MIT license.
|
// 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 begin() { return m_items.begin(); }
|
||||||
Items::iterator end() { return m_items.end(); }
|
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 setEditable(bool state);
|
||||||
void setClickOpen(bool state);
|
void setClickOpen(bool state);
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,15 @@ int Entry::lastCaretPos() const
|
||||||
return int(m_boxes.size() - 1);
|
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)
|
void Entry::setCaretPos(const int pos)
|
||||||
{
|
{
|
||||||
gfx::Size caretSize = theme()->getEntryCaretSize(this);
|
gfx::Size caretSize = theme()->getEntryCaretSize(this);
|
||||||
|
|
@ -160,6 +169,8 @@ void Entry::setCaretPos(const int pos)
|
||||||
startTimer();
|
startTimer();
|
||||||
m_state = true;
|
m_state = true;
|
||||||
|
|
||||||
|
os::System::instance()->setTextInput(true, caretPosOnScreen());
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,7 +262,7 @@ gfx::Rect Entry::getEntryTextBounds() const
|
||||||
return onGetEntryTextBounds();
|
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()));
|
ASSERT(i >= 0 && i < int(m_boxes.size()));
|
||||||
if (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
|
// Start processing dead keys
|
||||||
if (m_translate_dead_keys)
|
if (m_translate_dead_keys) {
|
||||||
os::System::instance()->setTranslateDeadKeys(true);
|
os::System::instance()->setTextInput(true, caretPosOnScreen());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case kFocusLeaveMessage:
|
case kFocusLeaveMessage:
|
||||||
|
|
@ -304,7 +316,7 @@ bool Entry::onProcessMessage(Message* msg)
|
||||||
|
|
||||||
// Stop processing dead keys
|
// Stop processing dead keys
|
||||||
if (m_translate_dead_keys)
|
if (m_translate_dead_keys)
|
||||||
os::System::instance()->setTranslateDeadKeys(false);
|
os::System::instance()->setTextInput(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case kKeyDownMessage:
|
case kKeyDownMessage:
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ public:
|
||||||
|
|
||||||
int caretPos() const { return m_caret; }
|
int caretPos() const { return m_caret; }
|
||||||
int lastCaretPos() const;
|
int lastCaretPos() const;
|
||||||
|
gfx::Point caretPosOnScreen() const;
|
||||||
|
|
||||||
void setCaretPos(int pos);
|
void setCaretPos(int pos);
|
||||||
void setCaretToEnd();
|
void setCaretToEnd();
|
||||||
|
|
@ -76,7 +77,7 @@ public:
|
||||||
obs::signal<void()> Change;
|
obs::signal<void()> Change;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
gfx::Rect getCharBoxBounds(int i);
|
gfx::Rect getCharBoxBounds(int i) const;
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
bool onProcessMessage(Message* msg) override;
|
bool onProcessMessage(Message* msg) override;
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,8 @@ bool IntEntry::onProcessMessage(Message* msg)
|
||||||
case kKeyDownMessage:
|
case kKeyDownMessage:
|
||||||
if (hasFocus() && !isReadOnly()) {
|
if (hasFocus() && !isReadOnly()) {
|
||||||
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
|
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
|
||||||
int chr = keymsg->unicodeChar();
|
const int chr = keymsg->unicodeChar();
|
||||||
if (chr >= 32 && (chr < '0' || chr > '9')) {
|
if (chr >= 32 && !onAcceptUnicodeChar(chr)) {
|
||||||
// "Eat" all keys that aren't number
|
// "Eat" all keys that aren't number
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -166,6 +166,11 @@ void IntEntry::onValueChange()
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IntEntry::onAcceptUnicodeChar(const int unicodeChar)
|
||||||
|
{
|
||||||
|
return (unicodeChar >= '0' && unicodeChar <= '9');
|
||||||
|
}
|
||||||
|
|
||||||
void IntEntry::openPopup()
|
void IntEntry::openPopup()
|
||||||
{
|
{
|
||||||
m_slider->setValue(getValue());
|
m_slider->setValue(getValue());
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite UI Library
|
// Aseprite UI Library
|
||||||
// Copyright (C) 2022 Igara Studio S.A.
|
// Copyright (C) 2022-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This file is released under the terms of the MIT license.
|
// This file is released under the terms of the MIT license.
|
||||||
|
|
@ -36,6 +36,7 @@ protected:
|
||||||
|
|
||||||
// New events
|
// New events
|
||||||
virtual void onValueChange();
|
virtual void onValueChange();
|
||||||
|
virtual bool onAcceptUnicodeChar(int unicodeChar);
|
||||||
|
|
||||||
int m_min;
|
int m_min;
|
||||||
int m_max;
|
int m_max;
|
||||||
|
|
|
||||||
|
|
@ -218,16 +218,16 @@ bool TipWindow::pointAt(int arrowAlign, const gfx::Rect& target, const ui::Displ
|
||||||
if (get_multiple_displays()) {
|
if (get_multiple_displays()) {
|
||||||
const gfx::Rect waBounds = nativeParentWindow->screen()->workarea();
|
const gfx::Rect waBounds = nativeParentWindow->screen()->workarea();
|
||||||
gfx::Point pt = nativeParentWindow->pointToScreen(gfx::Point(x, y));
|
gfx::Point pt = nativeParentWindow->pointToScreen(gfx::Point(x, y));
|
||||||
pt.x = std::clamp(pt.x, waBounds.x, waBounds.x2() - w);
|
pt.x = std::clamp(pt.x, waBounds.x, std::max(waBounds.x, waBounds.x2() - w));
|
||||||
pt.y = std::clamp(pt.y, waBounds.y, waBounds.y2() - h);
|
pt.y = std::clamp(pt.y, waBounds.y, std::max(waBounds.y, waBounds.y2() - h));
|
||||||
pt = nativeParentWindow->pointFromScreen(pt);
|
pt = nativeParentWindow->pointFromScreen(pt);
|
||||||
x = pt.x;
|
x = pt.x;
|
||||||
y = pt.y;
|
y = pt.y;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const gfx::Rect displayBounds = display->bounds();
|
const gfx::Rect displayBounds = display->bounds();
|
||||||
x = std::clamp(x, displayBounds.x, displayBounds.x2() - w);
|
x = std::clamp(x, displayBounds.x, std::max(displayBounds.x, displayBounds.x2() - w));
|
||||||
y = std::clamp(y, displayBounds.y, displayBounds.y2() - h);
|
y = std::clamp(y, displayBounds.y, std::max(displayBounds.y, displayBounds.y2() - h));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_target.intersects(gfx::Rect(x, y, w, h))) {
|
if (m_target.intersects(gfx::Rect(x, y, w, h))) {
|
||||||
|
|
|
||||||
|
|
@ -228,3 +228,69 @@ do
|
||||||
c = app.open(fn)
|
c = app.open(fn)
|
||||||
assert(c.tileManagementPlugin == nil)
|
assert(c.tileManagementPlugin == nil)
|
||||||
end
|
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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue