linux-presets/gui/gnome/autocustom-gnome-macos/res/extensions/dash-to-dock@micxgx.gmail.com/locations.js

1472 lines
47 KiB
JavaScript
Executable File

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import {
Gio,
GLib,
GObject,
Shell,
St,
} from './dependencies/gi.js';
import {ShellMountOperation} from './dependencies/shell/ui.js';
import {
Docking,
Utils,
} from './imports.js';
import {Extension} from './dependencies/shell/extensions/extension.js';
// Use __ () and N__() for the extension gettext domain, and reuse
// the shell domain with the default _() and N_()
const {gettext: __} = Extension;
const {signals: Signals} = imports;
const FALLBACK_REMOVABLE_MEDIA_ICON = 'drive-removable-media';
const FALLBACK_TRASH_ICON = 'user-trash';
const FILE_MANAGER_DESKTOP_APP_ID = 'org.gnome.Nautilus.desktop';
const ATTRIBUTE_METADATA_CUSTOM_ICON = 'metadata::custom-icon';
const TRASH_URI = 'trash://';
const UPDATE_TRASH_DELAY = 1000;
const LAUNCH_HANDLER_MAX_WAIT = 200;
const NautilusFileOperations2Interface = '<node>\
<interface name="org.gnome.Nautilus.FileOperations2">\
<method name="EmptyTrash">\
<arg type="b" name="ask_confirmation" direction="in"/>\
<arg type="a{sv}" name="platform_data" direction="in"/>\
</method>\
</interface>\
</node>';
const NautilusFileOperations2ProxyInterface =
Gio.DBusProxy.makeProxyWrapper(NautilusFileOperations2Interface);
const Labels = Object.freeze({
LOCATION_WINDOWS: Symbol('location-windows'),
WINDOWS_CHANGED: Symbol('windows-changed'),
});
const GJS_SUPPORTS_FILE_IFACE_PROMISES = imports.system.version >= 17101;
if (GJS_SUPPORTS_FILE_IFACE_PROMISES) {
Gio._promisify(Gio.File.prototype, 'query_info_async');
Gio._promisify(Gio.File.prototype, 'query_default_handler_async');
}
/**
*
*/
function makeNautilusFileOperationsProxy() {
const proxy = new NautilusFileOperations2ProxyInterface(
Gio.DBus.session,
'org.gnome.Nautilus',
'/org/gnome/Nautilus/FileOperations2', (_p, error) => {
if (error)
logError(error, 'Error connecting to Nautilus');
}
);
proxy.platformData = params => {
const defaultParams = {
parentHandle: '',
timestamp: global.get_current_time(),
windowPosition: 'center',
};
const {parentHandle, timestamp, windowPosition} = {
...defaultParams,
...params,
};
return {
'parent-handle': new GLib.Variant('s', parentHandle),
'timestamp': new GLib.Variant('u', timestamp),
'window-position': new GLib.Variant('s', windowPosition),
};
};
return proxy;
}
export const LocationAppInfo = GObject.registerClass({
Implements: [Gio.AppInfo],
Properties: {
'location': GObject.ParamSpec.object(
'location', 'location', 'location',
GObject.ParamFlags.READWRITE,
Gio.File.$gtype),
'name': GObject.ParamSpec.string(
'name', 'name', 'name',
GObject.ParamFlags.READWRITE,
null),
'icon': GObject.ParamSpec.object(
'icon', 'icon', 'icon',
GObject.ParamFlags.READWRITE,
Gio.Icon.$gtype),
'cancellable': GObject.ParamSpec.object(
'cancellable', 'cancellable', 'cancellable',
GObject.ParamFlags.READWRITE,
Gio.Cancellable.$gtype),
},
}, class LocationAppInfo extends Gio.DesktopAppInfo {
static get GJS_BINARY_PATH() {
if (!this._gjsBinaryPath)
this._gjsBinaryPath = GLib.find_program_in_path('gjs');
return this._gjsBinaryPath;
}
list_actions() {
return [];
}
get_action_name() {
return null;
}
get_boolean() {
return false;
}
vfunc_dup() {
return new LocationAppInfo({
location: this.location,
name: this.name,
icon: this.icon,
cancellable: this.cancellable,
});
}
vfunc_equal(other) {
if (this.location)
return this.location.equal(other?.location);
return this.name === other.name &&
(this.icon ? this.icon.equal(other?.icon) : !other?.icon);
}
vfunc_get_id() {
return 'location:%s'.format(this.location?.get_uri());
}
vfunc_get_name() {
return this.name;
}
vfunc_get_description() {
return null;
}
vfunc_get_executable() {
return null;
}
vfunc_get_icon() {
return this.icon;
}
vfunc_launch(files, context) {
if (files?.length) {
throw new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.NOT_SUPPORTED, 'Launching with files not supported');
}
return this.getHandlerApp().launch_uris([this.location.get_uri()], context);
}
vfunc_supports_uris() {
return false;
}
vfunc_supports_files() {
return false;
}
vfunc_launch_uris(uris, context) {
return this.launch(uris, context);
}
vfunc_should_show() {
return true;
}
vfunc_set_as_default_for_type() {
throw new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported');
}
vfunc_set_as_default_for_extension() {
throw new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported');
}
vfunc_add_supports_type() {
throw new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported');
}
vfunc_can_remove_supports_type() {
return false;
}
vfunc_remove_supports_type() {
return false;
}
vfunc_can_delete() {
return false;
}
vfunc_do_delete() {
return false;
}
vfunc_get_commandline() {
try {
return this.getHandlerApp().get_commandline();
} catch {
return this._getFallbackCommandLine();
}
}
vfunc_get_display_name() {
return this.name;
}
vfunc_set_as_last_used_for_type() {
throw new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported');
}
vfunc_get_supported_types() {
return [];
}
_getFallbackCommandLine() {
return `gio open ${this.location?.get_uri()}`;
}
async _queryLocationIcons(params) {
const icons = {standard: null, custom: null};
if (!this.location)
return icons;
const cancellable = params.cancellable ?? this.cancellable;
const iconsQuery = [];
if (params?.standard)
iconsQuery.push(Gio.FILE_ATTRIBUTE_STANDARD_ICON);
if (params?.custom)
iconsQuery.push(ATTRIBUTE_METADATA_CUSTOM_ICON);
if (!iconsQuery.length)
throw new Error('Invalid Query Location Icons parameters');
let info;
try {
if (!GJS_SUPPORTS_FILE_IFACE_PROMISES) {
Gio._promisify(this.location.constructor.prototype,
'query_info_async', 'query_info_finish');
}
info = await this.location.query_info_async(
iconsQuery.join(','),
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_LOW, cancellable);
if (info.has_attribute(Gio.FILE_ATTRIBUTE_STANDARD_ICON))
icons.standard = info.get_icon();
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND) ||
e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED))
return icons;
throw e;
}
const customIcon = info.get_attribute_string(ATTRIBUTE_METADATA_CUSTOM_ICON);
if (customIcon) {
const customIconFile = GLib.uri_parse_scheme(customIcon)
? Gio.File.new_for_uri(customIcon) : Gio.File.new_for_path(customIcon);
const iconFileInfo = await customIconFile.query_info_async(
Gio.FILE_ATTRIBUTE_STANDARD_TYPE,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_LOW, cancellable);
if (iconFileInfo.get_file_type() === Gio.FileType.REGULAR)
icons.custom = Gio.FileIcon.new(customIconFile);
}
return icons;
}
async _updateLocationIcon(params = {standard: true, custom: true}) {
const cancellable = new Utils.CancellableChild(this.cancellable);
try {
this._updateIconCancellable?.cancel();
this._updateIconCancellable = cancellable;
const icons = await this._queryLocationIcons({cancellable, ...params});
const icon = icons.custom ?? icons.standard;
if (icon && !icon.equal(this.icon))
this.icon = icon;
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e, 'Impossible to update icon for %s'.format(this.get_id()));
} finally {
cancellable.cancel();
if (this._updateIconCancellable === cancellable)
delete this._updateIconCancellable;
}
}
async _getHandlerAppAsync(cancellable) {
if (!this.location)
return null;
try {
if (!GJS_SUPPORTS_FILE_IFACE_PROMISES) {
Gio._promisify(this.location.constructor.prototype,
'query_default_handler_async',
'query_default_handler_finish');
}
return await this.location.query_default_handler_async(
GLib.PRIORITY_DEFAULT, cancellable);
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED))
return getFileManagerApp()?.appInfo;
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
logError(e, 'Impossible to find an URI handler for %s'.format(
this.get_id()));
}
throw e;
}
}
_getHandlerAppFromWorker(cancellable) {
const locationsWorker = GLib.build_filenamev([
Docking.DockManager.extension.path,
'locationsWorker.js',
]);
const locationsWorkerArgs = [LocationAppInfo.GJS_BINARY_PATH, '-m',
locationsWorker, 'handler', this.location.get_uri(),
'--timeout', `${LAUNCH_HANDLER_MAX_WAIT}`];
const subProcess = Gio.Subprocess.new(locationsWorkerArgs,
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
try {
const [, stdOut, stdErr] = subProcess.communicate(null, cancellable);
subProcess.wait(cancellable);
const errorCode = subProcess.get_exit_status();
const textDecoder = new TextDecoder();
if (errorCode) {
const errorLines = textDecoder.decode(stdErr.toArray()).split('\n');
const error = new GLib.Error(Gio.IOErrorEnum,
errorCode === GLib.MAXUINT8 ? 0 : errorCode, errorLines[0]);
error.stack = `${errorLines.slice(3).join('\n')}${error.stack}`;
throw error;
}
const desktopId = textDecoder.decode(stdOut.toArray()).trim();
const handlerApp = Shell.AppSystem.get_default().lookup_app(desktopId)?.appInfo;
return handlerApp;
} finally {
subProcess.force_exit();
}
}
getHandlerApp() {
if (this._handlerApp)
return this._handlerApp;
if (!this.location)
return null;
const cancellable = new Utils.CancellableChild(this.cancellable);
try {
if (LocationAppInfo.GJS_BINARY_PATH)
this._handlerApp = this._getHandlerAppFromWorker(cancellable);
else
this._handlerApp = this.location.query_default_handler(cancellable);
if (!this._handlerApp) {
throw new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.NOT_FOUND, `Handler for ${this.location} not found`);
}
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED))
return getFileManagerApp()?.appInfo;
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
logError(e, 'Impossible to find an URI handler for %s'.format(
this.get_id()));
}
throw e;
}
return this._handlerApp;
}
destroy() {
this.location = null;
this.icon = null;
this.name = null;
this._handlerApp = null;
this.cancellable?.cancel();
}
});
const MountableVolumeAppInfo = GObject.registerClass({
Implements: [Gio.AppInfo],
Properties: {
'volume': GObject.ParamSpec.object(
'volume', 'volume', 'volume',
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
Gio.Volume.$gtype),
'mount': GObject.ParamSpec.object(
'mount', 'mount', 'mount',
GObject.ParamFlags.READWRITE,
Gio.Mount.$gtype),
'busy': GObject.ParamSpec.boolean(
'busy', 'busy', 'busy',
GObject.ParamFlags.READWRITE,
false),
},
},
class MountableVolumeAppInfo extends LocationAppInfo {
_init(volume, cancellable = null) {
super._init({
volume,
cancellable,
});
this._signalsHandler = new Utils.GlobalSignalsHandler();
const updateAndMonitor = () => {
this._update();
this._monitorChanges();
};
updateAndMonitor();
this._mountChanged = this.connect('notify::mount', updateAndMonitor);
if (!this.mount && this.volume.get_identifier('class') === 'network') {
// For some devices the mount point isn't advertised promptly
// even if it's already existing, and there's no signaling about
this._lazyUpdater = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2, () => {
this._update();
delete this._lazyUpdater;
return GLib.SOURCE_REMOVE;
});
}
}
get busy() {
return !!this._currentAction;
}
get currentAction() {
return this._currentAction;
}
destroy() {
if (this._lazyUpdater) {
GLib.source_remove(this._lazyUpdater);
delete this._lazyUpdater;
}
this.disconnect(this._mountChanged);
this.mount = null;
this._signalsHandler.destroy();
super.destroy();
}
vfunc_dup() {
return new MountableVolumeAppInfo({
volume: this.volume,
cancellable: this.cancellable,
});
}
vfunc_get_id() {
const uuid = this.mount?.get_uuid() ?? this.volume.get_uuid();
return uuid ? 'mountable-volume:%s'.format(uuid) : super.vfunc_get_id();
}
vfunc_equal(other) {
if (this.volume === other?.volume && this.mount === other?.mount)
return true;
return this.get_id() === other?.get_id();
}
list_actions() {
const actions = [];
const {mount} = this;
if (mount) {
if (this.mount.can_unmount())
actions.push('unmount');
if (this.mount.can_eject())
actions.push('eject');
return actions;
}
if (this.volume.can_mount())
actions.push('mount');
if (this.volume.can_eject())
actions.push('eject');
return actions;
}
get_action_name(action) {
switch (action) {
case 'mount':
return __('Mount');
case 'unmount':
return __('Unmount');
case 'eject':
return __('Eject');
default:
return null;
}
}
vfunc_launch(files, context) {
if (this.mount || files?.length)
return super.vfunc_launch(files, context);
this.mountAndLaunch(files, context);
return true;
}
_update() {
this.mount = this.volume.get_mount();
const removable = this.mount ?? this.volume;
this.name = removable.get_name();
this.icon = removable.get_icon();
this.location = this.mount?.get_default_location() ??
this.volume.get_activation_root();
this._updateLocationIcon({custom: true});
}
_monitorChanges() {
this._signalsHandler.destroy();
const removable = this.mount ?? this.volume;
this._signalsHandler.add(removable, 'changed', () => this._update());
if (this.mount) {
this._signalsHandler.add(this.mount, 'pre-unmount', () => this._update());
this._signalsHandler.add(this.mount, 'unmounted', () => this._update());
}
}
async mountAndLaunch(files, context) {
if (this.mount)
return super.vfunc_launch(files, context);
try {
await this.launchAction('mount');
if (!this.mount) {
throw new Error('No mounted location to open for %s'.format(
this.get_id()));
}
return super.vfunc_launch(files, context);
} catch (e) {
logError(e, 'Mount and launch %s'.format(this.get_id()));
return false;
}
}
_notifyActionError(action, message) {
if (action === 'mount') {
global.notify_error(__('Failed to mount “%s”'.format(
this.get_name())), message);
} else if (action === 'unmount') {
global.notify_error(__('Failed to umount “%s”'.format(
this.get_name())), message);
} else if (action === 'eject') {
global.notify_error(__('Failed to eject “%s”'.format(
this.get_name())), message);
}
}
async launchAction(action) {
if (!this.list_actions().includes(action))
throw new Error('Action %s is not supported by %s', action, this);
if (this._currentAction) {
if (this._currentAction === 'mount') {
this._notifyActionError(action,
__('Mount operation already in progress'));
} else if (this._currentAction === 'unmount') {
this._notifyActionError(action,
__('Umount operation already in progress'));
} else if (this._currentAction === 'eject') {
this._notifyActionError(action,
__('Eject operation already in progress'));
}
throw new Error('Another action %s is being performed in %s'.format(
this._currentAction, this));
}
this._currentAction = action;
this.notify('busy');
const removable = this.mount ?? this.volume;
const operation = new ShellMountOperation.ShellMountOperation(removable);
try {
if (action === 'mount') {
await this.volume.mount(Gio.MountMountFlags.NONE, operation.mountOp,
this.cancellable);
} else if (action === 'unmount') {
await this.mount.unmount_with_operation(Gio.MountUnmountFlags.FORCE,
operation.mountOp, this.cancellable);
} else if (action === 'eject') {
await removable.eject_with_operation(Gio.MountUnmountFlags.FORCE,
operation.mountOp, this.cancellable);
} else {
logError(new Error(), 'No action %s on removable %s'.format(action,
removable.get_name()));
return false;
}
return true;
} catch (e) {
if (action === 'mount' &&
e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.ALREADY_MOUNTED))
return true;
else if (action === 'umount' &&
e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED))
return true;
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED))
this._notifyActionError(action, e.message);
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
logError(e, 'Impossible to %s removable %s'.format(action,
removable.get_name()));
}
return false;
} finally {
delete this._currentAction;
this.notify('busy');
this._update();
operation.close();
}
}
});
const TrashAppInfo = GObject.registerClass({
Implements: [Gio.AppInfo],
Properties: {
'empty': GObject.ParamSpec.boolean(
'empty', 'empty', 'empty',
GObject.ParamFlags.READWRITE,
true),
},
},
class TrashAppInfo extends LocationAppInfo {
static initPromises(file) {
if (TrashAppInfo._promisified)
return;
const trashProto = file.constructor.prototype;
Gio._promisify(Gio.FileEnumerator.prototype, 'close_async', 'close_finish');
Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async', 'next_files_finish');
Gio._promisify(trashProto, 'enumerate_children_async', 'enumerate_children_finish');
Gio._promisify(trashProto, 'query_info_async', 'query_info_finish');
TrashAppInfo._promisified = true;
}
_init(cancellable = null) {
super._init({
location: Gio.file_new_for_uri(TRASH_URI),
name: __('Trash'),
icon: Gio.ThemedIcon.new(FALLBACK_TRASH_ICON),
cancellable,
});
TrashAppInfo.initPromises(this.location);
try {
this._monitor = this.location.monitor_directory(0, this.cancellable);
this._schedUpdateId = 0;
this._monitorChangedId = this._monitor.connect('changed', () =>
this._onTrashChange());
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
return;
logError(e, 'Impossible to monitor trash');
}
this._updateTrash();
this.connect('notify::empty', () => this._updateLocationIcon());
this.notify('empty');
}
destroy() {
if (this._schedUpdateId) {
GLib.source_remove(this._schedUpdateId);
this._schedUpdateId = 0;
}
this._updateTrashCancellable?.cancel();
this._monitor?.disconnect(this._monitorChangedId);
this._monitor = null;
super.destroy();
}
list_actions() {
return this.empty ? [] : ['empty-trash'];
}
get_action_name(action) {
switch (action) {
case 'empty-trash':
return __('Empty Trash');
default:
return null;
}
}
_onTrashChange() {
if (this._schedUpdateId) {
GLib.source_remove(this._schedUpdateId);
this._schedUpdateId = 0;
}
if (this._monitor.is_cancelled())
return;
this._schedUpdateId = GLib.timeout_add(GLib.PRIORITY_LOW,
UPDATE_TRASH_DELAY, () => {
this._schedUpdateId = 0;
this._updateTrash();
return GLib.SOURCE_REMOVE;
});
}
async _updateTrash() {
const priority = GLib.PRIORITY_LOW;
this._updateTrashCancellable?.cancel();
const cancellable = new Utils.CancellableChild(this.cancellable);
this._updateTrashCancellable = cancellable;
try {
const trashInfo = await this.location.query_info_async(
Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT,
Gio.FileQueryInfoFlags.NONE,
priority, cancellable);
this.empty = !trashInfo.get_attribute_uint32(
Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT);
return;
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e, 'Impossible to get trash children from infos');
} finally {
cancellable.cancel();
if (this._updateIconCancellable === cancellable)
delete this._updateTrashCancellable;
}
try {
const childrenEnumerator = await this.location.enumerate_children_async(
Gio.FILE_ATTRIBUTE_STANDARD_TYPE, Gio.FileQueryInfoFlags.NONE,
priority, cancellable);
const children = await childrenEnumerator.next_files_async(1,
priority, cancellable);
this.empty = !children.length;
await childrenEnumerator.close_async(priority, null);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e, 'Impossible to enumerate trash children');
} finally {
cancellable.cancel();
if (this._updateIconCancellable === cancellable)
delete this._updateTrashCancellable;
}
}
launchAction(action, timestamp) {
if (!this.list_actions().includes(action))
throw new Error('Action %s is not supported by %s', action, this);
const nautilus = makeNautilusFileOperationsProxy();
const askConfirmation = true;
nautilus.EmptyTrashRemote(askConfirmation,
nautilus.platformData({timestamp}), (_p, error) => {
if (error)
logError(error, 'Empty trash failed');
}, this.cancellable);
}
});
/**
* @param shellApp
*/
function wrapWindowsBackedApp(shellApp) {
if (shellApp._dtdData)
throw new Error('%s has been already wrapped'.format(shellApp));
shellApp._dtdData = {
windows: [],
state: undefined,
startingWorkspace: 0,
isFocused: false,
proxyProperties: [],
sources: new Set(),
signalConnections: new Utils.GlobalSignalsHandler(),
methodInjections: new Utils.InjectionsHandler(),
propertyInjections: new Utils.PropertyInjectionsHandler(),
addProxyProperties(parent, proxyProperties) {
Object.entries(proxyProperties).forEach(([p, o]) => {
const publicProp = o.public ? p : `_${p}`;
const get = o.getter && o.value instanceof Function
? () => this[p]() : () => this[p];
Object.defineProperty(parent, publicProp, Object.assign({
get,
set: v => (this[p] = v),
configurable: true,
enumerable: !!o.enumerable,
}, o.readOnly ? {set: undefined} : {}));
if (o.value)
this[p] = o.value;
this.proxyProperties.push(publicProp);
});
},
destroy() {
this.windows = [];
this.proxyProperties = [];
this.sources.forEach(s => GLib.source_remove(s));
this.sources.clear();
this.signalConnections.destroy();
this.methodInjections.destroy();
this.propertyInjections.destroy();
},
};
shellApp._dtdData.addProxyProperties(shellApp, {
windows: {},
state: {},
startingWorkspace: {},
isFocused: {public: true},
signalConnections: {readOnly: true},
sources: {readOnly: true},
checkFocused: {},
setDtdData: {},
});
shellApp._setDtdData = function (data, params = {}) {
for (const [name, value] of Object.entries(data)) {
if (params.readOnly && name in this._dtdData)
throw new Error('Property %s is already defined'.format(name));
const defaultParams = {public: true, readOnly: true};
this._dtdData.addProxyProperties(this, {
[name]: {...defaultParams, ...params, value},
});
}
};
const m = (...args) => shellApp._dtdData.methodInjections.add(shellApp, ...args);
const p = (...args) => shellApp._dtdData.propertyInjections.add(shellApp, ...args);
// mi is Method injector, pi is Property injector
shellApp._setDtdData({mi: m, pi: p}, {public: false});
m('get_state', () => shellApp._state ?? shellApp._getStateByWindows());
p('state', {get: () => shellApp.get_state()});
m('get_windows', () => shellApp._windows);
m('get_n_windows', () => shellApp._windows.length);
m('get_pids', () => shellApp._windows.reduce((pids, w) => {
if (w.get_pid() > 0 && !pids.includes(w.get_pid()))
pids.push(w.get_pid());
return pids;
}, []));
m('is_on_workspace', (_om, workspace) => shellApp._windows.some(w =>
w.get_workspace() === workspace) ||
(shellApp.state === Shell.AppState.STARTING &&
[-1, workspace.index()].includes(shellApp._startingWorkspace)));
m('request_quit', () => shellApp._windows.filter(w =>
w.can_close()).forEach(w => w.delete(global.get_current_time())));
shellApp._setDtdData({
_getStateByWindows() {
return this.get_n_windows() ? Shell.AppState.RUNNING : Shell.AppState.STOPPED;
},
_updateWindows() {
throw new GObject.NotImplementedError(`_updateWindows in ${this.constructor.name}`);
},
_notifyStateChanged() {
Shell.AppSystem.get_default().emit('app-state-changed', this);
this.notify('state');
},
_setState(state) {
const oldState = this.state;
this._state = state;
if (this.state !== oldState)
this._notifyStateChanged();
},
_setWindows(windows) {
const oldState = this.state;
const oldWindows = this._windows.slice();
const result = {windowsChanged: false, stateChanged: false};
this._state = undefined;
if (windows.length !== oldWindows.length ||
windows.some((win, index) => win !== oldWindows[index])) {
this._windows = windows.filter(w => !w.is_override_redirect());
this.emit('windows-changed');
result.windowsChanged = true;
}
if (this.state !== oldState) {
this._notifyStateChanged();
this._checkFocused();
result.stateChanged = true;
}
return result;
},
}, {readOnly: false});
shellApp._sources.add(GLib.idle_add(GLib.DEFAULT_PRIORITY, () => {
shellApp._updateWindows();
shellApp._sources.delete(GLib.main_current_source().source_id);
return GLib.SOURCE_REMOVE;
}));
const windowTracker = Shell.WindowTracker.get_default();
shellApp._checkFocused = function () {
if (this._windows.some(w => w.has_focus())) {
this.isFocused = true;
windowTracker.notify('focus-app');
} else if (this.isFocused) {
this.isFocused = false;
windowTracker.notify('focus-app');
}
};
shellApp._checkFocused();
shellApp._signalConnections.add(global.display, 'notify::focus-window', () =>
shellApp._checkFocused());
// Re-implements shell_app_activate_window for generic activation and alt-tab support
m('activate_window', function (_om, window, timestamp) {
/* eslint-disable no-invalid-this */
if (!window)
[window] = this.get_windows();
else if (!this._windows.includes(window))
return;
const currentWorkspace = global.workspace_manager.get_active_workspace();
const workspace = window.get_workspace();
const sameWorkspaceWindows = this.get_windows().filter(w =>
w.get_workspace() === workspace);
sameWorkspaceWindows.forEach(w => w.raise());
if (workspace !== currentWorkspace)
workspace.activate_with_focus(window, timestamp);
else
window.activate(timestamp);
/* eslint-enable no-invalid-this */
});
// Re-implements shell_app_activate_full for generic activation and dash support
m('activate_full', function (_om, workspace, timestamp) {
/* eslint-disable no-invalid-this */
if (!timestamp)
timestamp = global.get_current_time();
switch (this.state) {
case Shell.AppState.STOPPED:
try {
this._startingWorkspace = workspace;
this._setState(Shell.AppState.STARTING);
this.launch(timestamp, workspace, Shell.AppLaunchGpu.APP_PREF);
} catch (e) {
logError(e);
this._setState(Shell.AppState.STOPPED);
global.notify_error(_('Failed to launch “%s”'.format(
this.get_name())), e.message);
}
break;
case Shell.AppState.RUNNING:
this.activate_window(null, timestamp);
break;
}
/* eslint-enable no-invalid-this */
});
m('activate', () => shellApp.activate_full(-1, 0));
m('compare', (_om, other) => Utils.shellAppCompare(shellApp, other));
const {destroy: defaultDestroy} = shellApp;
shellApp.destroy = function () {
/* eslint-disable no-invalid-this */
this._dtdData.proxyProperties.forEach(prop => delete this[prop]);
this._dtdData.destroy();
this._dtdData = undefined;
this.appInfo.destroy?.();
this.destroy = defaultDestroy;
defaultDestroy?.call(this);
/* eslint-enable no-invalid-this */
};
return shellApp;
}
/**
* We can't inherit from Shell.App as it's a final type, so let's patch it
*
* @param params
*/
function makeLocationApp(params) {
if (!(params?.appInfo instanceof LocationAppInfo))
throw new TypeError('Invalid location');
const {fallbackIconName} = params;
delete params.fallbackIconName;
const shellApp = new Shell.App(params);
wrapWindowsBackedApp(shellApp);
shellApp._setDtdData({
location: () => shellApp.appInfo.location,
isTrash: shellApp.appInfo instanceof TrashAppInfo,
}, {getter: true, enumerable: true});
shellApp._mi('toString', defaultToString =>
'[LocationApp "%s" - %s]'.format(shellApp.get_id(),
defaultToString.call(shellApp)));
shellApp._mi('launch', (_om, timestamp, workspace, _gpuPref) =>
shellApp.appInfo.launch([],
global.create_app_launch_context(timestamp, workspace)));
shellApp._mi('launch_action', (_om, actionName, ...args) =>
shellApp.appInfo.launchAction(actionName, ...args));
shellApp._mi('create_icon_texture', (_om, iconSize) => new St.Icon({
iconSize,
gicon: shellApp.icon,
fallbackIconName,
}));
// FIXME: We need to add a new API to Nautilus to open new windows
shellApp._mi('can_open_new_window', () => {
try {
if (!shellApp.get_n_windows())
return true;
const handlerApp = shellApp.appInfo.getHandlerApp();
if (handlerApp.has_key('SingleMainWindow'))
return !handlerApp.get_boolean('SingleMainWindow');
if (handlerApp.has_key('X-GNOME-SingleWindow'))
return !handlerApp.get_boolean('X-GNOME-SingleWindow');
if (handlerApp.get_commandline()?.split(' ').includes('--new-window'))
return true;
const [window] = shellApp.get_windows();
if (window && window.get_gtk_window_object_path())
return window.get_gtk_application_id() === null;
return true;
} catch {
return false;
}
});
shellApp._mi('open_new_window', function (_om, workspace) {
/* eslint-disable no-invalid-this */
const context = global.create_app_launch_context(0, workspace);
if (!this.get_n_windows()) {
this.appInfo.launch([], context);
return;
}
const appId = this.appInfo.get_id();
Gio.AppInfo.create_from_commandline(this.appInfo.get_commandline(),
this.appInfo.get_id(), appId
? Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION
: Gio.AppInfoCreateFlags.NONE).launch_uris(
[this.appInfo.location.get_uri()], context);
/* eslint-enable no-invalid-this */
});
if (shellApp.appInfo instanceof MountableVolumeAppInfo) {
shellApp._mi('get_busy', function (parentGetBusy) {
/* eslint-disable no-invalid-this */
if (this.appInfo.busy)
return true;
return parentGetBusy.call(this);
/* eslint-enable no-invalid-this */
});
shellApp._pi('busy', {get: () => shellApp.get_busy()});
shellApp._signalConnections.add(shellApp.appInfo, 'notify::busy', _ =>
shellApp.notify('busy'));
}
shellApp._mi('get_windows', function () {
/* eslint-disable no-invalid-this */
if (this._needsResort)
this._sortWindows();
return this._windows;
/* eslint-enable no-invalid-this */
});
const {fm1Client} = Docking.DockManager.getDefault();
shellApp._setDtdData({
_needsResort: true,
_windowsOrderChanged() {
this._needsResort = true;
this.emit('windows-changed');
},
_sortWindows() {
this._windows.sort(Utils.shellWindowsCompare);
this._needsResort = false;
},
_updateWindows() {
const windows = fm1Client.getWindows(this.location?.get_uri()).sort(
Utils.shellWindowsCompare);
const {windowsChanged} = this._setWindows(windows);
if (!windowsChanged)
return;
this._signalConnections.removeWithLabel(Labels.LOCATION_WINDOWS);
windows.forEach(w =>
this._signalConnections.addWithLabel(Labels.LOCATION_WINDOWS, w,
'notify::user-time', () => {
if (w !== this._windows[0])
this._windowsOrderChanged();
}));
},
}, {readOnly: false});
shellApp._signalConnections.add(fm1Client, 'windows-changed', () =>
shellApp._updateWindows());
shellApp._signalConnections.add(shellApp.appInfo, 'notify::icon', () =>
shellApp.notify('icon'));
shellApp._signalConnections.add(global.workspaceManager,
'workspace-switched', () => shellApp._windowsOrderChanged());
return shellApp;
}
/**
*
*/
function getFileManagerApp() {
return Shell.AppSystem.get_default().lookup_app(FILE_MANAGER_DESKTOP_APP_ID);
}
/**
*
*/
export function wrapFileManagerApp() {
const fileManagerApp = getFileManagerApp();
if (!fileManagerApp)
return null;
if (fileManagerApp._dtdData)
return fileManagerApp;
const originalGetWindows = fileManagerApp.get_windows;
wrapWindowsBackedApp(fileManagerApp);
const {removables, trash} = Docking.DockManager.getDefault();
fileManagerApp._signalConnections.addWithLabel(Labels.WINDOWS_CHANGED,
fileManagerApp, 'windows-changed', () => {
fileManagerApp.stop_emission_by_name('windows-changed');
// Let's wait for the location app to take control before of us
const id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => {
fileManagerApp._sources.delete(id);
fileManagerApp._updateWindows();
return GLib.SOURCE_REMOVE;
});
fileManagerApp._sources.add(id);
});
fileManagerApp._signalConnections.add(global.workspaceManager,
'workspace-switched', () => {
fileManagerApp._signalConnections.blockWithLabel(Labels.WINDOWS_CHANGED);
fileManagerApp.emit('windows-changed');
fileManagerApp._signalConnections.unblockWithLabel(Labels.WINDOWS_CHANGED);
});
if (removables) {
fileManagerApp._signalConnections.add(removables, 'changed', () =>
fileManagerApp._updateWindows());
fileManagerApp._signalConnections.add(removables, 'windows-changed', () =>
fileManagerApp._updateWindows());
}
if (trash?.getApp()) {
fileManagerApp._signalConnections.add(trash.getApp(), 'windows-changed', () =>
fileManagerApp._updateWindows());
}
fileManagerApp._updateWindows = function () {
const locationWindows = [];
getRunningApps().forEach(a => locationWindows.push(...a.get_windows()));
const windows = originalGetWindows.call(this).filter(w =>
!locationWindows.includes(w));
this._signalConnections.blockWithLabel(Labels.WINDOWS_CHANGED);
this._setWindows(windows);
this._signalConnections.unblockWithLabel(Labels.WINDOWS_CHANGED);
};
fileManagerApp._mi('toString', defaultToString =>
'[FileManagerApp - %s]'.format(defaultToString.call(fileManagerApp)));
return fileManagerApp;
}
/**
*
*/
export function unWrapFileManagerApp() {
const fileManagerApp = getFileManagerApp();
if (!fileManagerApp || !fileManagerApp._dtdData)
return;
fileManagerApp.destroy();
}
/**
* This class maintains a Shell.App representing the Trash and keeps it
* up-to-date as the trash fills and is emptied over time.
*/
export class Trash {
destroy() {
this._trashApp?.destroy();
}
_ensureApp() {
if (this._trashApp)
return;
this._trashApp = makeLocationApp({
appInfo: new TrashAppInfo(new Gio.Cancellable()),
fallbackIconName: FALLBACK_TRASH_ICON,
});
}
getApp() {
this._ensureApp();
return this._trashApp;
}
}
/**
* This class maintains Shell.App representations for removable devices
* plugged into the system, and keeps the list of Apps up-to-date as
* devices come and go and are mounted and unmounted.
*/
export class Removables {
static initVolumePromises(object) {
// TODO: This can be simplified using actual interface type when we
// can depend on gjs 1.72
if (!(object instanceof Gio.Volume) || object.constructor.prototype._d2dPromisified)
return;
Gio._promisify(object.constructor.prototype, 'mount', 'mount_finish');
Gio._promisify(object.constructor.prototype, 'eject_with_operation',
'eject_with_operation_finish');
object.constructor.prototype._d2dPromisified = true;
}
static initMountPromises(object) {
// TODO: This can be simplified using actual interface type when we
// can depend on gjs 1.72
if (!(object instanceof Gio.Mount) || object.constructor.prototype._d2dPromisified)
return;
Gio._promisify(object.constructor.prototype, 'eject_with_operation',
'eject_with_operation_finish');
Gio._promisify(object.constructor.prototype, 'unmount_with_operation',
'unmount_with_operation_finish');
object.constructor.prototype._d2dPromisified = true;
}
constructor() {
this._signalsHandler = new Utils.GlobalSignalsHandler();
this._monitor = Gio.VolumeMonitor.get();
this._cancellable = new Gio.Cancellable();
this._monitor.get_mounts().forEach(m => Removables.initMountPromises(m));
this._updateVolumes();
this._signalsHandler.add([
this._monitor,
'volume-added',
(_, volume) => this._onVolumeAdded(volume),
], [
this._monitor,
'volume-removed',
(_, volume) => this._onVolumeRemoved(volume),
], [
this._monitor,
'mount-added',
(_, mount) => this._onMountAdded(mount),
], [
Docking.DockManager.settings,
'changed::show-mounts-only-mounted',
() => this._updateVolumes(),
], [
Docking.DockManager.settings,
'changed::show-mounts-network',
() => this._updateVolumes(),
]);
}
destroy() {
this._volumeApps.forEach(a => a.destroy());
this._volumeApps = [];
this._cancellable.cancel();
this._cancellable = null;
this._signalsHandler.destroy();
this._monitor = null;
}
_updateVolumes() {
this._volumeApps?.forEach(a => a.destroy());
this._volumeApps = [];
this.emit('changed');
this._monitor.get_volumes().forEach(v => this._onVolumeAdded(v));
}
_onVolumeAdded(volume) {
Removables.initVolumePromises(volume);
if (!Docking.DockManager.settings.showMountsNetwork &&
volume.get_identifier('class') === 'network')
return;
const mount = volume.get_mount();
if (mount) {
if (mount.is_shadowed())
return;
if (!mount.can_eject() && !mount.can_unmount())
return;
} else {
if (Docking.DockManager.settings.showMountsOnlyMounted)
return;
if (!volume.can_mount() && !volume.can_eject())
return;
}
const appInfo = new MountableVolumeAppInfo(volume,
new Utils.CancellableChild(this._cancellable));
const volumeApp = makeLocationApp({
appInfo,
fallbackIconName: FALLBACK_REMOVABLE_MEDIA_ICON,
});
volumeApp._signalConnections.add(volumeApp, 'windows-changed',
() => this.emit('windows-changed', volumeApp));
if (Docking.DockManager.settings.showMountsOnlyMounted) {
volumeApp._signalConnections.add(appInfo, 'notify::mount',
() => !appInfo.mount && this._onVolumeRemoved(appInfo.volume));
}
this._volumeApps.push(volumeApp);
this.emit('changed');
}
_onVolumeRemoved(volume) {
const volumeIndex = this._volumeApps.findIndex(({appInfo}) =>
appInfo.volume === volume);
if (volumeIndex !== -1) {
const [volumeApp] = this._volumeApps.splice(volumeIndex, 1);
// We don't care about cancelling the ongoing operations from now on.
volumeApp.appInfo.cancellable = null;
volumeApp.destroy();
this.emit('changed');
}
}
_onMountAdded(mount) {
Removables.initMountPromises(mount);
if (!Docking.DockManager.settings.showMountsOnlyMounted)
return;
if (!this._volumeApps.find(({appInfo}) => appInfo.mount === mount)) {
// In some Gio.Mount implementations the volume may be set after
// mount is emitted, so we could just ignore it as we'll get it
// later via volume-added
const volume = mount.get_volume();
if (volume)
this._onVolumeAdded(volume);
}
}
getApps() {
return this._volumeApps;
}
}
Signals.addSignalMethods(Removables.prototype);
/**
*
*/
function getApps() {
const dockManager = Docking.DockManager.getDefault();
const locationApps = [];
if (dockManager.removables)
locationApps.push(...dockManager.removables.getApps());
if (dockManager.trash)
locationApps.push(dockManager.trash.getApp());
return locationApps;
}
/**
*
*/
export function getRunningApps() {
return getApps().filter(a => a.state === Shell.AppState.RUNNING);
}
/**
*
*/
export function getStartingApps() {
return getApps().filter(a => a.state === Shell.AppState.STARTING);
}