From 7fbf8ae4c9888393de191b70b118ea20986c8803 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Thu, 20 May 2010 11:18:46 -0400 Subject: [PATCH] [popupMenu] split this out from panel.js We want to use this menu style in other places as well https://bugzilla.gnome.org/show_bug.cgi?id=619541 --- data/theme/gnome-shell.css | 83 +++--- js/ui/Makefile.am | 1 + js/ui/panel.js | 585 +++---------------------------------- js/ui/popupMenu.js | 572 ++++++++++++++++++++++++++++++++++++ js/ui/statusMenu.js | 23 +- 5 files changed, 675 insertions(+), 589 deletions(-) create mode 100644 js/ui/popupMenu.js diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 61afab79a..87c925088 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -93,6 +93,50 @@ StTooltip { color: #ffffff; } +/* PopupMenu */ + +.popup-menu-boxpointer { + -arrow-border-radius: 9px; + -arrow-background-color: rgba(0,0,0,0.9); + -arrow-border-width: 2px; + -arrow-border-color: #5f5f5f; + -arrow-base: 30px; + -arrow-rise: 15px; +} + +.popup-menu { + color: #ffffff; + font-size: 16px; + min-width: 200px; +} + +/* 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. + */ +.popup-menu-content { + padding: .5em 0px; +} + +.popup-menu-item { + padding: .4em 1.25em; +} + +.popup-menu-item:active { + background-color: #4c4c4c; +} + +.popup-image-menu-item { + spacing: .75em; +} + +.popup-separator-menu-item { + -gradient-height: 2px; + -gradient-start: rgba(8,8,8,0); + -gradient-end: #333333; + -margin-horizontal: 1.5em; + height: 1em; +} + /* Panel */ #panel { @@ -119,45 +163,6 @@ StTooltip { border-radius: 4px 4px 0px 0px; } -.panel-menu-boxpointer { - -arrow-border-radius: 9px; - -arrow-background-color: rgba(0,0,0,0.9); - -arrow-border-width: 2px; - -arrow-border-color: #5f5f5f; - -arrow-base: 30px; - -arrow-rise: 15px; -} - -.panel-menu { - color: #ffffff; - font-size: 16px; - min-width: 200px; -} - -.panel-menu-content { - padding: 10px 0px; -} - -.panel-menu-item { - padding: 6px 20px; -} - -.panel-menu-item:active { - background-color: #4c4c4c; -} - -.panel-image-menu-item { - spacing: 12px; -} - -.panel-separator-menu-item { - -gradient-height: 2px; - -gradient-start: rgba(8,8,8,0); - -gradient-end: #333333; - -margin-horizontal: 30px; - height: 16px; -} - #appMenu { spacing: 4px; } diff --git a/js/ui/Makefile.am b/js/ui/Makefile.am index b701fc677..9dcd24e64 100644 --- a/js/ui/Makefile.am +++ b/js/ui/Makefile.am @@ -24,6 +24,7 @@ dist_jsui_DATA = \ overview.js \ panel.js \ placeDisplay.js \ + popupMenu.js \ runDialog.js \ scripting.js \ search.js \ diff --git a/js/ui/panel.js b/js/ui/panel.js index 6546e8b5c..157113fb5 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -9,22 +9,21 @@ const Meta = imports.gi.Meta; const Pango = imports.gi.Pango; const Shell = imports.gi.Shell; const St = imports.gi.St; -const Tweener = imports.ui.tweener; const Signals = imports.signals; const DBus = imports.dbus; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; const AppDisplay = imports.ui.appDisplay; +const BoxPointer = imports.ui.boxpointer; const Calendar = imports.ui.calendar; const Overview = imports.ui.overview; +const PopupMenu = imports.ui.popupMenu; const Main = imports.ui.main; -const BoxPointer = imports.ui.boxpointer; +const Tweener = imports.ui.tweener; const PANEL_HEIGHT = 26; -const POPUP_ANIMATION_TIME = 0.1; - const PANEL_ICON_SIZE = 24; const HOT_CORNER_ACTIVATION_TIMEOUT = 0.5; @@ -128,292 +127,12 @@ TextShadower.prototype = { } }; -function PanelBaseMenuItem(reactive) { - this._init(reactive); -} - -PanelBaseMenuItem.prototype = { - _init: function (reactive) { - this.actor = new St.Bin({ style_class: 'panel-menu-item', - reactive: reactive, - track_hover: reactive, - x_fill: true, - y_fill: true, - x_align: St.Align.START }); - this.actor._delegate = this; - this.active = false; - - if (reactive) { - this.actor.connect('button-release-event', Lang.bind(this, function (actor, event) { - this.emit('activate', event); - })); - this.actor.connect('notify::hover', Lang.bind(this, this._hoverChanged)); - } - }, - - _hoverChanged: function (actor) { - this.setActive(actor.hover); - }, - - activate: function (event) { - this.emit('activate', event); - }, - - setActive: function (active) { - let activeChanged = active != this.active; - - if (activeChanged) { - this.active = active; - if (active) - this.actor.add_style_pseudo_class('active'); - else - this.actor.remove_style_pseudo_class('active'); - this.emit('active-changed', active); - } - } -}; -Signals.addSignalMethods(PanelBaseMenuItem.prototype); - -function PanelMenuItem(text) { - this._init(text); -} - -PanelMenuItem.prototype = { - __proto__: PanelBaseMenuItem.prototype, - - _init: function (text) { - PanelBaseMenuItem.prototype._init.call(this, true); - - this.label = new St.Label({ text: text }); - this.actor.set_child(this.label); - } -}; - -function PanelSeparatorMenuItem() { - this._init(); -} - -PanelSeparatorMenuItem.prototype = { - __proto__: PanelBaseMenuItem.prototype, - - _init: function () { - PanelBaseMenuItem.prototype._init.call(this, false); - - this._drawingArea = new St.DrawingArea({ style_class: 'panel-separator-menu-item' }); - this.actor.set_child(this._drawingArea); - this._drawingArea.connect('repaint', Lang.bind(this, this._onRepaint)); - }, - - _onRepaint: function(area) { - let cr = area.get_context(); - let themeNode = area.get_theme_node(); - let [width, height] = area.get_surface_size(); - let found, margin, gradientHeight; - [found, margin] = themeNode.get_length('-margin-horizontal', false); - [found, gradientHeight] = themeNode.get_length('-gradient-height', false); - let startColor = new Clutter.Color(); - themeNode.get_color('-gradient-start', false, startColor); - let endColor = new Clutter.Color(); - themeNode.get_color('-gradient-end', false, endColor); - - let gradientWidth = (width - margin * 2); - let gradientOffset = (height - gradientHeight) / 2; - let pattern = new Cairo.LinearGradient(margin, gradientOffset, width - margin, gradientOffset + gradientHeight); - pattern.addColorStopRGBA(0, startColor.red / 255, startColor.green / 255, startColor.blue / 255, startColor.alpha / 255); - pattern.addColorStopRGBA(0.5, endColor.red / 255, endColor.green / 255, endColor.blue / 255, endColor.alpha / 0xFF); - pattern.addColorStopRGBA(1, startColor.red / 255, startColor.green / 255, startColor.blue / 255, startColor.alpha / 255); - cr.setSource(pattern); - cr.rectangle(margin, gradientOffset, gradientWidth, gradientHeight); - cr.fill(); - } -}; - -function PanelImageMenuItem(text, iconName, alwaysShowImage) { - this._init(text, iconName, alwaysShowImage); -} - -// We need to instantiate a GtkImageMenuItem so it -// hooks up its properties on the GtkSettings -var _gtkImageMenuItemCreated = false; - -PanelImageMenuItem.prototype = { - __proto__: PanelBaseMenuItem.prototype, - - _init: function (text, iconName, alwaysShowImage) { - PanelBaseMenuItem.prototype._init.call(this, true); - - if (!_gtkImageMenuItemCreated) { - let menuItem = new Gtk.ImageMenuItem(); - menuItem.destroy(); - _gtkImageMenuItemCreated = true; - } - - this._alwaysShowImage = alwaysShowImage; - this._iconName = iconName; - this._size = 16; - - let box = new St.BoxLayout({ style_class: 'panel-image-menu-item' }); - this.actor.set_child(box); - this._imageBin = new St.Bin({ width: this._size, height: this._size }); - box.add(this._imageBin, { y_fill: false }); - box.add(new St.Label({ text: text }), { expand: true }); - - if (!alwaysShowImage) { - let settings = Gtk.Settings.get_default(); - settings.connect('notify::gtk-menu-images', Lang.bind(this, this._onMenuImagesChanged)); - } - this._onMenuImagesChanged(); - }, - - _onMenuImagesChanged: function() { - let show; - if (this._alwaysShowImage) { - show = true; - } else { - let settings = Gtk.Settings.get_default(); - show = settings.gtk_menu_images; - } - if (!show) { - this._imageBin.hide(); - } else { - let img = St.TextureCache.get_default().load_icon_name(this._iconName, this._size); - this._imageBin.set_child(img); - this._imageBin.show(); - } - } -}; - -function mod(a, b) { - return (a + b) % b; -} - -function findNextInCycle(items, current, direction) { - let cur; - - if (items.length == 0) - return current; - else if (items.length == 1) - return items[0]; - - if (current) - cur = items.indexOf(current); - else if (direction == 1) - cur = items.length - 1; - else - cur = 0; - - return items[mod(cur + direction, items.length)]; -} - -function PanelMenu(sourceButton) { - this._init(sourceButton); -} - -PanelMenu.prototype = { - _init: function(sourceButton) { - this._sourceButton = sourceButton; - this._boxPointer = new BoxPointer.BoxPointer(St.Side.TOP, { x_fill: true, y_fill: true, x_align: St.Align.START }); - this.actor = this._boxPointer.actor; - this.actor.style_class = 'panel-menu-boxpointer'; - this._box = new St.BoxLayout({ style_class: 'panel-menu-content', - vertical: true }); - this._boxPointer.bin.set_child(this._box); - this.actor.add_style_class_name('panel-menu'); - - this._activeMenuItem = null; - }, - - addAction: function(title, callback) { - var menuItem = new PanelMenuItem(title); - this.addMenuItem(menuItem); - menuItem.connect('activate', Lang.bind(this, function (menuItem, event) { - callback(event); - })); - }, - - addMenuItem: function(menuItem) { - this._box.add(menuItem.actor); - menuItem.connect('active-changed', Lang.bind(this, function (menuItem, active) { - if (active && this._activeMenuItem != menuItem) { - if (this._activeMenuItem) - this._activeMenuItem.setActive(false); - this._activeMenuItem = menuItem; - } else if (!active && this._activeMenuItem == menuItem) { - this._activeMenuItem = null; - } - })); - menuItem.connect('activate', Lang.bind(this, function (menuItem, event) { - this.emit('activate'); - })); - }, - - addActor: function(actor) { - this._box.add(actor); - }, - - setArrowOrigin: function(origin) { - this._boxPointer.setArrowOrigin(origin); - }, - - open: function() { - let panelActor = Main.panel.actor; - this.actor.lower(panelActor); - - this.actor.show(); - this.actor.opacity = 0; - this.actor.reactive = true; - Tweener.addTween(this.actor, { opacity: 255, - transition: "easeOutQuad", - time: POPUP_ANIMATION_TIME }); - }, - - close: function() { - this.actor.reactive = false; - Tweener.addTween(this.actor, { opacity: 0, - transition: "easeOutQuad", - time: POPUP_ANIMATION_TIME, - onComplete: Lang.bind(this, function () { this.actor.hide(); })}); - if (this._activeMenuItem) - this._activeMenuItem.setActive(false); - }, - - handleKeyPress: function(event) { - if (event.get_key_symbol() == Clutter.space || - event.get_key_symbol() == Clutter.Return) { - if (this._activeMenuItem) - this._activeMenuItem.activate(event); - return true; - } else if (event.get_key_symbol() == Clutter.Down - || event.get_key_symbol() == Clutter.Up) { - let items = this._box.get_children().filter(function (child) { return child.visible && child.reactive; }); - let current = this._activeMenuItem ? this._activeMenuItem.actor : null; - let direction = event.get_key_symbol() == Clutter.Down ? 1 : -1; - - let next = findNextInCycle(items, current, direction); - if (next) { - next._delegate.setActive(true); - return true; - } - } - - return false; - } -}; -Signals.addSignalMethods(PanelMenu.prototype); - function PanelMenuButton(menuAlignment) { this._init(menuAlignment); } PanelMenuButton.prototype = { - State: { - OPEN: 0, - TRANSITIONING: 1, - CLOSED: 2 - }, - _init: function(menuAlignment) { - this._menuAlignment = menuAlignment; this.actor = new St.Bin({ style_class: 'panel-button', reactive: true, x_fill: true, @@ -421,225 +140,22 @@ PanelMenuButton.prototype = { track_hover: true }); this.actor._delegate = this; this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress)); - this._state = this.State.CLOSED; - this.menu = new PanelMenu(this.actor); - this.menu.connect('activate', Lang.bind(this, this._onActivated)); + this.menu = new PopupMenu.PopupMenu(this.actor, menuAlignment, St.Side.TOP, /* FIXME */ 0); + this.menu.connect('open-state-changed', Lang.bind(this, this._onOpenStateChanged)); Main.chrome.addActor(this.menu.actor, { visibleInOverview: true, affectsStruts: false }); this.menu.actor.hide(); }, - open: function() { - if (this._state != this.State.CLOSED) - return; - this._state = this.State.OPEN; - - this.menu.open(); - this._repositionMenu(); - - this.actor.add_style_pseudo_class('pressed'); - this.emit('open-state-changed', true); - }, - - _onActivated: function(button) { - this.emit('activate'); - this.close(); - }, - _onButtonPress: function(actor, event) { - this.toggle(); + this.menu.toggle(); }, - _repositionMenu: function() { - let primary = global.get_primary_monitor(); - - // Positioning for the source button - let [buttonX, buttonY] = this.actor.get_transformed_position(); - let [buttonWidth, buttonHeight] = this.actor.get_transformed_size(); - - let [minWidth, minHeight, natWidth, natHeight] = this.menu.actor.get_preferred_size(); - - // Adjust X position for alignment - let stageX = buttonX; - switch (this._menuAlignment) { - case St.Align.END: - stageX -= (natWidth - buttonWidth); - break; - case St.Align.MIDDLE: - stageX -= Math.floor((natWidth - buttonWidth) / 2); - break; - } - - // Ensure we fit on the x position - stageX = Math.min(stageX, primary.x + primary.width - natWidth); - stageX = Math.max(stageX, primary.x); - - // Actually set the position - let panelActor = Main.panel.actor; - this.menu.actor.x = stageX; - this.menu.actor.y = Math.floor(panelActor.y + panelActor.height); - - // And adjust the arrow - this.menu.setArrowOrigin((buttonX - stageX) + Math.floor(buttonWidth / 2)); - }, - - close: function() { - if (this._state != this.State.OPEN) - return; - this._state = this.State.CLOSED; - this.menu.close(); - this.actor.remove_style_pseudo_class('pressed'); - this.emit('open-state-changed', false); - }, - - toggle: function() { - if (this._state == this.State.OPEN) - this.close(); + _onOpenStateChanged: function(menu, open) { + if (open) + this.actor.add_style_pseudo_class('pressed'); else - this.open(); - } -}; -Signals.addSignalMethods(PanelMenuButton.prototype); - - -/* Basic implementation of a menu container. - * Call _addMenu to add menu buttons. - */ -function PanelMenuBar() { - this._init(); -} - -PanelMenuBar.prototype = { - _init: function() { - this.actor = new St.BoxLayout({ style_class: 'menu-bar', - reactive: true }); - this.isMenuOpen = false; - - // these are more "private" - this._eventCaptureId = 0; - this._activeMenuButton = null; - this._menus = []; - }, - - _addMenu: function(button) { - this._menus.push(button); - button.actor.connect('enter-event', Lang.bind(this, this._onMenuEnter, button)); - button.actor.connect('leave-event', Lang.bind(this, this._onMenuLeave, button)); - button.actor.connect('button-press-event', Lang.bind(this, this._onMenuPress, button)); - button.connect('open-state-changed', Lang.bind(this, this._onMenuOpenState)); - button.connect('activate', Lang.bind(this, this._onMenuActivated)); - }, - - _onMenuOpenState: function(button, isOpen) { - if (!isOpen && button == this._activeMenuButton) { - this._activeMenuButton = null; - } else if (isOpen) { - this._activeMenuButton = button; - } - }, - - _onMenuEnter: function(actor, event, button) { - if (!this.isMenuOpen || button == this._activeMenuButton) - return false; - - if (this._activeMenuButton != null) - this._activeMenuButton.close(); - button.open(); - return false; - }, - - _onMenuLeave: function(actor, event, button) { - return false; - }, - - _onMenuPress: function(actor, event, button) { - if (this.isMenuOpen) - return false; - Main.pushModal(this.actor); - this._eventCaptureId = global.stage.connect('captured-event', Lang.bind(this, this._onEventCapture)); - this.isMenuOpen = true; - return false; - }, - - _onMenuActivated: function(button) { - if (this.isMenuOpen) - this._closeMenu(); - }, - - _containsActor: function(container, actor) { - let parent = actor; - while (parent != null) { - if (parent == container) - return true; - parent = parent.get_parent(); - } - return false; - }, - - _eventIsOnActiveMenu: function(event) { - let src = event.get_source(); - return this._activeMenuButton != null - && (this._containsActor(this._activeMenuButton.actor, src) || - this._containsActor(this._activeMenuButton.menu.actor, src)); - }, - - _eventIsOnAnyMenuButton: function(event) { - let src = event.get_source(); - for (let i = 0; i < this._menus.length; i++) { - let actor = this._menus[i].actor; - if (this._containsActor(actor, src)) - return true; - } - return false; - }, - - _onEventCapture: function(actor, event) { - if (!this.isMenuOpen) - return false; - let activeMenuContains = this._eventIsOnActiveMenu(event); - let eventType = event.type(); - if (eventType == Clutter.EventType.BUTTON_RELEASE) { - if (activeMenuContains) { - return false; - } else { - if (this._activeMenuButton != null) - this._activeMenuButton.close(); - this._closeMenu(); - return true; - } - } else if ((eventType == Clutter.EventType.BUTTON_PRESS && !activeMenuContains) - || (eventType == Clutter.EventType.KEY_PRESS && event.get_key_symbol() == Clutter.Escape)) { - if (this._activeMenuButton != null) - this._activeMenuButton.close(); - this._closeMenu(); - return true; - } else if (eventType == Clutter.EventType.KEY_PRESS - && this._activeMenuButton != null - && this._activeMenuButton.menu.handleKeyPress(event)) { - return true; - } else if (eventType == Clutter.EventType.KEY_PRESS - && this._activeMenuButton != null - && (event.get_key_symbol() == Clutter.Left - || event.get_key_symbol() == Clutter.Right)) { - let direction = event.get_key_symbol() == Clutter.Right ? 1 : -1; - let next = findNextInCycle(this._menus, this._activeMenuButton, direction); - if (next != this._activeMenuButton) { - this._activeMenuButton.close(); - next.open(); - } - return true; - } else if (activeMenuContains || this._eventIsOnAnyMenuButton(event)) { - return false; - } - - return true; - }, - - _closeMenu: function() { - global.stage.disconnect(this._eventCaptureId); - this._eventCaptureId = 0; - Main.popModal(this.actor); - this.isMenuOpen = false; + this.actor.remove_style_pseudo_class('pressed'); } }; @@ -677,7 +193,7 @@ AppMenuButton.prototype = { this._label = new TextShadower(); this._container.add_actor(this._label.actor); - this._quitMenu = new PanelMenuItem(''); + this._quitMenu = new PopupMenu.PopupMenuItem(''); this.menu.addMenuItem(this._quitMenu); this._quitMenu.connect('activate', Lang.bind(this, this._onQuit)); @@ -840,7 +356,6 @@ ClockButton.prototype = { this._clock = new St.Label(); this.actor.set_child(this._clock); - this._calendarState = this.State.CLOSED; this._calendarPopup = null; let gconf = Shell.GConf.get_default(); @@ -852,49 +367,31 @@ ClockButton.prototype = { _onButtonPress: function(actor, event) { let button = event.get_button(); - if (button == 3 && this._calendarState != this.State.OPEN) - this.toggle(); + if (button == 3 && + (!this._calendarPopup || !this._calendarPopup.isOpen)) + this.menu.toggle(); else this._toggleCalendar(); }, closeCalendar: function() { - if (this._calendarState == this.State.CLOSED) + if (!this._calendarPopup || !this._calendarPopup.isOpen) return; - this._calendarState = this.State.CLOSED; this._calendarPopup.hide(); - // closing the calendar should toggle off the menubar as well - this.emit('open-state-changed', false); + this.menu.isOpen = false; this.actor.remove_style_pseudo_class('pressed'); }, openCalendar: function() { - this._calendarState = this.State.OPEN; this._calendarPopup.show(); + // simulate an open menu, so it won't appear beneath the calendar + this.menu.isOpen = true; this.actor.add_style_pseudo_class('pressed'); }, - close: function() { - if (this._calendarState == this.State.OPEN) - return; - - PanelMenuButton.prototype.close.call(this); - }, - - open: function() { - if (this._calendarState == this.State.OPEN) { - // trick the menubar into assuming an open menu so it'll - // pass button events on to us - this.emit('open-state-changed', true); - return; - } - - PanelMenuButton.prototype.open.call(this); - }, - _onPrefs: function() { let args = ['gnome-shell-clock-preferences']; let p = new Shell.Process({ args: args }); @@ -903,21 +400,20 @@ ClockButton.prototype = { }, _toggleCalendar: function() { - if (this._state == this.State.OPEN) { - this.close(); - return; - } - if (this._calendarPopup == null) { this._calendarPopup = new CalendarPopup(); this._calendarPopup.actor.hide(); } - if (this._calendarState == this.State.CLOSED) { - this.openCalendar(); - } else { - this.closeCalendar(); + if (this.menu.isOpen && !this._calendarPopup.isOpen) { + this.menu.close(); + return; } + + if (!this._calendarPopup.isOpen) + this.openCalendar(); + else + this.closeCalendar(); }, _updateClock: function() { @@ -993,13 +489,14 @@ function Panel() { } Panel.prototype = { - __proto__: PanelMenuBar.prototype, - _init : function() { - PanelMenuBar.prototype._init.call(this); - this.actor.name = 'panel'; + this.actor = new St.BoxLayout({ style_class: 'menu-bar', + name: 'panel', + reactive: true }); this.actor._delegate = this; + this._menus = new PopupMenu.PopupMenuManager(this); + this._leftBox = new St.BoxLayout({ name: 'panelLeft' }); this._centerBox = new St.BoxLayout({ name: 'panelCenter' }); this._rightBox = new St.BoxLayout({ name: 'panelRight' }); @@ -1115,14 +612,14 @@ Panel.prototype = { let appMenuButton = new AppMenuButton(); this._leftBox.add(appMenuButton.actor); - this._addMenu(appMenuButton); + this._menus.addMenu(appMenuButton.menu); /* center */ this._clockButton = new ClockButton(); this._centerBox.add(this._clockButton.actor, { y_fill: true }); - this._addMenu(this._clockButton); + this._menus.addMenu(this._clockButton.menu); /* right */ @@ -1152,7 +649,7 @@ Panel.prototype = { // prototype dependencies. let StatusMenu = imports.ui.statusMenu; this._statusmenu = new StatusMenu.StatusMenuButton(); - this._addMenu(this._statusmenu); + this._menus.addMenu(this._statusmenu.menu); this._rightBox.add(this._statusmenu.actor); // TODO: decide what to do with the rest of the panel in the Overview mode (make it fade-out, become non-reactive, etc.) @@ -1266,7 +763,7 @@ Panel.prototype = { }, _onHotCornerEntered : function() { - if (this.isMenuOpen) + if (this._menus.grabbed) return false; if (!this._hotCornerEntered) { this._hotCornerEntered = true; @@ -1288,7 +785,7 @@ Panel.prototype = { }, _onHotCornerClicked : function() { - if (this.isMenuOpen) + if (this._menus.grabbed) return false; if (!Main.overview.animationInProgress) { this._maybeToggleOverviewOnClick(); @@ -1333,6 +830,8 @@ CalendarPopup.prototype = { this.calendar = new Calendar.Calendar(); this.actor.set_child(this.calendar.actor); + this.isOpen = false; + Main.chrome.addActor(this.actor, { visibleInOverview: true, affectsStruts: false }); this.actor.y = (panelActor.y + panelActor.height - this.actor.height); @@ -1342,6 +841,10 @@ CalendarPopup.prototype = { show: function() { let panelActor = Main.panel.actor; + if (this.isOpen) + return; + this.isOpen = true; + // Reset the calendar to today's date this.calendar.setDate(new Date()); @@ -1358,6 +861,10 @@ CalendarPopup.prototype = { hide: function() { let panelActor = Main.panel.actor; + if (!this.isOpen) + return; + this.isOpen = false; + Tweener.addTween(this.actor, { y: panelActor.y + panelActor.height - this.actor.height, time: 0.2, diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js new file mode 100644 index 000000000..22702f1c6 --- /dev/null +++ b/js/ui/popupMenu.js @@ -0,0 +1,572 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Cairo = imports.cairo; +const Clutter = imports.gi.Clutter; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Mainloop = imports.mainloop; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Signals = imports.signals; + +const Main = imports.ui.main; +const BoxPointer = imports.ui.boxpointer; +const Tweener = imports.ui.tweener; + +const POPUP_ANIMATION_TIME = 0.1; + +function PopupBaseMenuItem(reactive) { + this._init(reactive); +} + +PopupBaseMenuItem.prototype = { + _init: function (reactive) { + this.actor = new St.Bin({ style_class: 'popup-menu-item', + reactive: reactive, + track_hover: reactive, + x_fill: true, + y_fill: true, + x_align: St.Align.START }); + this.actor._delegate = this; + this.active = false; + + if (reactive) { + this.actor.connect('button-release-event', Lang.bind(this, function (actor, event) { + this.emit('activate', event); + })); + this.actor.connect('notify::hover', Lang.bind(this, this._hoverChanged)); + } + }, + + _hoverChanged: function (actor) { + this.setActive(actor.hover); + }, + + activate: function (event) { + this.emit('activate', event); + }, + + setActive: function (active) { + let activeChanged = active != this.active; + + if (activeChanged) { + this.active = active; + if (active) + this.actor.add_style_pseudo_class('active'); + else + this.actor.remove_style_pseudo_class('active'); + this.emit('active-changed', active); + } + } +}; +Signals.addSignalMethods(PopupBaseMenuItem.prototype); + +function PopupMenuItem(text) { + this._init(text); +} + +PopupMenuItem.prototype = { + __proto__: PopupBaseMenuItem.prototype, + + _init: function (text) { + PopupBaseMenuItem.prototype._init.call(this, true); + + this.label = new St.Label({ text: text }); + this.actor.set_child(this.label); + } +}; + +function PopupSeparatorMenuItem() { + this._init(); +} + +PopupSeparatorMenuItem.prototype = { + __proto__: PopupBaseMenuItem.prototype, + + _init: function () { + PopupBaseMenuItem.prototype._init.call(this, false); + + this._drawingArea = new St.DrawingArea({ style_class: 'popup-separator-menu-item' }); + this.actor.set_child(this._drawingArea); + this._drawingArea.connect('repaint', Lang.bind(this, this._onRepaint)); + }, + + _onRepaint: function(area) { + let cr = area.get_context(); + let themeNode = area.get_theme_node(); + let [width, height] = area.get_surface_size(); + let found, margin, gradientHeight; + [found, margin] = themeNode.get_length('-margin-horizontal', false); + [found, gradientHeight] = themeNode.get_length('-gradient-height', false); + let startColor = new Clutter.Color(); + themeNode.get_color('-gradient-start', false, startColor); + let endColor = new Clutter.Color(); + themeNode.get_color('-gradient-end', false, endColor); + + let gradientWidth = (width - margin * 2); + let gradientOffset = (height - gradientHeight) / 2; + let pattern = new Cairo.LinearGradient(margin, gradientOffset, width - margin, gradientOffset + gradientHeight); + pattern.addColorStopRGBA(0, startColor.red / 255, startColor.green / 255, startColor.blue / 255, startColor.alpha / 255); + pattern.addColorStopRGBA(0.5, endColor.red / 255, endColor.green / 255, endColor.blue / 255, endColor.alpha / 255); + pattern.addColorStopRGBA(1, startColor.red / 255, startColor.green / 255, startColor.blue / 255, startColor.alpha / 255); + cr.setSource(pattern); + cr.rectangle(margin, gradientOffset, gradientWidth, gradientHeight); + cr.fill(); + } +}; + +function PopupImageMenuItem(text, iconName, alwaysShowImage) { + this._init(text, iconName, alwaysShowImage); +} + +// We need to instantiate a GtkImageMenuItem so it +// hooks up its properties on the GtkSettings +var _gtkImageMenuItemCreated = false; + +PopupImageMenuItem.prototype = { + __proto__: PopupBaseMenuItem.prototype, + + _init: function (text, iconName, alwaysShowImage) { + PopupBaseMenuItem.prototype._init.call(this, true); + + if (!_gtkImageMenuItemCreated) { + let menuItem = new Gtk.ImageMenuItem(); + menuItem.destroy(); + _gtkImageMenuItemCreated = true; + } + + this._alwaysShowImage = alwaysShowImage; + this._iconName = iconName; + this._size = 16; + + let box = new St.BoxLayout({ style_class: 'popup-image-menu-item' }); + this.actor.set_child(box); + this._imageBin = new St.Bin({ width: this._size, height: this._size }); + box.add(this._imageBin, { y_fill: false }); + box.add(new St.Label({ text: text }), { expand: true }); + + if (!alwaysShowImage) { + let settings = Gtk.Settings.get_default(); + settings.connect('notify::gtk-menu-images', Lang.bind(this, this._onMenuImagesChanged)); + } + this._onMenuImagesChanged(); + }, + + _onMenuImagesChanged: function() { + let show; + if (this._alwaysShowImage) { + show = true; + } else { + let settings = Gtk.Settings.get_default(); + show = settings.gtk_menu_images; + } + if (!show) { + this._imageBin.hide(); + } else { + let img = St.TextureCache.get_default().load_icon_name(this._iconName, this._size); + this._imageBin.set_child(img); + this._imageBin.show(); + } + } +}; + +function mod(a, b) { + return (a + b) % b; +} + +function findNextInCycle(items, current, direction) { + let cur; + + if (items.length == 0) + return current; + else if (items.length == 1) + return items[0]; + + if (current) + cur = items.indexOf(current); + else if (direction == 1) + cur = items.length - 1; + else + cur = 0; + + return items[mod(cur + direction, items.length)]; +} + +function PopupMenu(sourceActor, alignment, arrowSide, gap) { + this._init(sourceActor, alignment, arrowSide, gap); +} + +PopupMenu.prototype = { + _init: function(sourceActor, alignment, arrowSide, gap) { + 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._box = new St.BoxLayout({ style_class: 'popup-menu-content', + vertical: true }); + this._boxPointer.bin.set_child(this._box); + this.actor.add_style_class_name('popup-menu'); + + this.isOpen = false; + this._activeMenuItem = null; + }, + + addAction: function(title, callback) { + var menuItem = new PopupMenuItem(title); + this.addMenuItem(menuItem); + menuItem.connect('activate', Lang.bind(this, function (menuItem, event) { + callback(event); + })); + }, + + addMenuItem: function(menuItem) { + this._box.add(menuItem.actor); + menuItem._activeChangeId = menuItem.connect('active-changed', Lang.bind(this, function (menuItem, active) { + if (active && this._activeMenuItem != menuItem) { + if (this._activeMenuItem) + this._activeMenuItem.setActive(false); + this._activeMenuItem = menuItem; + this.emit('active-changed', menuItem); + } else if (!active && this._activeMenuItem == menuItem) { + this._activeMenuItem = null; + this.emit('active-changed', null); + } + })); + menuItem._activateId = menuItem.connect('activate', Lang.bind(this, function (menuItem, event) { + this.emit('activate', menuItem); + this.close(); + })); + }, + + addActor: function(actor) { + this._box.add(actor); + }, + + getMenuItems: function() { + return this._box.get_children().map(function (actor) { return actor._delegate; }); + }, + + removeAll: function() { + let children = this.getMenuItems(); + for (let i = 0; i < children.length; i++) { + let item = children[i]; + if (item._activeChangeId != 0) + item.disconnect(item._activeChangeId); + if (item._activateId != 0) + item.disconnect(item._activateId); + item.actor.destroy(); + } + }, + + setArrowOrigin: function(origin) { + this._boxPointer.setArrowOrigin(origin); + }, + + open: function() { + if (this.isOpen) + return; + + this.emit('opening'); + + let primary = global.get_primary_monitor(); + + // Position correctly relative to the sourceActor + let [sourceX, sourceY] = this.sourceActor.get_transformed_position(); + let [sourceWidth, sourceHeight] = this.sourceActor.get_transformed_size(); + + let [minWidth, minHeight, natWidth, natHeight] = this.actor.get_preferred_size(); + + let menuX, menuY; + let menuWidth = natWidth, menuHeight = natHeight; + + // Position the non-pointing axis + switch (this._arrowSide) { + case St.Side.TOP: + menuY = sourceY + sourceHeight + this._gap; + break; + case St.Side.BOTTOM: + menuY = sourceY - menuHeight - this._gap; + break; + case St.Side.LEFT: + menuX = sourceX + sourceWidth + this._gap; + break; + case St.Side.RIGHT: + menuX = sourceX - menuWidth - this._gap; + break; + } + + // Now align and position the pointing axis, making sure + // it fits on screen + switch (this._arrowSide) { + case St.Side.TOP: + case St.Side.BOTTOM: + switch (this._alignment) { + case St.Align.START: + menuX = sourceX; + break; + case St.Align.MIDDLE: + menuX = sourceX - Math.floor((menuWidth - sourceWidth) / 2); + break; + case St.Align.END: + menuX = sourceX - (menuWidth - sourceWidth); + break; + } + + menuX = Math.min(menuX, primary.x + primary.width - menuWidth); + menuX = Math.max(menuX, primary.x); + + this._boxPointer.setArrowOrigin((sourceX - menuX) + Math.floor(sourceWidth / 2)); + break; + + case St.Side.LEFT: + case St.Side.RIGHT: + switch (this._alignment) { + case St.Align.START: + menuY = sourceY; + break; + case St.Align.MIDDLE: + menuY = sourceY - Math.floor((menuHeight - sourceHeight) / 2); + break; + case St.Align.END: + menuY = sourceY - (menuHeight - sourceHeight); + break; + } + + menuY = Math.min(menuY, primary.y + primary.height - menuHeight); + menuY = Math.max(menuY, primary.y); + + this._boxPointer.setArrowOrigin((sourceY - menuY) + Math.floor(sourceHeight / 2)); + break; + } + + // Actually set the position + this.actor.x = Math.floor(menuX); + this.actor.y = Math.floor(menuY); + + // Now show it + this.actor.show(); + this.actor.opacity = 0; + this.actor.reactive = true; + Tweener.addTween(this.actor, { opacity: 255, + transition: "easeOutQuad", + time: POPUP_ANIMATION_TIME }); + this.isOpen = true; + this.emit('open-state-changed', true); + }, + + close: function() { + if (!this.isOpen) + return; + + this.actor.reactive = false; + Tweener.addTween(this.actor, { opacity: 0, + transition: "easeOutQuad", + time: POPUP_ANIMATION_TIME, + onComplete: Lang.bind(this, function () { this.actor.hide(); })}); + if (this._activeMenuItem) + this._activeMenuItem.setActive(false); + this.isOpen = false; + this.emit('open-state-changed', false); + }, + + + toggle: function() { + if (this.isOpen) + this.close(); + else + this.open(); + }, + + handleKeyPress: function(event) { + if (event.get_key_symbol() == Clutter.space || + event.get_key_symbol() == Clutter.Return) { + if (this._activeMenuItem) + this._activeMenuItem.activate(event); + return true; + } else if (event.get_key_symbol() == Clutter.Down + || event.get_key_symbol() == Clutter.Up) { + let items = this._box.get_children().filter(function (child) { return child.visible && child.reactive; }); + let current = this._activeMenuItem ? this._activeMenuItem.actor : null; + let direction = event.get_key_symbol() == Clutter.Down ? 1 : -1; + + let next = findNextInCycle(items, current, direction); + if (next) { + next._delegate.setActive(true); + return true; + } + } + + return false; + } +}; +Signals.addSignalMethods(PopupMenu.prototype); + +/* Basic implementation of a menu manager. + * Call addMenu to add menus + */ +function PopupMenuManager(owner) { + this._init(owner); +} + +PopupMenuManager.prototype = { + _init: function(owner) { + this._owner = owner; + this.grabbed = false; + + this._eventCaptureId = 0; + this._enterEventId = 0; + this._leaveEventId = 0; + this._activeMenu = null; + this._menus = []; + this._delayedMenus = []; + }, + + addMenu: function(menu, noGrab) { + this._menus.push(menu); + menu.connect('open-state-changed', Lang.bind(this, this._onMenuOpenState)); + menu.connect('activate', Lang.bind(this, this._onMenuActivated)); + + let source = menu.sourceActor; + if (source) { + source.connect('enter-event', Lang.bind(this, this._onMenuSourceEnter, menu)); + if (!noGrab) + source.connect('button-press-event', Lang.bind(this, this._onMenuSourcePress, menu)); + } + }, + + grab: function() { + Main.pushModal(this._owner.actor); + + this._eventCaptureId = global.stage.connect('captured-event', Lang.bind(this, this._onEventCapture)); + // captured-event doesn't see enter/leave events + this._enterEventId = global.stage.connect('enter-event', Lang.bind(this, this._onEventCapture)); + this._leaveEventId = global.stage.connect('leave-event', Lang.bind(this, this._onEventCapture)); + + this.grabbed = true; + }, + + ungrab: function() { + global.stage.disconnect(this._eventCaptureId); + this._eventCaptureId = 0; + global.stage.disconnect(this._enterEventId); + this._enterEventId = 0; + global.stage.disconnect(this._leaveEventId); + this._leaveEventId = 0; + + Main.popModal(this._owner.actor); + + this.grabbed = false; + }, + + _onMenuOpenState: function(menu, open) { + if (!open && menu == this._activeMenu) + this._activeMenu = null; + else if (open) + this._activeMenu = menu; + }, + + _onMenuSourceEnter: function(actor, event, menu) { + if (!this.grabbed || menu == this._activeMenu) + return false; + + if (this._activeMenu != null) + this._activeMenu.close(); + menu.open(); + return false; + }, + + _onMenuSourcePress: function(actor, event, menu) { + if (this.grabbed) + return false; + this.grab(); + return false; + }, + + _onMenuActivated: function(menu, item) { + if (this.grabbed) + this.ungrab(); + }, + + _containsActor: function(container, actor) { + let parent = actor; + while (parent != null) { + if (parent == container) + return true; + parent = parent.get_parent(); + } + return false; + }, + + _eventIsOnActiveMenu: function(event) { + let src = event.get_source(); + return this._activeMenu != null + && (this._containsActor(this._activeMenu.actor, src) || + this._containsActor(this._activeMenu.sourceActor, src)); + }, + + _eventIsOnAnyMenuSource: function(event) { + let src = event.get_source(); + for (let i = 0; i < this._menus.length; i++) { + let actor = this._menus[i].sourceActor; + if (this._containsActor(actor, src)) + return true; + } + return false; + }, + + _onEventCapture: function(actor, event) { + if (!this.grabbed) + return false; + + if (this._owner.menuEventFilter && + this._owner.menuEventFilter(event)) + return true; + + let activeMenuContains = this._eventIsOnActiveMenu(event); + let eventType = event.type(); + if (eventType == Clutter.EventType.BUTTON_RELEASE) { + if (activeMenuContains) { + return false; + } else { + this._closeMenu(); + return true; + } + } else if ((eventType == Clutter.EventType.BUTTON_PRESS && !activeMenuContains) + || (eventType == Clutter.EventType.KEY_PRESS && event.get_key_symbol() == Clutter.Escape)) { + this._closeMenu(); + return true; + } else if (eventType == Clutter.EventType.KEY_PRESS + && this._activeMenu != null + && this._activeMenu.handleKeyPress(event)) { + return true; + } else if (eventType == Clutter.EventType.KEY_PRESS + && this._activeMenu != null + && (event.get_key_symbol() == Clutter.Left + || event.get_key_symbol() == Clutter.Right)) { + let direction = event.get_key_symbol() == Clutter.Right ? 1 : -1; + let next = findNextInCycle(this._menus, this._activeMenu, direction); + if (next != this._activeMenu) { + this._activeMenu.close(); + next.open(); + } + return true; + } else if (activeMenuContains || this._eventIsOnAnyMenuSource(event)) { + return false; + } + + return true; + }, + + _closeMenu: function() { + if (this._activeMenu != null) + this._activeMenu.close(); + this.ungrab(); + } +}; diff --git a/js/ui/statusMenu.js b/js/ui/statusMenu.js index bf79186ee..ac095dd03 100644 --- a/js/ui/statusMenu.js +++ b/js/ui/statusMenu.js @@ -11,6 +11,7 @@ const _ = Gettext.gettext; const GnomeSession = imports.misc.gnomeSession; const Main = imports.ui.main; const Panel = imports.ui.panel; +const PopupMenu = imports.ui.popupMenu; // Adapted from gdm/gui/user-switch-applet/applet.c // @@ -88,46 +89,46 @@ StatusMenuButton.prototype = { _createSubMenu: function() { let item; - item = new Panel.PanelImageMenuItem(_("Available"), 'gtk-yes', true); + item = new PopupMenu.PopupImageMenuItem(_("Available"), 'gtk-yes', true); item.connect('activate', Lang.bind(this, this._setPresenceStatus, GnomeSession.PresenceStatus.AVAILABLE)); this.menu.addMenuItem(item); - item = new Panel.PanelImageMenuItem(_("Busy"), 'gtk-no', true); + item = new PopupMenu.PopupImageMenuItem(_("Busy"), 'gtk-no', true); item.connect('activate', Lang.bind(this, this._setPresenceStatus, GnomeSession.PresenceStatus.BUSY)); this.menu.addMenuItem(item); - item = new Panel.PanelImageMenuItem(_("Invisible"), 'gtk-close', true); + item = new PopupMenu.PopupImageMenuItem(_("Invisible"), 'gtk-close', true); item.connect('activate', Lang.bind(this, this._setPresenceStatus, GnomeSession.PresenceStatus.INVISIBLE)); this.menu.addMenuItem(item); - item = new Panel.PanelSeparatorMenuItem(); + item = new PopupMenu.PopupSeparatorMenuItem(); this.menu.addMenuItem(item); - item = new Panel.PanelImageMenuItem(_("Account Information..."), 'user-info'); + item = new PopupMenu.PopupImageMenuItem(_("Account Information..."), 'user-info'); item.connect('activate', Lang.bind(this, this._onAccountInformationActivate)); this.menu.addMenuItem(item); - item = new Panel.PanelImageMenuItem(_("System Preferences..."), 'preferences-desktop'); + item = new PopupMenu.PopupImageMenuItem(_("System Preferences..."), 'preferences-desktop'); item.connect('activate', Lang.bind(this, this._onPreferencesActivate)); this.menu.addMenuItem(item); - item = new Panel.PanelSeparatorMenuItem(); + item = new PopupMenu.PopupSeparatorMenuItem(); this.menu.addMenuItem(item); - item = new Panel.PanelImageMenuItem(_("Lock Screen"), 'system-lock-screen'); + item = new PopupMenu.PopupImageMenuItem(_("Lock Screen"), 'system-lock-screen'); item.connect('activate', Lang.bind(this, this._onLockScreenActivate)); this.menu.addMenuItem(item); - item = new Panel.PanelImageMenuItem(_("Switch User"), 'system-users'); + item = new PopupMenu.PopupImageMenuItem(_("Switch User"), 'system-users'); item.connect('activate', Lang.bind(this, this._onLoginScreenActivate)); this.menu.addMenuItem(item); this._loginScreenItem = item; - item = new Panel.PanelImageMenuItem(_("Log Out..."), 'system-log-out'); + item = new PopupMenu.PopupImageMenuItem(_("Log Out..."), 'system-log-out'); item.connect('activate', Lang.bind(this, this._onQuitSessionActivate)); this.menu.addMenuItem(item); - item = new Panel.PanelImageMenuItem(_("Shut Down..."), 'system-shutdown'); + item = new PopupMenu.PopupImageMenuItem(_("Shut Down..."), 'system-shutdown'); item.connect('activate', Lang.bind(this, this._onShutDownActivate)); this.menu.addMenuItem(item); },