diff --git a/CHANGELOG.md b/CHANGELOG.md index 32a0a82759..ca3f1e4b68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Thanks to the following contributors who worked on this release: - Adjusted the layout of the layers panel to avoid issues with overlay scrollbars preventing the visibility toggle from being clicked (#1828, #2021) - Gradient types and colors can now be changed before finalizing the gradient (#2058, #2059) - Moved the Layers menu out of the main menu. The actions are available from a menu button in the Layers panel, or from right-clicking on layers (#1386, #2056) +- Dropdowns in the toolbar now show the icons of each option and highlight the currently selected item when opened (#1977, #2092) - Adjusted layout of toolbar options in the shape tools to improve usability (#2012, #2019, #2039, #2107) - Added new icons for several effects and menu buttons (#2102) diff --git a/Pinta.Core/Widgets/ToolBarDropDownButton.cs b/Pinta.Core/Widgets/ToolBarDropDownButton.cs index 7ac6211c05..8a729b28f6 100644 --- a/Pinta.Core/Widgets/ToolBarDropDownButton.cs +++ b/Pinta.Core/Widgets/ToolBarDropDownButton.cs @@ -5,31 +5,76 @@ namespace Pinta.Core; -public sealed class ToolBarDropDownButton : Gtk.MenuButton +public sealed class ToolBarDropDownButton : Gtk.DropDown { - private const string ACTION_PREFIX = "tool"; - private readonly bool show_label; - private readonly Gio.Menu dropdown; - private readonly Gio.SimpleActionGroup action_group; - private ToolBarItem? selected_item; + + private Gtk.Box selected_box; + private Gtk.Image dropdown_icon; + private Gtk.Label dropdown_label; + + // We store the index of the previous selection to avoid having to iterate through all items on the list. + private int previous_index = 0; + private Gtk.StringList string_list; private readonly List items; + private readonly List toolbar_item_widgets; public ReadOnlyCollection Items { get; } public ToolBarDropDownButton (bool showLabel = false) { - show_label = showLabel; + // We create the widgets inside the dropdown to avoid having to create yet another custom widget + // for the selectedFactory. Also, we can reference them directly when updated, avoiding + // .nextSibling hacks. + selected_box = new (); + dropdown_icon = new (); + dropdown_label = new (); + selected_box.Append (dropdown_icon); + selected_box.Append (dropdown_label); items = []; - Items = new ReadOnlyCollection (items); - AlwaysShowArrow = true; + Items = new (items); + toolbar_item_widgets = []; + show_label = showLabel; + + string_list = new (); + SetModel (string_list); + + Gtk.SignalListItemFactory selectedFactory = new (); + selectedFactory.OnSetup += OnSetupSelectedItem; + selectedFactory.OnBind += OnBindSelectedItem; + SetFactory (selectedFactory); + + Gtk.SignalListItemFactory listFactory = new (); + listFactory.OnBind += OnBindListItem; + SetListFactory (listFactory); + } + + private void OnSetupSelectedItem (Gtk.SignalListItemFactory factory, Gtk.SignalListItemFactory.SetupSignalArgs args) + { + Gtk.ListItem item = (Gtk.ListItem) args.Object; + item.SetChild (selected_box); + } + + private void OnBindSelectedItem (Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args) + { + ToolBarItem toolbar_item = items[(int) Selected]; + + dropdown_icon.SetFromIconName (toolbar_item.ImageId); + if (show_label) { dropdown_label.SetText (toolbar_item.Text); } - dropdown = Gio.Menu.New (); - MenuModel = dropdown; + // SetSelectedIndex checks if the index changed, so we don't need to check here again. This check + // is important because OnBindSelectedItem gets called both when the selected item changes and on + // widget initialization/setup. + SetSelectedIndex ((int) Selected); + } + + private void OnBindListItem (Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args) + { + Gtk.ListItem item = (Gtk.ListItem) args.Object; - action_group = Gio.SimpleActionGroup.New (); - InsertActionGroup (ACTION_PREFIX, action_group); + ToolBarItemWidget toolbar_item = toolbar_item_widgets[(int) item.Position]; + item.SetChild (toolbar_item); } public ToolBarItem AddItem (string text, string imageId) @@ -39,53 +84,46 @@ public ToolBarItem AddItem (string text, string imageId) public ToolBarItem AddItem (string text, string imageId, object? tag) { - ToolBarItem item = new ToolBarItem (text, imageId, tag); - action_group.AddAction (item.Action); - dropdown.AppendItem (Gio.MenuItem.New (text, $"{ACTION_PREFIX}.{item.Action.Name}")); - + ToolBarItemWidget widget = new (text, imageId); + toolbar_item_widgets.Add (widget); + // We append an empty string because we only need the list's index. + // Otherwise, we'd need to make ToolBarItem inherit from GObject, which is undesired. + // Also, we'd need the indexes anyway to update the previous selection, so + // storing anything else is not required. + string_list.Append (""); + + ToolBarItem item = new (text, imageId, tag); + // This is done to ensure the first item has a checkmark if it was selected. + if (items.Count == 0) { widget.SetCheckmarkVisible (true); } items.Add (item); - item.Action.OnActivate += delegate { SetSelectedItem (item); }; - - if (selected_item == null) - SetSelectedItem (item); return item; } - public ToolBarItem SelectedItem { - get => - selected_item is not null - ? selected_item - : throw new InvalidOperationException ("Attempted to get SelectedItem from a drop down with no items."); - set { - if (selected_item != value) - SetSelectedItem (value); - } + public new ToolBarItem SelectedItem { + get => items.Count == 0 + ? throw new InvalidOperationException ("Attempted to get SelectedItem from a drop down with no items.") + : items[previous_index]; + set { SetSelectedIndex (items.IndexOf (value)); } } public int SelectedIndex { - get => selected_item is null ? -1 : items.IndexOf (selected_item); - set { - if (value < 0 || value >= items.Count) - return; - - var item = items[value]; - - if (item != selected_item) - SetSelectedItem (item); - } + get => items.Count == 0 ? -1 : previous_index; + set { SetSelectedIndex (value); } } - private void SetSelectedItem (ToolBarItem item) + private void SetSelectedIndex (int index) { - IconName = item.ImageId; - - selected_item = item; - TooltipText = item.Text; + if (index < 0 || index >= items.Count || index == previous_index) { + return; + } - if (show_label) - Label = item.Text; + toolbar_item_widgets[previous_index].SetCheckmarkVisible (false); + toolbar_item_widgets[index].SetCheckmarkVisible (true); + TooltipText = items[index].Text; + Selected = (uint) index; + previous_index = index; OnSelectedItemChanged (); } @@ -103,22 +141,44 @@ public ToolBarItem (string text, string imageId) : this (text, imageId, null) { public ToolBarItem (string text, string imageId, object? tag) { - var actionName = AdjustName (text); - Text = text; ImageId = imageId; - Action = Gio.SimpleAction.New (actionName, null); Tag = tag; } - private static string AdjustName (string baseName) - => string.Concat (baseName.Where (c => !char.IsWhiteSpace (c))); - public string ImageId { get; } public object? Tag { get; } public string Text { get; } - public Gio.SimpleAction Action { get; } public T GetTagOrDefault (T defaultValue) => Tag is T value ? value : defaultValue; } + +public sealed class ToolBarItemWidget : Gtk.Box +{ + public ToolBarItemWidget (string text, string imageId) + { + Gtk.Image image = new (); + image.SetFromIconName (imageId); + Gtk.Label label = new (); + label.SetText (text); + + Append (image); + Append (label); + + selected_icon = new (); + selected_icon.SetFromIconName (Resources.StandardIcons.ObjectSelect); + selected_icon.Visible = false; + selected_icon.Hexpand = true; + selected_icon.Halign = Gtk.Align.End; + + Append (selected_icon); + } + + public void SetCheckmarkVisible (bool visible) + { + selected_icon.Visible = visible; + } + + private Gtk.Image selected_icon; +} diff --git a/Pinta.Resources/Icons.cs b/Pinta.Resources/Icons.cs index 18d47f74a7..86772f8cbf 100644 --- a/Pinta.Resources/Icons.cs +++ b/Pinta.Resources/Icons.cs @@ -65,6 +65,7 @@ public static class StandardIcons public const string LayerMoveDown = "pan-down-symbolic"; public const string OpenMenu = "open-menu-symbolic"; + public const string ObjectSelect = "object-select-symbolic"; public const string ApplicationAddon = "application-x-addon-symbolic"; public const string SystemSearch = "system-search-symbolic";