Files
gnome-shell-extensions/extensions/apps-menu/extension.js
Debarshi Ray 9211fa4409 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
2013-01-30 16:15:32 +01:00

558 lines
20 KiB
JavaScript

/* -*- 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, '&lt;');
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 }));
},
activate: function(event) {
this._app.open_new_window(event.get_time());
this._button.selectCategory(null, null);
this._button.menu.toggle();
this.parent(event);
},
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(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 }));
},
activate: function(event) {
this._button.selectCategory(this._category, this);
this._button.scrollToCatButton(this);
this.parent(event);
},
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();
},
_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: 'ApplicationsButton',
Extends: PanelMenu.Button,
_init: function() {
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);
// 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();
}));
},
_containerGetPreferredWidth: function(actor, forHeight, alloc) {
[alloc.min_size, alloc.natural_size] = this._label.get_preferred_width(forHeight);
},
_containerGetPreferredHeight: function(actor, forWidth, alloc) {
[alloc.min_size, alloc.natural_size] = this._label.get_preferred_height(forWidth);
},
_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);
},
_redisplay: function() {
this.applicationsBox.destroy_all_children();
this.categoriesBox.destroy_all_children();
this._display();
},
_loadCategory: function(dir) {
let iter = dir.iter();
let nextType;
while ((nextType = iter.next()) != GMenu.TreeItemType.INVALID) {
if (nextType == GMenu.TreeItemType.ENTRY) {
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) {
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);
}
}
}
},
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();
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(metadata) {
Convenience.initTranslations();
}