/* * ArcMenu - A traditional application menu for GNOME 3 * * ArcMenu Lead Developer and Maintainer * Andrew Zaech https://gitlab.com/AndrewZaech * * ArcMenu Founder, Former Maintainer, and Former Graphic Designer * LinxGem33 https://gitlab.com/LinxGem33 - (No Longer Active) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Credits: This file leverages the work from places-menu extension * (https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/tree/master/extensions/places-menu) * and Dash to Dock extension 'location.js' file to implement a trash shortcut * (https://github.com/micheleg/dash-to-dock/blob/master/locations.js) */ const Me = imports.misc.extensionUtils.getCurrentExtension(); const {St, Gio, GLib, Shell } = imports.gi; const Clutter = imports.gi.Clutter; const Constants = Me.imports.constants; const Gettext = imports.gettext.domain(Me.metadata['gettext-domain']); const GObject = imports.gi.GObject; const Main = imports.ui.main; const MW = Me.imports.menuWidgets; const PopupMenu = imports.ui.popupMenu; const ShellMountOperation = imports.ui.shellMountOperation; const Signals = imports.signals; const Utils = Me.imports.utils; const _ = Gettext.gettext; const BACKGROUND_SCHEMA = 'org.gnome.desktop.background'; const Hostname1Iface = ' \ \ \ \ '; const Hostname1 = Gio.DBusProxy.makeProxyWrapper(Hostname1Iface); var PlaceMenuItem = GObject.registerClass(class Arc_Menu_PlaceMenuItem2 extends MW.ArcMenuPopupBaseMenuItem{ _init(menuLayout, info) { super._init(menuLayout); this._info = info; this._menuLayout = menuLayout; this._settings = this._menuLayout._settings; this._iconBin = new St.Bin(); this.add_child(this._iconBin); this._updateIcon(); this.label = new St.Label({ text: info.name, x_expand: true, y_expand: false, x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.CENTER }); this.add_child(this.label); if (info.isRemovable()) { this.style = "padding-right: 15px;"; this._ejectButton = new MW.ArcMenuButtonItem(this._menuLayout, null, 'media-eject-symbolic'); this._ejectButton.add_style_class_name("arcmenu-small-button") this._ejectButton.setIconSize(14); this._ejectButton.x_align = Clutter.ActorAlign.END; this._ejectButton.x_expand = true; this._ejectButton.connect('activate', info.eject.bind(info)); this.add_child(this._ejectButton); } this._changedId = info.connect('changed', this._propertiesChanged.bind(this)); this.actor.connect('destroy',()=>{ if (this._changedId) { this._info.disconnect(this._changedId); this._changedId = 0; } }); } createIcon(){ const IconSizeEnum = this._settings.get_enum('quicklinks-item-icon-size'); let defaultIconSize = this._menuLayout.layoutProperties.DefaultQuickLinksIconSize; let iconSize = Utils.getIconSize(IconSizeEnum, defaultIconSize); return new St.Icon({ gicon: this._info.icon, icon_size: iconSize }); } activate(event) { this._info.launch(event.get_time()); this._menuLayout.arcMenu.toggle(); super.activate(event); } _propertiesChanged(info) { this._info = info; this._iconBin.set_child(this.createIcon()); this.label.text = info.name; } }); var PlaceInfo = class Arc_Menu_PlaceInfo2 { constructor() { this._init.apply(this, arguments); } _init(kind, file, name, icon) { this.kind = kind; this.file = file; this.name = name || this._getFileName(); this.icon = icon ? new Gio.ThemedIcon({ name: icon }) : this.getIcon(); } destroy() { } isRemovable() { return false; } async _ensureMountAndLaunch(context, tryMount) { try { await this._launchDefaultForUri(this.file.get_uri(), context, null); } catch (e) { if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) { Main.notifyError(_('Failed to launch “%s”').format(this.name), e.message); return; } let source = { get_icon: () => this.icon }; let op = new ShellMountOperation.ShellMountOperation(source); try { await this._mountEnclosingVolume(0, op.mountOp, null); if (tryMount) this._ensureMountAndLaunch(context, false); } catch (e) { if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED)) Main.notifyError(_('Failed to mount volume for “%s”').format(this.name), e.message); } finally { op.close(); } } } launch(timestamp) { let launchContext = global.create_app_launch_context(timestamp, -1); this._ensureMountAndLaunch(launchContext, true); } getIcon() { this.file.query_info_async('standard::symbolic-icon', 0, 0, null, (file, result) => { try { let info = file.query_info_finish(result); this.icon = info.get_symbolic_icon(); this.emit('changed'); } catch (e) { if (e instanceof Gio.IOErrorEnum) return; throw e; } }); // return a generic icon for this kind for now, until we have the // icon from the query info above switch (this.kind) { case 'network': return new Gio.ThemedIcon({ name: 'folder-remote-symbolic' }); case 'devices': return new Gio.ThemedIcon({ name: 'drive-harddisk-symbolic' }); case 'special': case 'bookmarks': default: if (!this.file.is_native()) return new Gio.ThemedIcon({ name: 'folder-remote-symbolic' }); else return new Gio.ThemedIcon({ name: 'folder-symbolic' }); } } _getFileName() { try { let info = this.file.query_info('standard::display-name', 0, null); return info.get_display_name(); } catch (e) { if (e instanceof Gio.IOErrorEnum) return this.file.get_basename(); throw e; } } _launchDefaultForUri(uri, context, cancel) { return new Promise((resolve, reject) => { Gio.AppInfo.launch_default_for_uri_async(uri, context, cancel, (o, res) => { try { Gio.AppInfo.launch_default_for_uri_finish(res); resolve(); } catch (e) { reject(e); } }); }); } _mountEnclosingVolume(flags, mountOp, cancel) { return new Promise((resolve, reject) => { this.file.mount_enclosing_volume(flags, mountOp, cancel, (o, res) => { try { this.file.mount_enclosing_volume_finish(res); resolve(); } catch (e) { reject(e); } }); }); } } Signals.addSignalMethods(PlaceInfo.prototype); var RootInfo = class Arc_Menu_RootInfo extends PlaceInfo { _init() { super._init('devices', Gio.File.new_for_path('/'), _('Computer')); let busName = 'org.freedesktop.hostname1'; let objPath = '/org/freedesktop/hostname1'; new Hostname1(Gio.DBus.system, busName, objPath, (obj, error) => { if (error) return; this._proxy = obj; this._proxyID = this._proxy.connect('g-properties-changed', this._propertiesChanged.bind(this)); this._propertiesChanged(obj); }); } getIcon() { return new Gio.ThemedIcon({ name: 'drive-harddisk-symbolic' }); } _propertiesChanged(proxy) { // GDBusProxy will emit a g-properties-changed when hostname1 goes down // ignore it if (proxy.g_name_owner) { this.name = proxy.PrettyHostname || _('Computer'); this.emit('changed'); } } destroy() { if (this._proxyID) { this._proxy.disconnect(this._proxyID); this._proxy = 0; } if (this._proxy) { this._proxy.run_dispose(); this._proxy = null; } super.destroy(); } }; var PlaceDeviceInfo = class Arc_Menu_PlaceDeviceInfo extends PlaceInfo { _init(kind, mount) { this._mount = mount; super._init(kind, mount.get_root(), mount.get_name()); } getIcon() { return this._mount.get_symbolic_icon(); } isRemovable() { return this._mount.can_eject(); } eject() { let unmountArgs = [ Gio.MountUnmountFlags.NONE, (new ShellMountOperation.ShellMountOperation(this._mount)).mountOp, null // Gio.Cancellable ]; if (this._mount.can_eject()) this._mount.eject_with_operation(...unmountArgs, this._ejectFinish.bind(this)); else this._mount.unmount_with_operation(...unmountArgs, this._unmountFinish.bind(this)); } _ejectFinish(mount, result) { try { mount.eject_with_operation_finish(result); } catch (e) { this._reportFailure(e); } } _unmountFinish(mount, result) { try { mount.unmount_with_operation_finish(result); } catch (e) { this._reportFailure(e); } } _reportFailure(exception) { let msg = _('Ejecting drive “%s” failed:').format(this._mount.get_name()); Main.notifyError(msg, exception.message); } }; var PlaceVolumeInfo = class Arc_Menu_PlaceVolumeInfo extends PlaceInfo { _init(kind, volume) { this._volume = volume; super._init(kind, volume.get_activation_root(), volume.get_name()); } launch(timestamp) { if (this.file) { super.launch(timestamp); return; } this._volume.mount(0, null, null, (volume, result) => { volume.mount_finish(result); let mount = volume.get_mount(); this.file = mount.get_root(); super.launch(timestamp); }); } getIcon() { return this._volume.get_symbolic_icon(); } }; const DefaultDirectories = [ GLib.UserDirectory.DIRECTORY_DOCUMENTS, GLib.UserDirectory.DIRECTORY_PICTURES, GLib.UserDirectory.DIRECTORY_MUSIC, GLib.UserDirectory.DIRECTORY_DOWNLOAD, GLib.UserDirectory.DIRECTORY_VIDEOS, ]; var PlacesManager = class Arc_Menu_PlacesManager { constructor() { this._places = { special: [], devices: [], bookmarks: [], network: [], }; this._settings = new Gio.Settings({ schema_id: BACKGROUND_SCHEMA }); this._showDesktopIconsChangedId = this._settings.connect('changed::show-desktop-icons', this._updateSpecials.bind(this)); this._updateSpecials(); /* * Show devices, code more or less ported from nautilus-places-sidebar.c */ this._volumeMonitor = Gio.VolumeMonitor.get(); this._connectVolumeMonitorSignals(); this._updateMounts(); this._bookmarksFile = this._findBookmarksFile(); this._bookmarkTimeoutId = 0; this._monitor = null; if (this._bookmarksFile) { this._monitor = this._bookmarksFile.monitor_file(Gio.FileMonitorFlags.NONE, null); this._monitor.connect('changed', () => { if (this._bookmarkTimeoutId > 0) return; /* Defensive event compression */ this._bookmarkTimeoutId = GLib.timeout_add( GLib.PRIORITY_DEFAULT, 100, () => { this._bookmarkTimeoutId = 0; this._reloadBookmarks(); return false; }); }); this._reloadBookmarks(); } } _connectVolumeMonitorSignals() { const signals = [ 'volume-added', 'volume-removed', 'volume-changed', 'mount-added', 'mount-removed', 'mount-changed', 'drive-connected', 'drive-disconnected', 'drive-changed' ]; this._volumeMonitorSignals = []; let func = this._updateMounts.bind(this); for (let i = 0; i < signals.length; i++) { let id = this._volumeMonitor.connect(signals[i], func); this._volumeMonitorSignals.push(id); } } destroy() { if (this._settings) this._settings.disconnect(this._showDesktopIconsChangedId); this._settings = null; for (let i = 0; i < this._volumeMonitorSignals.length; i++) this._volumeMonitor.disconnect(this._volumeMonitorSignals[i]); if (this._monitor) this._monitor.cancel(); if (this._bookmarkTimeoutId) GLib.source_remove(this._bookmarkTimeoutId); } _updateSpecials() { this._places.special.forEach(p => p.destroy()); this._places.special = []; let homePath = GLib.get_home_dir(); this._places.special.push(new PlaceInfo('special', Gio.File.new_for_path(homePath), _('Home'))); let specials = []; let dirs = DefaultDirectories.slice(); if (this._settings.get_boolean('show-desktop-icons')) dirs.push(GLib.UserDirectory.DIRECTORY_DESKTOP); for (let i = 0; i < dirs.length; i++) { let specialPath = GLib.get_user_special_dir(dirs[i]); if (specialPath == null || specialPath == homePath) continue; let file = Gio.File.new_for_path(specialPath), info; try { info = new PlaceInfo('special', file); } catch (e) { if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) continue; throw e; } specials.push(info); } specials.sort((a, b) => GLib.utf8_collate(a.name, b.name)); this._places.special = this._places.special.concat(specials); this.emit('special-updated'); } _updateMounts() { let networkMounts = []; let networkVolumes = []; this._places.devices.forEach(p => p.destroy()); this._places.devices = []; this._places.network.forEach(p => p.destroy()); this._places.network = []; /* Add standard places */ // this._places.devices.push(new RootInfo()); /* this._places.network.push(new PlaceInfo('network', Gio.File.new_for_uri('network:///'), _('Network'), 'network-workgroup-symbolic'));*/ /* first go through all connected drives */ let drives = this._volumeMonitor.get_connected_drives(); for (let i = 0; i < drives.length; i++) { let volumes = drives[i].get_volumes(); for (let j = 0; j < volumes.length; j++) { let identifier = volumes[j].get_identifier('class'); if (identifier && identifier.includes('network')) { networkVolumes.push(volumes[j]); } else { let mount = volumes[j].get_mount(); if (mount != null) this._addMount('devices', mount); } } } /* add all volumes that is not associated with a drive */ let volumes = this._volumeMonitor.get_volumes(); for (let i = 0; i < volumes.length; i++) { if (volumes[i].get_drive() != null) continue; let identifier = volumes[i].get_identifier('class'); if (identifier && identifier.includes('network')) { networkVolumes.push(volumes[i]); } else { let mount = volumes[i].get_mount(); if (mount != null) this._addMount('devices', mount); } } /* add mounts that have no volume (/etc/mtab mounts, ftp, sftp,...) */ let mounts = this._volumeMonitor.get_mounts(); for (let i = 0; i < mounts.length; i++) { if (mounts[i].is_shadowed()) continue; if (mounts[i].get_volume()) continue; let root = mounts[i].get_default_location(); if (!root.is_native()) { networkMounts.push(mounts[i]); continue; } this._addMount('devices', mounts[i]); } for (let i = 0; i < networkVolumes.length; i++) { let mount = networkVolumes[i].get_mount(); if (mount) { networkMounts.push(mount); continue; } this._addVolume('network', networkVolumes[i]); } for (let i = 0; i < networkMounts.length; i++) { this._addMount('network', networkMounts[i]); } this.emit('devices-updated'); this.emit('network-updated'); } _findBookmarksFile() { let paths = [ GLib.build_filenamev([GLib.get_user_config_dir(), 'gtk-3.0', 'bookmarks']), GLib.build_filenamev([GLib.get_home_dir(), '.gtk-bookmarks']), ]; for (let i = 0; i < paths.length; i++) { if (GLib.file_test(paths[i], GLib.FileTest.EXISTS)) return Gio.File.new_for_path(paths[i]); } return null; } _reloadBookmarks() { this._bookmarks = []; let content = Shell.get_file_contents_utf8_sync(this._bookmarksFile.get_path()); let lines = content.split('\n'); let bookmarks = []; for (let i = 0; i < lines.length; i++) { let line = lines[i]; let components = line.split(' '); let bookmark = components[0]; if (!bookmark) continue; let file = Gio.File.new_for_uri(bookmark); if (file.is_native() && !file.query_exists(null)) continue; let duplicate = false; for (let i = 0; i < this._places.special.length; i++) { if (file.equal(this._places.special[i].file)) { duplicate = true; break; } } if (duplicate) continue; for (let i = 0; i < bookmarks.length; i++) { if (file.equal(bookmarks[i].file)) { duplicate = true; break; } } if (duplicate) continue; let label = null; if (components.length > 1) label = components.slice(1).join(' '); bookmarks.push(new PlaceInfo('bookmarks', file, label)); } this._places.bookmarks = bookmarks; this.emit('bookmarks-updated'); } _addMount(kind, mount) { let devItem; try { devItem = new PlaceDeviceInfo(kind, mount); } catch (e) { if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) return; throw e; } this._places[kind].push(devItem); } _addVolume(kind, volume) { let volItem; try { volItem = new PlaceVolumeInfo(kind, volume); } catch (e) { if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) return; throw e; } this._places[kind].push(volItem); } get(kind) { return this._places[kind]; } }; Signals.addSignalMethods(PlacesManager.prototype); //Trash can class implemented from Dash to Dock https://github.com/micheleg/dash-to-dock/blob/master/locations.js var Trash = class Arc_Menu_Trash { constructor(menuItem) { this._menuItem = menuItem; let trashPath = GLib.get_home_dir() + '/.local/share/Trash/files/'; this._file = Gio.file_new_for_path(trashPath); try { this._monitor = this._file.monitor_directory(0, null); this._signalId = this._monitor.connect( 'changed', this._onTrashChange.bind(this) ); } catch (e) { log(`Impossible to monitor trash: ${e}`); } this._lastEmpty = true; this._empty = true; this._schedUpdateId = 0; this._updateTrash(); } destroy() { if (this._monitor) { this._monitor.disconnect(this._signalId); this._monitor.run_dispose(); } this._file.run_dispose(); } _onTrashChange() { if (this._schedUpdateId) { GLib.source_remove(this._schedUpdateId); } this._schedUpdateId = GLib.timeout_add( GLib.PRIORITY_DEFAULT, 500, () => { this._schedUpdateId = 0; this._updateTrash(); return GLib.SOURCE_REMOVE; }); } _updateTrash() { try { let children = this._file.enumerate_children('*', 0, null); this._empty = children.next_file(null) == null; children.close(null); } catch (e) { log(`Impossible to enumerate trash children: ${e}`) return; } this._ensureApp(); } _ensureApp() { if (this._trashApp == null || this._lastEmpty != this._empty) { let trashKeys = new GLib.KeyFile(); trashKeys.set_string('Desktop Entry', 'Name', _('Trash')); trashKeys.set_string('Desktop Entry', 'Id', 'ArcMenu_Trash'); trashKeys.set_string('Desktop Entry', 'Icon', this._empty ? 'user-trash-symbolic' : 'user-trash-full-symbolic'); trashKeys.set_string('Desktop Entry', 'Type', 'Application'); trashKeys.set_string('Desktop Entry', 'Exec', 'gio open trash:///'); trashKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); trashKeys.set_string('Desktop Entry', 'XdtdUri', 'trash:///'); if (!this._empty) { trashKeys.set_string('Desktop Entry', 'Actions', 'empty-trash;'); trashKeys.set_string('Desktop Action empty-trash', 'Name', _('Empty Trash')); trashKeys.set_string('Desktop Action empty-trash', 'Exec', 'dbus-send --print-reply --dest=org.gnome.Nautilus /org/gnome/Nautilus org.gnome.Nautilus.FileOperations.EmptyTrash'); } else{ trashKeys.set_string('Desktop Entry', 'Actions', 'empty-trash-inactive;'); trashKeys.set_string('Desktop Action empty-trash-inactive', 'Name', _('Empty Trash')); } let trashAppInfo = Gio.DesktopAppInfo.new_from_keyfile(trashKeys); this._trashApp = new Shell.App({appInfo: trashAppInfo}); this._lastEmpty = this._empty; this._menuItem._app = this._trashApp; if(this._menuItem.contextMenu) this._menuItem.contextMenu._app = this._trashApp; let trashIcon = this._trashApp.create_icon_texture(Constants.MEDIUM_ICON_SIZE); if(this._menuItem._iconBin && trashIcon) this._menuItem.iconName = trashIcon.gicon.to_string(); this._menuItem._updateIcon(); this.emit('changed'); } } getApp() { this._ensureApp(); return this._trashApp; } } Signals.addSignalMethods(Trash.prototype);