From 75259bce0ed00d30e78890e3073374f5f81d5dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 8 Apr 2025 10:32:53 -0700 Subject: [PATCH 01/17] ListItem: add menu_model property --- demo/Views/ListsView.vala | 26 +++++++++++++++- lib/Widgets/ListItem.vala | 63 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/demo/Views/ListsView.vala b/demo/Views/ListsView.vala index 31ef554cc..025f433a7 100644 --- a/demo/Views/ListsView.vala +++ b/demo/Views/ListsView.vala @@ -34,6 +34,28 @@ public class ListsView : DemoPage { secondary_text = "ScrolledWindow with \"has-frame = true\" has a view level background color" }; + var reply_menuitem = new GLib.MenuItem ("Reply", null); + reply_menuitem.set_attribute_value ("verb-icon", "mail-reply-sender-symbolic"); + + var reply_all_menuitem = new GLib.MenuItem ("Reply All", null); + reply_all_menuitem.set_attribute_value ("verb-icon", "mail-reply-all-symbolic"); + + var forward_menuitem = new GLib.MenuItem ("Forward", null); + forward_menuitem.set_attribute_value ("verb-icon", "mail-forward-symbolic"); + + var button_menu = new GLib.Menu (); + button_menu.append_item (reply_menuitem); + button_menu.append_item (reply_all_menuitem); + button_menu.append_item (forward_menuitem); + + var button_section = new GLib.MenuItem.section (null, button_menu); + button_section.set_attribute_value ("display-hint", "circular-buttons"); + + var menu_model = new GLib.Menu (); + menu_model.append_item (button_section); + menu_model.append ("Move", null); + menu_model.append ("Delete", null); + var liststore = new GLib.ListStore (typeof (ListObject)); liststore.append (new ListObject () { text = "Row 1" @@ -56,7 +78,9 @@ public class ListsView : DemoPage { var factory = new Gtk.SignalListItemFactory (); factory.setup.connect ((obj) => { var list_item = (Gtk.ListItem) obj; - list_item.child = new Granite.ListItem (); + list_item.child = new Granite.ListItem () { + menu_model = menu_model + }; }); factory.bind.connect ((obj) => { diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index 7dcdab2f6..d032c50f8 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -20,6 +20,16 @@ public class Granite.ListItem : Granite.Bin { */ public string? description { get; set; } + /** + * Context menu model + * When a menu is shown with secondary click or long press will be constructed from the provided menu model + */ + public GLib.Menu? menu_model { get; set; } + + private Gtk.GestureClick? secondary_click_controller; + private Gtk.GestureLongPress? long_press_controller; + private Gtk.PopoverMenu? context_menu; + class construct { set_css_name ("granite-listitem"); } @@ -58,5 +68,58 @@ public class Granite.ListItem : Granite.Bin { text_box.append (description_label); } }); + + notify["menu-model"].connect (construct_menu); + } + + private void construct_menu () { + if (menu_model == null) { + remove_controller (secondary_click_controller); + remove_controller (long_press_controller); + + secondary_click_controller = null; + long_press_controller = null; + + context_menu.unparent (); + context_menu = null; + + return; + } + + if (context_menu != null) { + context_menu.menu_model = menu_model; + return; + } + + context_menu = new Gtk.PopoverMenu.from_model (menu_model) { + halign = START, + has_arrow = false, + position = BOTTOM + }; + context_menu.set_parent (this); + + secondary_click_controller = new Gtk.GestureClick () { + button = Gdk.BUTTON_SECONDARY + }; + secondary_click_controller.pressed.connect ((n_press, x, y) => { + menu_popup_at_pointer (context_menu, x, y); + }); + + long_press_controller = new Gtk.GestureLongPress (); + long_press_controller.pressed.connect ((x, y) => { + menu_popup_at_pointer (context_menu, x, y); + }); + + add_controller (secondary_click_controller); + add_controller (long_press_controller); + } + + private void menu_popup_at_pointer (Gtk.PopoverMenu popover, double x, double y) { + var rect = Gdk.Rectangle () { + x = (int) x, + y = (int) y + }; + popover.pointing_to = rect; + popover.popup (); } } From 3fb0cc90a49d12a03619e942fe3b9c1de12038ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Sat, 12 Apr 2025 10:21:38 -0700 Subject: [PATCH 02/17] Use event.triggers_context_menu --- lib/Widgets/ListItem.vala | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index d032c50f8..5b87fbdf0 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -26,7 +26,7 @@ public class Granite.ListItem : Granite.Bin { */ public GLib.Menu? menu_model { get; set; } - private Gtk.GestureClick? secondary_click_controller; + private Gtk.GestureClick? context_menu_controller; private Gtk.GestureLongPress? long_press_controller; private Gtk.PopoverMenu? context_menu; @@ -74,10 +74,10 @@ public class Granite.ListItem : Granite.Bin { private void construct_menu () { if (menu_model == null) { - remove_controller (secondary_click_controller); + remove_controller (context_menu_controller); remove_controller (long_press_controller); - secondary_click_controller = null; + context_menu_controller = null; long_press_controller = null; context_menu.unparent (); @@ -98,11 +98,20 @@ public class Granite.ListItem : Granite.Bin { }; context_menu.set_parent (this); - secondary_click_controller = new Gtk.GestureClick () { - button = Gdk.BUTTON_SECONDARY + context_menu_controller = new Gtk.GestureClick () { + button = 0, + exclusive = true }; - secondary_click_controller.pressed.connect ((n_press, x, y) => { - menu_popup_at_pointer (context_menu, x, y); + context_menu_controller.pressed.connect ((n_press, x, y) => { + var sequence = context_menu_controller.get_current_sequence (); + var event = context_menu_controller.get_last_event (sequence); + + if (event.triggers_context_menu ()) { + menu_popup_at_pointer (context_menu, x, y); + + context_menu_controller.set_state (CLAIMED); + context_menu_controller.reset (); + } }); long_press_controller = new Gtk.GestureLongPress (); @@ -110,7 +119,7 @@ public class Granite.ListItem : Granite.Bin { menu_popup_at_pointer (context_menu, x, y); }); - add_controller (secondary_click_controller); + add_controller (context_menu_controller); add_controller (long_press_controller); } From 142569a5e53277c94983c23ba4b2a4f847d79a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Sat, 12 Apr 2025 11:00:29 -0700 Subject: [PATCH 03/17] Add keypress handling --- lib/Widgets/ListItem.vala | 40 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index 5b87fbdf0..1f1c95be6 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -28,6 +28,7 @@ public class Granite.ListItem : Granite.Bin { private Gtk.GestureClick? context_menu_controller; private Gtk.GestureLongPress? long_press_controller; + private Gtk.EventControllerKey menu_key_controller; private Gtk.PopoverMenu? context_menu; class construct { @@ -76,9 +77,11 @@ public class Granite.ListItem : Granite.Bin { if (menu_model == null) { remove_controller (context_menu_controller); remove_controller (long_press_controller); + parent.remove_controller (menu_key_controller); context_menu_controller = null; long_press_controller = null; + menu_key_controller = null; context_menu.unparent (); context_menu = null; @@ -92,7 +95,6 @@ public class Granite.ListItem : Granite.Bin { } context_menu = new Gtk.PopoverMenu.from_model (menu_model) { - halign = START, has_arrow = false, position = BOTTOM }; @@ -107,6 +109,7 @@ public class Granite.ListItem : Granite.Bin { var event = context_menu_controller.get_last_event (sequence); if (event.triggers_context_menu ()) { + context_menu.halign = START; menu_popup_at_pointer (context_menu, x, y); context_menu_controller.set_state (CLAIMED); @@ -116,11 +119,46 @@ public class Granite.ListItem : Granite.Bin { long_press_controller = new Gtk.GestureLongPress (); long_press_controller.pressed.connect ((x, y) => { + // Don't want menu under your hand + context_menu.halign = END; menu_popup_at_pointer (context_menu, x, y); }); + menu_key_controller = new Gtk.EventControllerKey (); + menu_key_controller.key_released.connect ((keyval, keycode, state) => { + var mods = state & Gtk.accelerator_get_default_mod_mask (); + switch (keyval) { + case Gdk.Key.F10: + if (mods == Gdk.ModifierType.SHIFT_MASK) { + menu_popup_on_keypress (context_menu); + } + break; + case Gdk.Key.Menu: + case Gdk.Key.MenuKB: + menu_popup_on_keypress (context_menu); + break; + default: + return; + } + }); + add_controller (context_menu_controller); add_controller (long_press_controller); + + // We don't get key events on the child list item widget + if (parent != null) { + parent.add_controller (menu_key_controller); + } else { + notify["parent"].connect (() => { + parent.add_controller (menu_key_controller); + }); + } + } + + private void menu_popup_on_keypress (Gtk.PopoverMenu popover) { + popover.halign = END; + popover.set_pointing_to (null); + popover.popup (); } private void menu_popup_at_pointer (Gtk.PopoverMenu popover, double x, double y) { From d4c5a48dcb627462a0308f352c9c8615d48564d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Sat, 12 Apr 2025 11:04:29 -0700 Subject: [PATCH 04/17] Align menu based on event coords --- lib/Widgets/ListItem.vala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index 1f1c95be6..ffe864eeb 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -119,8 +119,13 @@ public class Granite.ListItem : Granite.Bin { long_press_controller = new Gtk.GestureLongPress (); long_press_controller.pressed.connect ((x, y) => { - // Don't want menu under your hand - context_menu.halign = END; + // Try to keep menu from under your hand + if (x > get_width () / 2) { + context_menu.halign = END; + } else { + context_menu.halign = START; + } + menu_popup_at_pointer (context_menu, x, y); }); From d54906689ea56cf23dc7b5f7463203ae2b386932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Sat, 12 Apr 2025 11:06:24 -0700 Subject: [PATCH 05/17] Get window width, not widget width --- lib/Widgets/ListItem.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index ffe864eeb..fa4c642a8 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -120,7 +120,7 @@ public class Granite.ListItem : Granite.Bin { long_press_controller = new Gtk.GestureLongPress (); long_press_controller.pressed.connect ((x, y) => { // Try to keep menu from under your hand - if (x > get_width () / 2) { + if (x > get_root ().get_width () / 2) { context_menu.halign = END; } else { context_menu.halign = START; From b028b2d8d83822fc1d6bb153c397e342434a2693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Sat, 12 Apr 2025 11:08:23 -0700 Subject: [PATCH 06/17] =?UTF-8?q?context=5Fmenu=5Fcontroller=20=E2=86=92?= =?UTF-8?q?=20click=5Fcontroller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/Widgets/ListItem.vala | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index fa4c642a8..81bfac884 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -26,7 +26,7 @@ public class Granite.ListItem : Granite.Bin { */ public GLib.Menu? menu_model { get; set; } - private Gtk.GestureClick? context_menu_controller; + private Gtk.GestureClick? click_controller; private Gtk.GestureLongPress? long_press_controller; private Gtk.EventControllerKey menu_key_controller; private Gtk.PopoverMenu? context_menu; @@ -75,11 +75,11 @@ public class Granite.ListItem : Granite.Bin { private void construct_menu () { if (menu_model == null) { - remove_controller (context_menu_controller); + remove_controller (click_controller); remove_controller (long_press_controller); parent.remove_controller (menu_key_controller); - context_menu_controller = null; + click_controller = null; long_press_controller = null; menu_key_controller = null; @@ -100,20 +100,20 @@ public class Granite.ListItem : Granite.Bin { }; context_menu.set_parent (this); - context_menu_controller = new Gtk.GestureClick () { + click_controller = new Gtk.GestureClick () { button = 0, exclusive = true }; - context_menu_controller.pressed.connect ((n_press, x, y) => { - var sequence = context_menu_controller.get_current_sequence (); - var event = context_menu_controller.get_last_event (sequence); + click_controller.pressed.connect ((n_press, x, y) => { + var sequence = click_controller.get_current_sequence (); + var event = click_controller.get_last_event (sequence); if (event.triggers_context_menu ()) { context_menu.halign = START; menu_popup_at_pointer (context_menu, x, y); - context_menu_controller.set_state (CLAIMED); - context_menu_controller.reset (); + click_controller.set_state (CLAIMED); + click_controller.reset (); } }); @@ -147,7 +147,7 @@ public class Granite.ListItem : Granite.Bin { } }); - add_controller (context_menu_controller); + add_controller (click_controller); add_controller (long_press_controller); // We don't get key events on the child list item widget From ebd83d5abb55ed6837c0677a97c8cd302e75d8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Sat, 12 Apr 2025 11:47:03 -0700 Subject: [PATCH 07/17] Move long press menu --- lib/Widgets/ListItem.vala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index 81bfac884..39d19a8f8 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -10,6 +10,9 @@ */ [Version (since = "7.7.0")] public class Granite.ListItem : Granite.Bin { + // https://www.w3.org/WAI/WCAG21/Understanding/target-size.html + private const int TOUCH_TARGET_WIDTH = 44; + /** * The main label for #this */ @@ -122,11 +125,13 @@ public class Granite.ListItem : Granite.Bin { // Try to keep menu from under your hand if (x > get_root ().get_width () / 2) { context_menu.halign = END; + x -= TOUCH_TARGET_WIDTH; } else { context_menu.halign = START; + x += TOUCH_TARGET_WIDTH; } - menu_popup_at_pointer (context_menu, x, y); + menu_popup_at_pointer (context_menu, x, y - (TOUCH_TARGET_WIDTH * 0.75)); }); menu_key_controller = new Gtk.EventControllerKey (); From fdf7d4eebb0ebc04c682e32575d0c3107ad8b437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Sat, 12 Apr 2025 11:56:05 -0700 Subject: [PATCH 08/17] Make keypress come from vcenter not bottom --- lib/Widgets/ListItem.vala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index 39d19a8f8..f0c6344af 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -167,7 +167,10 @@ public class Granite.ListItem : Granite.Bin { private void menu_popup_on_keypress (Gtk.PopoverMenu popover) { popover.halign = END; - popover.set_pointing_to (null); + popover.set_pointing_to (Gdk.Rectangle () { + x = (int) get_width (), + y = (int) get_height () / 2 + }); popover.popup (); } From 0c66a17c3dda7b2a5624458043450753a5e44fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Thu, 8 May 2025 09:37:09 -0700 Subject: [PATCH 09/17] Remove menu key controller if we unparent --- lib/Widgets/ListItem.vala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index f0c6344af..877337b7f 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -74,6 +74,12 @@ public class Granite.ListItem : Granite.Bin { }); notify["menu-model"].connect (construct_menu); + + notify["parent"].connect (() => { + if (parent == null && menu_model != null) { + parent.remove_controller (menu_key_controller); + } + }); } private void construct_menu () { From 63733a89192d536a030383c41a6c59cfb5a3dc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Thu, 8 May 2025 09:38:02 -0700 Subject: [PATCH 10/17] nevermind that doesn't make sense, do it on destruction --- lib/Widgets/ListItem.vala | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index 877337b7f..0fb510dd9 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -74,12 +74,10 @@ public class Granite.ListItem : Granite.Bin { }); notify["menu-model"].connect (construct_menu); + } - notify["parent"].connect (() => { - if (parent == null && menu_model != null) { - parent.remove_controller (menu_key_controller); - } - }); + ~ListItem { + parent.remove_controller (menu_key_controller); } private void construct_menu () { From 167d8e2aff5ef0280c715e4760613111c78c8dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 24 Jun 2025 13:13:06 -0700 Subject: [PATCH 11/17] Allow focusing for key events --- lib/Styles/Granite/ListItem.scss | 5 +++++ lib/Widgets/ListItem.vala | 18 ++++-------------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/Styles/Granite/ListItem.scss b/lib/Styles/Granite/ListItem.scss index ad6b24917..65d646eee 100644 --- a/lib/Styles/Granite/ListItem.scss +++ b/lib/Styles/Granite/ListItem.scss @@ -3,6 +3,11 @@ granite-listitem { min-height: rem(32px); //Try to force homogeneous row height .text-box { + border-radius: rem($window_radius / 2); padding: $button-spacing; } + + &:focus-visible .text-box { + background: rgba($fg_color, 0.1); + } } diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index 0fb510dd9..5b434d2e3 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -58,6 +58,8 @@ public class Granite.ListItem : Granite.Bin { text_box.append (label); text_box.add_css_class ("text-box"); + // So we can receive key events + focusable = true; child = text_box; bind_property ("text", label, "label"); @@ -76,15 +78,11 @@ public class Granite.ListItem : Granite.Bin { notify["menu-model"].connect (construct_menu); } - ~ListItem { - parent.remove_controller (menu_key_controller); - } - private void construct_menu () { if (menu_model == null) { remove_controller (click_controller); remove_controller (long_press_controller); - parent.remove_controller (menu_key_controller); + remove_controller (menu_key_controller); click_controller = null; long_press_controller = null; @@ -158,15 +156,7 @@ public class Granite.ListItem : Granite.Bin { add_controller (click_controller); add_controller (long_press_controller); - - // We don't get key events on the child list item widget - if (parent != null) { - parent.add_controller (menu_key_controller); - } else { - notify["parent"].connect (() => { - parent.add_controller (menu_key_controller); - }); - } + add_controller (menu_key_controller); } private void menu_popup_on_keypress (Gtk.PopoverMenu popover) { From d51d7dedd903eb75f39f97eaa2d57540363d9742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Sat, 20 Dec 2025 05:14:23 -0800 Subject: [PATCH 12/17] Update ListItem.vala Co-authored-by: Leonhard --- lib/Widgets/ListItem.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index 5b434d2e3..1253e292d 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -27,7 +27,7 @@ public class Granite.ListItem : Granite.Bin { * Context menu model * When a menu is shown with secondary click or long press will be constructed from the provided menu model */ - public GLib.Menu? menu_model { get; set; } + public GLib.MenuModel? menu_model { get; set; } private Gtk.GestureClick? click_controller; private Gtk.GestureLongPress? long_press_controller; From d7e22ef3d55537df0828b67b125ff4f7f0e30365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Thu, 15 Jan 2026 10:33:41 -0800 Subject: [PATCH 13/17] Make long press touch only --- lib/Widgets/ListItem.vala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index b44935d95..60c288497 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -150,7 +150,9 @@ public class Granite.ListItem : Gtk.Widget { } }); - long_press_controller = new Gtk.GestureLongPress (); + long_press_controller = new Gtk.GestureLongPress () { + touch_only = true + }; long_press_controller.pressed.connect ((x, y) => { // Try to keep menu from under your hand if (x > get_root ().get_width () / 2) { From 363d6279aa00636936195859b818e7fb2dd2afa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Sun, 25 Jan 2026 12:45:01 -0800 Subject: [PATCH 14/17] Resolve review comments --- lib/Widgets/ListItem.vala | 88 +++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index 60c288497..b7b6338ed 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -107,7 +107,7 @@ public class Granite.ListItem : Gtk.Widget { } private void construct_menu () { - if (menu_model == null) { + if (menu_model == null && context_menu != null) { remove_controller (click_controller); remove_controller (long_press_controller); remove_controller (menu_key_controller); @@ -137,58 +137,64 @@ public class Granite.ListItem : Gtk.Widget { button = 0, exclusive = true }; - click_controller.pressed.connect ((n_press, x, y) => { - var sequence = click_controller.get_current_sequence (); - var event = click_controller.get_last_event (sequence); - - if (event.triggers_context_menu ()) { - context_menu.halign = START; - menu_popup_at_pointer (context_menu, x, y); - - click_controller.set_state (CLAIMED); - click_controller.reset (); - } - }); + click_controller.pressed.connect (on_click); long_press_controller = new Gtk.GestureLongPress () { touch_only = true }; - long_press_controller.pressed.connect ((x, y) => { - // Try to keep menu from under your hand - if (x > get_root ().get_width () / 2) { - context_menu.halign = END; - x -= TOUCH_TARGET_WIDTH; - } else { - context_menu.halign = START; - x += TOUCH_TARGET_WIDTH; - } - - menu_popup_at_pointer (context_menu, x, y - (TOUCH_TARGET_WIDTH * 0.75)); - }); + long_press_controller.pressed.connect (on_long_press); menu_key_controller = new Gtk.EventControllerKey (); - menu_key_controller.key_released.connect ((keyval, keycode, state) => { - var mods = state & Gtk.accelerator_get_default_mod_mask (); - switch (keyval) { - case Gdk.Key.F10: - if (mods == Gdk.ModifierType.SHIFT_MASK) { - menu_popup_on_keypress (context_menu); - } - break; - case Gdk.Key.Menu: - case Gdk.Key.MenuKB: - menu_popup_on_keypress (context_menu); - break; - default: - return; - } - }); + menu_key_controller.key_released.connect (on_key_released); add_controller (click_controller); add_controller (long_press_controller); add_controller (menu_key_controller); } + private void on_click (Gtk.GestureClick gesture, int n_press, double x, double y) { + var sequence = gesture.get_current_sequence (); + var event = gesture.get_last_event (sequence); + + if (event.triggers_context_menu ()) { + context_menu.halign = START; + menu_popup_at_pointer (context_menu, x, y); + + gesture.set_state (CLAIMED); + gesture.reset (); + } + } + + private void on_long_press (double x, double y) { + // Try to keep menu from under your hand + if (x > get_root ().get_width () / 2) { + context_menu.halign = END; + x -= TOUCH_TARGET_WIDTH; + } else { + context_menu.halign = START; + x += TOUCH_TARGET_WIDTH; + } + + menu_popup_at_pointer (context_menu, x, y - (TOUCH_TARGET_WIDTH * 0.75)); + } + + private void on_key_released (uint keyval, uint keycode, Gdk.ModifierType state) { + var mods = state & Gtk.accelerator_get_default_mod_mask (); + switch (keyval) { + case Gdk.Key.F10: + if (mods == Gdk.ModifierType.SHIFT_MASK) { + menu_popup_on_keypress (context_menu); + } + break; + case Gdk.Key.Menu: + case Gdk.Key.MenuKB: + menu_popup_on_keypress (context_menu); + break; + default: + return; + } + } + private void menu_popup_on_keypress (Gtk.PopoverMenu popover) { popover.halign = END; popover.set_pointing_to (Gdk.Rectangle () { From 0ee55f362f031779bf9fb7d749ab534f81eba7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 27 Jan 2026 10:28:19 -0800 Subject: [PATCH 15/17] Add comments --- lib/Widgets/ListItem.vala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index b7b6338ed..da8883693 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -107,6 +107,7 @@ public class Granite.ListItem : Gtk.Widget { } private void construct_menu () { + // Menu model is being set null for the first time if (menu_model == null && context_menu != null) { remove_controller (click_controller); remove_controller (long_press_controller); @@ -122,6 +123,12 @@ public class Granite.ListItem : Gtk.Widget { return; } + // Popover and controllers have already been cleaned up + if (menu_model == null) { + return; + } + + // New menu model, recycling popover and controllers if (context_menu != null) { context_menu.menu_model = menu_model; return; From 7184165589d3e9e2da6f584ac3ab055da2cdebc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 27 Jan 2026 10:31:09 -0800 Subject: [PATCH 16/17] We can nest as a treat --- lib/Widgets/ListItem.vala | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index da8883693..750b3d74f 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -107,24 +107,21 @@ public class Granite.ListItem : Gtk.Widget { } private void construct_menu () { - // Menu model is being set null for the first time - if (menu_model == null && context_menu != null) { - remove_controller (click_controller); - remove_controller (long_press_controller); - remove_controller (menu_key_controller); - - click_controller = null; - long_press_controller = null; - menu_key_controller = null; - - context_menu.unparent (); - context_menu = null; - - return; - } - - // Popover and controllers have already been cleaned up if (menu_model == null) { + // Menu model is being set null for the first time + if (context_menu != null) { + remove_controller (click_controller); + remove_controller (long_press_controller); + remove_controller (menu_key_controller); + + click_controller = null; + long_press_controller = null; + menu_key_controller = null; + + context_menu.unparent (); + context_menu = null; + } + return; } From 866009d2e81a17236a0f42898b6c8ff39a234cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Thu, 5 Feb 2026 08:40:19 -0800 Subject: [PATCH 17/17] Update ListItem.vala --- lib/Widgets/ListItem.vala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index 750b3d74f..553cbcd27 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -53,7 +53,10 @@ public class Granite.ListItem : Gtk.Widget { /** * Context menu model * When a menu is shown with secondary click or long press will be constructed from the provided menu model + * + * @since 7.8.0 */ + [Version (since = "7.8.0")] public GLib.MenuModel? menu_model { get; set; } private Gtk.GestureClick? click_controller;