ed31f08f2f
Ever since gnome-shell stopped using a stippled separator in the
calendar menu, the styling required by the separator has been
missing.
There haven't been any complaints about the invisible separator,
so we can just as well drop it altogether.
Part-of:
<https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/290>
(cherry picked from commit c3cbef19ac)
666 lines
21 KiB
JavaScript
666 lines
21 KiB
JavaScript
// SPDX-FileCopyrightText: 2011 Vamsi Krishna Brahmajosyula <vamsikrishna.brahmajosyula@gmail.com>
|
|
// SPDX-FileCopyrightText: 2011 Giovanni Campagna <gcampagna@src.gnome.org>
|
|
// SPDX-FileCopyrightText: 2013 Debarshi Ray <debarshir@gnome.org>
|
|
// SPDX-FileCopyrightText: 2013 Florian Müllner <fmuellner@gnome.org>
|
|
//
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
import Atk from 'gi://Atk';
|
|
import Clutter from 'gi://Clutter';
|
|
import Gio from 'gi://Gio';
|
|
import GLib from 'gi://GLib';
|
|
import GMenu from 'gi://GMenu';
|
|
import GObject from 'gi://GObject';
|
|
import Gtk from 'gi://Gtk';
|
|
import Meta from 'gi://Meta';
|
|
import Shell from 'gi://Shell';
|
|
import St from 'gi://St';
|
|
import {EventEmitter} from 'resource:///org/gnome/shell/misc/signals.js';
|
|
|
|
import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
|
|
|
|
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
|
|
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
|
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
|
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
|
|
|
const appSys = Shell.AppSystem.get_default();
|
|
|
|
const APPLICATION_ICON_SIZE = 32;
|
|
const HORIZ_FACTOR = 5;
|
|
const MENU_HEIGHT_OFFSET = 132;
|
|
const NAVIGATION_REGION_OVERSHOOT = 50;
|
|
|
|
Gio._promisify(Gio._LocalFilePrototype, 'query_info_async', 'query_info_finish');
|
|
Gio._promisify(Gio._LocalFilePrototype, 'set_attributes_async', 'set_attributes_finish');
|
|
|
|
class ApplicationMenuItem extends PopupMenu.PopupBaseMenuItem {
|
|
static {
|
|
GObject.registerClass(this);
|
|
}
|
|
|
|
constructor(button, app) {
|
|
super();
|
|
this._app = app;
|
|
this._button = button;
|
|
|
|
this._iconBin = new St.Bin();
|
|
this.add_child(this._iconBin);
|
|
|
|
let appLabel = new St.Label({
|
|
text: app.get_name(),
|
|
y_expand: true,
|
|
y_align: Clutter.ActorAlign.CENTER,
|
|
});
|
|
this.add_child(appLabel);
|
|
this.label_actor = appLabel;
|
|
|
|
let textureCache = St.TextureCache.get_default();
|
|
textureCache.connectObject('icon-theme-changed',
|
|
() => this._updateIcon(), this);
|
|
this._updateIcon();
|
|
|
|
this._delegate = this;
|
|
let draggable = DND.makeDraggable(this);
|
|
|
|
let maybeStartDrag = draggable._maybeStartDrag;
|
|
draggable._maybeStartDrag = event => {
|
|
if (this._dragEnabled)
|
|
return maybeStartDrag.call(draggable, event);
|
|
return false;
|
|
};
|
|
}
|
|
|
|
activate(event) {
|
|
this._app.open_new_window(-1);
|
|
this._button.selectCategory(null);
|
|
this._button.menu.toggle();
|
|
super.activate(event);
|
|
|
|
Main.overview.hide();
|
|
}
|
|
|
|
setActive(active, params) {
|
|
if (active)
|
|
this._button.scrollToButton(this);
|
|
super.setActive(active, params);
|
|
}
|
|
|
|
setDragEnabled(enabled) {
|
|
this._dragEnabled = enabled;
|
|
}
|
|
|
|
getDragActor() {
|
|
return this._app.create_icon_texture(APPLICATION_ICON_SIZE);
|
|
}
|
|
|
|
getDragActorSource() {
|
|
return this._iconBin;
|
|
}
|
|
|
|
_updateIcon() {
|
|
let icon = this.getDragActor();
|
|
icon.style_class = 'icon-dropshadow';
|
|
this._iconBin.set_child(icon);
|
|
}
|
|
}
|
|
|
|
class CategoryMenuItem extends PopupMenu.PopupBaseMenuItem {
|
|
static {
|
|
GObject.registerClass(this);
|
|
}
|
|
|
|
constructor(button, category) {
|
|
super();
|
|
this._category = category;
|
|
this._button = button;
|
|
|
|
this._oldX = -1;
|
|
this._oldY = -1;
|
|
|
|
let name;
|
|
if (this._category)
|
|
name = this._category.get_name();
|
|
else
|
|
name = _('Favorites');
|
|
|
|
this.add_child(new St.Label({text: name}));
|
|
this.connect('motion-event', this._onMotionEvent.bind(this));
|
|
this.connect('notify::active', this._onActiveChanged.bind(this));
|
|
}
|
|
|
|
activate(event) {
|
|
this._button.selectCategory(this._category);
|
|
this._button.scrollToCatButton(this);
|
|
super.activate(event);
|
|
}
|
|
|
|
_isNavigatingSubmenu([x, y]) {
|
|
let [posX, posY] = this.get_transformed_position();
|
|
|
|
if (this._oldX === -1) {
|
|
this._oldX = x;
|
|
this._oldY = y;
|
|
return true;
|
|
}
|
|
|
|
let deltaX = Math.abs(x - this._oldX);
|
|
let deltaY = Math.abs(y - this._oldY);
|
|
|
|
this._oldX = x;
|
|
this._oldY = y;
|
|
|
|
// If it lies outside the x-coordinates then it is definitely outside.
|
|
if (posX > x || posX + this.width < x)
|
|
return false;
|
|
|
|
// If it lies inside the menu item then it is definitely inside.
|
|
if (posY <= y && posY + this.height >= y)
|
|
return true;
|
|
|
|
// We want the keep-up triangle only if the movement is more
|
|
// horizontal than vertical.
|
|
if (deltaX * HORIZ_FACTOR < deltaY)
|
|
return false;
|
|
|
|
// Check whether the point lies inside triangle ABC, and a similar
|
|
// triangle on the other side of the menu item.
|
|
//
|
|
// +---------------------+
|
|
// | menu item |
|
|
// A +---------------------+ C
|
|
// P |
|
|
// B
|
|
|
|
// Ensure that the point P always lies below line AC so that we can
|
|
// only check for triangle ABC.
|
|
if (posY > y) {
|
|
let offset = posY - y;
|
|
y = posY + this.height + offset;
|
|
}
|
|
|
|
// Ensure that A is (0, 0).
|
|
x -= posX;
|
|
y -= posY + this.height;
|
|
|
|
// Check which side of line AB the point P lies on by taking the
|
|
// cross-product of AB and AP. See:
|
|
// http://stackoverflow.com/questions/3461453/determine-which-side-of-a-line-a-point-lies
|
|
if (this.width * y - NAVIGATION_REGION_OVERSHOOT * x <= 0)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
_onMotionEvent(actor, event) {
|
|
if (!this._grab) {
|
|
this._oldX = -1;
|
|
this._oldY = -1;
|
|
const grab = global.stage.grab(this);
|
|
if (grab.get_seat_state() !== Clutter.GrabState.NONE)
|
|
this._grab = grab;
|
|
else
|
|
grab.dismiss();
|
|
}
|
|
this.hover = true;
|
|
|
|
if (this._isNavigatingSubmenu(event.get_coords()))
|
|
return true;
|
|
|
|
this._oldX = -1;
|
|
this._oldY = -1;
|
|
this.hover = false;
|
|
this._grab?.dismiss();
|
|
delete this._grab;
|
|
|
|
const targetActor = global.stage.get_event_actor(event);
|
|
if (targetActor instanceof St.Widget)
|
|
targetActor.sync_hover();
|
|
|
|
return false;
|
|
}
|
|
|
|
_onActiveChanged() {
|
|
if (!this.active)
|
|
return;
|
|
|
|
this._button.selectCategory(this._category);
|
|
this._button.scrollToCatButton(this);
|
|
}
|
|
}
|
|
|
|
class ApplicationsMenu extends PopupMenu.PopupMenu {
|
|
constructor(sourceActor, arrowAlignment, arrowSide, button) {
|
|
super(sourceActor, arrowAlignment, arrowSide);
|
|
this._button = button;
|
|
}
|
|
|
|
isEmpty() {
|
|
return false;
|
|
}
|
|
|
|
toggle() {
|
|
if (this.isOpen)
|
|
this._button.selectCategory(null);
|
|
super.toggle();
|
|
}
|
|
}
|
|
|
|
class DesktopTarget extends EventEmitter {
|
|
constructor() {
|
|
super();
|
|
|
|
this._desktop = null;
|
|
this._desktopDestroyedId = 0;
|
|
|
|
this._windowAddedId =
|
|
global.window_group.connect('actor-added',
|
|
this._onWindowAdded.bind(this));
|
|
|
|
global.get_window_actors().forEach(a => {
|
|
this._onWindowAdded(a.get_parent(), a);
|
|
});
|
|
}
|
|
|
|
get hasDesktop() {
|
|
return this._desktop !== null;
|
|
}
|
|
|
|
_onWindowAdded(group, actor) {
|
|
if (!(actor instanceof Meta.WindowActor))
|
|
return;
|
|
|
|
if (actor.meta_window.get_window_type() === Meta.WindowType.DESKTOP)
|
|
this._setDesktop(actor);
|
|
}
|
|
|
|
_setDesktop(desktop) {
|
|
if (this._desktop) {
|
|
this._desktop.disconnectObject(this);
|
|
delete this._desktop._delegate;
|
|
}
|
|
|
|
this._desktop = desktop;
|
|
this.emit('desktop-changed');
|
|
|
|
if (this._desktop) {
|
|
this._desktop.connectObject('destroy', () => {
|
|
this._setDesktop(null);
|
|
}, this);
|
|
this._desktop._delegate = this;
|
|
}
|
|
}
|
|
|
|
_getSourceAppInfo(source) {
|
|
if (!(source instanceof ApplicationMenuItem))
|
|
return null;
|
|
return source._app.app_info;
|
|
}
|
|
|
|
async _markTrusted(file) {
|
|
let modeAttr = Gio.FILE_ATTRIBUTE_UNIX_MODE;
|
|
let trustedAttr = 'metadata::trusted';
|
|
let queryFlags = Gio.FileQueryInfoFlags.NONE;
|
|
let ioPriority = GLib.PRIORITY_DEFAULT;
|
|
|
|
try {
|
|
let info = await file.query_info_async(modeAttr, queryFlags, ioPriority, null);
|
|
|
|
let mode = info.get_attribute_uint32(modeAttr) | 0o100;
|
|
info.set_attribute_uint32(modeAttr, mode);
|
|
info.set_attribute_string(trustedAttr, 'yes');
|
|
await file.set_attributes_async(info, queryFlags, ioPriority, null);
|
|
|
|
// Hack: force nautilus to reload file info
|
|
info = new Gio.FileInfo();
|
|
info.set_attribute_uint64(
|
|
Gio.FILE_ATTRIBUTE_TIME_ACCESS, GLib.get_real_time());
|
|
try {
|
|
await file.set_attributes_async(info, queryFlags, ioPriority, null);
|
|
} catch (e) {
|
|
log(`Failed to update access time: ${e.message}`);
|
|
}
|
|
} catch (e) {
|
|
log(`Failed to mark file as trusted: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
global.window_group.disconnectObject(this);
|
|
this._setDesktop(null);
|
|
}
|
|
|
|
handleDragOver(source, _actor, _x, _y, _time) {
|
|
let appInfo = this._getSourceAppInfo(source);
|
|
if (!appInfo)
|
|
return DND.DragMotionResult.CONTINUE;
|
|
|
|
return DND.DragMotionResult.COPY_DROP;
|
|
}
|
|
|
|
acceptDrop(source, _actor, _x, _y, _time) {
|
|
let appInfo = this._getSourceAppInfo(source);
|
|
if (!appInfo)
|
|
return false;
|
|
|
|
this.emit('app-dropped');
|
|
|
|
let desktop = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP);
|
|
|
|
let src = Gio.File.new_for_path(appInfo.get_filename());
|
|
let dst = Gio.File.new_for_path(GLib.build_filenamev([desktop, src.get_basename()]));
|
|
|
|
try {
|
|
// copy_async() isn't introspectable :-(
|
|
src.copy(dst, Gio.FileCopyFlags.OVERWRITE, null, null);
|
|
this._markTrusted(dst);
|
|
} catch (e) {
|
|
log(`Failed to copy to desktop: ${e.message}`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class ApplicationsButton extends PanelMenu.Button {
|
|
static {
|
|
GObject.registerClass(this);
|
|
}
|
|
|
|
constructor() {
|
|
super(1.0, null, false);
|
|
|
|
this.setMenu(new ApplicationsMenu(this, 1.0, St.Side.TOP, this));
|
|
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.accessible_role = Atk.Role.LABEL;
|
|
|
|
this._label = new St.Label({
|
|
text: _('Applications'),
|
|
y_expand: true,
|
|
y_align: Clutter.ActorAlign.CENTER,
|
|
});
|
|
|
|
this.add_actor(this._label);
|
|
this.name = 'panelApplications';
|
|
this.label_actor = this._label;
|
|
|
|
Main.overview.connectObject(
|
|
'showing', () => this.add_accessible_state(Atk.StateType.CHECKED),
|
|
'hiding', () => this.remove_accessible_state(Atk.StateType.CHECKED),
|
|
this);
|
|
|
|
Main.wm.addKeybinding(
|
|
'apps-menu-toggle-menu',
|
|
Extension.lookupByURL(import.meta.url).getSettings(),
|
|
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
|
|
Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
|
|
() => this.menu.toggle());
|
|
|
|
this._desktopTarget = new DesktopTarget();
|
|
this._desktopTarget.connect('app-dropped', () => {
|
|
this.menu.close();
|
|
});
|
|
this._desktopTarget.connect('desktop-changed', () => {
|
|
this._applicationsButtons.forEach(item => {
|
|
item.setDragEnabled(this._desktopTarget.hasDesktop);
|
|
});
|
|
});
|
|
|
|
this._tree = new GMenu.Tree({menu_basename: 'applications.menu'});
|
|
this._tree.connectObject('changed',
|
|
() => this._onTreeChanged(), this);
|
|
|
|
this._applicationsButtons = new Map();
|
|
this.reloadFlag = false;
|
|
this._createLayout();
|
|
this._display();
|
|
appSys.connectObject('installed-changed',
|
|
() => this._onTreeChanged(), this);
|
|
}
|
|
|
|
_onTreeChanged() {
|
|
if (this.menu.isOpen) {
|
|
this._redisplay();
|
|
this.mainBox.show();
|
|
} else {
|
|
this.reloadFlag = true;
|
|
}
|
|
}
|
|
|
|
_onDestroy() {
|
|
super._onDestroy();
|
|
|
|
delete this._tree;
|
|
|
|
Main.wm.removeKeybinding('apps-menu-toggle-menu');
|
|
|
|
this._desktopTarget.destroy();
|
|
}
|
|
|
|
_onMenuKeyPress(actor, event) {
|
|
let symbol = event.get_key_symbol();
|
|
if (symbol === Clutter.KEY_Left || symbol === Clutter.KEY_Right) {
|
|
let direction = symbol === Clutter.KEY_Left
|
|
? Gtk.DirectionType.LEFT : Gtk.DirectionType.RIGHT;
|
|
if (this.menu.actor.navigate_focus(global.stage.key_focus, direction, false))
|
|
return true;
|
|
}
|
|
return super._onMenuKeyPress(actor, event);
|
|
}
|
|
|
|
_onOpenStateChanged(menu, open) {
|
|
if (open) {
|
|
if (this.reloadFlag) {
|
|
this._redisplay();
|
|
this.reloadFlag = false;
|
|
}
|
|
this.mainBox.show();
|
|
}
|
|
super._onOpenStateChanged(menu, open);
|
|
}
|
|
|
|
_redisplay() {
|
|
this.applicationsBox.destroy_all_children();
|
|
this.categoriesBox.destroy_all_children();
|
|
this._display();
|
|
}
|
|
|
|
_loadCategory(categoryId, dir) {
|
|
let iter = dir.iter();
|
|
let nextType;
|
|
while ((nextType = iter.next()) !== GMenu.TreeItemType.INVALID) {
|
|
if (nextType === GMenu.TreeItemType.ENTRY) {
|
|
let entry = iter.get_entry();
|
|
let id;
|
|
try {
|
|
id = entry.get_desktop_file_id(); // catch non-UTF8 filenames
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
let app = appSys.lookup_app(id);
|
|
if (!app)
|
|
app = new Shell.App({app_info: entry.get_app_info()});
|
|
if (app.get_app_info().should_show())
|
|
this.applicationsByCategory[categoryId].push(app);
|
|
} else if (nextType === GMenu.TreeItemType.SEPARATOR) {
|
|
this.applicationsByCategory[categoryId].push('separator');
|
|
} else if (nextType === GMenu.TreeItemType.DIRECTORY) {
|
|
let subdir = iter.get_directory();
|
|
if (!subdir.get_is_nodisplay())
|
|
this._loadCategory(categoryId, subdir);
|
|
}
|
|
}
|
|
}
|
|
|
|
scrollToButton(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.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(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.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() {
|
|
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({
|
|
style_class: 'apps-menu vfade',
|
|
x_expand: true,
|
|
});
|
|
this.applicationsScrollBox.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
|
|
let vscroll = this.applicationsScrollBox.get_vscroll_bar();
|
|
vscroll.connect('scroll-start', () => {
|
|
this.menu.passEvents = true;
|
|
});
|
|
vscroll.connect('scroll-stop', () => {
|
|
this.menu.passEvents = false;
|
|
});
|
|
this.categoriesScrollBox = new St.ScrollView({
|
|
style_class: 'vfade',
|
|
});
|
|
this.categoriesScrollBox.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
|
|
vscroll = this.categoriesScrollBox.get_vscroll_bar();
|
|
vscroll.connect('scroll-start', () => (this.menu.passEvents = true));
|
|
vscroll.connect('scroll-stop', () => (this.menu.passEvents = false));
|
|
this.leftBox.add_child(this.categoriesScrollBox);
|
|
|
|
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);
|
|
|
|
this.mainBox.add(this.leftBox);
|
|
this.mainBox.add_child(this.applicationsScrollBox);
|
|
section.actor.add_actor(this.mainBox);
|
|
}
|
|
|
|
_display() {
|
|
this._applicationsButtons.clear();
|
|
this.mainBox.hide();
|
|
|
|
// Load categories
|
|
this.applicationsByCategory = {};
|
|
this._tree.load_sync();
|
|
let root = this._tree.get_root_directory();
|
|
let categoryMenuItem = new CategoryMenuItem(this, null);
|
|
this.categoriesBox.add_actor(categoryMenuItem);
|
|
let iter = root.iter();
|
|
let nextType;
|
|
while ((nextType = iter.next()) !== GMenu.TreeItemType.INVALID) {
|
|
if (nextType !== GMenu.TreeItemType.DIRECTORY)
|
|
continue;
|
|
|
|
let dir = iter.get_directory();
|
|
if (dir.get_is_nodisplay())
|
|
continue;
|
|
|
|
let categoryId = dir.get_menu_id();
|
|
this.applicationsByCategory[categoryId] = [];
|
|
this._loadCategory(categoryId, dir);
|
|
if (this.applicationsByCategory[categoryId].length > 0) {
|
|
categoryMenuItem = new CategoryMenuItem(this, dir);
|
|
this.categoriesBox.add_actor(categoryMenuItem);
|
|
}
|
|
}
|
|
|
|
// Load applications
|
|
this._displayButtons(this._listApplications(null));
|
|
|
|
let themeContext = St.ThemeContext.get_for_stage(global.stage);
|
|
let scaleFactor = themeContext.scale_factor;
|
|
let categoriesHeight = this.categoriesBox.height / scaleFactor;
|
|
let height = Math.round(categoriesHeight) + MENU_HEIGHT_OFFSET;
|
|
this.mainBox.style += `height: ${height}px`;
|
|
}
|
|
|
|
selectCategory(dir) {
|
|
this.applicationsBox.get_children().forEach(c => {
|
|
if (c._delegate instanceof PopupMenu.PopupSeparatorMenuItem)
|
|
c._delegate.destroy();
|
|
else
|
|
this.applicationsBox.remove_actor(c);
|
|
});
|
|
|
|
if (dir)
|
|
this._displayButtons(this._listApplications(dir.get_menu_id()));
|
|
else
|
|
this._displayButtons(this._listApplications(null));
|
|
}
|
|
|
|
_displayButtons(apps) {
|
|
for (let i = 0; i < apps.length; i++) {
|
|
let app = apps[i];
|
|
let item;
|
|
if (app instanceof Shell.App)
|
|
item = this._applicationsButtons.get(app);
|
|
else
|
|
item = new PopupMenu.PopupSeparatorMenuItem();
|
|
if (!item) {
|
|
item = new ApplicationMenuItem(this, app);
|
|
item.setDragEnabled(this._desktopTarget.hasDesktop);
|
|
this._applicationsButtons.set(app, item);
|
|
}
|
|
if (!item.get_parent())
|
|
this.applicationsBox.add_actor(item);
|
|
}
|
|
}
|
|
|
|
_listApplications(categoryMenuId) {
|
|
let applist;
|
|
|
|
if (categoryMenuId) {
|
|
applist = this.applicationsByCategory[categoryMenuId];
|
|
} else {
|
|
applist = global.settings.get_strv('favorite-apps')
|
|
.map(id => appSys.lookup_app(id))
|
|
.filter(app => app);
|
|
}
|
|
|
|
return applist;
|
|
}
|
|
}
|
|
|
|
export default class AppsMenuExtension extends Extension {
|
|
enable() {
|
|
this._appsMenuButton = new ApplicationsButton();
|
|
const index = Main.sessionMode.panel.left.indexOf('activities') + 1;
|
|
Main.panel.addToStatusArea(
|
|
'apps-menu', this._appsMenuButton, index, 'left');
|
|
}
|
|
|
|
disable() {
|
|
Main.panel.menuManager.removeMenu(this._appsMenuButton.menu);
|
|
this._appsMenuButton.destroy();
|
|
delete this._appsMenuButton;
|
|
}
|
|
}
|