From 898d202f2c6d82d5e838d54c078251af3c3128c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Wed, 25 Jun 2025 13:19:43 -0700 Subject: [PATCH] ListItem: add swipe action --- demo/Views/ListsView.vala | 15 +++++++ lib/Styles/Granite/ListItem.scss | 64 +++++++++++++++++++++++++++ lib/Widgets/ListItem.vala | 75 +++++++++++++++++++++++++++++++- 3 files changed, 153 insertions(+), 1 deletion(-) diff --git a/demo/Views/ListsView.vala b/demo/Views/ListsView.vala index 5418f5359..e918efe65 100644 --- a/demo/Views/ListsView.vala +++ b/demo/Views/ListsView.vala @@ -61,8 +61,23 @@ public class ListsView : DemoPage { var list_item = (Gtk.ListItem) obj; var list_object = (ListObject) list_item.item; + var mark_menuitem = new GLib.MenuItem ("Star", null); + mark_menuitem.set_attribute_value ("icon", "non-starred-symbolic"); + mark_menuitem.set_attribute_value ("css-class", "yellow"); + + var replyall_menuitem = new GLib.MenuItem ("Reply All", null); + replyall_menuitem.set_attribute_value ("icon", "mail-reply-all-symbolic"); + replyall_menuitem.set_attribute_value ("css-class", "purple"); + + var trash_menuitem = new GLib.MenuItem ("Trash", null); + trash_menuitem.set_attribute_value ("icon", "edit-delete-symbolic"); + trash_menuitem.set_attribute_value ("css-class", "destructive"); + var granite_list_item = ((Granite.ListItem) list_item.child); granite_list_item.text = list_object.text; + granite_list_item.prepend_swipe_action (replyall_menuitem); + granite_list_item.prepend_swipe_action (mark_menuitem); + granite_list_item.append_swipe_action (trash_menuitem); }); var list_view = new Gtk.ListView (list_selection, list_factory) { diff --git a/lib/Styles/Granite/ListItem.scss b/lib/Styles/Granite/ListItem.scss index ad6b24917..70429bddf 100644 --- a/lib/Styles/Granite/ListItem.scss +++ b/lib/Styles/Granite/ListItem.scss @@ -1,4 +1,5 @@ granite-listitem { + border-spacing: $button-spacing; padding: $button-spacing; min-height: rem(32px); //Try to force homogeneous row height @@ -6,3 +7,66 @@ granite-listitem { padding: $button-spacing; } } + +button.swipe-button { + background-color: #{'@selected_bg_color'}; + border-radius: $button-spacing; + border-spacing: 0.5rem; + color: #{'@selected_fg_color'}; + font-weight: 600; + // https://www.w3.org/WAI/WCAG21/Understanding/target-size.html + min-width: 44px; + padding: 0.25rem; + + &.red, + &.destructive { + background-color: #{'mix(@bg_color, @STRAWBERRY_500, 0.3)'}; + color: #{'mix(@fg_color, @STRAWBERRY_500, 0.6)'}; + } + + &.orange { + background-color: #{'mix(@bg_color, @GRAPE_500, 0.3)'}; + color: #{'mix(@fg_color, @GRAPE_500, 0.6)'}; + } + + &.banana, + &.yellow { + background-color: #{'mix(@bg_color, @BANANA_500, 0.3)'}; + color: #{'mix(@fg_color, @BANANA_500, 0.4)'}; + } + + &.lime, + &.green { + background-color: #{'mix(@bg_color, @GRAPE_500, 0.3)'}; + color: #{'mix(@fg_color, @GRAPE_500, 0.6)'}; + } + + &.blueberry, + &.blue { + background-color: #{'mix(@bg_color, @GRAPE_500, 0.3)'}; + color: #{'mix(@fg_color, @GRAPE_500, 0.6)'}; + } + + &.teal, + &.mint { + background-color: #{'mix(@bg_color, @GRAPE_500, 0.3)'}; + color: #{'mix(@fg_color, @GRAPE_500, 0.6)'}; + } + + &.grape, + &.purple { + background-color: #{'mix(@bg_color, @GRAPE_500, 0.3)'}; + color: #{'mix(@fg_color, @GRAPE_500, 0.6)'}; + } + + &.bubblegum, + &.pink { + background-color: #{'mix(@bg_color, @GRAPE_500, 0.3)'}; + color: #{'mix(@fg_color, @GRAPE_500, 0.6)'}; + } + + &:backdrop { + background-color: scale-color($fg-color, $alpha: -90%); + color: inherit; + } +} diff --git a/lib/Widgets/ListItem.vala b/lib/Widgets/ListItem.vala index 878a9609f..d614c30f3 100644 --- a/lib/Widgets/ListItem.vala +++ b/lib/Widgets/ListItem.vala @@ -43,13 +43,14 @@ public class Granite.ListItem : Gtk.Widget { if (_child != null) { _child.set_parent (this); + _child.hexpand = true; } } } class construct { set_css_name ("granite-listitem"); - set_layout_manager_type (typeof (Gtk.BinLayout)); + set_layout_manager_type (typeof (Gtk.BoxLayout)); } construct { @@ -93,4 +94,76 @@ public class Granite.ListItem : Gtk.Widget { child.unparent (); } } + + /** + * The following attributes are used when constructing menu items: + * + * - "label": a user-visible string to display + * - "action": the prefixed name of the action to trigger + * - "target": the parameter to use when activating the action + * - "icon" and "verb-icon": names of icons that may be displayed or a question mark by default + * - "css-class": a css style class for assigning a color or user accent colored by default + * + * The following style class values are supported: + * + * - "red" or "destructive" + * - "orange" + * - "yellow" or "banana" + * - "green" or "lime" + * - "blue" or "blueberry" + * - "teal" or "mint" + * - "purple" or "grape" + * - "pink" or "bubblegum" + */ + public void prepend_swipe_action (GLib.MenuItem menu_item) { + new SwipeButton (menu_item).insert_before (this, child); + } + + /** + * See prepend_swipe_action for menu item attribute details + */ + public void append_swipe_action (GLib.MenuItem menu_item) { + new SwipeButton (menu_item).insert_after (this, child); + } + + private class SwipeButton : Gtk.Button { + public SwipeButton (GLib.MenuItem menu_item) { + var icon_name = menu_item.get_attribute_value ("icon", VariantType.STRING).get_string (); + if (icon_name == "") { + icon_name = menu_item.get_attribute_value ("verb-icon", VariantType.STRING).get_string (); + if (icon_name == "") { + icon_name = "dialog-question-symbolic"; + } + } + + var image = new Gtk.Image.from_icon_name (icon_name); + + var label = new Gtk.Label ( + menu_item.get_attribute_value ("label", VariantType.STRING).get_string () + ) { + ellipsize = END, + justify = CENTER, + lines = 2, + max_width_chars = 10 + }; + label.add_css_class (Granite.CssClass.SMALL); + + var box = new Gtk.Box (VERTICAL, 0) { + valign = CENTER + }; + box.append (image); + box.append (label); + + child = box; + + var css_class = menu_item.get_attribute_value ("css-class", VariantType.STRING); + if (css_class != null) { + add_css_class (css_class.get_string ()); + } + } + + construct { + add_css_class ("swipe-button"); + } + } }