Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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);
}
}
117 changes: 117 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 : Granite.Bin {
// https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
private const int TOUCH_TARGET_WIDTH = 44;

/**
* The main label for #this
*/
Expand All @@ -20,6 +23,17 @@ 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.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");
}
Expand All @@ -44,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");
Expand All @@ -58,5 +74,106 @@ 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 (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;
}

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

long_press_controller = new Gtk.GestureLongPress ();
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));
});

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 (click_controller);
add_controller (long_press_controller);
add_controller (menu_key_controller);
}

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