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
558 lines
20 KiB
JavaScript
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, '<');
|
|
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();
|
|
}
|