Files
gnome-shell-extensions/extensions/apps-menu/extension.js
T
Florian Müllner c0454db0c6 appsMenu: Consider scale-factor in height computation
Actor heights are in physical pixels, while CSS measures are in logical
pixels, so we need to adjust accordingly to prevent the scale factor
from being applied twice.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/issues/102
2018-09-23 17:31:03 +02:00

771 lines
26 KiB
JavaScript

/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
const Atk = imports.gi.Atk;
const DND = imports.ui.dnd;
const GMenu = imports.gi.GMenu;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const Meta = imports.gi.Meta;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const Gtk = imports.gi.Gtk;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Signals = imports.signals;
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 HORIZ_FACTOR = 5;
const MENU_HEIGHT_OFFSET = 132;
const NAVIGATION_REGION_OVERSHOOT = 50;
class ActivitiesMenuItem extends PopupMenu.PopupBaseMenuItem {
constructor(button) {
super();
this._button = button;
this.actor.add_child(new St.Label({ text: _("Activities Overview") }));
}
activate(event) {
this._button.menu.toggle();
Main.overview.toggle();
super.activate(event);
}
};
class ApplicationMenuItem extends PopupMenu.PopupBaseMenuItem {
constructor(button, app) {
super();
this._app = app;
this._button = button;
this._iconBin = new St.Bin();
this.actor.add_child(this._iconBin);
let appLabel = new St.Label({ text: app.get_name(), y_expand: true,
y_align: Clutter.ActorAlign.CENTER });
this.actor.add_child(appLabel);
this.actor.label_actor = appLabel;
let textureCache = St.TextureCache.get_default();
let iconThemeChangedId = textureCache.connect('icon-theme-changed',
this._updateIcon.bind(this));
this.actor.connect('destroy', () => {
textureCache.disconnect(iconThemeChangedId);
});
this._updateIcon();
this.actor._delegate = this;
let draggable = DND.makeDraggable(this.actor);
let maybeStartDrag = draggable._maybeStartDrag;
draggable._maybeStartDrag = (event) => {
if (this._dragEnabled)
return maybeStartDrag.call(draggable, event);
return false;
};
draggable.connect('drag-begin', () => {
Shell.util_set_hidden_from_pick(Main.legacyTray.actor, true);
});
draggable.connect('drag-end', () => {
Shell.util_set_hidden_from_pick(Main.legacyTray.actor, false);
});
}
activate(event) {
this._app.open_new_window(-1);
this._button.selectCategory(null, null);
this._button.menu.toggle();
super.activate(event);
}
setActive(active, params) {
if (active)
this._button.scrollToButton(this);
super.setActive(active, params);
}
setDragEnabled(enable) {
this._dragEnabled = enable;
}
getDragActor() {
return this._app.create_icon_texture(APPLICATION_ICON_SIZE);
}
getDragActorSource() {
return this._iconBin;
}
_updateIcon() {
this._iconBin.set_child(this.getDragActor());
}
};
class CategoryMenuItem extends PopupMenu.PopupBaseMenuItem {
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.actor.add_child(new St.Label({ text: name }));
this.actor.connect('motion-event', this._onMotionEvent.bind(this));
}
activate(event) {
this._button.selectCategory(this._category, this);
this._button.scrollToCatButton(this);
super.activate(event);
}
_isNavigatingSubmenu([x, y]) {
let [posX, posY] = this.actor.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.actor.width < x)
return false;
// If it lies inside the menu item then it is definitely inside.
if (posY <= y && posY + this.actor.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.actor.height + offset;
}
// Ensure that A is (0, 0).
x -= posX;
y -= posY + this.actor.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.actor.width * y) - (NAVIGATION_REGION_OVERSHOOT * x)) <= 0)
return true;
return false;
}
_onMotionEvent(actor, event) {
if (!Clutter.get_pointer_grab()) {
this._oldX = -1;
this._oldY = -1;
Clutter.grab_pointer(this.actor);
}
this.actor.hover = true;
if (this._isNavigatingSubmenu(event.get_coords()))
return true;
this._oldX = -1;
this._oldY = -1;
this.actor.hover = false;
Clutter.ungrab_pointer();
let source = event.get_source();
if (source instanceof St.Widget)
source.sync_hover();
return false;
}
setActive(active, params) {
if (active) {
this._button.selectCategory(this._category, this);
this._button.scrollToCatButton(this);
}
super.setActive(active, params);
}
};
class ApplicationsMenu extends PopupMenu.PopupMenu {
constructor(sourceActor, arrowAlignment, arrowSide, button) {
super(sourceActor, arrowAlignment, arrowSide);
this._button = button;
}
isEmpty() {
return false;
}
open(animate) {
this._button.hotCorner.setBarrierSize(0);
if (this._button.hotCorner.actor) // fallback corner
this._button.hotCorner.actor.hide();
super.open(animate);
}
close(animate) {
let size = Main.layoutManager.panelBox.height;
this._button.hotCorner.setBarrierSize(size);
if (this._button.hotCorner.actor) // fallback corner
this._button.hotCorner.actor.show();
super.close(animate);
}
toggle() {
if (this.isOpen) {
this._button.selectCategory(null, null);
} else {
if (Main.overview.visible)
Main.overview.hide();
}
super.toggle();
}
};
class DesktopTarget {
constructor() {
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.disconnect(this._desktopDestroyedId);
this._desktopDestroyedId = 0;
delete this._desktop._delegate;
}
this._desktop = desktop;
this.emit('desktop-changed');
if (this._desktop) {
this._desktopDestroyedId = this._desktop.connect('destroy', () => {
this._setDesktop(null);
});
this._desktop._delegate = this;
}
}
_getSourceAppInfo(source) {
if (!(source instanceof ApplicationMenuItem))
return null;
return source._app.app_info;
}
_touchFile(file) {
let queryFlags = Gio.FileQueryInfoFlags.NONE;
let ioPriority = GLib.PRIORITY_DEFAULT;
let info = new Gio.FileInfo();
info.set_attribute_uint64(Gio.FILE_ATTRIBUTE_TIME_ACCESS,
GLib.get_real_time());
file.set_attributes_async (info, queryFlags, ioPriority, null,
(o, res) => {
try {
o.set_attributes_finish(res);
} catch(e) {
log('Failed to update access time: ' + e.message);
}
});
}
_markTrusted(file) {
let modeAttr = Gio.FILE_ATTRIBUTE_UNIX_MODE;
let trustedAttr = 'metadata::trusted';
let queryFlags = Gio.FileQueryInfoFlags.NONE;
let ioPriority = GLib.PRIORITY_DEFAULT;
file.query_info_async(modeAttr, queryFlags, ioPriority, null,
(o, res) => {
try {
let info = o.query_info_finish(res);
let mode = info.get_attribute_uint32(modeAttr) | 0o100;
info.set_attribute_uint32(modeAttr, mode);
info.set_attribute_string(trustedAttr, 'yes');
file.set_attributes_async (info, queryFlags, ioPriority, null,
(o, res) => {
o.set_attributes_finish(res);
// Hack: force nautilus to reload file info
this._touchFile(file);
});
} catch(e) {
log('Failed to mark file as trusted: ' + e.message);
}
});
}
destroy() {
if (this._windowAddedId)
global.window_group.disconnect(this._windowAddedId);
this._windowAddedId = 0;
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;
}
};
Signals.addSignalMethods(DesktopTarget.prototype);
class ApplicationsButton extends PanelMenu.Button {
constructor() {
super(1.0, null, false);
this.setMenu(new ApplicationsMenu(this.actor, 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.actor.accessible_role = Atk.Role.LABEL;
let hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' });
this._label = new St.Label({ text: _("Applications"),
y_expand: true,
y_align: Clutter.ActorAlign.CENTER });
hbox.add_child(this._label);
hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));
this.actor.add_actor(hbox);
this.actor.name = 'panelApplications';
this.actor.label_actor = this._label;
this.actor.connect('captured-event', this._onCapturedEvent.bind(this));
this.actor.connect('destroy', this._onDestroy.bind(this));
this._showingId = Main.overview.connect('showing', () => {
this.actor.add_accessible_state (Atk.StateType.CHECKED);
});
this._hidingId = Main.overview.connect('hiding', () => {
this.actor.remove_accessible_state (Atk.StateType.CHECKED);
});
Main.layoutManager.connect('startup-complete',
this._setKeybinding.bind(this));
this._setKeybinding();
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._treeChangedId = this._tree.connect('changed',
this._onTreeChanged.bind(this));
this._applicationsButtons = new Map();
this.reloadFlag = false;
this._createLayout();
this._display();
this._installedChangedId = appSys.connect('installed-changed',
this._onTreeChanged.bind(this));
}
_onTreeChanged() {
if (this.menu.isOpen) {
this._redisplay();
this.mainBox.show();
} else {
this.reloadFlag = true;
}
}
get hotCorner() {
return Main.layoutManager.hotCorners[Main.layoutManager.primaryIndex];
}
_createVertSeparator() {
let separator = new St.DrawingArea({ style_class: 'calendar-vertical-separator',
pseudo_class: 'highlighted' });
separator.connect('repaint', this._onVertSepRepaint.bind(this));
return separator;
}
_onDestroy() {
Main.overview.disconnect(this._showingId);
Main.overview.disconnect(this._hidingId);
appSys.disconnect(this._installedChangedId);
this._tree.disconnect(this._treeChangedId);
this._tree = null;
Main.wm.setCustomKeybindingHandler('panel-main-menu',
Shell.ActionMode.NORMAL |
Shell.ActionMode.OVERVIEW,
Main.sessionMode.hasOverview ?
Main.overview.toggle.bind(Main.overview) :
null);
this._desktopTarget.destroy();
}
_onCapturedEvent(actor, event) {
if (event.type() == Clutter.EventType.BUTTON_PRESS) {
if (!Main.overview.shouldToggleByCornerOrButton())
return true;
}
return false;
}
_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);
}
_onVertSepRepaint(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(menu, open) {
if (open) {
if (this.reloadFlag) {
this._redisplay();
this.reloadFlag = false;
}
this.mainBox.show();
}
super._onOpenStateChanged(menu, open);
}
_setKeybinding() {
Main.wm.setCustomKeybindingHandler('panel-main-menu',
Shell.ActionMode.NORMAL |
Shell.ActionMode.OVERVIEW,
() => { this.menu.toggle(); });
}
_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.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(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() {
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: 'apps-menu vfade' });
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({ 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', () => { this.menu.passEvents = true; });
vscroll.connect('scroll-stop', () => { 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);
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() {
this._applicationsButtons.clear();
this.mainBox.style=('width: 35em;');
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.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 categoryId = dir.get_menu_id();
this.applicationsByCategory[categoryId] = [];
this._loadCategory(categoryId, dir);
if (this.applicationsByCategory[categoryId].length > 0) {
let categoryMenuItem = new CategoryMenuItem(this, dir);
this.categoriesBox.add_actor(categoryMenuItem.actor);
}
}
}
}
//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 + 'px';
this.mainBox.style+=('height: ' + height);
}
selectCategory(dir, categoryMenuItem) {
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) {
if (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.actor.get_parent())
this.applicationsBox.add_actor(item.actor);
}
}
}
_listApplications(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);
}
}
return applist;
}
destroy() {
super.destroy();
}
};
let appsMenuButton;
let activitiesButton;
function enable() {
activitiesButton = Main.panel.statusArea['activities'];
activitiesButton.container.hide();
appsMenuButton = new ApplicationsButton();
Main.panel.addToStatusArea('apps-menu', appsMenuButton, 1, 'left');
}
function disable() {
Main.panel.menuManager.removeMenu(appsMenuButton.menu);
appsMenuButton.destroy();
activitiesButton.container.show();
}
function init(metadata) {
Convenience.initTranslations();
}