[gnome-shell-extensions] apps-menu: Replace it with a new version based on AxeMenu
- From: Debarshi Ray <debarshir src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell-extensions] apps-menu: Replace it with a new version based on AxeMenu
- Date: Wed, 30 Jan 2013 16:54:25 +0000 (UTC)
commit 9211fa4409daf395b08d8598499c9e4b2c6b4916
Author: Debarshi Ray <debarshir gnome org>
Date: Fri Jan 4 18:31:57 2013 +0100
apps-menu: Replace it with a new version based on AxeMenu
This is a severely toned down version of the original AxeMenu:
- the column on the left has been removed, because it duplicates
functionality provided by the places-menu and the user menu
- the application search functionality has been removed because it
is already provided by vanilla gnome-shell
- the "All" category ended up being too crowded and has been replaced
by "Favorites", so there is no separate page for it any more
https://bugzilla.gnome.org/show_bug.cgi?id=692527
extensions/apps-menu/extension.js | 536 ++++++++++++++++++++++++++++++---
extensions/apps-menu/metadata.json.in | 2 +-
2 files changed, 492 insertions(+), 46 deletions(-)
---
diff --git a/extensions/apps-menu/extension.js b/extensions/apps-menu/extension.js
index faf099f..c214ff4 100644
--- a/extensions/apps-menu/extension.js
+++ b/extensions/apps-menu/extension.js
@@ -1,111 +1,557 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+const Atk = imports.gi.Atk;
const GMenu = imports.gi.GMenu;
const Lang = imports.lang;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
-
+const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
+const Gtk = imports.gi.Gtk;
+const GLib = imports.gi.GLib;
+const Signals = imports.signals;
+const Layout = imports.ui.layout;
+const Pango = imports.gi.Pango;
+
+const Gettext = imports.gettext.domain('gnome-shell-extensions');
+const _ = Gettext.gettext;
+
+const ExtensionUtils = imports.misc.extensionUtils;
+const Me = ExtensionUtils.getCurrentExtension();
+const Convenience = Me.imports.convenience;
+
+const appSys = Shell.AppSystem.get_default();
+
+const APPLICATION_ICON_SIZE = 32;
+const MENU_HEIGHT_OFFSET = 132;
+
+function fixMarkup(text, allowMarkup) {
+ if (allowMarkup) {
+ let _text = text.replace(/&(?!amp;|quot;|apos;|lt;|gt;)/g, '&');
+ _text = _text.replace(/<(?!\/?[biu]>)/g, '<');
+ try {
+ Pango.parse_markup(_text, -1, '');
+ return _text;
+ } catch (e) {
+ }
+ }
+ return GLib.markup_escape_text(text, -1);
+}
+
+const ActivitiesMenuItem = new Lang.Class({
+ Name: 'ActivitiesMenuItem',
+ Extends: PopupMenu.PopupBaseMenuItem,
+
+ _init: function(button) {
+ this.parent();
+ this._button = button;
+ this.addActor(new St.Label({ text: _("Activities Overview") }));
+ },
+
+ activate: function(event) {
+ this._button.menu.toggle();
+ Main.overview.toggle();
+ this.parent(event);
+ },
+});
+
+const ApplicationMenuItem = new Lang.Class({
+ Name: 'ApplicationMenuItem',
+ Extends: PopupMenu.PopupBaseMenuItem,
+
+ _init: function(button, app) {
+ this.parent();
+ this._app = app;
+ this._button = button;
+
+ let icon = this._app.create_icon_texture(APPLICATION_ICON_SIZE);
+ this.addActor(icon);
+
+ let appName = fixMarkup(this._app.get_name());
+ this.addActor(new St.Label({ text: appName }));
+ },
-const ICON_SIZE = 28;
+ activate: function(event) {
+ this._app.open_new_window(event.get_time());
+ this._button.selectCategory(null, null);
+ this._button.menu.toggle();
+ this.parent(event);
+ },
-const AppMenuItem = new Lang.Class({
- Name: 'AppsMenu.AppMenuItem',
+ setActive: function(active, params) {
+ if (active)
+ this._button.scrollToButton(this);
+ this.parent(active, params);
+ }
+});
+
+const CategoryMenuItem = new Lang.Class({
+ Name: 'CategoryMenuItem',
Extends: PopupMenu.PopupBaseMenuItem,
- _init: function (app, params) {
- this.parent(params);
+ _init: function(button, category) {
+ this.parent();
+ this._category = category;
+ this._button = button;
+
+ let name;
+ if (this._category)
+ name = this._category.get_name();
+ else
+ name = _("Favorites");
+
+ this.addActor(new St.Label({ text: name }));
+ },
- this._app = app;
- this.label = new St.Label({ text: app.get_name() });
- this.addActor(this.label);
- this._icon = app.create_icon_texture(ICON_SIZE);
- this.addActor(this._icon, { expand: false });
+ activate: function(event) {
+ this._button.selectCategory(this._category, this);
+ this._button.scrollToCatButton(this);
+ this.parent(event);
},
- activate: function (event) {
- this._app.activate_full(-1, event.get_time());
+ setActive: function(active, params) {
+ if (active) {
+ this._button.selectCategory(this._category, this);
+ this._button.scrollToCatButton(this);
+ }
+ this.parent(active, params);
+ }
+});
+
+const HotCorner = new Lang.Class({
+ Name: 'HotCorner',
+ Extends: Layout.HotCorner,
+
+ _init : function() {
+ this.parent();
+ },
- this.parent(event);
+ _onCornerEntered : function() {
+ if (!this._entered) {
+ this._entered = true;
+ if (!Main.overview.animationInProgress) {
+ this._activationTime = Date.now() / 1000;
+ this.rippleAnimation();
+ Main.overview.toggle();
+ }
+ }
+ return false;
}
+});
+
+const ApplicationsMenu = new Lang.Class({
+ Name: 'ApplicationsMenu',
+ Extends: PopupMenu.PopupMenu,
+
+ _init: function(sourceActor, arrowAlignment, arrowSide, button, hotCorner) {
+ this.parent(sourceActor, arrowAlignment, arrowSide);
+ this._button = button;
+ this._hotCorner = hotCorner;
+ },
+ open: function(animate) {
+ this._hotCorner.actor.hide();
+ this.parent(animate);
+ },
+
+ close: function(animate) {
+ this._hotCorner.actor.show();
+ this.parent(animate);
+ },
+
+ toggle: function() {
+ if (this.isOpen) {
+ this._button.selectCategory(null, null);
+ } else {
+ if (Main.overview.visible)
+ Main.overview.hide();
+ }
+ this.parent();
+ }
});
const ApplicationsButton = new Lang.Class({
- Name: 'AppsMenu.ApplicationsButton',
- Extends: PanelMenu.SystemStatusButton,
+ Name: 'ApplicationsButton',
+ Extends: PanelMenu.Button,
_init: function() {
- this.parent('start-here-symbolic');
+ this.parent(1.0, null, false);
+ this._hotCorner = new HotCorner();
+ this.setMenu(new ApplicationsMenu(this.actor, 1.0, St.Side.TOP, this, this._hotCorner));
+ Main.panel.menuManager.addMenu(this.menu);
- this._appSys = Shell.AppSystem.get_default();
- this._installedChangedId = this._appSys.connect('installed-changed', Lang.bind(this, this._refresh));
+ // At this moment applications menu is not keyboard navigable at
+ // all (so not accessible), so it doesn't make sense to set as
+ // role ATK_ROLE_MENU like other elements of the panel.
+ this.actor.accessible_role = Atk.Role.LABEL;
+ let container = new Shell.GenericContainer();
+ container.connect('get-preferred-width', Lang.bind(this, this._containerGetPreferredWidth));
+ container.connect('get-preferred-height', Lang.bind(this, this._containerGetPreferredHeight));
+ container.connect('allocate', Lang.bind(this, this._containerAllocate));
+ this.actor.add_actor(container);
+ this.actor.name = 'panelApplications';
+
+ this._label = new St.Label({ text: _("Applications") });
+ container.add_actor(this._label);
+
+ this.actor.label_actor = this._label;
+
+ container.add_actor(this._hotCorner.actor);
+ Main.messageTray._grabHelper.addActor(this._hotCorner.actor);
+
+ this.actor.connect('captured-event', Lang.bind(this, this._onCapturedEvent));
+
+ _showingId = Main.overview.connect('showing', Lang.bind(this, function() {
+ this.actor.add_accessible_state (Atk.StateType.CHECKED);
+ }));
+ _hidingId = Main.overview.connect('hiding', Lang.bind(this, function() {
+ this.actor.remove_accessible_state (Atk.StateType.CHECKED);
+ }));
+
+ this.reloadFlag = false;
+ this._createLayout();
this._display();
+ _installedChangedId = appSys.connect('installed-changed', Lang.bind(this, function() {
+ if (this.menu.isOpen) {
+ this._redisplay();
+ this.mainBox.show();
+ } else {
+ this.reloadFlag = true;
+ }
+ }));
+
+ // Since the hot corner uses stage coordinates, Clutter won't
+ // queue relayouts for us when the panel moves. Queue a relayout
+ // when that happens.
+ _panelBoxChangedId = Main.layoutManager.connect('panel-box-changed', Lang.bind(this, function() {
+ container.queue_relayout();
+ }));
},
- destroy: function() {
- this._appSys.disconnect(this._installedChangedId);
+ _containerGetPreferredWidth: function(actor, forHeight, alloc) {
+ [alloc.min_size, alloc.natural_size] = this._label.get_preferred_width(forHeight);
+ },
- this.parent();
+ _containerGetPreferredHeight: function(actor, forWidth, alloc) {
+ [alloc.min_size, alloc.natural_size] = this._label.get_preferred_height(forWidth);
},
- _refresh: function() {
- this._clearAll();
- this._display();
+ _containerAllocate: function(actor, box, flags) {
+ this._label.allocate(box, flags);
+
+ // The hot corner needs to be outside any padding/alignment
+ // that has been imposed on us
+ let primary = Main.layoutManager.primaryMonitor;
+ let hotBox = new Clutter.ActorBox();
+ let ok, x, y;
+ if (actor.get_text_direction() == Clutter.TextDirection.LTR) {
+ [ok, x, y] = actor.transform_stage_point(primary.x, primary.y);
+ } else {
+ [ok, x, y] = actor.transform_stage_point(primary.x + primary.width, primary.y);
+ // hotCorner.actor has northeast gravity, so we don't need
+ // to adjust x for its width
+ }
+
+ hotBox.x1 = Math.round(x);
+ hotBox.x2 = hotBox.x1 + this._hotCorner.actor.width;
+ hotBox.y1 = Math.round(y);
+ hotBox.y2 = hotBox.y1 + this._hotCorner.actor.height;
+ this._hotCorner.actor.allocate(hotBox, flags);
+ },
+
+ _createVertSeparator: function() {
+ let separator = new St.DrawingArea({ style_class: 'calendar-vertical-separator',
+ pseudo_class: 'highlighted' });
+ separator.connect('repaint', Lang.bind(this, this._onVertSepRepaint));
+ return separator;
+ },
+
+ _onCapturedEvent: function(actor, event) {
+ if (event.type() == Clutter.EventType.BUTTON_PRESS) {
+ if (!this._hotCorner.shouldToggleOverviewOnClick())
+ return true;
+ }
+ return false;
+ },
+
+ _onVertSepRepaint: function(area) {
+ let cr = area.get_context();
+ let themeNode = area.get_theme_node();
+ let [width, height] = area.get_surface_size();
+ let stippleColor = themeNode.get_color('-stipple-color');
+ let stippleWidth = themeNode.get_length('-stipple-width');
+ let x = Math.floor(width/2) + 0.5;
+ cr.moveTo(x, 0);
+ cr.lineTo(x, height);
+ Clutter.cairo_set_source_color(cr, stippleColor);
+ cr.setDash([1, 3], 1); // Hard-code for now
+ cr.setLineWidth(stippleWidth);
+ cr.stroke();
+ },
+
+ _onOpenStateChanged: function(menu, open) {
+ if (open) {
+ if (this.reloadFlag) {
+ this._redisplay();
+ this.reloadFlag = false;
+ }
+ this.mainBox.show();
+ }
+ this.parent(menu, open);
},
- _clearAll: function() {
- this.menu.removeAll();
+ _redisplay: function() {
+ this.applicationsBox.destroy_all_children();
+ this.categoriesBox.destroy_all_children();
+ this._display();
},
- // Recursively load a GMenuTreeDirectory; we could put this in ShellAppSystem too
- // (taken from js/ui/appDisplay.js in core shell)
- _loadCategory: function(dir, menu) {
- var iter = dir.iter();
- var nextType;
+ _loadCategory: function(dir) {
+ let iter = dir.iter();
+ let nextType;
while ((nextType = iter.next()) != GMenu.TreeItemType.INVALID) {
if (nextType == GMenu.TreeItemType.ENTRY) {
- var entry = iter.get_entry();
- var app = this._appSys.lookup_app_by_tree_entry(entry);
- if (!entry.get_app_info().get_nodisplay())
- menu.addMenuItem(new AppMenuItem(app));
+ let entry = iter.get_entry();
+ if (!entry.get_app_info().get_nodisplay()) {
+ let app = appSys.lookup_app_by_tree_entry(entry);
+ let menu_id = dir.get_menu_id();
+ if (!this.applicationsByCategory[menu_id])
+ this.applicationsByCategory[menu_id] = new Array();
+ this.applicationsByCategory[menu_id].push(app);
+ }
} else if (nextType == GMenu.TreeItemType.DIRECTORY) {
- this._loadCategory(iter.get_directory(), menu);
+ let subdir = iter.get_directory();
+ if (subdir.get_is_nodisplay())
+ continue;
+
+ let menu_id = subdir.get_menu_id();
+ this.applicationsByCategory[menu_id] = new Array();
+ this._loadCategory(subdir);
+ if (this.applicationsByCategory[menu_id].length > 0) {
+ let categoryMenuItem = new CategoryMenuItem(this, subdir);
+ this.categoriesBox.add_actor(categoryMenuItem.actor);
+ }
}
}
},
- _display : function() {
- let tree = this._appSys.get_tree();
- let root = tree.get_root_directory();
+ scrollToButton: function(button) {
+ let appsScrollBoxAdj = this.applicationsScrollBox.get_vscroll_bar().get_adjustment();
+ let appsScrollBoxAlloc = this.applicationsScrollBox.get_allocation_box();
+ let currentScrollValue = appsScrollBoxAdj.get_value();
+ let boxHeight = appsScrollBoxAlloc.y2 - appsScrollBoxAlloc.y1;
+ let buttonAlloc = button.actor.get_allocation_box();
+ let newScrollValue = currentScrollValue;
+ if (currentScrollValue > buttonAlloc.y1 - 10)
+ newScrollValue = buttonAlloc.y1 - 10;
+ if (boxHeight + currentScrollValue < buttonAlloc.y2 + 10)
+ newScrollValue = buttonAlloc.y2 - boxHeight + 10;
+ if (newScrollValue != currentScrollValue)
+ appsScrollBoxAdj.set_value(newScrollValue);
+ },
+
+ scrollToCatButton: function(button) {
+ let catsScrollBoxAdj = this.categoriesScrollBox.get_vscroll_bar().get_adjustment();
+ let catsScrollBoxAlloc = this.categoriesScrollBox.get_allocation_box();
+ let currentScrollValue = catsScrollBoxAdj.get_value();
+ let boxHeight = catsScrollBoxAlloc.y2 - catsScrollBoxAlloc.y1;
+ let buttonAlloc = button.actor.get_allocation_box();
+ let newScrollValue = currentScrollValue;
+ if (currentScrollValue > buttonAlloc.y1 - 10)
+ newScrollValue = buttonAlloc.y1 - 10;
+ if (boxHeight + currentScrollValue < buttonAlloc.y2 + 10)
+ newScrollValue = buttonAlloc.y2 - boxHeight + 10;
+ if (newScrollValue != currentScrollValue)
+ catsScrollBoxAdj.set_value(newScrollValue);
+ },
+
+ _createLayout: function() {
+ let section = new PopupMenu.PopupMenuSection();
+ this.menu.addMenuItem(section);
+ this.mainBox = new St.BoxLayout({ vertical: false });
+ this.leftBox = new St.BoxLayout({ vertical: true });
+ this.applicationsScrollBox = new St.ScrollView({ x_fill: true, y_fill: false,
+ y_align: St.Align.START,
+ style_class: 'vfade' });
+ this.applicationsScrollBox.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
+ let vscroll = this.applicationsScrollBox.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.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();
+ 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.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);
+ },
+
+ _display: function() {
+ this._applicationsButtons = new Array();
+ this.mainBox.style=('width: 640px;');
+ this.mainBox.hide();
+
+ //Load categories
+ this.applicationsByCategory = {};
+ let tree = appSys.get_tree();
+ let root = tree.get_root_directory();
+ let categoryMenuItem = new CategoryMenuItem(this, null);
+ this.categoriesBox.add_actor(categoryMenuItem.actor);
let iter = root.iter();
let nextType;
while ((nextType = iter.next()) != GMenu.TreeItemType.INVALID) {
if (nextType == GMenu.TreeItemType.DIRECTORY) {
let dir = iter.get_directory();
- let item = new PopupMenu.PopupSubMenuMenuItem(dir.get_name());
- this._loadCategory(dir, item.menu);
- this.menu.addMenuItem(item);
+ if (dir.get_is_nodisplay())
+ continue;
+
+ let menu_id = dir.get_menu_id();
+ this.applicationsByCategory[menu_id] = new Array();
+ this._loadCategory(dir);
+ if (this.applicationsByCategory[menu_id].length > 0) {
+ let categoryMenuItem = new CategoryMenuItem(this, dir);
+ this.categoriesBox.add_actor(categoryMenuItem.actor);
+ }
}
}
+
+ //Load applications
+ this._displayButtons(this._listApplications(null));
+
+ let height = this.categoriesBox.height + MENU_HEIGHT_OFFSET + 'px';
+ this.mainBox.style+=('height: ' + height);
+ },
+
+ _clearApplicationsBox: function(selectedActor) {
+ let actors = this.applicationsBox.get_children();
+ for (let i = 0; i < actors.length; i++) {
+ let actor = actors[i];
+ this.applicationsBox.remove_actor(actor);
+ }
+ },
+
+ selectCategory: function(dir, categoryMenuItem) {
+ if (categoryMenuItem)
+ this._clearApplicationsBox(categoryMenuItem.actor);
+ else
+ this._clearApplicationsBox(null);
+
+ if (dir)
+ this._displayButtons(this._listApplications(dir.get_menu_id()));
+ else
+ this._displayButtons(this._listApplications(null));
+ },
+
+ _displayButtons: function(apps) {
+ if (apps) {
+ for (let i = 0; i < apps.length; i++) {
+ let app = apps[i];
+ if (!this._applicationsButtons[app]) {
+ let applicationMenuItem = new ApplicationMenuItem(this, app);
+ this._applicationsButtons[app] = applicationMenuItem;
+ }
+ if (!this._applicationsButtons[app].actor.get_parent())
+ this.applicationsBox.add_actor(this._applicationsButtons[app].actor);
+ }
+ }
+ },
+
+ _listApplications: function(category_menu_id) {
+ let applist;
+
+ if (category_menu_id) {
+ applist = this.applicationsByCategory[category_menu_id];
+ } else {
+ applist = new Array();
+ let favorites = global.settings.get_strv('favorite-apps');
+ for (let i = 0; i < favorites.length; i++) {
+ let app = appSys.lookup_app(favorites[i]);
+ if (app)
+ applist.push(app);
+ }
+ }
+
+ applist.sort(function(a,b) {
+ return a.get_name().toLowerCase() > b.get_name().toLowerCase();
+ });
+ return applist;
+ },
+
+ destroy: function() {
+ this.menu.actor.get_children().forEach(function(c) { c.destroy() });
+ this.parent();
}
});
let appsMenuButton;
+let activitiesButton;
+let _hidingId;
+let _installedChangedId;
+let _panelBoxChangedId;
+let _showingId;
function enable() {
+ activitiesButton = Main.panel.statusArea['activities'];
+ activitiesButton.hotCorner.actor.hide();
+ activitiesButton.container.hide();
appsMenuButton = new ApplicationsButton();
Main.panel.addToStatusArea('apps-menu', appsMenuButton, 1, 'left');
+
+ Main.wm.setCustomKeybindingHandler('panel-main-menu',
+ Main.KeybindingMode.NORMAL |
+ Main.KeybindingMode.OVERVIEW,
+ function() {
+ appsMenuButton.menu.toggle();
+ });
}
function disable() {
+ Main.panel.menuManager.removeMenu(appsMenuButton.menu);
+ appSys.disconnect(_installedChangedId);
+ Main.layoutManager.disconnect(_panelBoxChangedId);
+ Main.overview.disconnect(_hidingId);
+ Main.overview.disconnect(_showingId);
appsMenuButton.destroy();
+ activitiesButton.container.show();
+ activitiesButton.hotCorner.actor.show();
+
+ Main.wm.setCustomKeybindingHandler('panel-main-menu',
+ Main.KeybindingMode.NORMAL |
+ Main.KeybindingMode.OVERVIEW,
+ Main.sessionMode.hasOverview ?
+ Lang.bind(Main.overview, Main.overview.toggle) :
+ null);
}
-function init() {
- /* do nothing */
+function init(metadata) {
+ Convenience.initTranslations();
}
diff --git a/extensions/apps-menu/metadata.json.in b/extensions/apps-menu/metadata.json.in
index 8d5380c..c7addee 100644
--- a/extensions/apps-menu/metadata.json.in
+++ b/extensions/apps-menu/metadata.json.in
@@ -1,10 +1,10 @@
{
"extension-id": "@extension_id@",
"uuid": "@uuid@",
-"settings-schema": "@gschemaname@",
"gettext-domain": "@gettext_domain@",
"name": "Applications Menu",
"description": "Add a gnome 2.x style menu for applications",
+"original-authors": [ "e2002 bk ru", "debarshir gnome org" ],
"shell-version": [ "@shell_current@" ],
"url": "@url@"
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]