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) {