Merge lp:~meese/slingshot/fix-1294917 into lp:~elementary-pantheon/slingshot/trunk

Proposed by meese on 2014-09-04
Status: Merged
Approved by: Cody Garver on 2014-10-17
Approved revision: 456
Merged at revision: 467
Proposed branch: lp:~meese/slingshot/fix-1294917
Merge into: lp:~elementary-pantheon/slingshot/trunk
Diff against target: 490 lines (+332/-10)
7 files modified
CMakeLists.txt (+1/-0)
src/Backend/App.vala (+97/-1)
src/SlingshotView.vala (+21/-4)
src/Widgets/AppEntry.vala (+40/-2)
src/Widgets/CategoryView.vala (+1/-2)
src/Widgets/PopoverMenu.vala (+169/-0)
src/Widgets/Switcher.vala (+3/-1)
To merge this branch: bzr merge lp:~meese/slingshot/fix-1294917
Reviewer Review Type Date Requested Status
Cody Garver Approve on 2014-10-16
Daniel Fore ux 2014-09-04 Approve on 2014-10-15
elementary Pantheon team code 2014-10-16 Pending
Review via email: mp+233304@code.launchpad.net

Commit message

adds a popover context menu used for showing quicklist items on right click (lp:1294917)

Description of the change

Adds a popover menu widget that avoids the focus issues of using a normal context menu. Used to show launcher quicklist items on right clicking an app.

To post a comment you must log in.
Cody Garver (codygarver) wrote :

Has merge conflicts

lp:~meese/slingshot/fix-1294917 updated on 2014-10-11
454. By meese on 2014-10-11

merge

meese (meese) wrote :

fixed

Daniel Fore (danrabbit) wrote :

Works completely as intended for me. Style issues were mine and have been fixed in trunk.

review: Approve (ux)
Cody Garver (codygarver) wrote :

All good aside from the background under the popover

review: Approve
lp:~meese/slingshot/fix-1294917 updated on 2014-10-16
455. By meese on 2014-10-16

remove shadow from popover menu css

Djax (parnold-x) wrote :

@meese
to fix the 1px shrink of the popover on hover over the longest entry you could solve it like this:

add @ PopOverMenu.vala:75
 button.enter_notify_event.connect ((evt) => {
            // fix width because there is a issue with resizing on hover
            button.width_request = button.get_allocated_width ();
            return false;
        });

Daniel Fore (danrabbit) wrote :

Djax, that shouldn't happen anymore. There was a theme bug that was causing it.

Cody Garver (codygarver) wrote :

Needs trunk merged in and conflicts resolved

lp:~meese/slingshot/fix-1294917 updated on 2014-10-17
456. By meese on 2014-10-17

merge from trunk

meese (meese) wrote :

done

Rico Tzschichholz (ricotz) wrote :

