Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
05dfbe8
Starts the change of ToolBarDropDownButton to Gtk.Dropdown
pedropaulosuzuki Apr 3, 2026
8458a05
Fix whitespace
pedropaulosuzuki Apr 3, 2026
8816bef
Fix selected property of ListItem and move widgets to ToolBarItem
pedropaulosuzuki Apr 3, 2026
6cebe2e
I'll never get used to { on another line
pedropaulosuzuki Apr 3, 2026
d9379b2
whitespace
pedropaulosuzuki Apr 3, 2026
72a32a6
Remove console.log
pedropaulosuzuki Apr 3, 2026
2d5ace6
Only change SelectedIndex if current_index is different from previous…
pedropaulosuzuki Apr 4, 2026
fa66ab2
ToolBarDropDownButton - Explicitly override base property SelectedIte…
pedropaulosuzuki Apr 4, 2026
8496c3a
Fix check icon not being visible when selection is zero
pedropaulosuzuki Apr 4, 2026
65f25f7
Fix typo
pedropaulosuzuki Apr 4, 2026
6dba0de
Reduce verbosity with syntactic sugar (omitting type on new)
pedropaulosuzuki Apr 4, 2026
882e504
Turns ToolBarItem into a Gtk.Box
pedropaulosuzuki Apr 7, 2026
f8f684e
Remove private Image and Label properties from ToolBarItem
pedropaulosuzuki Apr 7, 2026
7f475e3
Use Gio.ListStore instead of Gtk.StringList
pedropaulosuzuki Apr 8, 2026
dbdaf0c
Update CHANGELOG
pedropaulosuzuki Apr 8, 2026
f572ab5
Merge branch 'PintaProject:master' into dropdown
pedropaulosuzuki Apr 8, 2026
00dd84a
ToolBarDropDownButton: Separate ToolBarItem from ToolBarItemWidget
pedropaulosuzuki Apr 10, 2026
ab99865
Merge branch 'master' into dropdown
pedropaulosuzuki Apr 12, 2026
16e078c
Merge branch 'PintaProject:master' into dropdown
pedropaulosuzuki Apr 13, 2026
d3fccf7
Use snake_case for private fields
pedropaulosuzuki Apr 13, 2026
e5b0760
Adds ObjectSelect icon
pedropaulosuzuki Apr 13, 2026
363a2f6
Fix typo
pedropaulosuzuki Apr 13, 2026
f4de0c3
Move ObjectSelect icon string to Resources.StandardIcons
pedropaulosuzuki Apr 13, 2026
f171717
Merge branch 'PintaProject:master' into dropdown
pedropaulosuzuki Apr 14, 2026
41e4ff8
Merge branch 'PintaProject:master' into dropdown
pedropaulosuzuki Apr 14, 2026
28ab4d9
Add comments for ToolBarDropDown implementation details
pedropaulosuzuki Apr 16, 2026
e9b4cbc
Revert to using Gtk.StringList instead of Gio.ListStore
pedropaulosuzuki Apr 16, 2026
e6494b8
Fix typo
pedropaulosuzuki Apr 16, 2026
04acad7
Merge branch 'PintaProject:master' into dropdown
pedropaulosuzuki Apr 18, 2026
f703636
Remove private selected_item field and organize selection update better
pedropaulosuzuki Apr 21, 2026
0c7a8fe
FIx Whitespace
pedropaulosuzuki Apr 21, 2026
8e7bae9
Fix whitespace 2
pedropaulosuzuki Apr 21, 2026
34791d3
Rename SetSelectedIconVisible to SetCheckmarkVisible
pedropaulosuzuki Apr 21, 2026
81aefff
Remove redundante previous_index check
pedropaulosuzuki Apr 21, 2026
d721ec8
Fix range comparison
pedropaulosuzuki Apr 22, 2026
4b24aac
Remove unnecessary null check
cameronwhite Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
168 changes: 114 additions & 54 deletions Pinta.Core/Widgets/ToolBarDropDownButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ToolBarItem> items;
private readonly List<ToolBarItemWidget> toolbar_item_widgets;
public ReadOnlyCollection<ToolBarItem> 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<ToolBarItem> (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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm uneasy about doing non-idiomatic GTK things here like reusing a separate widget.
Could this just create a ToolBarItemWidget? It's basically the same as the list widgets, except you'd need the option for whether to show the label.
A method like ToolBarItemWidget.Bind(toolbarItem, showLabel, showSelected) could work

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is, it is not a ToolbarItemWidget. It is another widget, where there is no "selected icon", there is, in some cases, no label, and there is only an icon. If we hide those items in one place, they will be hidden everywhere, so we cannot reuse them. So we would need to create another widget just for that, which is not worth the code complexity. It's all local anyway, so this wouldn't be exposed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we hide those items in one place, they will be hidden everywhere

I'm not really understanding what you mean here?
My suggestion was that you'd create a separate instance of a ToolbarItemWidget in OnSetupSelectedItem (different than the ones created in OnSetupListItem). So that widget instance can have its options configured appropriately for showing the checkmark / label, and it's a bit more organized than maintaining references to several separate widgets

Copy link
Copy Markdown
Contributor Author

@pedropaulosuzuki pedropaulosuzuki Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we hide those items in one place, they will be hidden everywhere

I'm not really understanding what you mean here? My suggestion was that you'd create a separate instance of a ToolbarItemWidget in OnSetupSelectedItem (different than the ones created in OnSetupListItem). So that widget instance can have its options configured appropriately for showing the checkmark / label, and it's a bit more organized than maintaining references to several separate widgets

Maybe, but the ToolBarItemWidget has immutable text/icon, we'd need to make it mutable (is it possible, but could conflict with the ToolBarItemWidgets, since the SelectedItem is a single mutable widget and the rest are a bunch of immutable ones). That's why either creating a specific widget for that or doing the widgets as private fields might be a better option.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any objections to making the widget mutable - that's actually the intended approach for GTK list views. (e.g. if you have a long list where not all items are visible, the list view might only create the number of widgets required for visible items, and then as you scroll it's just rebinding those widgets to different items in the model - this is the reason for the separate "setup" and "bind" methods in the factory.

But I don't feel too strongly either way since it's conceptually a different type of widget from the list items - it just happens to be similar enough that you could make use of it.

Copy link
Copy Markdown
Contributor Author

@pedropaulosuzuki pedropaulosuzuki Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any objections to making the widget mutable - that's actually the intended approach for GTK list views. (e.g. if you have a long list where not all items are visible, the list view might only create the number of widgets required for visible items, and then as you scroll it's just rebinding those widgets to different items in the model - this is the reason for the separate "setup" and "bind" methods in the factory.

I think the idea of having the SelectedItem as a model is for added flexibility, I can think of specific cases where you want to show a different widget depending on which item is selected, maybe with additional buttons and actions for some fancy programs. However, for the general case, you probably want an immutable widget where you just use methods to change the text or an icon. Or, as Alice said, most people don't even go there and just avoid custom factories and use the dropdown as-is. You would never create/show more than one of those widgets at the same time tho, so it's a bit disjoint from the usual ListView logic.

But I don't feel too strongly either way since it's conceptually a different type of widget from the list items - it just happens to be similar enough that you could make use of it.

Yeah, it's one of those cases of "we can, but maybe we shouldn't". For example, we would never want a checked item in the SelectedItem, so it would be always there, hidden in the widget tree, taking up memory for something that will never come true. Not that it matters that much, but I think those widgets have different expectations.

}

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)
Expand All @@ -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 ();
}

Expand All @@ -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> (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;
}
1 change: 1 addition & 0 deletions Pinta.Resources/Icons.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading