[gnome-shell] PopupMenu: handle submenus inline
- From: Giovanni Campagna <gcampagna src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell] PopupMenu: handle submenus inline
- Date: Mon, 20 Dec 2010 16:46:45 +0000 (UTC)
commit 6024b87d27b4d52a4d3d3d681d4a3ed09d6acbfa
Author: Giovanni Campagna <gcampagna src gnome org>
Date: Mon Nov 1 16:03:28 2010 +0100
PopupMenu: handle submenus inline
Instead of showing submenus on the left side, make PopupSubMenuMenuItem
act like an expander. The sub menu is toggled on click, opened on
right/enter/space on the parent item, closed on left on any item
or when closing the parent menu.
https://bugzilla.gnome.org/show_bug.cgi?id=633476
data/theme/gnome-shell.css | 4 +
js/ui/panelMenu.js | 2 +-
js/ui/popupMenu.js | 424 +++++++++++++++++++++++++-------------------
3 files changed, 243 insertions(+), 187 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 98899a2..6e777fd 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -105,6 +105,10 @@ StTooltip StLabel {
min-width: 200px;
}
+.popup-sub-menu {
+ background-color: #606060;
+}
+
/* The remaining popup-menu sizing is all done in ems, so that if you
* override .popup-menu.font-size, everything else will scale with it.
*/
diff --git a/js/ui/panelMenu.js b/js/ui/panelMenu.js
index b0dad5e..07a7220 100644
--- a/js/ui/panelMenu.js
+++ b/js/ui/panelMenu.js
@@ -50,7 +50,7 @@ Button.prototype = {
if (open) {
this.actor.add_style_pseudo_class('pressed');
let focus = global.stage.get_key_focus();
- if (!focus || (focus != this.actor && !menu.contains(focus)))
+ if (!focus || (focus != this.actor && !menu.actor.contains(focus)))
this.actor.grab_key_focus();
} else
this.actor.remove_style_pseudo_class('pressed');
diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js
index 728c171..d493c75 100644
--- a/js/ui/popupMenu.js
+++ b/js/ui/popupMenu.js
@@ -87,7 +87,7 @@ PopupBaseMenuItem.prototype = {
},
_onButtonReleaseEvent: function (actor, event) {
- this.emit('activate', event);
+ this.activate(event);
return true;
},
@@ -95,7 +95,7 @@ PopupBaseMenuItem.prototype = {
let symbol = event.get_key_symbol();
if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) {
- this.emit('activate', event);
+ this.activate(event);
return true;
}
return false;
@@ -132,11 +132,6 @@ PopupBaseMenuItem.prototype = {
this.emit('destroy');
},
- // true if non descendant content includes @actor
- contains: function(actor) {
- return false;
- },
-
// adds an actor to the menu item; @params can contain %span
// (column span; defaults to 1, -1 means "all the remaining width"),
// %expand (defaults to #false), and %align (defaults to
@@ -612,76 +607,21 @@ function findNextInCycle(items, current, direction) {
return items[mod(cur + direction, items.length)];
}
-function PopupMenu() {
- this._init.apply(this, arguments);
+function PopupMenuBase() {
+ throw new TypeError('Trying to instantiate abstract class PopupMenuBase');
}
-PopupMenu.prototype = {
- _init: function(sourceActor, alignment, arrowSide, gap) {
+PopupMenuBase.prototype = {
+ _init: function(sourceActor, styleClass) {
this.sourceActor = sourceActor;
- this._alignment = alignment;
- this._arrowSide = arrowSide;
- this._gap = gap;
- this._boxPointer = new BoxPointer.BoxPointer(arrowSide,
- { x_fill: true,
- y_fill: true,
- x_align: St.Align.START });
- this.actor = this._boxPointer.actor;
- this.actor.style_class = 'popup-menu-boxpointer';
- this._boxWrapper = new Shell.GenericContainer();
- this._boxWrapper.connect('get-preferred-width', Lang.bind(this, this._boxGetPreferredWidth));
- this._boxWrapper.connect('get-preferred-height', Lang.bind(this, this._boxGetPreferredHeight));
- this._boxWrapper.connect('allocate', Lang.bind(this, this._boxAllocate));
- this._boxPointer.bin.set_child(this._boxWrapper);
-
- this._box = new St.BoxLayout({ style_class: 'popup-menu-content',
- vertical: true });
- this._boxWrapper.add_actor(this._box);
- this.actor.add_style_class_name('popup-menu');
-
- global.focus_manager.add_group(this.actor);
-
- if (sourceActor._delegate instanceof PopupSubMenuMenuItem) {
- this._isSubMenu = true;
- this.actor.reactive = true;
- this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
- }
+ this.box = new St.BoxLayout({ style_class: styleClass,
+ vertical: true });
this.isOpen = false;
this._activeMenuItem = null;
},
- _boxGetPreferredWidth: function (actor, forHeight, alloc) {
- // Update the menuitem column widths
- let columnWidths = [];
- let items = this._box.get_children();
- for (let i = 0; i < items.length; i++) {
- if (items[i]._delegate instanceof PopupBaseMenuItem) {
- let itemColumnWidths = items[i]._delegate.getColumnWidths();
- for (let j = 0; j < itemColumnWidths.length; j++) {
- if (j >= columnWidths.length || itemColumnWidths[j] > columnWidths[j])
- columnWidths[j] = itemColumnWidths[j];
- }
- }
- }
- for (let i = 0; i < items.length; i++) {
- if (items[i]._delegate instanceof PopupBaseMenuItem)
- items[i]._delegate.setColumnWidths(columnWidths);
- }
-
- // Now they will request the right sizes
- [alloc.min_size, alloc.natural_size] = this._box.get_preferred_width(forHeight);
- },
-
- _boxGetPreferredHeight: function (actor, forWidth, alloc) {
- [alloc.min_size, alloc.natural_size] = this._box.get_preferred_height(forWidth);
- },
-
- _boxAllocate: function (actor, box, flags) {
- this._box.allocate(box, flags);
- },
-
addAction: function(title, callback) {
var menuItem = new PopupMenuItem(title);
this.addMenuItem(menuItem);
@@ -692,9 +632,29 @@ PopupMenu.prototype = {
addMenuItem: function(menuItem, position) {
if (position == undefined)
- this._box.add(menuItem.actor);
+ this.box.add(menuItem.actor);
else
- this._box.insert_actor(menuItem.actor, position);
+ this.box.insert_actor(menuItem.actor, position);
+ if (menuItem instanceof PopupSubMenuMenuItem) {
+ if (position == undefined)
+ this.box.add(menuItem.menu.actor);
+ else
+ this.box.insert_actor(menuItem.menu.actor, position + 1);
+ menuItem._subMenuActivateId = menuItem.menu.connect('activate', Lang.bind(this, function() {
+ this.emit('activate');
+ this.close();
+ }));
+ menuItem._subMenuActiveChangeId = menuItem.menu.connect('active-changed', Lang.bind(this, function(submenu, submenuItem) {
+ if (this._activeMenuItem && this._activeMenuItem != submenuItem)
+ this._activeMenuItem.setActive(false);
+ this._activeMenuItem = submenuItem;
+ this.emit('active-changed', submenuItem);
+ }));
+ menuItem._closingId = this.connect('open-state-changed', function(self, open) {
+ if (!open)
+ menuItem.menu.immediateClose();
+ });
+ }
menuItem._activeChangeId = menuItem.connect('active-changed', Lang.bind(this, function (menuItem, active) {
if (active && this._activeMenuItem != menuItem) {
if (this._activeMenuItem)
@@ -713,17 +673,45 @@ PopupMenu.prototype = {
menuItem.connect('destroy', Lang.bind(this, function(emitter) {
menuItem.disconnect(menuItem._activateId);
menuItem.disconnect(menuItem._activeChangeId);
+ if (menuItem.menu) {
+ menuItem.menu.disconnect(menuItem._subMenuActivateId);
+ menuItem.menu.disconnect(menuItem._subMenuActiveChangeId);
+ this.disconnect(menuItem._closingId);
+ }
if (menuItem == this._activeMenuItem)
this._activeMenuItem = null;
}));
},
+ getColumnWidths: function() {
+ let columnWidths = [];
+ let items = this.box.get_children();
+ for (let i = 0; i < items.length; i++) {
+ if (items[i]._delegate instanceof PopupBaseMenuItem || items[i]._delegate instanceof PopupMenuBase) {
+ let itemColumnWidths = items[i]._delegate.getColumnWidths();
+ for (let j = 0; j < itemColumnWidths.length; j++) {
+ if (j >= columnWidths.length || itemColumnWidths[j] > columnWidths[j])
+ columnWidths[j] = itemColumnWidths[j];
+ }
+ }
+ }
+ return columnWidths;
+ },
+
+ setColumnWidths: function(widths) {
+ let items = this.box.get_children();
+ for (let i = 0; i < items.length; i++) {
+ if (items[i]._delegate instanceof PopupBaseMenuItem || items[i]._delegate instanceof PopupMenuBase)
+ items[i]._delegate.setColumnWidths(widths);
+ }
+ },
+
addActor: function(actor) {
- this._box.add(actor);
+ this.box.add(actor);
},
getMenuItems: function() {
- return this._box.get_children().map(function (actor) { return actor._delegate; });
+ return this.box.get_children().map(function (actor) { return actor._delegate; }).filter(function(item) { return item instanceof PopupBaseMenuItem; });
},
removeAll: function() {
@@ -735,61 +723,94 @@ PopupMenu.prototype = {
},
activateFirst: function() {
- let children = this._box.get_children();
+ let children = this.box.get_children();
for (let i = 0; i < children.length; i++) {
let actor = children[i];
- if (actor._delegate && actor.visible && actor.reactive) {
+ if (actor._delegate && actor._delegate instanceof PopupBaseMenuItem && actor.visible && actor.reactive) {
actor._delegate.setActive(true);
break;
}
}
},
- open: function() {
+ toggle: function() {
if (this.isOpen)
- return;
+ this.close();
+ else
+ this.open();
+ },
- let primary = global.get_primary_monitor();
+ destroy: function() {
+ this.removeAll();
+ this.actor.destroy();
- // We need to show it now to force an allocation,
- // so that we can query the correct size.
- this.actor.show();
+ this.emit('destroy');
+ }
+};
+Signals.addSignalMethods(PopupMenuBase.prototype);
- // Position correctly relative to the sourceActor
- let [sourceX, sourceY] = this.sourceActor.get_transformed_position();
- let [sourceWidth, sourceHeight] = this.sourceActor.get_transformed_size();
+function PopupMenu() {
+ this._init.apply(this, arguments);
+}
- let [minWidth, minHeight, natWidth, natHeight] = this.actor.get_preferred_size();
+PopupMenu.prototype = {
+ __proto__: PopupMenuBase.prototype,
- let menuWidth = natWidth, menuHeight = natHeight;
+ _init: function(sourceActor, alignment, arrowSide, gap) {
+ PopupMenuBase.prototype._init.call (this, sourceActor, 'popup-menu-content');
- // Position the non-pointing axis
- if (this._isSubmenu) {
- if (this._arrowSide == St.Side.TOP || this._arrowSide == St.Side.BOTTOM) {
- // vertical submenu
- if (sourceY + sourceHeigth + menuHeight + this._gap < primary.y + primary.height)
- this._boxPointer._arrowSide = this._arrowSide = St.Side.TOP;
- else if (primary.y + menuHeight + this._gap < sourceY)
- this._boxPointer._arrowSide = this._arrowSide = St.Side.BOTTOM;
- else
- this._boxPointer._arrowSide = this._arrowSide = St.Side.TOP;
- } else {
- // horizontal submenu
- if (sourceX + sourceWidth + menuWidth + this._gap < primary.x + primary.width)
- this._boxPointer._arrowSide = this._arrowSide = St.Side.LEFT;
- else if (primary.x + menuWidth + this._gap < sourceX)
- this._boxPointer._arrowSide = this._arrowSide = St.Side.RIGHT;
- else
- this._boxPointer._arrowSide = this._arrowSide = St.Side.LEFT;
- }
- }
+ this._alignment = alignment;
+ this._arrowSide = arrowSide;
+ this._gap = gap;
- this._boxPointer.setPosition(this.sourceActor, this._gap, this._alignment);
+ this._boxPointer = new BoxPointer.BoxPointer(arrowSide,
+ { x_fill: true,
+ y_fill: true,
+ x_align: St.Align.START });
+ this.actor = this._boxPointer.actor;
+ this.actor._delegate = this;
+ this.actor.style_class = 'popup-menu-boxpointer';
+ this._boxWrapper = new Shell.GenericContainer();
+ this._boxWrapper.connect('get-preferred-width', Lang.bind(this, this._boxGetPreferredWidth));
+ this._boxWrapper.connect('get-preferred-height', Lang.bind(this, this._boxGetPreferredHeight));
+ this._boxWrapper.connect('allocate', Lang.bind(this, this._boxAllocate));
+ this._boxPointer.bin.set_child(this._boxWrapper);
+ this._boxWrapper.add_actor(this.box);
+ this.actor.add_style_class_name('popup-menu');
- // Now show it
+ global.focus_manager.add_group(this.actor);
this.actor.reactive = true;
- this._boxPointer.animateAppear();
+ },
+
+ _boxGetPreferredWidth: function (actor, forHeight, alloc) {
+ let columnWidths = this.getColumnWidths();
+ this.setColumnWidths(columnWidths);
+
+ // Now they will request the right sizes
+ [alloc.min_size, alloc.natural_size] = this.box.get_preferred_width(forHeight);
+ },
+
+ _boxGetPreferredHeight: function (actor, forWidth, alloc) {
+ [alloc.min_size, alloc.natural_size] = this.box.get_preferred_height(forWidth);
+ },
+
+ _boxAllocate: function (actor, box, flags) {
+ this.box.allocate(box, flags);
+ },
+
+ setArrowOrigin: function(origin) {
+ this._boxPointer.setArrowOrigin(origin);
+ },
+
+ open: function() {
+ if (this.isOpen)
+ return;
+
this.isOpen = true;
+
+ this._boxPointer.setPosition(this.sourceActor, this._gap, this._alignment);
+ this._boxPointer.animateAppear();
+
this.emit('open-state-changed', true);
},
@@ -799,51 +820,113 @@ PopupMenu.prototype = {
if (this._activeMenuItem)
this._activeMenuItem.setActive(false);
- this.actor.reactive = false;
+
this._boxPointer.animateDisappear();
+
this.isOpen = false;
this.emit('open-state-changed', false);
+ }
+};
+
+function PopupSubMenu() {
+ this._init.apply(this, arguments);
+}
+
+PopupSubMenu.prototype = {
+ __proto__: PopupMenuBase.prototype,
+
+ _init: function(sourceActor, sourceArrow) {
+ PopupMenuBase.prototype._init.call(this, sourceActor, 'popup-sub-menu');
+
+ this._arrow = sourceArrow;
+ this._arrow.rotation_center_z_gravity = Clutter.Gravity.CENTER;
+
+ this.actor = this.box;
+ this.actor._delegate = this;
+ this.actor.clip_to_allocation = true;
+ this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
+ this.actor.hide();
},
- toggle: function() {
+ open: function() {
if (this.isOpen)
- this.close();
- else
- this.open();
+ return;
+
+ this.isOpen = true;
+
+ let [naturalHeight, minHeight] = this.actor.get_preferred_height(-1);
+ this.actor.height = 0;
+ this.actor.show();
+ this.actor._arrow_rotation = this._arrow.rotation_angle_z;
+ Tweener.addTween(this.actor,
+ { _arrow_rotation: 90,
+ height: naturalHeight,
+ time: 0.25,
+ onUpdateScope: this,
+ onUpdate: function() {
+ this._arrow.rotation_angle_z = this.actor._arrow_rotation;
+ },
+ onCompleteScope: this,
+ onComplete: function() {
+ this.actor.set_height(-1);
+ this.emit('open-state-changed', true);
+ }
+ });
},
- _onKeyPressEvent: function(actor, event) {
- // Move focus back to parent menu if the user types Left.
- // (This handler is only connected if the PopupMenu is a
- // submenu.)
- if (this.isOpen &&
- this._activeMenuItem &&
- event.get_key_symbol() == Clutter.KEY_Left) {
+ close: function() {
+ if (!this.isOpen)
+ return;
+
+ this.isOpen = false;
+
+ if (this._activeMenuItem)
this._activeMenuItem.setActive(false);
- return true;
- }
- return false;
- },
+ this.actor._arrow_rotation = this._arrow.rotation_angle_z;
+ Tweener.addTween(this.actor,
+ { _arrow_rotation: 0,
+ height: 0,
+ time: 0.25,
+ onCompleteScope: this,
+ onComplete: function() {
+ this.actor.hide();
+ this.actor.set_height(-1);
+
+ this.emit('open-state-changed', false);
+ },
+ onUpdateScope: this,
+ onUpdate: function() {
+ this._arrow.rotation_angle_z = this.actor._arrow_rotation;
+ }
+ });
+ },
+
+ immediateClose: function() {
+ if (!this.isOpen)
+ return;
- // return true if the actor is inside the menu or
- // any actor related to the active submenu
- contains: function(actor) {
- if (this.actor.contains(actor))
- return true;
if (this._activeMenuItem)
- return this._activeMenuItem.contains(actor);
- return false;
+ this._activeMenuItem.setActive(false);
+
+ this.actor.hide();
+
+ this.isOpen = false;
+ this.emit('open-state-changed', false);
},
- destroy: function() {
- this.removeAll();
- this.actor.destroy();
+ _onKeyPressEvent: function(actor, event) {
+ // Move focus back to parent menu if the user types Left.
- this.emit('destroy');
+ if (this.isOpen && event.get_key_symbol() == Clutter.KEY_Left) {
+ this.close();
+ this.sourceActor._delegate.setActive(true);
+ return true;
+ }
+
+ return false;
}
};
-Signals.addSignalMethods(PopupMenu.prototype);
function PopupSubMenuMenuItem() {
this._init.apply(this, arguments);
@@ -853,77 +936,46 @@ PopupSubMenuMenuItem.prototype = {
__proto__: PopupBaseMenuItem.prototype,
_init: function(text) {
- PopupBaseMenuItem.prototype._init.call(this, { activate: false, hover: false });
- this.actor.connect('enter-event', Lang.bind(this, this._mouseEnter));
- this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
+ PopupBaseMenuItem.prototype._init.call(this);
+
+ this.actor.add_style_class_name('popup-submenu-menu-item');
this.label = new St.Label({ text: text });
this.addActor(this.label);
- this.addActor(new St.Label({ text: '>' }), { align: St.Align.END });
+ this._triangle = new St.Label({ text: '\u25B8' });
+ this.addActor(this._triangle, { align: St.Align.END });
- this.menu = new PopupMenu(this.actor, St.Align.MIDDLE, St.Side.LEFT, 0, true);
- Main.chrome.addActor(this.menu.actor, { visibleInOverview: true,
- affectsStruts: false });
- this.menu.actor.hide();
-
- this._openStateChangedId = this.menu.connect('open-state-changed', Lang.bind(this, this._subMenuOpenStateChanged));
- this._activateId = this.menu.connect('activate', Lang.bind(this, this._subMenuActivate));
+ this.menu = new PopupSubMenu(this.actor, this._triangle);
+ this.menu.connect('open-state-changed', Lang.bind(this, this._subMenuOpenStateChanged));
},
_subMenuOpenStateChanged: function(menu, open) {
- PopupBaseMenuItem.prototype.setActive.call(this, open);
- },
-
- _subMenuActivate: function(menu, menuItem) {
- this.emit('activate', null);
- },
-
- setMenu: function(newmenu) {
- if (this.menu) {
- this.menu.close();
- this.menu.disconnect(this._openStateChangedId);
- this.menu.disconnect(this._activateId);
- }
- if (newmenu) {
- this._openStateChangedId = newmenu.connect('open-state-changed', Lang.bind(this, this._subMenuOpenStateChanged));
- this._activateId = newmenu.connect('activate', Lang.bind(this, this._subMenuActivate));
- }
- this.menu = newmenu;
+ if (open)
+ this.actor.add_style_pseudo_class('open');
+ else
+ this.actor.remove_style_pseudo_class('open');
},
destroy: function() {
- if (this.menu)
- this.menu.destroy();
+ this.menu.destroy();
PopupBaseMenuItem.prototype.destroy.call(this);
},
- setActive: function(active) {
- if (this.menu) {
- if (active)
- this.menu.open();
- else
- this.menu.close();
- }
-
- PopupBaseMenuItem.prototype.setActive.call(this, active);
- },
-
_onKeyPressEvent: function(actor, event) {
- if (!this.menu)
- return false;
if (event.get_key_symbol() == Clutter.KEY_Right) {
+ this.menu.open();
this.menu.activateFirst();
return true;
}
- return false;
+ return PopupBaseMenuItem.prototype._onKeyPressEvent.call(this, actor, event);
},
- contains: function(actor) {
- return this.menu && this.menu.contains(actor);
+ activate: function(event) {
+ this.menu.open();
},
- _mouseEnter: function(event) {
- this.setActive(true);
+ _onButtonReleaseEvent: function(actor) {
+ this.menu.toggle();
}
};
@@ -1052,7 +1104,7 @@ PopupMenuManager.prototype = {
_eventIsOnActiveMenu: function(event) {
let src = event.get_source();
return this._activeMenu != null
- && (this._activeMenu.contains(src) ||
+ && (this._activeMenu.actor.contains(src) ||
(this._activeMenu.sourceActor && this._activeMenu.sourceActor.contains(src)));
},
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]