@messe: while copy&paste from plank, it would have been nice to mention this!
(Sharing code by linking against libplank is also an option if this is reasonable.)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2014-10-14 17:02:40 +0000
3+++ CMakeLists.txt 2014-10-17 01:18:18 +0000
4@@ -83,6 +83,7 @@
5 src/Widgets/SearchItem.vala
6 src/Widgets/Sidebar.vala
7 src/Widgets/CategoryView.vala
8+ src/Widgets/PopoverMenu.vala
9 PACKAGES
10 ${CORE_DEPS}
11 ${UI_DEPS}
12
13=== modified file 'src/Backend/App.vala'
14--- src/Backend/App.vala 2014-10-15 22:16:39 +0000
15+++ src/Backend/App.vala 2014-10-17 01:18:18 +0000
16@@ -47,7 +47,10 @@
17
18 public Synapse.Match? match { get; private set; default = null; }
19 public Synapse.Match? target { get; private set; default = null; }
20-
21+ public Gee.ArrayList<string> actions { get; private set; default = null; }
22+ public Gee.HashMap<string, string> actions_map { get; private set; default = null; }
23+ public Gdk.Pixbuf? quicklist_icon { get; private set; default = null; }
24+
25 public signal void icon_changed ();
26 public signal void launched (App app);
27
28@@ -57,6 +60,18 @@
29
30 private LoadableIcon loadable_icon = null;
31
32+ // for FDO Desktop Actions
33+ // see http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#extra-actions
34+ private const string DESKTOP_ACTION_KEY = "Actions";
35+ private const string DESKTOP_ACTION_GROUP_NAME = "Desktop Action %s";
36+ // for the Unity static quicklists
37+ // see https://wiki.edubuntu.org/Unity/LauncherAPI#Static_Quicklist_entries
38+ private const string UNITY_QUICKLISTS_KEY = "X-Ayatana-Desktop-Shortcuts";
39+ private const string UNITY_QUICKLISTS_SHORTCUT_GROUP_NAME = "%s Shortcut Group";
40+ private const string UNITY_QUICKLISTS_TARGET_KEY = "TargetEnvironment";
41+ private const string UNITY_QUICKLISTS_TARGET_VALUE = "Unity";
42+ private const string[] SUPPORTED_GETTEXT_DOMAINS_KEYS = {"X-Ubuntu-Gettext-Domain", "X-GNOME-Gettext-Domain"};
43+
44 public App (GMenu.TreeEntry entry) {
45 app_type = AppType.APP;
46
47@@ -280,4 +295,85 @@
48 return true;
49 }
50
51+ public void init_actions () throws KeyFileError {
52+ actions = new Gee.ArrayList<string> ();
53+ actions_map = new Gee.HashMap<string, string> ();
54+ quicklist_icon = load_icon (16);
55+
56+ // get FDO Desktop Actions
57+ // see http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#extra-actions
58+ // get the Unity static quicklists
59+ // see https://wiki.edubuntu.org/Unity/LauncherAPI#Static Quicklist entries
60+ KeyFile file;
61+ try {
62+ file = new KeyFile ();
63+ file.load_from_file (desktop_path, 0);
64+ } catch (Error e) {
65+ critical ("%s: %s", desktop_path, e.message);
66+ }
67+
68+ string? textdomain = null;
69+ foreach (var domain_key in SUPPORTED_GETTEXT_DOMAINS_KEYS)
70+ if (file.has_key (KeyFileDesktop.GROUP, domain_key)) {
71+ textdomain = file.get_string (KeyFileDesktop.GROUP, domain_key);
72+ break;
73+ }
74+ if (actions != null && actions_map != null) {
75+ actions.clear ();
76+ actions_map.clear ();
77+ string[] keys = {DESKTOP_ACTION_KEY, UNITY_QUICKLISTS_KEY};
78+
79+ foreach (var key in keys) {
80+ if (!file.has_key (KeyFileDesktop.GROUP, key))
81+ continue;
82+
83+ foreach (var action in file.get_string_list (KeyFileDesktop.GROUP, key)) {
84+ var group = DESKTOP_ACTION_GROUP_NAME.printf (action);
85+ if (!file.has_group (group)) {
86+ group = UNITY_QUICKLISTS_SHORTCUT_GROUP_NAME.printf (action);
87+ if (!file.has_group (group))
88+ continue;
89+ }
90+
91+ // check for TargetEnvironment
92+ if (file.has_key (group, UNITY_QUICKLISTS_TARGET_KEY)) {
93+ var target = file.get_string (group, UNITY_QUICKLISTS_TARGET_KEY);
94+ if (target != UNITY_QUICKLISTS_TARGET_VALUE)
95+ continue;
96+ }
97+
98+ // check for OnlyShowIn
99+ if (file.has_key (group, KeyFileDesktop.KEY_ONLY_SHOW_IN)) {
100+ var found = false;
101+
102+ foreach (var s in file.get_string_list (group, KeyFileDesktop.KEY_ONLY_SHOW_IN))
103+ if (s == UNITY_QUICKLISTS_TARGET_VALUE) {
104+ found = true;
105+ break;
106+ }
107+
108+ if (!found)
109+ continue;
110+ }
111+
112+ var action_name = file.get_locale_string (group, KeyFileDesktop.KEY_NAME);
113+
114+ var action_icon = "";
115+ if (file.has_key (group, KeyFileDesktop.KEY_ICON))
116+ action_icon = file.get_locale_string (group, KeyFileDesktop.KEY_ICON);
117+
118+ var action_exec = "";
119+ if (file.has_key (group, KeyFileDesktop.KEY_EXEC))
120+ action_exec = file.get_string (group, KeyFileDesktop.KEY_EXEC);
121+
122+ // apply given gettext-domain if available
123+ if (textdomain != null)
124+ action_name = GLib.dgettext (textdomain, action_name).dup ();
125+
126+ actions.add (action_name);
127+ actions_map.set (action_name, "%s;;%s".printf (action_exec, action_icon));
128+ }
129+ }
130+ }
131+ }
132 }
133\ No newline at end of file
134
135=== modified file 'src/SlingshotView.vala' (properties changed: +x to -x)
136--- src/SlingshotView.vala 2014-10-15 21:57:37 +0000
137+++ src/SlingshotView.vala 2014-10-17 01:18:18 +0000
138@@ -31,6 +31,8 @@
139 public Gtk.Stack stack;
140 public Granite.Widgets.ModeButton view_selector;
141 private Gtk.Revealer view_selector_revealer;
142+ // Single popover to use for all of app as context menu
143+ private Widgets.NofocusPopover context_popover;
144
145 // Views
146 private Widgets.Grid grid_view;
147@@ -97,8 +99,9 @@
148
149 height_request = calculate_grid_height () + Pixels.BOTTOM_SPACE;
150 setup_ui ();
151+ context_popover = new Widgets.NofocusPopover (this, event_box);
152+
153 connect_signals ();
154-
155 debug ("Apps loaded");
156 }
157
158@@ -311,6 +314,7 @@
159
160 categories = app_system.get_categories ();
161 apps = app_system.get_apps ();
162+
163 populate_grid_view ();
164 category_view.setup_sidebar ();
165 });
166@@ -336,6 +340,19 @@
167 motion_notify_event.connect (hotcorner_trigger);
168 }
169
170+ public void show_popover_menu (Gtk.Widget menu, Gtk.Widget relative) {
171+ context_popover = new Widgets.NofocusPopover (this, event_box);
172+ context_popover.set_relative_to (relative);
173+ context_popover.add (menu);
174+ context_popover.show_all ();
175+
176+ var entry = relative as Widgets.AppEntry;
177+ if (entry != null)
178+ entry.app_launched.connect (() => {context_popover.hide ();});
179+
180+ relative.grab_focus ();
181+ }
182+
183 private void gala_settings_changed () {
184 if (Slingshot.settings.gala_settings.hotcorner_topleft == "open-launcher") {
185 can_trigger_hotcorner = true;
186@@ -650,7 +667,7 @@
187 }
188
189 public override bool scroll_event (Gdk.EventScroll event) {
190-
191+ context_popover.hide ();
192 switch (event.direction.to_string ()) {
193 case "GDK_SCROLL_UP":
194 case "GDK_SCROLL_LEFT":
195@@ -674,7 +691,7 @@
196 }
197
198 public void show_slingshot () {
199-
200+ context_popover.hide ();
201 search_entry.text = "";
202
203 reposition ();
204@@ -768,7 +785,7 @@
205
206 foreach (Backend.App app in app_system.get_apps_by_name ()) {
207
208- var app_entry = new Widgets.AppEntry (app);
209+ var app_entry = new Widgets.AppEntry (app, this);
210 app_entry.app_launched.connect (() => hide ());
211 grid_view.append (app_entry);
212 app_entry.show_all ();
213
214=== modified file 'src/Widgets/AppEntry.vala' (properties changed: +x to -x)
215--- src/Widgets/AppEntry.vala 2014-10-15 21:57:37 +0000
216+++ src/Widgets/AppEntry.vala 2014-10-17 01:18:18 +0000
217@@ -32,8 +32,10 @@
218 private bool dragging = false; //prevent launching
219
220 private Backend.App application;
221+ private unowned SlingshotView view;
222
223- public AppEntry (Backend.App app) {
224+ public AppEntry (Backend.App app, SlingshotView view) {
225+ this.view = view;
226 Gtk.TargetEntry dnd = {"text/uri-list", 0, 0};
227 Gtk.drag_source_set (this, Gdk.ModifierType.BUTTON1_MASK, {dnd},
228 Gdk.DragAction.COPY);
229@@ -75,7 +77,13 @@
230
231 this.clicked.connect (launch_app);
232
233- this.button_press_event.connect ((e) => {return e.button == 3;});
234+ this.button_release_event.connect ((e) => {
235+ if (e.button == Gdk.BUTTON_SECONDARY) {
236+ show_menu ();
237+ return true;
238+ }
239+ return false;
240+ });
241
242 this.drag_begin.connect ( (ctx) => {
243 this.dragging = true;
244@@ -111,4 +119,34 @@
245 application.launch ();
246 app_launched ();
247 }
248+
249+ private void show_menu () {
250+ // Display the apps static quicklist items in a popover menu
251+ if (application.actions == null) {
252+ try {
253+ application.init_actions ();
254+ } catch (KeyFileError e) {
255+ critical ("%s: %s", desktop_path, e.message);
256+ }
257+ }
258+
259+ var menu = new PopoverMenu ();
260+ foreach (var action in application.actions) {
261+ var values = application.actions_map.get (action).split (";;");
262+ var menuitem = new Widgets.PopoverMenuItem (action, application.quicklist_icon);
263+ menu.add_menu_item (menuitem);
264+ menuitem.activated.connect (() => {
265+ try {
266+ AppInfo.create_from_commandline (values[0], null, AppInfoCreateFlags.NONE).launch (null, null);
267+ app_launched ();
268+ } catch (Error e) {
269+ critical ("%s: %s", desktop_path, e.message);
270+ }
271+ });
272+ }
273+
274+ if (menu.get_size () > 0)
275+ view.show_popover_menu (menu, this);
276+ }
277+
278 }
279\ No newline at end of file
280
281=== modified file 'src/Widgets/CategoryView.vala'
282--- src/Widgets/CategoryView.vala 2014-10-15 21:44:44 +0000
283+++ src/Widgets/CategoryView.vala 2014-10-17 01:18:18 +0000
284@@ -96,8 +96,7 @@
285 }
286
287 private void add_app (Backend.App app) {
288-
289- var app_entry = new AppEntry (app);
290+ var app_entry = new AppEntry (app, view);
291 app_entry.app_launched.connect (() => view.hide ());
292 app_view.append (app_entry);
293 app_view.show_all ();
294
295=== added file 'src/Widgets/PopoverMenu.vala'
296--- src/Widgets/PopoverMenu.vala 1970-01-01 00:00:00 +0000
297+++ src/Widgets/PopoverMenu.vala 2014-10-17 01:18:18 +0000
298@@ -0,0 +1,169 @@
299+// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
300+//
301+// Copyright (C) 2011-2012 Giulio Collura
302+// Copyright (C) 2014 Maddie May <madelynn@madelynnmay.com>
303+//
304+// This program is free software: you can redistribute it and/or modify
305+// it under the terms of the GNU General Public License as published by
306+// the Free Software Foundation, either version 3 of the License, or
307+// (at your option) any later version.
308+//
309+// This program is distributed in the hope that it will be useful,
310+// but WITHOUT ANY WARRANTY; without even the implied warranty of
311+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
312+// GNU General Public License for more details.
313+//
314+// You should have received a copy of the GNU General Public License
315+// along with this program. If not, see <http://www.gnu.org/licenses/>.
316+//
317+
318+// A menu to be used inside of popovers for when a normal context
319+// menu can't be used
320+namespace Slingshot.Widgets {
321+
322+public class PopoverMenu : Gtk.Grid {
323+ private int line_count = 0;
324+ private Gee.ArrayList<PopoverMenuItem> items = new Gee.ArrayList<PopoverMenuItem> ();
325+
326+ public PopoverMenu () {
327+ row_homogeneous = true;
328+ column_homogeneous = true;
329+ }
330+
331+ public void add_menu_item (PopoverMenuItem item) {
332+ line_count++;
333+ this.items.add (item);
334+ this.attach (item.child, 0, line_count, 1 , 1);
335+ }
336+
337+ public int get_size () {
338+ return items.size;
339+ }
340+}
341+
342+public class PopoverMenuItem : Object {
343+ public Gtk.Widget child = null;
344+ public signal void activated ();
345+
346+ public PopoverMenuItem (string label, Gdk.Pixbuf? icon) {
347+ var button = new Gtk.Button ();
348+ button.get_style_context ().add_class (Gtk.STYLE_CLASS_MENUITEM);
349+
350+ var grid = new Gtk.Grid ();
351+ if (icon != null) {
352+ var image = new Gtk.Image.from_pixbuf (icon);
353+ image.margin_left = 2;
354+ image.margin_right = 2;
355+ image.halign = Gtk.Align.START;
356+ grid.attach (image, 0, 0, 1, 1);
357+ } else {
358+ var space_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
359+ space_box.set_size_request (16, 16);
360+ grid.attach (space_box, 0, 0, 1, 1);
361+ }
362+
363+ var label_widget = new Gtk.Label.with_mnemonic (label);
364+ label_widget.margin_right = 16;
365+ label_widget.justify = Gtk.Justification.LEFT;
366+ label_widget.set_alignment (0, 0);
367+ grid.attach (label_widget, 1, 0, 1, 1);
368+
369+ button.add (grid);
370+ button.relief = Gtk.ReliefStyle.NONE;
371+ button.clicked.connect (on_activate);
372+ child = button;
373+ }
374+
375+ private void on_activate () {
376+ activated ();
377+ }
378+}
379+
380+// A popover that doesn't grab focus
381+public class NofocusPopover : Gtk.Popover {
382+ private Gtk.Container parent_container;
383+ private unowned SlingshotView view;
384+
385+ private const string POPOVER_STYLESHEET = """
386+ .popover,
387+ .popover.osd,
388+ GtkPopover {
389+ border-radius: 0px;
390+ margin: 0px;
391+ text-shadow: none;
392+ }
393+ """;
394+
395+ public NofocusPopover (SlingshotView view, Gtk.Container parent) {
396+ this.view = view;
397+ this.parent_container = parent;
398+ connect_popover_signals (parent);
399+ modal = false;
400+ parent.button_release_event.connect (hide_popover_menu);
401+ parent.button_press_event.connect (hide_popover_menu);
402+ get_style_context ().add_class (Gtk.STYLE_CLASS_MENU);
403+ Granite.Widgets.Utils.set_theming_for_screen (get_screen (), POPOVER_STYLESHEET,
404+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
405+ }
406+
407+ public void connect_popover_signals (Gtk.Container parent) {
408+ foreach (Gtk.Widget child in parent.get_children ()) {
409+ if (child is Gtk.Container) {
410+ Gtk.Container container = child as Gtk.Container;
411+ connect_popover_signals (container);
412+ }
413+
414+ if (child is Widgets.Switcher) {
415+ var switcher = child as Widgets.Switcher;
416+ switcher.on_stack_changed.connect (switched);
417+ }
418+
419+ child.button_release_event.connect (hide_popover_menu);
420+ child.button_press_event.connect (hide_popover_menu);
421+ }
422+ }
423+
424+ private void disconnect_popover_signals (Gtk.Container parent) {
425+ foreach (Gtk.Widget child in parent.get_children ()) {
426+ if (child is Gtk.Container) {
427+ Gtk.Container container = child as Gtk.Container;
428+ disconnect_popover_signals (container);
429+ }
430+
431+ if (child is Widgets.Switcher) {
432+ var switcher = child as Widgets.Switcher;
433+ switcher.on_stack_changed.disconnect (switched);
434+ }
435+
436+ child.button_release_event.disconnect (hide_popover_menu);
437+ child.button_press_event.disconnect (hide_popover_menu);
438+ }
439+ }
440+
441+ public void switched () {
442+ this.hide ();
443+ }
444+
445+ public bool hide_popover_menu (Gdk.EventButton event) {
446+ if (this.visible) {
447+ view.set_focus (null);
448+ view.search_entry.grab_focus ();
449+
450+ this.hide ();
451+ // Block here to replicate context menu behavior
452+ return true;
453+ }
454+ // Visible can be false but the popover still thinks it's up
455+ // and will show when it's view is switched back to
456+ // in that case don't block forwarding but make sure it is
457+ // really hidden
458+ this.hide ();
459+ return false;
460+ }
461+
462+ ~NofocusPopover () {
463+ disconnect_popover_signals (parent_container);
464+ }
465+}
466+
467+} // End namespace
468\ No newline at end of file
469
470=== modified file 'src/Widgets/Switcher.vala'
471--- src/Widgets/Switcher.vala 2014-05-26 06:26:17 +0000
472+++ src/Widgets/Switcher.vala 2014-10-17 01:18:18 +0000
473@@ -27,7 +27,8 @@
474
475 private Gtk.Stack stack;
476 private Gee.HashMap<Gtk.Widget, Gtk.ToggleButton> buttons;
477-
478+ public signal void on_stack_changed ();
479+
480 public Switcher () {
481 orientation = Gtk.Orientation.HORIZONTAL;
482 spacing = 2;
483@@ -86,6 +87,7 @@
484
485 private void on_button_clicked (Gtk.Widget widget) {
486 stack.set_visible_child (widget);
487+ on_stack_changed ();
488 }
489
490 private void populate_switcher () {

Subscribers

People subscribed via source and target branches