.gitlab
.gitlab-ci
.settings
data
docs
js
dbusServices
extensions
gdm
misc
portalHelper
ui
components
automountManager.js
autorunManager.js
keyring.js
networkAgent.js
polkitAgent.js
telepathyClient.js
status
accessDialog.js
altTab.js
animation.js
appDisplay.js
appFavorites.js
appMenu.js
audioDeviceSelection.js
background.js
backgroundMenu.js
barLevel.js
boxpointer.js
calendar.js
checkBox.js
closeDialog.js
components.js
ctrlAltTab.js
dash.js
dateMenu.js
dialog.js
dnd.js
edgeDragAction.js
endSessionDialog.js
environment.js
extensionDownloader.js
extensionSystem.js
focusCaretTracker.js
grabHelper.js
ibusCandidatePopup.js
iconGrid.js
inhibitShortcutsDialog.js
init.js
kbdA11yDialog.js
keyboard.js
layout.js
lightbox.js
listModes.js
locatePointer.js
lookingGlass.js
magnifier.js
main.js
messageList.js
messageTray.js
modalDialog.js
mpris.js
notificationDaemon.js
osdMonitorLabeler.js
osdWindow.js
overview.js
overviewControls.js
padOsd.js
pageIndicators.js
panel.js
panelMenu.js
pointerA11yTimeout.js
pointerWatcher.js
popupMenu.js
quickSettings.js
remoteSearch.js
ripples.js
runDialog.js
screenShield.js
screenshot.js
scripting.js
search.js
searchController.js
sessionMode.js
shellDBus.js
shellEntry.js
shellMountOperation.js
slider.js
swipeTracker.js
switchMonitor.js
switcherPopup.js
unlockDialog.js
userWidget.js
welcomeDialog.js
windowAttentionHandler.js
windowManager.js
windowMenu.js
windowPreview.js
workspace.js
workspaceAnimation.js
workspaceSwitcherPopup.js
workspaceThumbnail.js
workspacesView.js
xdndHandler.js
js-resources.gresource.xml
meson.build
portal-resources.gresource.xml
lint
man
meson
po
src
subprojects
tests
tools
.editorconfig
.eslintrc.yml
.gitignore
.gitlab-ci.yml
.gitmodules
.jscheckignore
COPYING
NEWS
README.md
config.h.meson
gnome-shell.doap
meson.build
meson_options.txt

