b9fba54b9f
PopupMenu.PopupBaseMenuItem uses the natural width of its children to set its own minimum and natural widths. We don't want that because some of our children can be too wide. So, we override it. We also set the span and expand parameters of the label to -1 and true respectively. Otherwise we won't get to see the "...". https://bugzilla.gnome.org/show_bug.cgi?id=693282
546 lines
20 KiB
JavaScript
546 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;
|
|
|
|
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 appLabel = new St.Label({ text: app.get_name() });
|
|
this.addActor(appLabel, { span: -1, expand: true });
|
|
this.actor.label_actor = appLabel;
|
|
},
|
|
|
|
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);
|
|
},
|
|
|
|
_getPreferredWidth: function(actor, forHeight, alloc) {
|
|
alloc.min_size = alloc.natural_size = -1;
|
|
}
|
|
});
|
|
|
|
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();
|
|
this.applicationsByCategory[menu_id].push(app);
|
|
}
|
|
} else if (nextType == GMenu.TreeItemType.DIRECTORY) {
|
|
let subdir = iter.get_directory();
|
|
if (!subdir.get_is_nodisplay()) {
|
|
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()) {
|
|
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',
|
|
Shell.KeyBindingMode.NORMAL |
|
|
Shell.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',
|
|
Shell.KeyBindingMode.NORMAL |
|
|
Shell.KeyBindingMode.OVERVIEW,
|
|
Main.sessionMode.hasOverview ?
|
|
Lang.bind(Main.overview, Main.overview.toggle) :
|
|
null);
|
|
}
|
|
|
|
function init(metadata) {
|
|
Convenience.initTranslations();
|
|
}
|