Codebase list gnome-shell-extensions / 17de2bd debian / patches / apps-menu-with-multiple-levels.patch
17de2bd

Tree @17de2bd (Download .tar.gz)

apps-menu-with-multiple-levels.patch @17de2bdraw · history · blame

Description: Add support for nested menus
Author: Emilio Pozuelo Monfort <[email protected]>
Bug-Kali: https://bugs.kali.org/view.php?id=2223
Bug: https://bugzilla.gnome.org/show_bug.cgi?id=739480

--- a/extensions/apps-menu/extension.js
+++ b/extensions/apps-menu/extension.js
@@ -97,10 +97,9 @@ const ApplicationMenuItem = new Lang.Cla
 
 const CategoryMenuItem = new Lang.Class({
     Name: 'CategoryMenuItem',
-    Extends: PopupMenu.PopupBaseMenuItem,
+    Extends: PopupMenu.PopupMenuItem,
 
     _init: function(button, category) {
-	this.parent();
 	this._category = category;
         this._button = button;
 
@@ -113,13 +112,126 @@ const CategoryMenuItem = new Lang.Class(
         else
             name = _("Favorites");
 
-        this.actor.add_child(new St.Label({ text: name }));
-        this.actor.connect('motion-event', Lang.bind(this, this._onMotionEvent));
+        this.parent(name);
+
+        //this.actor.connect('motion-event', Lang.bind(this, this._onMotionEvent));
+    },
+
+    activate: function(event) {
+        this._button.selectCategory(this._category, this);
+        //this._button.scrollToCatButton(this);
+        // we don't chain up here so that clicking on a category doesn't
+        // close the menu
+    },
+
+    _isNavigatingSubmenu: function([x, y]) {
+        let [posX, posY] = this.actor.get_transformed_position();
+
+        if (this._oldX == -1) {
+            this._oldX = x;
+            this._oldY = y;
+            return true;
+        }
+
+        let deltaX = Math.abs(x - this._oldX);
+        let deltaY = Math.abs(y - this._oldY);
+
+        this._oldX = x;
+        this._oldY = y;
+
+        // If it lies outside the x-coordinates then it is definitely outside.
+        if (posX > x || posX + this.actor.width < x)
+            return false;
+
+        // If it lies inside the menu item then it is definitely inside.
+        if (posY <= y && posY + this.actor.height >= y)
+            return true;
+
+        // We want the keep-up triangle only if the movement is more
+        // horizontal than vertical.
+        if (deltaX * HORIZ_FACTOR < deltaY)
+            return false;
+
+        // Check whether the point lies inside triangle ABC, and a similar
+        // triangle on the other side of the menu item.
+        //
+        //   +---------------------+
+        //   | menu item           |
+        // A +---------------------+ C
+        //              P          |
+        //                         B
+
+        // Ensure that the point P always lies below line AC so that we can
+        // only check for triangle ABC.
+        if (posY > y) {
+            let offset = posY - y;
+            y = posY + this.actor.height + offset;
+        }
+
+        // Ensure that A is (0, 0).
+        x -= posX;
+        y -= posY + this.actor.height;
+
+        // Check which side of line AB the point P lies on by taking the
+        // cross-product of AB and AP. See:
+        // http://stackoverflow.com/questions/3461453/determine-which-side-of-a-line-a-point-lies
+        if (((this.actor.width * y) - (NAVIGATION_REGION_OVERSHOOT * x)) <= 0)
+             return true;
+
+        return false;
+    },
+
+    _onMotionEvent: function(actor, event) {
+        if (!Clutter.get_pointer_grab()) {
+            this._oldX = -1;
+            this._oldY = -1;
+            Clutter.grab_pointer(this.actor);
+        }
+        this.actor.hover = true;
+
+        if (this._isNavigatingSubmenu(event.get_coords()))
+            return true;
+
+        this._oldX = -1;
+        this._oldY = -1;
+        this.actor.hover = false;
+        Clutter.ungrab_pointer();
+        return false;
+    },
+
+    setActive: function(active, params) {
+        if (active) {
+            this._button.selectCategory(this._category, this);
+            //this._button.scrollToCatButton(this);
+        }
+        this.parent(active, params);
+    }
+});
+const ParentCategoryMenuItem = new Lang.Class({
+    Name: 'ParentCategoryMenuItem',
+    Extends: PopupMenu.PopupSubMenuMenuItem,
+
+    _init: function(button, category) {
+	this._category = category;
+        this._button = button;
+
+        this._oldX = -1;
+        this._oldY = -1;
+
+        let name;
+        if (this._category)
+            name = this._category.get_name();
+        else
+            name = _("Favorites");
+
+        this.parent(name, false);
+
+        //this.actor.connect('motion-event', Lang.bind(this, this._onMotionEvent));
     },
 
     activate: function(event) {
         this._button.selectCategory(this._category, this);
-        this._button.scrollToCatButton(this);
+        //this._button.scrollToCatButton(this);
 	this.parent(event);
     },
 
@@ -201,12 +313,31 @@ const CategoryMenuItem = new Lang.Class(
     setActive: function(active, params) {
         if (active) {
             this._button.selectCategory(this._category, this);
-            this._button.scrollToCatButton(this);
+            //this._button.scrollToCatButton(this);
         }
         this.parent(active, params);
     }
 });
 
+const PopupMenuScrollView = new Lang.Class({
+    Name: 'PopupMenuScrollView',
+    Extends: PopupMenu.PopupMenuSection,
+
+    _init: function() {
+        this.parent();
+
+        this.actor = new St.ScrollView({ style_class: 'vfade',
+                                         hscrollbar_policy: Gtk.PolicyType.NEVER,
+                                         vscrollbar_policy: Gtk.PolicyType.AUTOMATIC });
+
+        this.container = new Shell.GenericContainer();
+        this.box.add_actor(this.container);
+        this.actor.add_actor(this.box);
+        this.actor._delegate = this;
+        this.actor.clip_to_allocation = true;
+    },
+});
+
 const HotCorner = new Lang.Class({
     Name: 'HotCorner',
     Extends: Layout.HotCorner,
@@ -305,7 +436,7 @@ const ApplicationsButton = new Lang.Clas
         _installedChangedId = appSys.connect('installed-changed', Lang.bind(this, function() {
             if (this.menu.isOpen) {
                 this._redisplay();
-                this.mainBox.show();
+                this.mainBox.actor.show();
             } else {
                 this.reloadFlag = true;
             }
@@ -370,18 +501,19 @@ const ApplicationsButton = new Lang.Clas
                this._redisplay();
                this.reloadFlag = false;
            }
-           this.mainBox.show();
+           //this.categoriesBox.box.width = this._menuContainerGetPreferredWidth(this.categoriesBox.box);
+           this.mainBox.actor.show();
        }
        this.parent(menu, open);
     },
 
     _redisplay: function() {
         this.applicationsBox.destroy_all_children();
-        this.categoriesBox.destroy_all_children();
+        this.categoriesBox.actor.destroy_all_children();
         this._display();
     },
 
-    _loadCategory: function(categoryId, dir) {
+    _loadCategory: function(dir, parentCategory) {
         let iter = dir.iter();
         let nextType;
         while ((nextType = iter.next()) != GMenu.TreeItemType.INVALID) {
@@ -391,12 +523,21 @@ const ApplicationsButton = new Lang.Clas
                 let app = appSys.lookup_app(entry.get_desktop_file_id());
                 if (appInfo.should_show()) {
                     let menu_id = dir.get_menu_id();
-                    this.applicationsByCategory[categoryId].push(app);
+                    this.applicationsByCategory[menu_id].push(app);
                 }
             } else if (nextType == GMenu.TreeItemType.DIRECTORY) {
                 let subdir = iter.get_directory();
-                if (!subdir.get_is_nodisplay())
-                    this._loadCategory(categoryId, subdir);
+                if (!subdir.get_is_nodisplay()) {
+                    let menu_id = subdir.get_menu_id();
+                    this.applicationsByCategory[menu_id] = [];
+                    let categoryMenuItem = new ParentCategoryMenuItem(this, subdir);
+                    this._loadCategory(subdir, categoryMenuItem);
+                    if (categoryMenuItem.menu.isEmpty())
+                        categoryMenuItem = new CategoryMenuItem(this, subdir);
+                    if (this.applicationsByCategory[menu_id].length > 0 || !categoryMenuItem.menu.isEmpty()) {
+                        parentCategory.menu.addMenuItem(categoryMenuItem);
+                    }
+                }
             }
         }
     },
@@ -417,8 +558,8 @@ const ApplicationsButton = new Lang.Clas
     },
 
     scrollToCatButton: function(button) {
-        let catsScrollBoxAdj = this.categoriesScrollBox.get_vscroll_bar().get_adjustment();
-        let catsScrollBoxAlloc = this.categoriesScrollBox.get_allocation_box();
+        let catsScrollBoxAdj = this.categoriesBox.actor.get_vscroll_bar().get_adjustment();
+        let catsScrollBoxAlloc = this.categoriesBox.actor.get_allocation_box();
         let currentScrollValue = catsScrollBoxAdj.get_value();
         let boxHeight = catsScrollBoxAlloc.y2 - catsScrollBoxAlloc.y1;
         let buttonAlloc = button.actor.get_allocation_box();
@@ -432,10 +573,16 @@ const ApplicationsButton = new Lang.Clas
     },
 
     _createLayout: function() {
+        // https://mail.gnome.org/archives/gnome-shell-list/2014-January/msg00010.html
+        this.menu._setOpenedSubMenu = Lang.bind(this, function(submenu) {
+            this._openedSubMenu = submenu;
+        });
+
         let section = new PopupMenu.PopupMenuSection();
         this.menu.addMenuItem(section);
-        this.mainBox = new St.BoxLayout({ vertical: false });
-        this.leftBox = new St.BoxLayout({ vertical: true });
+        this.mainBox = new PopupMenu.PopupMenuSection();
+        this.mainBox.actor.vertical = false;
+        this.leftBox = new PopupMenu.PopupMenuSection();
         this.applicationsScrollBox = new St.ScrollView({ x_fill: true, y_fill: false,
                                                          y_align: St.Align.START,
                                                          style_class: 'apps-menu vfade' });
@@ -447,41 +594,52 @@ const ApplicationsButton = new Lang.Clas
         vscroll.connect('scroll-stop', Lang.bind(this, function() {
             this.menu.passEvents = false;
         }));
-        this.categoriesScrollBox = new St.ScrollView({ x_fill: true, y_fill: false,
-                                                       y_align: St.Align.START,
-                                                       style_class: 'vfade' });
-        this.categoriesScrollBox.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
-        vscroll = this.categoriesScrollBox.get_vscroll_bar();
+
+        let activities = new ActivitiesMenuItem(this);
+
+        this.applicationsBox = new St.BoxLayout({ vertical: true });
+        this.applicationsScrollBox.add_actor(this.applicationsBox);
+
+        this.categoriesBox = new PopupMenuScrollView();
+        vscroll = this.categoriesBox.actor.get_vscroll_bar();
         vscroll.connect('scroll-start', Lang.bind(this, function() {
                               this.menu.passEvents = true;
                           }));
         vscroll.connect('scroll-stop', Lang.bind(this, function() {
             this.menu.passEvents = false;
         }));
-        this.leftBox.add(this.categoriesScrollBox, { expand: true,
-                                                     x_fill: true, y_fill: true,
-                                                     y_align: St.Align.START });
-
-        let activities = new ActivitiesMenuItem(this);
-        this.leftBox.add(activities.actor, { expand: false,
-                                             x_fill: true, y_fill: false,
-                                             y_align: St.Align.START });
+        this.leftBox.addMenuItem(this.categoriesBox);
+        // FIXME we re-add it to apply the right properties, but re-adding it causes a warning
+        this.leftBox.actor.add(this.categoriesBox.actor, { expand: true, x_fill: true, y_fill: true, y_align: St.Align.START });
+
+        this.leftBox.actor.add(activities.actor, { expand: false,
+                                                   x_fill: true, y_fill: false,
+                                                   y_align: St.Align.START });
+
+        this.mainBox.addMenuItem(this.leftBox);
+        this.mainBox.actor.add(this._createVertSeparator(), { expand: false, x_fill: false, y_fill: true});
+        this.mainBox.actor.add(this.applicationsScrollBox, { expand: true, x_fill: true, y_fill: true });
+        section.addMenuItem(this.mainBox);
+    },
+
+    _menuContainerGetPreferredWidth: function(container) {
+        let max_width = 0;
+        for (let child = container.get_first_child();
+             child;
+             child = child.get_next_sibling()) {
+            // recurse into submenus
+            if (child._delegate instanceof ParentCategoryMenuItem)
+                max_width = Math.max(max_width, this._menuContainerGetPreferredWidth(child._delegate.menu.box));
 
-        this.applicationsBox = new St.BoxLayout({ vertical: true });
-        this.applicationsScrollBox.add_actor(this.applicationsBox);
-        this.categoriesBox = new St.BoxLayout({ vertical: true });
-        this.categoriesScrollBox.add_actor(this.categoriesBox, { expand: true, x_fill: false });
-
-        this.mainBox.add(this.leftBox);
-        this.mainBox.add(this._createVertSeparator(), { expand: false, x_fill: false, y_fill: true});
-        this.mainBox.add(this.applicationsScrollBox, { expand: true, x_fill: true, y_fill: true });
-        section.actor.add_actor(this.mainBox);
+            max_width = Math.max(max_width, child.width);
+        }
+        return max_width;
     },
 
     _display: function() {
         this._applicationsButtons = new Array();
-        this.mainBox.style=('width: 640px;');
-        this.mainBox.hide();
+        this.mainBox.actor.style=('width: 640px;');
+        this.mainBox.actor.hide();
 
         //Load categories
         this.applicationsByCategory = {};
@@ -489,7 +647,7 @@ const ApplicationsButton = new Lang.Clas
         tree.load_sync();
         let root = tree.get_root_directory();
         let categoryMenuItem = new CategoryMenuItem(this, null);
-        this.categoriesBox.add_actor(categoryMenuItem.actor);
+        this.categoriesBox.addMenuItem(categoryMenuItem);
         let iter = root.iter();
         let nextType;
         while ((nextType = iter.next()) != GMenu.TreeItemType.INVALID) {
@@ -498,10 +656,12 @@ const ApplicationsButton = new Lang.Clas
                 if (!dir.get_is_nodisplay()) {
                     let categoryId = dir.get_menu_id();
                     this.applicationsByCategory[categoryId] = [];
-                    this._loadCategory(categoryId, dir);
-                    if (this.applicationsByCategory[categoryId].length > 0) {
-                        let categoryMenuItem = new CategoryMenuItem(this, dir);
-                        this.categoriesBox.add_actor(categoryMenuItem.actor);
+                    let categoryMenuItem = new ParentCategoryMenuItem(this, dir);
+                    this._loadCategory(dir, categoryMenuItem);
+                    if (categoryMenuItem.menu.isEmpty())
+                        categoryMenuItem = new CategoryMenuItem(this, dir);
+                    if (this.applicationsByCategory[categoryId].length > 0 || !categoryMenuItem.menu.isEmpty()) {
+                        this.categoriesBox.addMenuItem(categoryMenuItem);
                     }
                 }
             }
@@ -510,8 +670,9 @@ const ApplicationsButton = new Lang.Clas
         //Load applications
         this._displayButtons(this._listApplications(null));
 
-        let height = this.categoriesBox.height + MENU_HEIGHT_OFFSET + 'px';
-        this.mainBox.style+=('height: ' + height);
+        let height = this.categoriesBox.actor.height + MENU_HEIGHT_OFFSET + 'px';
+        this.mainBox.actor.style+=('height: ' + height);
+        this.categoriesBox.box.width = 220;
     },
 
     _clearApplicationsBox: function(selectedActor) {