[gnome-shell] PopupMenu: handle submenus inline



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]