478 lines
15 KiB
JavaScript
478 lines
15 KiB
JavaScript
/* eslint-disable jsdoc/require-jsdoc */
|
|
import Clutter from 'gi://Clutter';
|
|
import Gio from 'gi://Gio';
|
|
import GLib from 'gi://GLib';
|
|
import Pango from 'gi://Pango';
|
|
import Shell from 'gi://Shell';
|
|
import St from 'gi://St';
|
|
|
|
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
|
|
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
|
import {getLoginManager} from 'resource:///org/gnome/shell/misc/loginManager.js';
|
|
|
|
import * as Constants from './constants.js';
|
|
import {ArcMenuManager} from './arcmenuManager.js';
|
|
|
|
import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
|
|
|
|
const [ShellVersion] = Config.PACKAGE_VERSION.split('.').map(s => Number(s));
|
|
|
|
const InterfaceXml = `<node>
|
|
<interface name="com.Extensions.ArcMenu">
|
|
<method name="ToggleArcMenu"/>
|
|
</interface>
|
|
</node>`;
|
|
|
|
export const DBusService = class {
|
|
constructor() {
|
|
this._exported = false;
|
|
this.ToggleArcMenu = () => {};
|
|
|
|
this._dbusExportedObject = Gio.DBusExportedObject.wrapJSObject(InterfaceXml, this);
|
|
|
|
const onBusAcquired = (connection, _name) => {
|
|
try {
|
|
this._dbusExportedObject.export(connection, '/com/Extensions/ArcMenu');
|
|
this._exported = true;
|
|
} catch (error) {
|
|
console.log(`ArcMenu Error - onBusAcquired export failed: ${error}`);
|
|
this._exported = false;
|
|
}
|
|
};
|
|
|
|
function onNameAcquired() { }
|
|
|
|
function onNameLost() { }
|
|
|
|
this._ownerId = Gio.bus_own_name(Gio.BusType.SESSION, 'com.Extensions.ArcMenu', Gio.BusNameOwnerFlags.NONE,
|
|
onBusAcquired, onNameAcquired, onNameLost);
|
|
}
|
|
|
|
destroy() {
|
|
if (this._ownerId) {
|
|
Gio.bus_unown_name(this._ownerId);
|
|
this._ownerId = null;
|
|
}
|
|
|
|
if (this._dbusExportedObject && this._exported)
|
|
this._dbusExportedObject.unexport();
|
|
this._dbusExportedObject = null;
|
|
this._exported = null;
|
|
this.ToggleArcMenu = null;
|
|
}
|
|
};
|
|
|
|
export function activateHibernateOrSleep(powerType) {
|
|
const loginManager = getLoginManager();
|
|
let callName, activateCall;
|
|
|
|
if (powerType === Constants.PowerType.HIBERNATE) {
|
|
callName = 'CanHibernate';
|
|
activateCall = 'Hibernate';
|
|
} else if (powerType === Constants.PowerType.HYBRID_SLEEP) {
|
|
callName = 'CanHybridSleep';
|
|
activateCall = 'HybridSleep';
|
|
}
|
|
|
|
if (!loginManager._proxy) {
|
|
Main.notifyError(`ArcMenu - ${activateCall} Error!`, `System unable to ${activateCall}.`);
|
|
return;
|
|
}
|
|
|
|
canHibernateOrSleep(callName, result => {
|
|
if (!result) {
|
|
Main.notifyError(`ArcMenu - ${activateCall} Error!`, `System unable to ${activateCall}.`);
|
|
return;
|
|
}
|
|
|
|
loginManager._proxy.call(activateCall,
|
|
GLib.Variant.new('(b)', [true]),
|
|
Gio.DBusCallFlags.NONE,
|
|
-1, null, null);
|
|
});
|
|
}
|
|
|
|
export function canHibernateOrSleep(callName, asyncCallback) {
|
|
const loginManager = getLoginManager();
|
|
|
|
if (!loginManager._proxy)
|
|
asyncCallback(false);
|
|
|
|
loginManager._proxy.call(callName, null, Gio.DBusCallFlags.NONE, -1, null, (proxy, asyncResult) => {
|
|
let result, error;
|
|
|
|
try {
|
|
result = proxy.call_finish(asyncResult).deep_unpack();
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
if (error)
|
|
asyncCallback(false);
|
|
else
|
|
asyncCallback(result[0] === 'yes');
|
|
});
|
|
}
|
|
|
|
export function convertToButton(item) {
|
|
item.tooltipLocation = Constants.TooltipLocation.BOTTOM_CENTERED;
|
|
item.remove_child(item.label);
|
|
item.set({
|
|
x_expand: false,
|
|
x_align: Clutter.ActorAlign.CENTER,
|
|
y_expand: false,
|
|
y_align: Clutter.ActorAlign.CENTER,
|
|
style_class: 'popup-menu-item arcmenu-button',
|
|
});
|
|
}
|
|
|
|
export function convertToGridLayout(item) {
|
|
const menuLayout = item._menuLayout;
|
|
const icon = item._iconBin;
|
|
|
|
const {settings} = ArcMenuManager;
|
|
|
|
const iconSizeEnum = settings.get_enum('menu-item-grid-icon-size');
|
|
const defaultIconSize = menuLayout.icon_grid_size;
|
|
const {width, height, iconSize_} = getGridIconSize(iconSizeEnum, defaultIconSize);
|
|
|
|
item.add_style_class_name('ArcMenuIconGrid');
|
|
item.set({
|
|
...getOrientationProp(true),
|
|
x_align: Clutter.ActorAlign.CENTER,
|
|
tooltipLocation: Constants.TooltipLocation.BOTTOM_CENTERED,
|
|
style: `width: ${width}px; height: ${height}px;`,
|
|
});
|
|
|
|
icon?.set({
|
|
y_align: Clutter.ActorAlign.CENTER,
|
|
y_expand: true,
|
|
});
|
|
|
|
if (item._indicator) {
|
|
item.remove_child(item._indicator);
|
|
item.insert_child_at_index(item._indicator, 0);
|
|
item._indicator.set({
|
|
x_align: Clutter.ActorAlign.CENTER,
|
|
y_align: Clutter.ActorAlign.START,
|
|
y_expand: false,
|
|
});
|
|
}
|
|
|
|
if (!settings.get_boolean('multi-lined-labels'))
|
|
return;
|
|
|
|
icon?.set({
|
|
y_align: Clutter.ActorAlign.TOP,
|
|
y_expand: false,
|
|
});
|
|
|
|
const clutterText = item.label.get_clutter_text();
|
|
clutterText.set({
|
|
line_wrap: true,
|
|
line_wrap_mode: Pango.WrapMode.WORD_CHAR,
|
|
});
|
|
}
|
|
|
|
export function getIconSize(iconSizeEnum, defaultIconSize) {
|
|
switch (iconSizeEnum) {
|
|
case Constants.IconSize.DEFAULT:
|
|
return defaultIconSize;
|
|
case Constants.IconSize.EXTRA_SMALL:
|
|
return Constants.EXTRA_SMALL_ICON_SIZE;
|
|
case Constants.IconSize.SMALL:
|
|
return Constants.SMALL_ICON_SIZE;
|
|
case Constants.IconSize.MEDIUM:
|
|
return Constants.MEDIUM_ICON_SIZE;
|
|
case Constants.IconSize.LARGE:
|
|
return Constants.LARGE_ICON_SIZE;
|
|
case Constants.IconSize.EXTRA_LARGE:
|
|
return Constants.EXTRA_LARGE_ICON_SIZE;
|
|
case Constants.IconSize.HIDDEN:
|
|
return Constants.ICON_HIDDEN;
|
|
default:
|
|
return defaultIconSize;
|
|
}
|
|
}
|
|
|
|
export function getGridIconSize(iconSizeEnum, defaultIconSize) {
|
|
const {settings} = ArcMenuManager;
|
|
|
|
if (iconSizeEnum === Constants.GridIconSize.CUSTOM) {
|
|
const {width, height, iconSize} = settings.get_value('custom-grid-icon-size').deep_unpack();
|
|
return {width, height, iconSize};
|
|
}
|
|
|
|
if (iconSizeEnum === Constants.GridIconSize.DEFAULT)
|
|
iconSizeEnum = defaultIconSize;
|
|
|
|
let width, height, iconSize;
|
|
Constants.GridIconInfo.forEach(info => {
|
|
if (iconSizeEnum === info.ENUM) {
|
|
width = info.WIDTH;
|
|
height = info.HEIGHT;
|
|
iconSize = info.ICON_SIZE;
|
|
}
|
|
});
|
|
return {width, height, iconSize};
|
|
}
|
|
|
|
export function getCategoryDetails(iconTheme, currentCategory) {
|
|
const extensionPath = ArcMenuManager.extension.path;
|
|
|
|
let name = null, gicon = null, fallbackIcon = null;
|
|
|
|
for (const entry of Constants.Categories) {
|
|
if (entry.CATEGORY === currentCategory) {
|
|
name = entry.NAME;
|
|
gicon = Gio.icon_new_for_string(entry.ICON);
|
|
return [name, gicon, fallbackIcon];
|
|
}
|
|
}
|
|
|
|
if (currentCategory === Constants.CategoryType.HOME_SCREEN) {
|
|
name = _('Home');
|
|
gicon = Gio.icon_new_for_string('computer-symbolic');
|
|
return [name, gicon, fallbackIcon];
|
|
} else {
|
|
name = currentCategory.get_name();
|
|
const categoryIcon = currentCategory.get_icon();
|
|
const fallbackIconDirectory = `${extensionPath}/icons/category-icons/`;
|
|
|
|
if (!categoryIcon)
|
|
return [name, gicon, fallbackIcon];
|
|
|
|
const categoryIconName = categoryIcon.to_string();
|
|
const symbolicName = `${categoryIconName}-symbolic`;
|
|
const symbolicIconFile = `${symbolicName}.svg`;
|
|
|
|
gicon = categoryIcon;
|
|
|
|
const categoryIconType = ArcMenuManager.settings.get_enum('category-icon-type');
|
|
if (categoryIconType === Constants.CategoryIconType.SYMBOLIC) {
|
|
const icon = iconTheme.lookup_icon(symbolicName, 26, St.IconLookupFlags.FORCE_SYMBOLIC);
|
|
if (icon) {
|
|
gicon = Gio.icon_new_for_string(symbolicName);
|
|
} else {
|
|
const filePath = `${fallbackIconDirectory}${symbolicIconFile}`;
|
|
const file = Gio.File.new_for_path(filePath);
|
|
if (file.query_exists(null))
|
|
gicon = Gio.icon_new_for_string(`${fallbackIconDirectory}${symbolicIconFile}`);
|
|
}
|
|
}
|
|
|
|
fallbackIcon = Gio.icon_new_for_string(`${fallbackIconDirectory}${symbolicIconFile}`);
|
|
return [name, gicon, fallbackIcon];
|
|
}
|
|
}
|
|
|
|
export function getPowerTypeFromShortcutCommand(command) {
|
|
switch (command) {
|
|
case Constants.ShortcutCommands.LOG_OUT:
|
|
return Constants.PowerType.LOGOUT;
|
|
case Constants.ShortcutCommands.LOCK:
|
|
return Constants.PowerType.LOCK;
|
|
case Constants.ShortcutCommands.POWER_OFF:
|
|
return Constants.PowerType.POWER_OFF;
|
|
case Constants.ShortcutCommands.RESTART:
|
|
return Constants.PowerType.RESTART;
|
|
case Constants.ShortcutCommands.SUSPEND:
|
|
return Constants.PowerType.SUSPEND;
|
|
case Constants.ShortcutCommands.HYBRID_SLEEP:
|
|
return Constants.PowerType.HYBRID_SLEEP;
|
|
case Constants.ShortcutCommands.HIBERNATE:
|
|
return Constants.PowerType.HIBERNATE;
|
|
case Constants.ShortcutCommands.SWITCH_USER:
|
|
return Constants.PowerType.SWITCH_USER;
|
|
default:
|
|
return Constants.PowerType.POWER_OFF;
|
|
}
|
|
}
|
|
|
|
export function getMenuButtonIcon(path) {
|
|
const extensionPath = ArcMenuManager.extension.path;
|
|
const {settings} = ArcMenuManager;
|
|
|
|
const iconType = settings.get_enum('menu-button-icon');
|
|
const iconDirectory = `${extensionPath}/icons/hicolor/16x16/actions/`;
|
|
|
|
if (iconType === Constants.MenuIconType.CUSTOM) {
|
|
if (path && GLib.file_test(path, GLib.FileTest.IS_REGULAR))
|
|
return path;
|
|
else
|
|
return path;
|
|
} else if (iconType === Constants.MenuIconType.DISTRO_ICON) {
|
|
const iconEnum = settings.get_int('distro-icon');
|
|
const iconPath = `${iconDirectory + Constants.DistroIcons[iconEnum].PATH}.svg`;
|
|
if (GLib.file_test(iconPath, GLib.FileTest.IS_REGULAR))
|
|
return iconPath;
|
|
} else {
|
|
const iconEnum = settings.get_int('arc-menu-icon');
|
|
const iconPath = `${iconDirectory + Constants.MenuIcons[iconEnum].PATH}.svg`;
|
|
if (Constants.MenuIcons[iconEnum].PATH === 'view-app-grid-symbolic')
|
|
return 'view-app-grid-symbolic';
|
|
else if (GLib.file_test(iconPath, GLib.FileTest.IS_REGULAR))
|
|
return iconPath;
|
|
}
|
|
|
|
console.log('ArcMenu Error - Failed to set menu button icon. Set to System Default.');
|
|
return 'start-here-symbolic';
|
|
}
|
|
|
|
export function findSoftwareManager() {
|
|
const appSys = Shell.AppSystem.get_default();
|
|
|
|
for (const softwareManagerID of Constants.SoftwareManagerIDs) {
|
|
if (appSys.lookup_app(softwareManagerID))
|
|
return softwareManagerID;
|
|
}
|
|
|
|
return 'ArcMenu_InvalidShortcut.desktop';
|
|
}
|
|
|
|
export function areaOfTriangle(p1, p2, p3) {
|
|
return Math.abs((p1[0] * (p2[1] - p3[1]) + p2[0] * (p3[1] - p1[1]) + p3[0] * (p1[1] - p2[1])) / 2.0);
|
|
}
|
|
|
|
// modified from GNOME shell's ensureActorVisibleInScrollView()
|
|
export function ensureActorVisibleInScrollView(actor, axis = Clutter.Orientation.VERTICAL, dummyBox = null) {
|
|
let box = dummyBox ? dummyBox : actor.get_allocation_box();
|
|
let {y1} = box, {y2} = box;
|
|
let {x1} = box, {x2} = box;
|
|
|
|
let parent = actor.get_parent();
|
|
while (!(parent instanceof St.ScrollView)) {
|
|
if (!parent)
|
|
return;
|
|
|
|
box = parent.get_allocation_box();
|
|
y1 += box.y1;
|
|
y2 += box.y1;
|
|
x1 += box.x1;
|
|
x2 += box.x1;
|
|
parent = parent.get_parent();
|
|
}
|
|
|
|
const isVertical = axis === Clutter.Orientation.VERTICAL;
|
|
const {hadjustment, vadjustment} = getScrollViewAdjustments(parent);
|
|
const adjustment = isVertical ? vadjustment : hadjustment;
|
|
const [startPoint, endPoint] = isVertical ? [y1, y2] : [x1, x2];
|
|
const [value, lower_, upper, stepIncrement_, pageIncrement_, pageSize] = adjustment.get_values();
|
|
|
|
let offset = 0;
|
|
let newValue;
|
|
|
|
const fade = parent.get_effect('fade');
|
|
if (fade)
|
|
offset = isVertical ? fade.fade_margins.top : fade.fade_margins.left;
|
|
|
|
if (startPoint < value + offset)
|
|
newValue = Math.max(0, startPoint - offset);
|
|
else if (endPoint > value + pageSize - offset)
|
|
newValue = Math.min(upper, endPoint + offset - pageSize);
|
|
else
|
|
return;
|
|
|
|
adjustment.ease(newValue, {
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
duration: 100,
|
|
});
|
|
}
|
|
|
|
export function getCategories(info) {
|
|
const categoriesStr = info.get_categories();
|
|
if (!categoriesStr)
|
|
return [];
|
|
return categoriesStr.split(';');
|
|
}
|
|
|
|
export function findBestFolderName(apps) {
|
|
const appInfos = apps.map(app => app.get_app_info());
|
|
|
|
const categoryCounter = {};
|
|
const commonCategories = [];
|
|
|
|
appInfos.reduce((categories, appInfo) => {
|
|
for (const category of getCategories(appInfo)) {
|
|
if (!(category in categoryCounter))
|
|
categoryCounter[category] = 0;
|
|
|
|
categoryCounter[category] += 1;
|
|
|
|
// If a category is present in all apps, its counter will
|
|
// reach appInfos.length
|
|
if (category.length > 0 &&
|
|
categoryCounter[category] === appInfos.length)
|
|
categories.push(category);
|
|
}
|
|
return categories;
|
|
}, commonCategories);
|
|
|
|
for (const category of commonCategories) {
|
|
const directory = `${category}.directory`;
|
|
const translated = Shell.util_get_translated_folder_name(directory);
|
|
if (translated !== null)
|
|
return translated;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function openPrefs(uuid) {
|
|
const extension = Extension.lookupByUUID(uuid);
|
|
if (extension !== null)
|
|
extension.openPreferences();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Clutter.Actor} parent
|
|
* @param {Clutter.Actor} child
|
|
* @description GNOME 46 no longer supports add_actor() method.\
|
|
* Check which method to use to maintain compatibility with GNOME 45 and 46.
|
|
*/
|
|
export function addChildToParent(parent, child) {
|
|
if (parent.add_actor)
|
|
parent.add_actor(child);
|
|
else if (parent instanceof St.Button || parent instanceof St.ScrollView)
|
|
parent.set_child(child);
|
|
else
|
|
parent.add_child(child);
|
|
}
|
|
|
|
/**
|
|
* GNOME 46 renamed the extension states. Use this const instead.
|
|
*/
|
|
export const ExtensionState = {
|
|
ACTIVE: 1,
|
|
INACTIVE: 2,
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {St.ScrollView} scrollView
|
|
* @description ScrollView.(hv)scroll was deprecated in GNOME 46.\
|
|
* Check which ScrollView property to use to maintain compatibility with GNOME 45 and 46.
|
|
*/
|
|
export function getScrollViewAdjustments(scrollView) {
|
|
const hadjustment = scrollView.hadjustment ?? scrollView.hscroll.adjustment;
|
|
const vadjustment = scrollView.vadjustment ?? scrollView.vscroll.adjustment;
|
|
|
|
return {
|
|
hadjustment,
|
|
vadjustment,
|
|
};
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {boolean} vertical
|
|
* @description GNOME 48 - St.BoxLayout uses 'orientation' instead of 'vertical'
|
|
*/
|
|
export function getOrientationProp(vertical) {
|
|
if (ShellVersion >= 48)
|
|
return {orientation: vertical ? Clutter.Orientation.VERTICAL : Clutter.Orientation.HORIZONTAL};
|
|
else
|
|
return {vertical};
|
|
}
|