This drops all subclasses of `MessageTray.Source` that were used to display system notifications. Now the `Source` returned from `MessageTray.getSystemSource()` is used. Ensure also that properties and methods that where set on the `Source` are moved to the `Notification` object itself. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3156>
308 lines
8.8 KiB
JavaScript
308 lines
8.8 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
import Clutter from 'gi://Clutter';
|
|
import Gio from 'gi://Gio';
|
|
import GObject from 'gi://GObject';
|
|
import St from 'gi://St';
|
|
|
|
import * as GnomeSession from '../../misc/gnomeSession.js';
|
|
import * as MessageTray from '../messageTray.js';
|
|
|
|
Gio._promisify(Gio.Mount.prototype, 'guess_content_type');
|
|
|
|
import {loadInterfaceXML} from '../../misc/fileUtils.js';
|
|
|
|
// GSettings keys
|
|
const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
|
|
const SETTING_DISABLE_AUTORUN = 'autorun-never';
|
|
const SETTING_START_APP = 'autorun-x-content-start-app';
|
|
const SETTING_IGNORE = 'autorun-x-content-ignore';
|
|
const SETTING_OPEN_FOLDER = 'autorun-x-content-open-folder';
|
|
|
|
/** @enum {number} */
|
|
const AutorunSetting = {
|
|
RUN: 0,
|
|
IGNORE: 1,
|
|
FILES: 2,
|
|
ASK: 3,
|
|
};
|
|
|
|
// misc utils
|
|
function shouldAutorunMount(mount) {
|
|
let root = mount.get_root();
|
|
let volume = mount.get_volume();
|
|
|
|
if (!volume || !volume.allowAutorun)
|
|
return false;
|
|
|
|
if (root.is_native() && isMountRootHidden(root))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
function isMountRootHidden(root) {
|
|
let path = root.get_path();
|
|
|
|
// skip any mounts in hidden directory hierarchies
|
|
return path.includes('/.');
|
|
}
|
|
|
|
function isMountNonLocal(mount) {
|
|
// If the mount doesn't have an associated volume, that means it's
|
|
// an uninteresting filesystem. Most devices that we care about will
|
|
// have a mount, like media players and USB sticks.
|
|
let volume = mount.get_volume();
|
|
if (volume == null)
|
|
return true;
|
|
|
|
return volume.get_identifier('class') === 'network';
|
|
}
|
|
|
|
function startAppForMount(app, mount) {
|
|
let files = [];
|
|
let root = mount.get_root();
|
|
let retval = false;
|
|
|
|
files.push(root);
|
|
|
|
try {
|
|
retval = app.launch(files,
|
|
global.create_app_launch_context(0, -1));
|
|
} catch (e) {
|
|
log(`Unable to launch the app ${app.get_name()}: ${e}`);
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
const HotplugSnifferIface = loadInterfaceXML('org.gnome.Shell.HotplugSniffer');
|
|
const HotplugSnifferProxy = Gio.DBusProxy.makeProxyWrapper(HotplugSnifferIface);
|
|
function HotplugSniffer() {
|
|
return new HotplugSnifferProxy(Gio.DBus.session,
|
|
'org.gnome.Shell.HotplugSniffer',
|
|
'/org/gnome/Shell/HotplugSniffer');
|
|
}
|
|
|
|
class ContentTypeDiscoverer {
|
|
constructor() {
|
|
this._settings = new Gio.Settings({schema_id: SETTINGS_SCHEMA});
|
|
}
|
|
|
|
async guessContentTypes(mount) {
|
|
let autorunEnabled = !this._settings.get_boolean(SETTING_DISABLE_AUTORUN);
|
|
let shouldScan = autorunEnabled && !isMountNonLocal(mount);
|
|
|
|
let contentTypes = [];
|
|
if (shouldScan) {
|
|
try {
|
|
contentTypes = await mount.guess_content_type(false, null);
|
|
} catch (e) {
|
|
log(`Unable to guess content types on added mount ${mount.get_name()}: ${e}`);
|
|
}
|
|
|
|
if (contentTypes.length === 0) {
|
|
const root = mount.get_root();
|
|
const hotplugSniffer = new HotplugSniffer();
|
|
[contentTypes] = await hotplugSniffer.SniffURIAsync(root.get_uri());
|
|
}
|
|
}
|
|
|
|
// we're not interested in win32 software content types here
|
|
contentTypes = contentTypes.filter(
|
|
type => type !== 'x-content/win32-software');
|
|
|
|
const apps = [];
|
|
contentTypes.forEach(type => {
|
|
const app = Gio.app_info_get_default_for_type(type, false);
|
|
|
|
if (app)
|
|
apps.push(app);
|
|
});
|
|
|
|
if (apps.length === 0)
|
|
apps.push(Gio.app_info_get_default_for_type('inode/directory', false));
|
|
|
|
return [apps, contentTypes];
|
|
}
|
|
}
|
|
|
|
class AutorunManager {
|
|
constructor() {
|
|
this._session = new GnomeSession.SessionManager();
|
|
this._volumeMonitor = Gio.VolumeMonitor.get();
|
|
|
|
this._dispatcher = new AutorunDispatcher(this);
|
|
}
|
|
|
|
enable() {
|
|
this._volumeMonitor.connectObject(
|
|
'mount-added', this._onMountAdded.bind(this),
|
|
'mount-removed', this._onMountRemoved.bind(this), this);
|
|
}
|
|
|
|
disable() {
|
|
this._volumeMonitor.disconnectObject(this);
|
|
}
|
|
|
|
async _onMountAdded(monitor, mount) {
|
|
// don't do anything if our session is not the currently
|
|
// active one
|
|
if (!this._session.SessionIsActive)
|
|
return;
|
|
|
|
const discoverer = new ContentTypeDiscoverer();
|
|
const [apps, contentTypes] = await discoverer.guessContentTypes(mount);
|
|
this._dispatcher.addMount(mount, apps, contentTypes);
|
|
}
|
|
|
|
_onMountRemoved(monitor, mount) {
|
|
this._dispatcher.removeMount(mount);
|
|
}
|
|
}
|
|
|
|
class AutorunDispatcher {
|
|
constructor(manager) {
|
|
this._manager = manager;
|
|
this._notifications = new Map();
|
|
this._settings = new Gio.Settings({schema_id: SETTINGS_SCHEMA});
|
|
}
|
|
|
|
_getAutorunSettingForType(contentType) {
|
|
let runApp = this._settings.get_strv(SETTING_START_APP);
|
|
if (runApp.includes(contentType))
|
|
return AutorunSetting.RUN;
|
|
|
|
let ignore = this._settings.get_strv(SETTING_IGNORE);
|
|
if (ignore.includes(contentType))
|
|
return AutorunSetting.IGNORE;
|
|
|
|
let openFiles = this._settings.get_strv(SETTING_OPEN_FOLDER);
|
|
if (openFiles.includes(contentType))
|
|
return AutorunSetting.FILES;
|
|
|
|
return AutorunSetting.ASK;
|
|
}
|
|
|
|
_addNotification(mount, apps) {
|
|
// Only show a new notification if there isn't already an existing one
|
|
if (this._notifications.has(mount))
|
|
return;
|
|
|
|
const source = MessageTray.getSystemSource();
|
|
const notification = new AutorunNotification(source, mount, apps);
|
|
notification.connect('destroy', () => this._notifications.delete(mount));
|
|
this._notifications.set(mount, notification);
|
|
source.showNotification(notification);
|
|
}
|
|
|
|
addMount(mount, apps, contentTypes) {
|
|
// if autorun is disabled globally, return
|
|
if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN))
|
|
return;
|
|
|
|
// if the mount doesn't want to be autorun, return
|
|
if (!shouldAutorunMount(mount))
|
|
return;
|
|
|
|
let setting;
|
|
if (contentTypes.length > 0)
|
|
setting = this._getAutorunSettingForType(contentTypes[0]);
|
|
else
|
|
setting = AutorunSetting.ASK;
|
|
|
|
// check at the settings for the first content type
|
|
// to see whether we should ask
|
|
if (setting === AutorunSetting.IGNORE)
|
|
return; // return right away
|
|
|
|
let success = false;
|
|
let app = null;
|
|
|
|
if (setting === AutorunSetting.RUN)
|
|
app = Gio.app_info_get_default_for_type(contentTypes[0], false);
|
|
else if (setting === AutorunSetting.FILES)
|
|
app = Gio.app_info_get_default_for_type('inode/directory', false);
|
|
|
|
if (app)
|
|
success = startAppForMount(app, mount);
|
|
|
|
// we fallback here also in case the settings did not specify 'ask',
|
|
// but we failed launching the default app or the default file manager
|
|
if (!success)
|
|
this._addNotification(mount, apps);
|
|
}
|
|
|
|
removeMount(mount) {
|
|
this._notifications.get(mount)?.destroy();
|
|
}
|
|
}
|
|
|
|
const AutorunNotification = GObject.registerClass(
|
|
class AutorunNotification extends MessageTray.Notification {
|
|
constructor(source, mount, apps) {
|
|
super(source, mount.get_name());
|
|
|
|
this.gicon = mount.get_icon();
|
|
|
|
this._mount = mount;
|
|
this._apps = apps;
|
|
}
|
|
|
|
createBanner() {
|
|
let banner = new MessageTray.NotificationBanner(this);
|
|
|
|
this._apps.forEach(app => {
|
|
let actor = this._buttonForApp(app);
|
|
|
|
if (actor)
|
|
banner.addButton(actor);
|
|
});
|
|
|
|
return banner;
|
|
}
|
|
|
|
_buttonForApp(app) {
|
|
let box = new St.BoxLayout({
|
|
x_expand: true,
|
|
x_align: Clutter.ActorAlign.START,
|
|
});
|
|
const icon = new St.Icon({
|
|
gicon: app.get_icon(),
|
|
style_class: 'hotplug-notification-item-icon',
|
|
});
|
|
box.add_child(icon);
|
|
|
|
let label = new St.Bin({
|
|
child: new St.Label({
|
|
text: _('Open with %s').format(app.get_name()),
|
|
y_align: Clutter.ActorAlign.CENTER,
|
|
}),
|
|
});
|
|
box.add_child(label);
|
|
|
|
const button = new St.Button({
|
|
child: box,
|
|
x_expand: true,
|
|
button_mask: St.ButtonMask.ONE,
|
|
style_class: 'hotplug-notification-item button',
|
|
});
|
|
|
|
button.connect('clicked', () => {
|
|
startAppForMount(app, this._mount);
|
|
this.destroy();
|
|
});
|
|
|
|
return button;
|
|
}
|
|
|
|
activate() {
|
|
super.activate();
|
|
|
|
let app = Gio.app_info_get_default_for_type('inode/directory', false);
|
|
startAppForMount(app, this._mount);
|
|
}
|
|
});
|
|
|
|
export {AutorunManager as Component};
|