diff --git a/demo/Views/ListsView.vala b/demo/Views/ListsView.vala index 5418f5359..f8421db7d 100644 --- a/demo/Views/ListsView.vala +++ b/demo/Views/ListsView.vala @@ -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" @@ -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) => { 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 878a9609f..553cbcd27 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -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 */ @@ -47,6 +50,20 @@ 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; + 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)); @@ -72,6 +89,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"); @@ -86,6 +105,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 () {