Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
75259bc
ListItem: add menu_model property
danirabbit Apr 8, 2025
5eae59d
Merge branch 'main' into danirabbit/listitem-menumodel
danirabbit Apr 8, 2025
3fb0cc9
Use event.triggers_context_menu
danirabbit Apr 12, 2025
142569a
Add keypress handling
danirabbit Apr 12, 2025
384edd2
Merge branch 'main' into danirabbit/listitem-menumodel
danirabbit Apr 12, 2025
d4c5a48
Align menu based on event coords
danirabbit Apr 12, 2025
d549066
Get window width, not widget width
danirabbit Apr 12, 2025
b028b2d
context_menu_controller → click_controller
danirabbit Apr 12, 2025
ebd83d5
Move long press menu
danirabbit Apr 12, 2025
fdf7d4e
Make keypress come from vcenter not bottom
danirabbit Apr 12, 2025
69187b1
Merge branch 'main' into danirabbit/listitem-menumodel
danirabbit Apr 17, 2025
2c2c32b
Merge branch 'main' into danirabbit/listitem-menumodel
zeebok May 4, 2025
9fc301a
merge main
danirabbit May 8, 2025
0c66a17
Remove menu key controller if we unparent
danirabbit May 8, 2025
63733a8
nevermind that doesn't make sense, do it on destruction
danirabbit May 8, 2025
57e398a
Merge branch 'main' into danirabbit/listitem-menumodel
danirabbit Jun 24, 2025
167d8e2
Allow focusing for key events
danirabbit Jun 24, 2025
d51d7de
Update ListItem.vala
danirabbit Dec 20, 2025
6da1462
Merge main
danirabbit Jan 15, 2026
d7e22ef
Make long press touch only
danirabbit Jan 15, 2026
94b6e92
Merge branch 'main' into danirabbit/listitem-menumodel
danirabbit Jan 25, 2026
363d627
Resolve review comments
danirabbit Jan 25, 2026
0ee55f3
Add comments
danirabbit Jan 27, 2026
7184165
We can nest as a treat
danirabbit Jan 27, 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
26 changes: 25 additions & 1 deletion demo/Views/ListsView.vala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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 list_store = new GLib.ListStore (typeof (ListObject));
list_store.append (new ListObject () {
text = "Row 1"
Expand All @@ -54,7 +76,9 @@ public class ListsView : DemoPage {
var list_factory = new Gtk.SignalListItemFactory ();
list_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
};
});

list_factory.bind.connect ((obj) => {
Expand Down
5 changes: 5 additions & 0 deletions lib/Styles/Granite/ListItem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
129 changes: 129 additions & 0 deletions lib/Widgets/ListItem.vala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
*/
[Version (since = "7.7.0")]
public class Granite.ListItem : Gtk.Widget {
// https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
private const int TOUCH_TARGET_WIDTH = 44;

/**
* The main label for #this
*/
Expand Down Expand Up @@ -47,6 +50,17 @@ 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
*/
public GLib.MenuModel? menu_model { get; set; }

private Gtk.GestureClick? click_controller;
private Gtk.GestureLongPress? long_press_controller;
private Gtk.EventControllerKey menu_key_controller;
private Gtk.PopoverMenu? context_menu;

class construct {
set_css_name ("granite-listitem");
set_layout_manager_type (typeof (Gtk.BinLayout));
Expand All @@ -72,6 +86,8 @@ public class Granite.ListItem : Gtk.Widget {
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");
Expand All @@ -86,6 +102,119 @@ public class Granite.ListItem : Gtk.Widget {
text_box.append (description_label);
}
});

notify["menu-model"].connect (construct_menu);
}

private void construct_menu () {
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;
}

// New menu model, recycling popover and controllers
if (context_menu != null) {
context_menu.menu_model = menu_model;
return;
}

context_menu = new Gtk.PopoverMenu.from_model (menu_model) {
has_arrow = false,
position = BOTTOM
};
context_menu.set_parent (this);

click_controller = new Gtk.GestureClick () {
button = 0,
exclusive = true
};
click_controller.pressed.connect (on_click);

long_press_controller = new Gtk.GestureLongPress () {
touch_only = true
};
long_press_controller.pressed.connect (on_long_press);

menu_key_controller = new Gtk.EventControllerKey ();
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 () {
x = (int) get_width (),
y = (int) get_height () / 2
});
popover.popup ();
}

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

~ListItem () {
Expand Down
Loading