mirror of
https://gitlab.com/thebiblelover7/dotfiles.git
synced 2025-09-14 07:33:49 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
/*
|
||||
* Singleton Tracker
|
||||
*/
|
||||
const Default = new Map();
|
||||
|
||||
|
||||
/**
|
||||
* Acquire a reference to a component. Calls to this function should always be
|
||||
* followed by a call to `release()`.
|
||||
*
|
||||
* @param {string} name - The module name
|
||||
* @return {*} The default instance of a component
|
||||
*/
|
||||
function acquire(name) {
|
||||
let component;
|
||||
|
||||
try {
|
||||
let info = Default.get(name);
|
||||
|
||||
if (info === undefined) {
|
||||
const module = imports.service.components[name];
|
||||
|
||||
info = {
|
||||
instance: new module.Component(),
|
||||
refcount: 0,
|
||||
};
|
||||
|
||||
Default.set(name, info);
|
||||
}
|
||||
|
||||
info.refcount++;
|
||||
component = info.instance;
|
||||
} catch (e) {
|
||||
debug(e, name);
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Release a reference on a component. If the caller was the last reference
|
||||
* holder, the component will be freed.
|
||||
*
|
||||
* @param {string} name - The module name
|
||||
* @return {null} A %null value, useful for overriding a traced variable
|
||||
*/
|
||||
function release(name) {
|
||||
try {
|
||||
const info = Default.get(name);
|
||||
|
||||
if (info.refcount === 1) {
|
||||
info.instance.destroy();
|
||||
Default.delete(name);
|
||||
}
|
||||
|
||||
info.refcount--;
|
||||
} catch (e) {
|
||||
debug(e, name);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@@ -0,0 +1,312 @@
|
||||
'use strict';
|
||||
|
||||
imports.gi.versions.Atspi = '2.0';
|
||||
|
||||
const Atspi = imports.gi.Atspi;
|
||||
const Gdk = imports.gi.Gdk;
|
||||
|
||||
|
||||
/**
|
||||
* Printable ASCII range
|
||||
*/
|
||||
const _ASCII = /[\x20-\x7E]/;
|
||||
|
||||
|
||||
/**
|
||||
* Modifier Keycode Defaults
|
||||
*/
|
||||
const XKeycode = {
|
||||
Alt_L: 0x40,
|
||||
Control_L: 0x25,
|
||||
Shift_L: 0x32,
|
||||
Super_L: 0x85,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A thin wrapper around Atspi for X11 sessions without Pipewire support.
|
||||
*/
|
||||
var Controller = class {
|
||||
constructor() {
|
||||
// Atspi.init() return 2 on fail, but still marks itself as inited. We
|
||||
// uninit before throwing an error otherwise any future call to init()
|
||||
// will appear successful and other calls will cause GSConnect to exit.
|
||||
// See: https://gitlab.gnome.org/GNOME/at-spi2-core/blob/master/atspi/atspi-misc.c
|
||||
if (Atspi.init() === 2) {
|
||||
this.destroy();
|
||||
throw new Error('Failed to start AT-SPI');
|
||||
}
|
||||
|
||||
try {
|
||||
this._display = Gdk.Display.get_default();
|
||||
this._seat = this._display.get_default_seat();
|
||||
this._pointer = this._seat.get_pointer();
|
||||
} catch (e) {
|
||||
this.destroy();
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Try to read modifier keycodes from Gdk
|
||||
try {
|
||||
const keymap = Gdk.Keymap.get_for_display(this._display);
|
||||
let modifier;
|
||||
|
||||
modifier = keymap.get_entries_for_keyval(Gdk.KEY_Alt_L)[1][0];
|
||||
XKeycode.Alt_L = modifier.keycode;
|
||||
|
||||
modifier = keymap.get_entries_for_keyval(Gdk.KEY_Control_L)[1][0];
|
||||
XKeycode.Control_L = modifier.keycode;
|
||||
|
||||
modifier = keymap.get_entries_for_keyval(Gdk.KEY_Shift_L)[1][0];
|
||||
XKeycode.Shift_L = modifier.keycode;
|
||||
|
||||
modifier = keymap.get_entries_for_keyval(Gdk.KEY_Super_L)[1][0];
|
||||
XKeycode.Super_L = modifier.keycode;
|
||||
} catch (e) {
|
||||
debug('using default modifier keycodes');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Pointer events
|
||||
*/
|
||||
clickPointer(button) {
|
||||
try {
|
||||
const [, x, y] = this._pointer.get_position();
|
||||
const monitor = this._display.get_monitor_at_point(x, y);
|
||||
const scale = monitor.get_scale_factor();
|
||||
Atspi.generate_mouse_event(scale * x, scale * y, `b${button}c`);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
doubleclickPointer(button) {
|
||||
try {
|
||||
const [, x, y] = this._pointer.get_position();
|
||||
const monitor = this._display.get_monitor_at_point(x, y);
|
||||
const scale = monitor.get_scale_factor();
|
||||
Atspi.generate_mouse_event(scale * x, scale * y, `b${button}d`);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
movePointer(dx, dy) {
|
||||
try {
|
||||
const [, x, y] = this._pointer.get_position();
|
||||
const monitor = this._display.get_monitor_at_point(x, y);
|
||||
const scale = monitor.get_scale_factor();
|
||||
Atspi.generate_mouse_event(scale * dx, scale * dy, 'rel');
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
pressPointer(button) {
|
||||
try {
|
||||
const [, x, y] = this._pointer.get_position();
|
||||
const monitor = this._display.get_monitor_at_point(x, y);
|
||||
const scale = monitor.get_scale_factor();
|
||||
Atspi.generate_mouse_event(scale * x, scale * y, `b${button}p`);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
releasePointer(button) {
|
||||
try {
|
||||
const [, x, y] = this._pointer.get_position();
|
||||
const monitor = this._display.get_monitor_at_point(x, y);
|
||||
const scale = monitor.get_scale_factor();
|
||||
Atspi.generate_mouse_event(scale * x, scale * y, `b${button}r`);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
scrollPointer(dx, dy) {
|
||||
if (dy > 0)
|
||||
this.clickPointer(4);
|
||||
else if (dy < 0)
|
||||
this.clickPointer(5);
|
||||
}
|
||||
|
||||
/*
|
||||
* Phony virtual keyboard helpers
|
||||
*/
|
||||
_modeLock(keycode) {
|
||||
Atspi.generate_keyboard_event(
|
||||
keycode,
|
||||
null,
|
||||
Atspi.KeySynthType.PRESS
|
||||
);
|
||||
}
|
||||
|
||||
_modeUnlock(keycode) {
|
||||
Atspi.generate_keyboard_event(
|
||||
keycode,
|
||||
null,
|
||||
Atspi.KeySynthType.RELEASE
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Simulate a printable-ASCII character.
|
||||
*
|
||||
*/
|
||||
_pressASCII(key, modifiers) {
|
||||
try {
|
||||
// Press Modifiers
|
||||
if (modifiers & Gdk.ModifierType.MOD1_MASK)
|
||||
this._modeLock(XKeycode.Alt_L);
|
||||
if (modifiers & Gdk.ModifierType.CONTROL_MASK)
|
||||
this._modeLock(XKeycode.Control_L);
|
||||
if (modifiers & Gdk.ModifierType.SHIFT_MASK)
|
||||
this._modeLock(XKeycode.Shift_L);
|
||||
if (modifiers & Gdk.ModifierType.SUPER_MASK)
|
||||
this._modeLock(XKeycode.Super_L);
|
||||
|
||||
Atspi.generate_keyboard_event(
|
||||
0,
|
||||
key,
|
||||
Atspi.KeySynthType.STRING
|
||||
);
|
||||
|
||||
// Release Modifiers
|
||||
if (modifiers & Gdk.ModifierType.MOD1_MASK)
|
||||
this._modeUnlock(XKeycode.Alt_L);
|
||||
if (modifiers & Gdk.ModifierType.CONTROL_MASK)
|
||||
this._modeUnlock(XKeycode.Control_L);
|
||||
if (modifiers & Gdk.ModifierType.SHIFT_MASK)
|
||||
this._modeUnlock(XKeycode.Shift_L);
|
||||
if (modifiers & Gdk.ModifierType.SUPER_MASK)
|
||||
this._modeUnlock(XKeycode.Super_L);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
_pressKeysym(keysym, modifiers) {
|
||||
try {
|
||||
// Press Modifiers
|
||||
if (modifiers & Gdk.ModifierType.MOD1_MASK)
|
||||
this._modeLock(XKeycode.Alt_L);
|
||||
if (modifiers & Gdk.ModifierType.CONTROL_MASK)
|
||||
this._modeLock(XKeycode.Control_L);
|
||||
if (modifiers & Gdk.ModifierType.SHIFT_MASK)
|
||||
this._modeLock(XKeycode.Shift_L);
|
||||
if (modifiers & Gdk.ModifierType.SUPER_MASK)
|
||||
this._modeLock(XKeycode.Super_L);
|
||||
|
||||
Atspi.generate_keyboard_event(
|
||||
keysym,
|
||||
null,
|
||||
Atspi.KeySynthType.PRESSRELEASE | Atspi.KeySynthType.SYM
|
||||
);
|
||||
|
||||
// Release Modifiers
|
||||
if (modifiers & Gdk.ModifierType.MOD1_MASK)
|
||||
this._modeUnlock(XKeycode.Alt_L);
|
||||
if (modifiers & Gdk.ModifierType.CONTROL_MASK)
|
||||
this._modeUnlock(XKeycode.Control_L);
|
||||
if (modifiers & Gdk.ModifierType.SHIFT_MASK)
|
||||
this._modeUnlock(XKeycode.Shift_L);
|
||||
if (modifiers & Gdk.ModifierType.SUPER_MASK)
|
||||
this._modeUnlock(XKeycode.Super_L);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate the composition of a unicode character with:
|
||||
* Control+Shift+u, [hex], Return
|
||||
*
|
||||
* @param {number} key - An XKeycode
|
||||
* @param {number} modifiers - A modifier mask
|
||||
*/
|
||||
_pressUnicode(key, modifiers) {
|
||||
try {
|
||||
if (modifiers > 0)
|
||||
log('GSConnect: ignoring modifiers for unicode keyboard event');
|
||||
|
||||
// TODO: Using Control and Shift keysym is not working (it triggers
|
||||
// key release). Probably using LOCKMODIFIERS will not work either
|
||||
// as unlocking the modifier will not trigger a release
|
||||
|
||||
// Activate compose sequence
|
||||
this._modeLock(XKeycode.Control_L);
|
||||
this._modeLock(XKeycode.Shift_L);
|
||||
|
||||
this.pressreleaseKeysym(Gdk.KEY_U);
|
||||
|
||||
this._modeUnlock(XKeycode.Control_L);
|
||||
this._modeUnlock(XKeycode.Shift_L);
|
||||
|
||||
// Enter the unicode sequence
|
||||
const ucode = key.charCodeAt(0).toString(16);
|
||||
let keysym;
|
||||
|
||||
for (let h = 0, len = ucode.length; h < len; h++) {
|
||||
keysym = Gdk.unicode_to_keyval(ucode.charAt(h).codePointAt(0));
|
||||
this.pressreleaseKeysym(keysym);
|
||||
}
|
||||
|
||||
// Finish the compose sequence
|
||||
this.pressreleaseKeysym(Gdk.KEY_Return);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Keyboard Events
|
||||
*/
|
||||
pressKeysym(keysym) {
|
||||
Atspi.generate_keyboard_event(
|
||||
keysym,
|
||||
null,
|
||||
Atspi.KeySynthType.PRESS | Atspi.KeySynthType.SYM
|
||||
);
|
||||
}
|
||||
|
||||
releaseKeysym(keysym) {
|
||||
Atspi.generate_keyboard_event(
|
||||
keysym,
|
||||
null,
|
||||
Atspi.KeySynthType.RELEASE | Atspi.KeySynthType.SYM
|
||||
);
|
||||
}
|
||||
|
||||
pressreleaseKeysym(keysym) {
|
||||
Atspi.generate_keyboard_event(
|
||||
keysym,
|
||||
null,
|
||||
Atspi.KeySynthType.PRESSRELEASE | Atspi.KeySynthType.SYM
|
||||
);
|
||||
}
|
||||
|
||||
pressKey(input, modifiers) {
|
||||
// We were passed a keysym
|
||||
if (typeof input === 'number')
|
||||
this._pressKeysym(input, modifiers);
|
||||
|
||||
// Regular ASCII
|
||||
else if (_ASCII.test(input))
|
||||
this._pressASCII(input, modifiers);
|
||||
|
||||
// Unicode
|
||||
else
|
||||
this._pressUnicode(input, modifiers);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
try {
|
||||
Atspi.exit();
|
||||
} catch (e) {
|
||||
// Silence errors
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -0,0 +1,283 @@
|
||||
'use strict';
|
||||
|
||||
const Gdk = imports.gi.Gdk;
|
||||
const GLib = imports.gi.GLib;
|
||||
const Gtk = imports.gi.Gtk;
|
||||
const Gio = imports.gi.Gio;
|
||||
const GObject = imports.gi.GObject;
|
||||
|
||||
|
||||
const DBUS_NAME = 'org.gnome.Shell.Extensions.GSConnect.Clipboard';
|
||||
const DBUS_PATH = '/org/gnome/Shell/Extensions/GSConnect/Clipboard';
|
||||
|
||||
|
||||
var Clipboard = GObject.registerClass({
|
||||
GTypeName: 'GSConnectClipboard',
|
||||
Properties: {
|
||||
'text': GObject.ParamSpec.string(
|
||||
'text',
|
||||
'Text Content',
|
||||
'The current text content of the clipboard',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
''
|
||||
),
|
||||
},
|
||||
}, class Clipboard extends GObject.Object {
|
||||
|
||||
_init() {
|
||||
super._init();
|
||||
|
||||
this._cancellable = new Gio.Cancellable();
|
||||
this._clipboard = null;
|
||||
|
||||
this._ownerChangeId = 0;
|
||||
this._nameWatcherId = Gio.bus_watch_name(
|
||||
Gio.BusType.SESSION,
|
||||
DBUS_NAME,
|
||||
Gio.BusNameWatcherFlags.NONE,
|
||||
this._onNameAppeared.bind(this),
|
||||
this._onNameVanished.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
get text() {
|
||||
if (this._text === undefined)
|
||||
this._text = '';
|
||||
|
||||
return this._text;
|
||||
}
|
||||
|
||||
set text(content) {
|
||||
if (this.text === content)
|
||||
return;
|
||||
|
||||
this._text = content;
|
||||
this.notify('text');
|
||||
|
||||
if (typeof content !== 'string')
|
||||
return;
|
||||
|
||||
if (this._clipboard instanceof Gtk.Clipboard)
|
||||
this._clipboard.set_text(content, -1);
|
||||
|
||||
if (this._clipboard instanceof Gio.DBusProxy)
|
||||
this._proxySetText(content);
|
||||
}
|
||||
|
||||
async _onNameAppeared(connection, name, name_owner) {
|
||||
try {
|
||||
// Cleanup the GtkClipboard
|
||||
if (this._clipboard && this._ownerChangeId > 0) {
|
||||
this._clipboard.disconnect(this._ownerChangeId);
|
||||
this._ownerChangeId = 0;
|
||||
}
|
||||
|
||||
// Create a proxy for the remote clipboard
|
||||
this._clipboard = new Gio.DBusProxy({
|
||||
g_bus_type: Gio.BusType.SESSION,
|
||||
g_name: DBUS_NAME,
|
||||
g_object_path: DBUS_PATH,
|
||||
g_interface_name: DBUS_NAME,
|
||||
g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
this._clipboard.init_async(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
this._cancellable,
|
||||
(proxy, res) => {
|
||||
try {
|
||||
resolve(proxy.init_finish(res));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
this._ownerChangeId = this._clipboard.connect(
|
||||
'g-signal',
|
||||
this._onOwnerChange.bind(this)
|
||||
);
|
||||
|
||||
this._onOwnerChange();
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
|
||||
debug(e);
|
||||
this._onNameVanished(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onNameVanished(connection, name) {
|
||||
if (this._clipboard && this._ownerChangeId > 0) {
|
||||
this._clipboard.disconnect(this._ownerChangeId);
|
||||
this._clipboardChangedId = 0;
|
||||
}
|
||||
|
||||
const display = Gdk.Display.get_default();
|
||||
this._clipboard = Gtk.Clipboard.get_default(display);
|
||||
|
||||
this._ownerChangeId = this._clipboard.connect(
|
||||
'owner-change',
|
||||
this._onOwnerChange.bind(this)
|
||||
);
|
||||
|
||||
this._onOwnerChange();
|
||||
}
|
||||
|
||||
async _onOwnerChange() {
|
||||
try {
|
||||
if (this._clipboard instanceof Gtk.Clipboard)
|
||||
await this._gtkUpdateText();
|
||||
|
||||
else if (this._clipboard instanceof Gio.DBusProxy)
|
||||
await this._proxyUpdateText();
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
_applyUpdate(text) {
|
||||
if (typeof text !== 'string' || this.text === text)
|
||||
return;
|
||||
|
||||
this._text = text;
|
||||
this.notify('text');
|
||||
}
|
||||
|
||||
/*
|
||||
* Proxy Clipboard
|
||||
*/
|
||||
_proxyGetMimetypes() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._clipboard.call(
|
||||
'GetMimetypes',
|
||||
null,
|
||||
Gio.DBusCallFlags.NO_AUTO_START,
|
||||
-1,
|
||||
this._cancellable,
|
||||
(proxy, res) => {
|
||||
try {
|
||||
const reply = proxy.call_finish(res);
|
||||
resolve(reply.deepUnpack()[0]);
|
||||
} catch (e) {
|
||||
Gio.DBusError.strip_remote_error(e);
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_proxyGetText() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._clipboard.call(
|
||||
'GetText',
|
||||
null,
|
||||
Gio.DBusCallFlags.NO_AUTO_START,
|
||||
-1,
|
||||
this._cancellable,
|
||||
(proxy, res) => {
|
||||
try {
|
||||
const reply = proxy.call_finish(res);
|
||||
resolve(reply.deepUnpack()[0]);
|
||||
} catch (e) {
|
||||
Gio.DBusError.strip_remote_error(e);
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_proxySetText(text) {
|
||||
this._clipboard.call(
|
||||
'SetText',
|
||||
new GLib.Variant('(s)', [text]),
|
||||
Gio.DBusCallFlags.NO_AUTO_START,
|
||||
-1,
|
||||
this._cancellable,
|
||||
(proxy, res) => {
|
||||
try {
|
||||
proxy.call_finish(res);
|
||||
} catch (e) {
|
||||
Gio.DBusError.strip_remote_error(e);
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async _proxyUpdateText() {
|
||||
const mimetypes = await this._proxyGetMimetypes();
|
||||
|
||||
// Special case for a cleared clipboard
|
||||
if (mimetypes.length === 0)
|
||||
return this._applyUpdate('');
|
||||
|
||||
// Special case to ignore copied files
|
||||
if (mimetypes.includes('text/uri-list'))
|
||||
return;
|
||||
|
||||
const text = await this._proxyGetText();
|
||||
|
||||
this._applyUpdate(text);
|
||||
}
|
||||
|
||||
/*
|
||||
* GtkClipboard
|
||||
*/
|
||||
_gtkGetMimetypes() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._clipboard.request_targets((clipboard, atoms) => resolve(atoms));
|
||||
});
|
||||
}
|
||||
|
||||
_gtkGetText() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._clipboard.request_text((clipboard, text) => resolve(text));
|
||||
});
|
||||
}
|
||||
|
||||
async _gtkUpdateText() {
|
||||
const mimetypes = await this._gtkGetMimetypes();
|
||||
|
||||
// Special case for a cleared clipboard
|
||||
if (mimetypes.length === 0)
|
||||
return this._applyUpdate('');
|
||||
|
||||
// Special case to ignore copied files
|
||||
if (mimetypes.includes('text/uri-list'))
|
||||
return;
|
||||
|
||||
const text = await this._gtkGetText();
|
||||
|
||||
this._applyUpdate(text);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._cancellable.is_cancelled())
|
||||
return;
|
||||
|
||||
this._cancellable.cancel();
|
||||
|
||||
if (this._clipboard && this._ownerChangeId > 0) {
|
||||
this._clipboard.disconnect(this._ownerChangeId);
|
||||
this._ownerChangedId = 0;
|
||||
}
|
||||
|
||||
if (this._nameWatcherId > 0) {
|
||||
Gio.bus_unwatch_name(this._nameWatcherId);
|
||||
this._nameWatcherId = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* The service class for this component
|
||||
*/
|
||||
var Component = Clipboard;
|
||||
|
@@ -0,0 +1,703 @@
|
||||
'use strict';
|
||||
|
||||
const ByteArray = imports.byteArray;
|
||||
|
||||
const Gio = imports.gi.Gio;
|
||||
const GLib = imports.gi.GLib;
|
||||
const GObject = imports.gi.GObject;
|
||||
|
||||
const Config = imports.config;
|
||||
|
||||
var HAVE_EDS = true;
|
||||
var EBook = null;
|
||||
var EBookContacts = null;
|
||||
var EDataServer = null;
|
||||
|
||||
try {
|
||||
EBook = imports.gi.EBook;
|
||||
EBookContacts = imports.gi.EBookContacts;
|
||||
EDataServer = imports.gi.EDataServer;
|
||||
} catch (e) {
|
||||
HAVE_EDS = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A store for contacts
|
||||
*/
|
||||
var Store = GObject.registerClass({
|
||||
GTypeName: 'GSConnectContactsStore',
|
||||
Properties: {
|
||||
'context': GObject.ParamSpec.string(
|
||||
'context',
|
||||
'Context',
|
||||
'Used as the cache directory, relative to Config.CACHEDIR',
|
||||
GObject.ParamFlags.CONSTRUCT_ONLY | GObject.ParamFlags.READWRITE,
|
||||
null
|
||||
),
|
||||
},
|
||||
Signals: {
|
||||
'contact-added': {
|
||||
flags: GObject.SignalFlags.RUN_FIRST,
|
||||
param_types: [GObject.TYPE_STRING],
|
||||
},
|
||||
'contact-removed': {
|
||||
flags: GObject.SignalFlags.RUN_FIRST,
|
||||
param_types: [GObject.TYPE_STRING],
|
||||
},
|
||||
'contact-changed': {
|
||||
flags: GObject.SignalFlags.RUN_FIRST,
|
||||
param_types: [GObject.TYPE_STRING],
|
||||
},
|
||||
},
|
||||
}, class Store extends GObject.Object {
|
||||
|
||||
_init(context = null) {
|
||||
super._init({
|
||||
context: context,
|
||||
});
|
||||
|
||||
this._cacheData = {};
|
||||
this._edsPrepared = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an EContact and add it to the store.
|
||||
*
|
||||
* @param {EBookContacts.Contact} econtact - an EContact to parse
|
||||
* @param {string} [origin] - an optional origin string
|
||||
*/
|
||||
async _parseEContact(econtact, origin = 'desktop') {
|
||||
try {
|
||||
const contact = {
|
||||
id: econtact.id,
|
||||
name: _('Unknown Contact'),
|
||||
numbers: [],
|
||||
origin: origin,
|
||||
timestamp: 0,
|
||||
};
|
||||
|
||||
// Try to get a contact name
|
||||
if (econtact.full_name)
|
||||
contact.name = econtact.full_name;
|
||||
|
||||
// Parse phone numbers
|
||||
const nums = econtact.get_attributes(EBookContacts.ContactField.TEL);
|
||||
|
||||
for (const attr of nums) {
|
||||
const number = {
|
||||
value: attr.get_value(),
|
||||
type: 'unknown',
|
||||
};
|
||||
|
||||
if (attr.has_type('CELL'))
|
||||
number.type = 'cell';
|
||||
else if (attr.has_type('HOME'))
|
||||
number.type = 'home';
|
||||
else if (attr.has_type('WORK'))
|
||||
number.type = 'work';
|
||||
|
||||
contact.numbers.push(number);
|
||||
}
|
||||
|
||||
// Try and get a contact photo
|
||||
const photo = econtact.photo;
|
||||
|
||||
if (photo) {
|
||||
if (photo.type === EBookContacts.ContactPhotoType.INLINED) {
|
||||
const data = photo.get_inlined()[0];
|
||||
contact.avatar = await this.storeAvatar(data);
|
||||
|
||||
} else if (photo.type === EBookContacts.ContactPhotoType.URI) {
|
||||
const uri = econtact.photo.get_uri();
|
||||
contact.avatar = uri.replace('file://', '');
|
||||
}
|
||||
}
|
||||
|
||||
this.add(contact, false);
|
||||
} catch (e) {
|
||||
logError(e, `Failed to parse VCard contact ${econtact.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* EDS Helpers
|
||||
*/
|
||||
_getEBookClient(source, cancellable = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
EBook.BookClient.connect(source, 0, cancellable, (source, res) => {
|
||||
try {
|
||||
resolve(EBook.BookClient.connect_finish(res));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getEBookView(client, query = '', cancellable = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.get_view(query, cancellable, (client, res) => {
|
||||
try {
|
||||
resolve(client.get_view_finish(res)[1]);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getEContacts(client, query = '', cancellable = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.get_contacts(query, cancellable, (client, res) => {
|
||||
try {
|
||||
resolve(client.get_contacts_finish(res)[1]);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getESourceRegistry(cancellable = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
EDataServer.SourceRegistry.new(cancellable, (registry, res) => {
|
||||
try {
|
||||
resolve(EDataServer.SourceRegistry.new_finish(res));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* AddressBook DBus callbacks
|
||||
*/
|
||||
_onObjectsAdded(connection, sender, path, iface, signal, params) {
|
||||
try {
|
||||
const adds = params.get_child_value(0).get_strv();
|
||||
|
||||
// NOTE: sequential pairs of vcard, id
|
||||
for (let i = 0, len = adds.length; i < len; i += 2) {
|
||||
try {
|
||||
const vcard = adds[i];
|
||||
const econtact = EBookContacts.Contact.new_from_vcard(vcard);
|
||||
this._parseEContact(econtact);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
_onObjectsRemoved(connection, sender, path, iface, signal, params) {
|
||||
try {
|
||||
const changes = params.get_child_value(0).get_strv();
|
||||
|
||||
for (const id of changes) {
|
||||
try {
|
||||
this.remove(id, false);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
_onObjectsModified(connection, sender, path, iface, signal, params) {
|
||||
try {
|
||||
const changes = params.get_child_value(0).get_strv();
|
||||
|
||||
// NOTE: sequential pairs of vcard, id
|
||||
for (let i = 0, len = changes.length; i < len; i += 2) {
|
||||
try {
|
||||
const vcard = changes[i];
|
||||
const econtact = EBookContacts.Contact.new_from_vcard(vcard);
|
||||
this._parseEContact(econtact);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* SourceRegistryWatcher callbacks
|
||||
*/
|
||||
async _onAppeared(watcher, source) {
|
||||
try {
|
||||
// Get an EBookClient and EBookView
|
||||
const uid = source.get_uid();
|
||||
const client = await this._getEBookClient(source);
|
||||
const view = await this._getEBookView(client, 'exists "tel"');
|
||||
|
||||
// Watch the view for changes to the address book
|
||||
const connection = view.get_connection();
|
||||
const objectPath = view.get_object_path();
|
||||
|
||||
view._objectsAddedId = connection.signal_subscribe(
|
||||
null,
|
||||
'org.gnome.evolution.dataserver.AddressBookView',
|
||||
'ObjectsAdded',
|
||||
objectPath,
|
||||
null,
|
||||
Gio.DBusSignalFlags.NONE,
|
||||
this._onObjectsAdded.bind(this)
|
||||
);
|
||||
|
||||
view._objectsRemovedId = connection.signal_subscribe(
|
||||
null,
|
||||
'org.gnome.evolution.dataserver.AddressBookView',
|
||||
'ObjectsRemoved',
|
||||
objectPath,
|
||||
null,
|
||||
Gio.DBusSignalFlags.NONE,
|
||||
this._onObjectsRemoved.bind(this)
|
||||
);
|
||||
|
||||
view._objectsModifiedId = connection.signal_subscribe(
|
||||
null,
|
||||
'org.gnome.evolution.dataserver.AddressBookView',
|
||||
'ObjectsModified',
|
||||
objectPath,
|
||||
null,
|
||||
Gio.DBusSignalFlags.NONE,
|
||||
this._onObjectsModified.bind(this)
|
||||
);
|
||||
|
||||
view.start();
|
||||
|
||||
// Store the EBook in a map
|
||||
this._ebooks.set(uid, {
|
||||
source: source,
|
||||
client: client,
|
||||
view: view,
|
||||
});
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
_onDisappeared(watcher, source) {
|
||||
try {
|
||||
const uid = source.get_uid();
|
||||
const ebook = this._ebooks.get(uid);
|
||||
|
||||
if (ebook === undefined)
|
||||
return;
|
||||
|
||||
// Disconnect the EBookView
|
||||
if (ebook.view) {
|
||||
const connection = ebook.view.get_connection();
|
||||
connection.signal_unsubscribe(ebook.view._objectsAddedId);
|
||||
connection.signal_unsubscribe(ebook.view._objectsRemovedId);
|
||||
connection.signal_unsubscribe(ebook.view._objectsModifiedId);
|
||||
|
||||
ebook.view.stop();
|
||||
}
|
||||
|
||||
this._ebooks.delete(uid);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
async _initEvolutionDataServer() {
|
||||
try {
|
||||
if (this._edsPrepared)
|
||||
return;
|
||||
|
||||
this._edsPrepared = true;
|
||||
this._ebooks = new Map();
|
||||
|
||||
// Get the current EBooks
|
||||
const registry = await this._getESourceRegistry();
|
||||
|
||||
for (const source of registry.list_sources('Address Book'))
|
||||
await this._onAppeared(null, source);
|
||||
|
||||
// Watch for new and removed sources
|
||||
this._watcher = new EDataServer.SourceRegistryWatcher({
|
||||
registry: registry,
|
||||
extension_name: 'Address Book',
|
||||
});
|
||||
|
||||
this._appearedId = this._watcher.connect(
|
||||
'appeared',
|
||||
this._onAppeared.bind(this)
|
||||
);
|
||||
this._disappearedId = this._watcher.connect(
|
||||
'disappeared',
|
||||
this._onDisappeared.bind(this)
|
||||
);
|
||||
} catch (e) {
|
||||
const service = Gio.Application.get_default();
|
||||
|
||||
if (service !== null)
|
||||
service.notify_error(e);
|
||||
else
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
const contacts = Object.values(this._cacheData);
|
||||
|
||||
for (let i = 0, len = contacts.length; i < len; i++)
|
||||
yield contacts[i];
|
||||
}
|
||||
|
||||
get contacts() {
|
||||
return Object.values(this._cacheData);
|
||||
}
|
||||
|
||||
get context() {
|
||||
if (this._context === undefined)
|
||||
this._context = null;
|
||||
|
||||
return this._context;
|
||||
}
|
||||
|
||||
set context(context) {
|
||||
this._context = context;
|
||||
this._cacheDir = Gio.File.new_for_path(Config.CACHEDIR);
|
||||
|
||||
if (context !== null)
|
||||
this._cacheDir = this._cacheDir.get_child(context);
|
||||
|
||||
GLib.mkdir_with_parents(this._cacheDir.get_path(), 448);
|
||||
this._cacheFile = this._cacheDir.get_child('contacts.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a ByteArray to file and return the path
|
||||
*
|
||||
* @param {ByteArray} contents - An image ByteArray
|
||||
* @return {string|undefined} File path or %undefined on failure
|
||||
*/
|
||||
storeAvatar(contents) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const md5 = GLib.compute_checksum_for_data(
|
||||
GLib.ChecksumType.MD5,
|
||||
contents
|
||||
);
|
||||
const file = this._cacheDir.get_child(`${md5}`);
|
||||
|
||||
if (file.query_exists(null)) {
|
||||
resolve(file.get_path());
|
||||
} else {
|
||||
file.replace_contents_bytes_async(
|
||||
new GLib.Bytes(contents),
|
||||
null,
|
||||
false,
|
||||
Gio.FileCreateFlags.REPLACE_DESTINATION,
|
||||
null,
|
||||
(file, res) => {
|
||||
try {
|
||||
file.replace_contents_finish(res);
|
||||
resolve(file.get_path());
|
||||
} catch (e) {
|
||||
debug(e, 'Storing avatar');
|
||||
resolve(undefined);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the Store for a contact by name and/or number.
|
||||
*
|
||||
* @param {Object} query - A query object
|
||||
* @param {string} [query.name] - The contact's name
|
||||
* @param {string} query.number - The contact's number
|
||||
* @return {Object} A contact object
|
||||
*/
|
||||
query(query) {
|
||||
// First look for an existing contact by number
|
||||
const contacts = this.contacts;
|
||||
const matches = [];
|
||||
const qnumber = query.number.toPhoneNumber();
|
||||
|
||||
for (let i = 0, len = contacts.length; i < len; i++) {
|
||||
const contact = contacts[i];
|
||||
|
||||
for (const num of contact.numbers) {
|
||||
const cnumber = num.value.toPhoneNumber();
|
||||
|
||||
if (qnumber.endsWith(cnumber) || cnumber.endsWith(qnumber)) {
|
||||
// If no query name or exact match, return immediately
|
||||
if (!query.name || query.name === contact.name)
|
||||
return contact;
|
||||
|
||||
// Otherwise we might find an exact name match that shares
|
||||
// the number with another contact
|
||||
matches.push(contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the first match (pretty much what Android does)
|
||||
if (matches.length > 0)
|
||||
return matches[0];
|
||||
|
||||
// No match; return a mock contact with a unique ID
|
||||
let id = GLib.uuid_string_random();
|
||||
|
||||
while (this._cacheData.hasOwnProperty(id))
|
||||
id = GLib.uuid_string_random();
|
||||
|
||||
return {
|
||||
id: id,
|
||||
name: query.name || query.number,
|
||||
numbers: [{value: query.number, type: 'unknown'}],
|
||||
origin: 'gsconnect',
|
||||
};
|
||||
}
|
||||
|
||||
get_contact(position) {
|
||||
if (this._cacheData[position] !== undefined)
|
||||
return this._cacheData[position];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a contact, checking for validity
|
||||
*
|
||||
* @param {Object} contact - A contact object
|
||||
* @param {boolean} write - Write to disk
|
||||
*/
|
||||
add(contact, write = true) {
|
||||
// Ensure the contact has a unique id
|
||||
if (!contact.id) {
|
||||
let id = GLib.uuid_string_random();
|
||||
|
||||
while (this._cacheData[id])
|
||||
id = GLib.uuid_string_random();
|
||||
|
||||
contact.id = id;
|
||||
}
|
||||
|
||||
// Ensure the contact has an origin
|
||||
if (!contact.origin)
|
||||
contact.origin = 'gsconnect';
|
||||
|
||||
// This is an updated contact
|
||||
if (this._cacheData[contact.id]) {
|
||||
this._cacheData[contact.id] = contact;
|
||||
this.emit('contact-changed', contact.id);
|
||||
|
||||
// This is a new contact
|
||||
} else {
|
||||
this._cacheData[contact.id] = contact;
|
||||
this.emit('contact-added', contact.id);
|
||||
}
|
||||
|
||||
// Write if requested
|
||||
if (write)
|
||||
this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a contact by id
|
||||
*
|
||||
* @param {string} id - The id of the contact to delete
|
||||
* @param {boolean} write - Write to disk
|
||||
*/
|
||||
remove(id, write = true) {
|
||||
// Only remove if the contact actually exists
|
||||
if (this._cacheData[id]) {
|
||||
delete this._cacheData[id];
|
||||
this.emit('contact-removed', id);
|
||||
|
||||
// Write if requested
|
||||
if (write)
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a contact for each address object in @addresses and return a
|
||||
* dictionary of address (eg. phone number) to contact object.
|
||||
*
|
||||
* { "555-5555": { "name": "...", "numbers": [], ... } }
|
||||
*
|
||||
* @param {Object[]} addresses - A list of address objects
|
||||
* @return {Object} A dictionary of phone numbers and contacts
|
||||
*/
|
||||
lookupAddresses(addresses) {
|
||||
const contacts = {};
|
||||
|
||||
// Lookup contacts for each address
|
||||
for (let i = 0, len = addresses.length; i < len; i++) {
|
||||
const address = addresses[i].address;
|
||||
|
||||
contacts[address] = this.query({
|
||||
number: address,
|
||||
});
|
||||
}
|
||||
|
||||
return contacts;
|
||||
}
|
||||
|
||||
async clear() {
|
||||
try {
|
||||
const contacts = this.contacts;
|
||||
|
||||
for (let i = 0, len = contacts.length; i < len; i++)
|
||||
await this.remove(contacts[i].id, false);
|
||||
|
||||
await this.save();
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the contact store from a dictionary of our custom contact objects.
|
||||
*
|
||||
* @param {Object} json - an Object of contact Objects
|
||||
*/
|
||||
async update(json = {}) {
|
||||
try {
|
||||
let contacts = Object.values(json);
|
||||
|
||||
for (let i = 0, len = contacts.length; i < len; i++) {
|
||||
const new_contact = contacts[i];
|
||||
const contact = this._cacheData[new_contact.id];
|
||||
|
||||
if (!contact || new_contact.timestamp !== contact.timestamp)
|
||||
await this.add(new_contact, false);
|
||||
}
|
||||
|
||||
// Prune contacts
|
||||
contacts = this.contacts;
|
||||
|
||||
for (let i = 0, len = contacts.length; i < len; i++) {
|
||||
const contact = contacts[i];
|
||||
|
||||
if (!json[contact.id])
|
||||
await this.remove(contact.id, false);
|
||||
}
|
||||
|
||||
await this.save();
|
||||
} catch (e) {
|
||||
debug(e, 'Updating contacts');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and update the contact store from its source.
|
||||
*
|
||||
* The default function initializes the EDS server, or logs a debug message
|
||||
* if EDS is unavailable. Derived classes should request an update from the
|
||||
* remote source.
|
||||
*/
|
||||
async fetch() {
|
||||
try {
|
||||
if (this.context === null && HAVE_EDS)
|
||||
await this._initEvolutionDataServer();
|
||||
else
|
||||
throw new Error('Evolution Data Server not available');
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the contacts from disk.
|
||||
*/
|
||||
async load() {
|
||||
try {
|
||||
this._cacheData = await new Promise((resolve, reject) => {
|
||||
this._cacheFile.load_contents_async(null, (file, res) => {
|
||||
try {
|
||||
const contents = file.load_contents_finish(res)[1];
|
||||
|
||||
resolve(JSON.parse(ByteArray.toString(contents)));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
} finally {
|
||||
this.notify('context');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the contacts to disk.
|
||||
*/
|
||||
async save() {
|
||||
// EDS is handling storage
|
||||
if (this.context === null && HAVE_EDS)
|
||||
return;
|
||||
|
||||
if (this.__cache_lock) {
|
||||
this.__cache_queue = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.__cache_lock = true;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
this._cacheFile.replace_contents_bytes_async(
|
||||
new GLib.Bytes(JSON.stringify(this._cacheData, null, 2)),
|
||||
null,
|
||||
false,
|
||||
Gio.FileCreateFlags.REPLACE_DESTINATION,
|
||||
null,
|
||||
(file, res) => {
|
||||
try {
|
||||
resolve(file.replace_contents_finish(res));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
} finally {
|
||||
this.__cache_lock = false;
|
||||
|
||||
if (this.__cache_queue) {
|
||||
this.__cache_queue = false;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._watcher !== undefined) {
|
||||
this._watcher.disconnect(this._appearedId);
|
||||
this._watcher.disconnect(this._disappearedId);
|
||||
this._watcher = undefined;
|
||||
|
||||
for (const ebook of this._ebooks.values())
|
||||
this._onDisappeared(null, ebook.source);
|
||||
|
||||
this._edsPrepared = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* The service class for this component
|
||||
*/
|
||||
var Component = Store;
|
||||
|
@@ -0,0 +1,641 @@
|
||||
'use strict';
|
||||
|
||||
const Gdk = imports.gi.Gdk;
|
||||
const Gio = imports.gi.Gio;
|
||||
const GLib = imports.gi.GLib;
|
||||
const GObject = imports.gi.GObject;
|
||||
|
||||
|
||||
const SESSION_TIMEOUT = 15;
|
||||
|
||||
|
||||
const RemoteSession = GObject.registerClass({
|
||||
GTypeName: 'GSConnectRemoteSession',
|
||||
Implements: [Gio.DBusInterface],
|
||||
Signals: {
|
||||
'closed': {
|
||||
flags: GObject.SignalFlags.RUN_FIRST,
|
||||
},
|
||||
},
|
||||
}, class RemoteSession extends Gio.DBusProxy {
|
||||
|
||||
_init(objectPath) {
|
||||
super._init({
|
||||
g_bus_type: Gio.BusType.SESSION,
|
||||
g_name: 'org.gnome.Mutter.RemoteDesktop',
|
||||
g_object_path: objectPath,
|
||||
g_interface_name: 'org.gnome.Mutter.RemoteDesktop.Session',
|
||||
g_flags: Gio.DBusProxyFlags.NONE,
|
||||
});
|
||||
|
||||
this._started = false;
|
||||
}
|
||||
|
||||
vfunc_g_signal(sender_name, signal_name, parameters) {
|
||||
if (signal_name === 'Closed')
|
||||
this.emit('closed');
|
||||
}
|
||||
|
||||
_call(name, parameters = null) {
|
||||
if (!this._started)
|
||||
return;
|
||||
|
||||
this.call(name, parameters, Gio.DBusCallFlags.NONE, -1, null, null);
|
||||
}
|
||||
|
||||
get session_id() {
|
||||
try {
|
||||
return this.get_cached_property('SessionId').unpack();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
try {
|
||||
if (this._started)
|
||||
return;
|
||||
|
||||
// Initialize the proxy
|
||||
await new Promise((resolve, reject) => {
|
||||
this.init_async(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
null,
|
||||
(proxy, res) => {
|
||||
try {
|
||||
proxy.init_finish(res);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Start the session
|
||||
await new Promise((resolve, reject) => {
|
||||
this.call(
|
||||
'Start',
|
||||
null,
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
null,
|
||||
(proxy, res) => {
|
||||
try {
|
||||
resolve(proxy.call_finish(res));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
this._started = true;
|
||||
} catch (e) {
|
||||
this.destroy();
|
||||
|
||||
Gio.DBusError.strip_remote_error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this._started) {
|
||||
this._started = false;
|
||||
this.call('Stop', null, Gio.DBusCallFlags.NONE, -1, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
_translateButton(button) {
|
||||
switch (button) {
|
||||
case Gdk.BUTTON_PRIMARY:
|
||||
return 0x110;
|
||||
|
||||
case Gdk.BUTTON_MIDDLE:
|
||||
return 0x112;
|
||||
|
||||
case Gdk.BUTTON_SECONDARY:
|
||||
return 0x111;
|
||||
|
||||
case 4:
|
||||
return 0; // FIXME
|
||||
|
||||
case 5:
|
||||
return 0x10F; // up
|
||||
}
|
||||
}
|
||||
|
||||
movePointer(dx, dy) {
|
||||
this._call(
|
||||
'NotifyPointerMotionRelative',
|
||||
GLib.Variant.new('(dd)', [dx, dy])
|
||||
);
|
||||
}
|
||||
|
||||
pressPointer(button) {
|
||||
button = this._translateButton(button);
|
||||
|
||||
this._call(
|
||||
'NotifyPointerButton',
|
||||
GLib.Variant.new('(ib)', [button, true])
|
||||
);
|
||||
}
|
||||
|
||||
releasePointer(button) {
|
||||
button = this._translateButton(button);
|
||||
|
||||
this._call(
|
||||
'NotifyPointerButton',
|
||||
GLib.Variant.new('(ib)', [button, false])
|
||||
);
|
||||
}
|
||||
|
||||
clickPointer(button) {
|
||||
button = this._translateButton(button);
|
||||
|
||||
this._call(
|
||||
'NotifyPointerButton',
|
||||
GLib.Variant.new('(ib)', [button, true])
|
||||
);
|
||||
|
||||
this._call(
|
||||
'NotifyPointerButton',
|
||||
GLib.Variant.new('(ib)', [button, false])
|
||||
);
|
||||
}
|
||||
|
||||
doubleclickPointer(button) {
|
||||
this.clickPointer(button);
|
||||
this.clickPointer(button);
|
||||
}
|
||||
|
||||
scrollPointer(dx, dy) {
|
||||
// NOTE: NotifyPointerAxis only seems to work on Wayland, but maybe
|
||||
// NotifyPointerAxisDiscrete is the better choice anyways
|
||||
if (HAVE_WAYLAND) {
|
||||
this._call(
|
||||
'NotifyPointerAxis',
|
||||
GLib.Variant.new('(ddu)', [dx, dy, 0])
|
||||
);
|
||||
this._call(
|
||||
'NotifyPointerAxis',
|
||||
GLib.Variant.new('(ddu)', [0, 0, 1])
|
||||
);
|
||||
} else if (dy > 0) {
|
||||
this._call(
|
||||
'NotifyPointerAxisDiscrete',
|
||||
GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, 1])
|
||||
);
|
||||
} else if (dy < 0) {
|
||||
this._call(
|
||||
'NotifyPointerAxisDiscrete',
|
||||
GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, -1])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Keyboard Events
|
||||
*/
|
||||
pressKeysym(keysym) {
|
||||
this._call(
|
||||
'NotifyKeyboardKeysym',
|
||||
GLib.Variant.new('(ub)', [keysym, true])
|
||||
);
|
||||
}
|
||||
|
||||
releaseKeysym(keysym) {
|
||||
this._call(
|
||||
'NotifyKeyboardKeysym',
|
||||
GLib.Variant.new('(ub)', [keysym, false])
|
||||
);
|
||||
}
|
||||
|
||||
pressreleaseKeysym(keysym) {
|
||||
this._call(
|
||||
'NotifyKeyboardKeysym',
|
||||
GLib.Variant.new('(ub)', [keysym, true])
|
||||
);
|
||||
this._call(
|
||||
'NotifyKeyboardKeysym',
|
||||
GLib.Variant.new('(ub)', [keysym, false])
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* High-level keyboard input
|
||||
*/
|
||||
pressKey(input, modifiers) {
|
||||
// Press Modifiers
|
||||
if (modifiers & Gdk.ModifierType.MOD1_MASK)
|
||||
this.pressKeysym(Gdk.KEY_Alt_L);
|
||||
if (modifiers & Gdk.ModifierType.CONTROL_MASK)
|
||||
this.pressKeysym(Gdk.KEY_Control_L);
|
||||
if (modifiers & Gdk.ModifierType.SHIFT_MASK)
|
||||
this.pressKeysym(Gdk.KEY_Shift_L);
|
||||
if (modifiers & Gdk.ModifierType.SUPER_MASK)
|
||||
this.pressKeysym(Gdk.KEY_Super_L);
|
||||
|
||||
if (typeof input === 'string') {
|
||||
const keysym = Gdk.unicode_to_keyval(input.codePointAt(0));
|
||||
this.pressreleaseKeysym(keysym);
|
||||
} else {
|
||||
this.pressreleaseKeysym(input);
|
||||
}
|
||||
|
||||
// Release Modifiers
|
||||
if (modifiers & Gdk.ModifierType.MOD1_MASK)
|
||||
this.releaseKeysym(Gdk.KEY_Alt_L);
|
||||
if (modifiers & Gdk.ModifierType.CONTROL_MASK)
|
||||
this.releaseKeysym(Gdk.KEY_Control_L);
|
||||
if (modifiers & Gdk.ModifierType.SHIFT_MASK)
|
||||
this.releaseKeysym(Gdk.KEY_Shift_L);
|
||||
if (modifiers & Gdk.ModifierType.SUPER_MASK)
|
||||
this.releaseKeysym(Gdk.KEY_Super_L);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.__disposed === undefined) {
|
||||
this.__disposed = true;
|
||||
GObject.signal_handlers_destroy(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class Controller {
|
||||
constructor() {
|
||||
this._nameAppearedId = 0;
|
||||
this._session = null;
|
||||
this._sessionCloseId = 0;
|
||||
this._sessionExpiry = 0;
|
||||
this._sessionExpiryId = 0;
|
||||
this._sessionStarting = false;
|
||||
|
||||
// Watch for the RemoteDesktop portal
|
||||
this._nameWatcherId = Gio.bus_watch_name(
|
||||
Gio.BusType.SESSION,
|
||||
'org.gnome.Mutter.RemoteDesktop',
|
||||
Gio.BusNameWatcherFlags.NONE,
|
||||
this._onNameAppeared.bind(this),
|
||||
this._onNameVanished.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
get connection() {
|
||||
if (this._connection === undefined)
|
||||
this._connection = null;
|
||||
|
||||
return this._connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a Wayland session, specifically for distributions that
|
||||
* don't ship pipewire support (eg. Debian/Ubuntu).
|
||||
*
|
||||
* FIXME: this is a super ugly hack that should go away
|
||||
*
|
||||
* @return {boolean} %true if wayland is not supported
|
||||
*/
|
||||
_checkWayland() {
|
||||
if (HAVE_WAYLAND) {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
HAVE_REMOTEINPUT = false;
|
||||
const service = Gio.Application.get_default();
|
||||
|
||||
if (service === null)
|
||||
return true;
|
||||
|
||||
// First we're going to disabled the affected plugins on all devices
|
||||
for (const device of service.manager.devices.values()) {
|
||||
const supported = device.settings.get_strv('supported-plugins');
|
||||
let index;
|
||||
|
||||
if ((index = supported.indexOf('mousepad')) > -1)
|
||||
supported.splice(index, 1);
|
||||
|
||||
if ((index = supported.indexOf('presenter')) > -1)
|
||||
supported.splice(index, 1);
|
||||
|
||||
device.settings.set_strv('supported-plugins', supported);
|
||||
}
|
||||
|
||||
// Second we need each backend to rebuild its identity packet and
|
||||
// broadcast the amended capabilities to the network
|
||||
for (const backend of service.manager.backends.values())
|
||||
backend.buildIdentity();
|
||||
|
||||
service.manager.identify();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_onNameAppeared(connection, name, name_owner) {
|
||||
try {
|
||||
this._connection = connection;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
_onNameVanished(connection, name) {
|
||||
try {
|
||||
if (this._session !== null)
|
||||
this._onSessionClosed(this._session);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
_onSessionClosed(session) {
|
||||
// Disconnect from the session
|
||||
if (this._sessionClosedId > 0) {
|
||||
session.disconnect(this._sessionClosedId);
|
||||
this._sessionClosedId = 0;
|
||||
}
|
||||
|
||||
// Destroy the session
|
||||
session.destroy();
|
||||
this._session = null;
|
||||
}
|
||||
|
||||
_onSessionExpired() {
|
||||
// If the session has been used recently, schedule a new expiry
|
||||
const remainder = Math.floor(this._sessionExpiry - (Date.now() / 1000));
|
||||
|
||||
if (remainder > 0) {
|
||||
this._sessionExpiryId = GLib.timeout_add_seconds(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
remainder,
|
||||
this._onSessionExpired.bind(this)
|
||||
);
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// Otherwise if there's an active session, close it
|
||||
if (this._session !== null)
|
||||
this._session.stop();
|
||||
|
||||
// Reset the GSource Id
|
||||
this._sessionExpiryId = 0;
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
_createRemoteDesktopSession() {
|
||||
if (this.connection === null)
|
||||
return Promise.reject(new Error('No DBus connection'));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connection.call(
|
||||
'org.gnome.Mutter.RemoteDesktop',
|
||||
'/org/gnome/Mutter/RemoteDesktop',
|
||||
'org.gnome.Mutter.RemoteDesktop',
|
||||
'CreateSession',
|
||||
null,
|
||||
null,
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
null,
|
||||
(connection, res) => {
|
||||
try {
|
||||
res = connection.call_finish(res);
|
||||
resolve(res.deepUnpack()[0]);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_createScreenCastSession(sessionId) {
|
||||
if (this.connection === null)
|
||||
return Promise.reject(new Error('No DBus connection'));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = new GLib.Variant('(a{sv})', [{
|
||||
'disable-animations': GLib.Variant.new_boolean(false),
|
||||
'remote-desktop-session-id': GLib.Variant.new_string(sessionId),
|
||||
}]);
|
||||
|
||||
this.connection.call(
|
||||
'org.gnome.Mutter.ScreenCast',
|
||||
'/org/gnome/Mutter/ScreenCast',
|
||||
'org.gnome.Mutter.ScreenCast',
|
||||
'CreateSession',
|
||||
options,
|
||||
null,
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
null,
|
||||
(connection, res) => {
|
||||
try {
|
||||
res = connection.call_finish(res);
|
||||
resolve(res.deepUnpack()[0]);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async _ensureAdapter() {
|
||||
try {
|
||||
// Update the timestamp of the last event
|
||||
this._sessionExpiry = Math.floor((Date.now() / 1000) + SESSION_TIMEOUT);
|
||||
|
||||
// Session is active
|
||||
if (this._session !== null)
|
||||
return;
|
||||
|
||||
// Mutter's RemoteDesktop is not available, fall back to Atspi
|
||||
if (this.connection === null) {
|
||||
debug('Falling back to Atspi');
|
||||
|
||||
// If we got here in Wayland, we need to re-adjust and bail
|
||||
if (this._checkWayland())
|
||||
return;
|
||||
|
||||
const fallback = imports.service.components.atspi;
|
||||
this._session = new fallback.Controller();
|
||||
|
||||
// Mutter is available and there isn't another session starting
|
||||
} else if (this._sessionStarting === false) {
|
||||
this._sessionStarting = true;
|
||||
|
||||
debug('Creating Mutter RemoteDesktop session');
|
||||
|
||||
// This takes three steps: creating the remote desktop session,
|
||||
// starting the session, and creating a screencast session for
|
||||
// the remote desktop session.
|
||||
const objectPath = await this._createRemoteDesktopSession();
|
||||
|
||||
this._session = new RemoteSession(objectPath);
|
||||
await this._session.start();
|
||||
|
||||
await this._createScreenCastSession(this._session.session_id);
|
||||
|
||||
// Watch for the session ending
|
||||
this._sessionClosedId = this._session.connect(
|
||||
'closed',
|
||||
this._onSessionClosed.bind(this)
|
||||
);
|
||||
|
||||
if (this._sessionExpiryId === 0) {
|
||||
this._sessionExpiryId = GLib.timeout_add_seconds(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
SESSION_TIMEOUT,
|
||||
this._onSessionExpired.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
this._sessionStarting = false;
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
|
||||
if (this._session !== null) {
|
||||
this._session.destroy();
|
||||
this._session = null;
|
||||
}
|
||||
|
||||
this._sessionStarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Pointer Events
|
||||
*/
|
||||
movePointer(dx, dy) {
|
||||
try {
|
||||
if (dx === 0 && dy === 0)
|
||||
return;
|
||||
|
||||
this._ensureAdapter();
|
||||
this._session.movePointer(dx, dy);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
pressPointer(button) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.pressPointer(button);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
releasePointer(button) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.releasePointer(button);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
clickPointer(button) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.clickPointer(button);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
doubleclickPointer(button) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.doubleclickPointer(button);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
scrollPointer(dx, dy) {
|
||||
if (dx === 0 && dy === 0)
|
||||
return;
|
||||
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.scrollPointer(dx, dy);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Keyboard Events
|
||||
*/
|
||||
pressKeysym(keysym) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.pressKeysym(keysym);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
releaseKeysym(keysym) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.releaseKeysym(keysym);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
pressreleaseKeysym(keysym) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.pressreleaseKeysym(keysym);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* High-level keyboard input
|
||||
*/
|
||||
pressKey(input, modifiers) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.pressKey(input, modifiers);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._session !== null) {
|
||||
// Disconnect from the session
|
||||
if (this._sessionClosedId > 0) {
|
||||
this._session.disconnect(this._sessionClosedId);
|
||||
this._sessionClosedId = 0;
|
||||
}
|
||||
|
||||
this._session.destroy();
|
||||
this._session = null;
|
||||
}
|
||||
|
||||
if (this._nameWatcherId > 0) {
|
||||
Gio.bus_unwatch_name(this._nameWatcherId);
|
||||
this._nameWatcherId = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The service class for this component
|
||||
*/
|
||||
var Component = Controller;
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,440 @@
|
||||
'use strict';
|
||||
|
||||
const Gio = imports.gi.Gio;
|
||||
const GLib = imports.gi.GLib;
|
||||
const GjsPrivate = imports.gi.GjsPrivate;
|
||||
const GObject = imports.gi.GObject;
|
||||
|
||||
const DBus = imports.service.utils.dbus;
|
||||
|
||||
|
||||
const _nodeInfo = Gio.DBusNodeInfo.new_for_xml(`
|
||||
<node>
|
||||
<interface name="org.freedesktop.Notifications">
|
||||
<method name="Notify">
|
||||
<arg name="appName" type="s" direction="in"/>
|
||||
<arg name="replacesId" type="u" direction="in"/>
|
||||
<arg name="iconName" type="s" direction="in"/>
|
||||
<arg name="summary" type="s" direction="in"/>
|
||||
<arg name="body" type="s" direction="in"/>
|
||||
<arg name="actions" type="as" direction="in"/>
|
||||
<arg name="hints" type="a{sv}" direction="in"/>
|
||||
<arg name="timeout" type="i" direction="in"/>
|
||||
</method>
|
||||
</interface>
|
||||
<interface name="org.gtk.Notifications">
|
||||
<method name="AddNotification">
|
||||
<arg type="s" direction="in"/>
|
||||
<arg type="s" direction="in"/>
|
||||
<arg type="a{sv}" direction="in"/>
|
||||
</method>
|
||||
<method name="RemoveNotification">
|
||||
<arg type="s" direction="in"/>
|
||||
<arg type="s" direction="in"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
||||
`);
|
||||
|
||||
|
||||
const FDO_IFACE = _nodeInfo.lookup_interface('org.freedesktop.Notifications');
|
||||
const FDO_MATCH = "interface='org.freedesktop.Notifications',member='Notify',type='method_call'";
|
||||
|
||||
const GTK_IFACE = _nodeInfo.lookup_interface('org.gtk.Notifications');
|
||||
const GTK_MATCH = "interface='org.gtk.Notifications',member='AddNotification',type='method_call'";
|
||||
|
||||
|
||||
/**
|
||||
* A class for snooping Freedesktop (libnotify) and Gtk (GNotification)
|
||||
* notifications and forwarding them to supporting devices.
|
||||
*/
|
||||
const Listener = GObject.registerClass({
|
||||
GTypeName: 'GSConnectNotificationListener',
|
||||
Signals: {
|
||||
'notification-added': {
|
||||
flags: GObject.SignalFlags.RUN_LAST,
|
||||
param_types: [GLib.Variant.$gtype],
|
||||
},
|
||||
},
|
||||
}, class Listener extends GObject.Object {
|
||||
|
||||
_init() {
|
||||
super._init();
|
||||
|
||||
// Respect desktop notification settings
|
||||
this._settings = new Gio.Settings({
|
||||
schema_id: 'org.gnome.desktop.notifications',
|
||||
});
|
||||
|
||||
// Watch for new application policies
|
||||
this._settingsId = this._settings.connect(
|
||||
'changed::application-children',
|
||||
this._onSettingsChanged.bind(this)
|
||||
);
|
||||
|
||||
// Cache for appName->desktop-id lookups
|
||||
this._names = {};
|
||||
|
||||
// Asynchronous setup
|
||||
this._init_async();
|
||||
}
|
||||
|
||||
get applications() {
|
||||
if (this._applications === undefined)
|
||||
this._onSettingsChanged();
|
||||
|
||||
return this._applications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update application notification settings
|
||||
*/
|
||||
_onSettingsChanged() {
|
||||
this._applications = {};
|
||||
|
||||
for (const app of this._settings.get_strv('application-children')) {
|
||||
const appSettings = new Gio.Settings({
|
||||
schema_id: 'org.gnome.desktop.notifications.application',
|
||||
path: `/org/gnome/desktop/notifications/application/${app}/`,
|
||||
});
|
||||
|
||||
const appInfo = Gio.DesktopAppInfo.new(
|
||||
appSettings.get_string('application-id')
|
||||
);
|
||||
|
||||
if (appInfo !== null)
|
||||
this._applications[appInfo.get_name()] = appSettings;
|
||||
}
|
||||
}
|
||||
|
||||
_listNames() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._session.call(
|
||||
'org.freedesktop.DBus',
|
||||
'/org/freedesktop/DBus',
|
||||
'org.freedesktop.DBus',
|
||||
'ListNames',
|
||||
null,
|
||||
null,
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
null,
|
||||
(connection, res) => {
|
||||
try {
|
||||
res = connection.call_finish(res);
|
||||
resolve(res.deepUnpack()[0]);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_getNameOwner(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._session.call(
|
||||
'org.freedesktop.DBus',
|
||||
'/org/freedesktop/DBus',
|
||||
'org.freedesktop.DBus',
|
||||
'GetNameOwner',
|
||||
new GLib.Variant('(s)', [name]),
|
||||
null,
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
null,
|
||||
(connection, res) => {
|
||||
try {
|
||||
res = connection.call_finish(res);
|
||||
resolve(res.deepUnpack()[0]);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try and find a well-known name for @sender on the session bus
|
||||
*
|
||||
* @param {string} sender - A DBus unique name (eg. :1.2282)
|
||||
* @param {string} appName - @appName passed to Notify() (Optional)
|
||||
* @return {string} A well-known name or %null
|
||||
*/
|
||||
async _getAppId(sender, appName) {
|
||||
try {
|
||||
// Get a list of well-known names, ignoring @sender
|
||||
const names = await this._listNames();
|
||||
names.splice(names.indexOf(sender), 1);
|
||||
|
||||
// Make a short list for substring matches (fractal/org.gnome.Fractal)
|
||||
const appLower = appName.toLowerCase();
|
||||
|
||||
const shortList = names.filter(name => {
|
||||
return name.toLowerCase().includes(appLower);
|
||||
});
|
||||
|
||||
// Run the short list first
|
||||
for (const name of shortList) {
|
||||
const nameOwner = await this._getNameOwner(name);
|
||||
|
||||
if (nameOwner === sender)
|
||||
return name;
|
||||
|
||||
names.splice(names.indexOf(name), 1);
|
||||
}
|
||||
|
||||
// Run the full list
|
||||
for (const name of names) {
|
||||
const nameOwner = await this._getNameOwner(name);
|
||||
|
||||
if (nameOwner === sender)
|
||||
return name;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try and find the application name for @sender
|
||||
*
|
||||
* @param {string} sender - A DBus unique name
|
||||
* @param {string} [appName] - `appName` supplied by Notify()
|
||||
* @return {string} A well-known name or %null
|
||||
*/
|
||||
async _getAppName(sender, appName = null) {
|
||||
// Check the cache first
|
||||
if (appName && this._names.hasOwnProperty(appName))
|
||||
return this._names[appName];
|
||||
|
||||
try {
|
||||
const appId = await this._getAppId(sender, appName);
|
||||
const appInfo = Gio.DesktopAppInfo.new(`${appId}.desktop`);
|
||||
this._names[appName] = appInfo.get_name();
|
||||
appName = appInfo.get_name();
|
||||
} catch (e) {
|
||||
// Silence errors
|
||||
}
|
||||
|
||||
return appName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for AddNotification()/Notify()
|
||||
*
|
||||
* @param {DBus.Interface} iface - The DBus interface
|
||||
* @param {string} name - The DBus method name
|
||||
* @param {GLib.Variant} parameters - The method parameters
|
||||
* @param {Gio.DBusMethodInvocation} invocation - The method invocation info
|
||||
*/
|
||||
async _onHandleMethodCall(iface, name, parameters, invocation) {
|
||||
try {
|
||||
// Check if notifications are disabled in desktop settings
|
||||
if (!this._settings.get_boolean('show-banners'))
|
||||
return;
|
||||
|
||||
parameters = parameters.full_unpack();
|
||||
|
||||
// GNotification
|
||||
if (name === 'AddNotification') {
|
||||
this.AddNotification(...parameters);
|
||||
|
||||
// libnotify
|
||||
} else if (name === 'Notify') {
|
||||
const message = invocation.get_message();
|
||||
|
||||
if (this._fdoNameOwner === undefined) {
|
||||
this._fdoNameOwner = await this._getNameOwner(
|
||||
'org.freedesktop.Notifications');
|
||||
}
|
||||
|
||||
if (this._fdoNameOwner !== message.get_destination())
|
||||
return;
|
||||
|
||||
// Try to brute-force an application name using DBus
|
||||
if (!this.applications.hasOwnProperty(parameters[0])) {
|
||||
const sender = message.get_sender();
|
||||
parameters[0] = await this._getAppName(sender, parameters[0]);
|
||||
}
|
||||
|
||||
this.Notify(...parameters);
|
||||
}
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export interfaces for proxying notifications and become a monitor
|
||||
*
|
||||
* @return {Promise} A promise for the operation
|
||||
*/
|
||||
_monitorConnection() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// libnotify Interface
|
||||
this._fdoNotifications = new GjsPrivate.DBusImplementation({
|
||||
g_interface_info: FDO_IFACE,
|
||||
});
|
||||
this._fdoMethodCallId = this._fdoNotifications.connect(
|
||||
'handle-method-call',
|
||||
this._onHandleMethodCall.bind(this)
|
||||
);
|
||||
this._fdoNotifications.export(
|
||||
this._monitor,
|
||||
'/org/freedesktop/Notifications'
|
||||
);
|
||||
|
||||
this._fdoNameOwnerChangedId = this._session.signal_subscribe(
|
||||
'org.freedesktop.DBus',
|
||||
'org.freedesktop.DBus',
|
||||
'NameOwnerChanged',
|
||||
'/org/freedesktop/DBus',
|
||||
'org.freedesktop.Notifications',
|
||||
Gio.DBusSignalFlags.MATCH_ARG0_NAMESPACE,
|
||||
this._onFdoNameOwnerChanged.bind(this)
|
||||
);
|
||||
|
||||
// GNotification Interface
|
||||
this._gtkNotifications = new GjsPrivate.DBusImplementation({
|
||||
g_interface_info: GTK_IFACE,
|
||||
});
|
||||
this._gtkMethodCallId = this._gtkNotifications.connect(
|
||||
'handle-method-call',
|
||||
this._onHandleMethodCall.bind(this)
|
||||
);
|
||||
this._gtkNotifications.export(
|
||||
this._monitor,
|
||||
'/org/gtk/Notifications'
|
||||
);
|
||||
|
||||
// Become a monitor for Fdo & Gtk notifications
|
||||
this._monitor.call(
|
||||
'org.freedesktop.DBus',
|
||||
'/org/freedesktop/DBus',
|
||||
'org.freedesktop.DBus.Monitoring',
|
||||
'BecomeMonitor',
|
||||
new GLib.Variant('(asu)', [[FDO_MATCH, GTK_MATCH], 0]),
|
||||
null,
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
null,
|
||||
(connection, res) => {
|
||||
try {
|
||||
resolve(connection.call_finish(res));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async _init_async() {
|
||||
try {
|
||||
this._session = await DBus.getConnection();
|
||||
this._monitor = await DBus.newConnection();
|
||||
await this._monitorConnection();
|
||||
} catch (e) {
|
||||
const service = Gio.Application.get_default();
|
||||
|
||||
if (service !== null)
|
||||
service.notify_error(e);
|
||||
else
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
_onFdoNameOwnerChanged(connection, sender, object, iface, signal, parameters) {
|
||||
this._fdoNameOwner = parameters.deepUnpack()[2];
|
||||
}
|
||||
|
||||
_sendNotification(notif) {
|
||||
// Check if this application is disabled in desktop settings
|
||||
const appSettings = this.applications[notif.appName];
|
||||
|
||||
if (appSettings && !appSettings.get_boolean('enable'))
|
||||
return;
|
||||
|
||||
// Send the notification to each supporting device
|
||||
// TODO: avoid the overhead of the GAction framework with a signal?
|
||||
const variant = GLib.Variant.full_pack(notif);
|
||||
this.emit('notification-added', variant);
|
||||
}
|
||||
|
||||
Notify(appName, replacesId, iconName, summary, body, actions, hints, timeout) {
|
||||
// Ignore notifications without an appName
|
||||
if (!appName)
|
||||
return;
|
||||
|
||||
this._sendNotification({
|
||||
appName: appName,
|
||||
id: `fdo|null|${replacesId}`,
|
||||
title: summary,
|
||||
text: body,
|
||||
ticker: `${summary}: ${body}`,
|
||||
isClearable: (replacesId !== 0),
|
||||
icon: iconName,
|
||||
});
|
||||
}
|
||||
|
||||
AddNotification(application, id, notification) {
|
||||
// Ignore our own notifications or we'll cause a notification loop
|
||||
if (application === 'org.gnome.Shell.Extensions.GSConnect')
|
||||
return;
|
||||
|
||||
const appInfo = Gio.DesktopAppInfo.new(`${application}.desktop`);
|
||||
|
||||
// Try to get an icon for the notification
|
||||
if (!notification.hasOwnProperty('icon'))
|
||||
notification.icon = appInfo.get_icon() || undefined;
|
||||
|
||||
this._sendNotification({
|
||||
appName: appInfo.get_name(),
|
||||
id: `gtk|${application}|${id}`,
|
||||
title: notification.title,
|
||||
text: notification.body,
|
||||
ticker: `${notification.title}: ${notification.body}`,
|
||||
isClearable: true,
|
||||
icon: notification.icon,
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
try {
|
||||
if (this._fdoNotifications) {
|
||||
this._fdoNotifications.disconnect(this._fdoMethodCallId);
|
||||
this._fdoNotifications.unexport();
|
||||
this._session.signal_unsubscribe(this._fdoNameOwnerChangedId);
|
||||
}
|
||||
|
||||
if (this._gtkNotifications) {
|
||||
this._gtkNotifications.disconnect(this._gtkMethodCallId);
|
||||
this._gtkNotifications.unexport();
|
||||
}
|
||||
|
||||
if (this._settings) {
|
||||
this._settings.disconnect(this._settingsId);
|
||||
this._settings.run_dispose();
|
||||
}
|
||||
|
||||
// TODO: Gio.IOErrorEnum: The connection is closed
|
||||
// this._monitor.close_sync(null);
|
||||
|
||||
GObject.signal_handlers_destroy(this);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* The service class for this component
|
||||
*/
|
||||
var Component = Listener;
|
||||
|
@@ -0,0 +1,265 @@
|
||||
'use strict';
|
||||
|
||||
const Tweener = imports.tweener.tweener;
|
||||
|
||||
const GIRepository = imports.gi.GIRepository;
|
||||
const GLib = imports.gi.GLib;
|
||||
const GObject = imports.gi.GObject;
|
||||
|
||||
const Config = imports.config;
|
||||
|
||||
|
||||
// Add gnome-shell's typelib dir to the search path
|
||||
const typelibDir = GLib.build_filenamev([Config.GNOME_SHELL_LIBDIR, 'gnome-shell']);
|
||||
GIRepository.Repository.prepend_search_path(typelibDir);
|
||||
GIRepository.Repository.prepend_library_path(typelibDir);
|
||||
|
||||
const Gvc = imports.gi.Gvc;
|
||||
|
||||
|
||||
/**
|
||||
* Extend Gvc.MixerStream with a property for returning a user-visible name
|
||||
*/
|
||||
Object.defineProperty(Gvc.MixerStream.prototype, 'display_name', {
|
||||
get: function () {
|
||||
try {
|
||||
if (!this.get_ports().length)
|
||||
return this.description;
|
||||
|
||||
return `${this.get_port().human_port} (${this.description})`;
|
||||
} catch (e) {
|
||||
return this.description;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* A convenience wrapper for Gvc.MixerStream
|
||||
*/
|
||||
class Stream {
|
||||
constructor(mixer, stream) {
|
||||
this._mixer = mixer;
|
||||
this._stream = stream;
|
||||
|
||||
this._max = mixer.get_vol_max_norm();
|
||||
}
|
||||
|
||||
get muted() {
|
||||
return this._stream.is_muted;
|
||||
}
|
||||
|
||||
set muted(bool) {
|
||||
this._stream.change_is_muted(bool);
|
||||
}
|
||||
|
||||
// Volume is a double in the range 0-1
|
||||
get volume() {
|
||||
return Math.floor(100 * this._stream.volume / this._max) / 100;
|
||||
}
|
||||
|
||||
set volume(num) {
|
||||
this._stream.volume = Math.floor(num * this._max);
|
||||
this._stream.push_volume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gradually raise or lower the stream volume to @value
|
||||
*
|
||||
* @param {number} value - A number in the range 0-1
|
||||
* @param {number} [duration] - Duration to fade in seconds
|
||||
*/
|
||||
fade(value, duration = 1) {
|
||||
Tweener.removeTweens(this);
|
||||
|
||||
if (this._stream.volume > value) {
|
||||
this._mixer.fading = true;
|
||||
|
||||
Tweener.addTween(this, {
|
||||
volume: value,
|
||||
time: duration,
|
||||
transition: 'easeOutCubic',
|
||||
onComplete: () => {
|
||||
this._mixer.fading = false;
|
||||
},
|
||||
});
|
||||
} else if (this._stream.volume < value) {
|
||||
this._mixer.fading = true;
|
||||
|
||||
Tweener.addTween(this, {
|
||||
volume: value,
|
||||
time: duration,
|
||||
transition: 'easeInCubic',
|
||||
onComplete: () => {
|
||||
this._mixer.fading = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A subclass of Gvc.MixerControl with convenience functions for controlling the
|
||||
* default input/output volumes.
|
||||
*
|
||||
* The Mixer class uses GNOME Shell's Gvc library to control the system volume
|
||||
* and offers a few convenience functions.
|
||||
*/
|
||||
const Mixer = GObject.registerClass({
|
||||
GTypeName: 'GSConnectAudioMixer',
|
||||
}, class Mixer extends Gvc.MixerControl {
|
||||
_init(params) {
|
||||
super._init({name: 'GSConnect'});
|
||||
|
||||
this._previousVolume = undefined;
|
||||
this._volumeMuted = false;
|
||||
this._microphoneMuted = false;
|
||||
|
||||
this.open();
|
||||
}
|
||||
|
||||
get fading() {
|
||||
if (this._fading === undefined)
|
||||
this._fading = false;
|
||||
|
||||
return this._fading;
|
||||
}
|
||||
|
||||
set fading(bool) {
|
||||
if (this.fading === bool)
|
||||
return;
|
||||
|
||||
this._fading = bool;
|
||||
|
||||
if (this.fading)
|
||||
this.emit('stream-changed', this._output._stream.id);
|
||||
}
|
||||
|
||||
get input() {
|
||||
if (this._input === undefined)
|
||||
this.vfunc_default_source_changed();
|
||||
|
||||
return this._input;
|
||||
}
|
||||
|
||||
get output() {
|
||||
if (this._output === undefined)
|
||||
this.vfunc_default_sink_changed();
|
||||
|
||||
return this._output;
|
||||
}
|
||||
|
||||
vfunc_default_sink_changed(id) {
|
||||
try {
|
||||
const sink = this.get_default_sink();
|
||||
this._output = (sink) ? new Stream(this, sink) : null;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
vfunc_default_source_changed(id) {
|
||||
try {
|
||||
const source = this.get_default_source();
|
||||
this._input = (source) ? new Stream(this, source) : null;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
vfunc_state_changed(new_state) {
|
||||
try {
|
||||
if (new_state === Gvc.MixerControlState.READY) {
|
||||
this.vfunc_default_sink_changed(null);
|
||||
this.vfunc_default_source_changed(null);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the current output volume then lower it to %15
|
||||
*
|
||||
* @param {number} duration - Duration in seconds to fade
|
||||
*/
|
||||
lowerVolume(duration = 1) {
|
||||
try {
|
||||
if (this.output.volume > 0.15) {
|
||||
this._previousVolume = Number(this.output.volume);
|
||||
this.output.fade(0.15, duration);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute the output volume (speakers)
|
||||
*/
|
||||
muteVolume() {
|
||||
try {
|
||||
if (this.output.muted)
|
||||
return;
|
||||
|
||||
this.output.muted = true;
|
||||
this._volumeMuted = true;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute the input volume (microphone)
|
||||
*/
|
||||
muteMicrophone() {
|
||||
try {
|
||||
if (this.input.muted)
|
||||
return;
|
||||
|
||||
this.input.muted = true;
|
||||
this._microphoneMuted = true;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore all mixer levels to their previous state
|
||||
*/
|
||||
restore() {
|
||||
try {
|
||||
// If we muted the microphone, unmute it before restoring the volume
|
||||
if (this._microphoneMuted) {
|
||||
this.input.muted = false;
|
||||
this._microphoneMuted = false;
|
||||
}
|
||||
|
||||
// If we muted the volume, unmute it before restoring the volume
|
||||
if (this._volumeMuted) {
|
||||
this.output.muted = false;
|
||||
this._volumeMuted = false;
|
||||
}
|
||||
|
||||
// If a previous volume is defined, raise it back up to that level
|
||||
if (this._previousVolume !== undefined) {
|
||||
this.output.fade(this._previousVolume);
|
||||
this._previousVolume = undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* The service class for this component
|
||||
*/
|
||||
var Component = Mixer;
|
||||
|
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
const Gio = imports.gi.Gio;
|
||||
const GLib = imports.gi.GLib;
|
||||
|
||||
|
||||
const Session = class {
|
||||
constructor() {
|
||||
this._connection = Gio.DBus.system;
|
||||
this._session = null;
|
||||
|
||||
this._initAsync();
|
||||
}
|
||||
|
||||
async _initAsync() {
|
||||
try {
|
||||
const userName = GLib.get_user_name();
|
||||
const sessions = await this._listSessions();
|
||||
let sessionPath = '/org/freedesktop/login1/session/auto';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (const [num, uid, name, seat, objectPath] of sessions) {
|
||||
if (name === userName) {
|
||||
sessionPath = objectPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this._session = await this._getSession(sessionPath);
|
||||
} catch (e) {
|
||||
this._session = null;
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
get idle() {
|
||||
if (this._session === null)
|
||||
return false;
|
||||
|
||||
return this._session.get_cached_property('IdleHint').unpack();
|
||||
}
|
||||
|
||||
get locked() {
|
||||
if (this._session === null)
|
||||
return false;
|
||||
|
||||
return this._session.get_cached_property('LockedHint').unpack();
|
||||
}
|
||||
|
||||
get active() {
|
||||
// Active if not idle and not locked
|
||||
return !(this.idle || this.locked);
|
||||
}
|
||||
|
||||
_listSessions() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._connection.call(
|
||||
'org.freedesktop.login1',
|
||||
'/org/freedesktop/login1',
|
||||
'org.freedesktop.login1.Manager',
|
||||
'ListSessions',
|
||||
null,
|
||||
null,
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
null,
|
||||
(connection, res) => {
|
||||
try {
|
||||
res = connection.call_finish(res);
|
||||
resolve(res.deepUnpack()[0]);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async _getSession(objectPath) {
|
||||
const session = new Gio.DBusProxy({
|
||||
g_connection: this._connection,
|
||||
g_name: 'org.freedesktop.login1',
|
||||
g_object_path: objectPath,
|
||||
g_interface_name: 'org.freedesktop.login1.Session',
|
||||
});
|
||||
|
||||
// Initialize the proxy
|
||||
await new Promise((resolve, reject) => {
|
||||
session.init_async(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
null,
|
||||
(proxy, res) => {
|
||||
try {
|
||||
resolve(proxy.init_finish(res));
|
||||
} catch (e) {
|
||||
Gio.DBusError.strip_remote_error(e);
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._session = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The service class for this component
|
||||
*/
|
||||
var Component = Session;
|
||||
|
@@ -0,0 +1,185 @@
|
||||
'use strict';
|
||||
|
||||
const Gdk = imports.gi.Gdk;
|
||||
const Gio = imports.gi.Gio;
|
||||
const GLib = imports.gi.GLib;
|
||||
|
||||
|
||||
/*
|
||||
* Used to ensure 'audible-bell' is enabled for fallback
|
||||
*/
|
||||
const WM_SETTINGS = new Gio.Settings({
|
||||
schema_id: 'org.gnome.desktop.wm.preferences',
|
||||
path: '/org/gnome/desktop/wm/preferences/',
|
||||
});
|
||||
|
||||
|
||||
var Player = class Player {
|
||||
|
||||
constructor() {
|
||||
this._playing = new Set();
|
||||
}
|
||||
|
||||
get backend() {
|
||||
if (this._backend === undefined) {
|
||||
// Prefer GSound
|
||||
try {
|
||||
this._gsound = new imports.gi.GSound.Context();
|
||||
this._gsound.init(null);
|
||||
this._backend = 'gsound';
|
||||
|
||||
// Try falling back to libcanberra, otherwise just re-run the test
|
||||
// in case one or the other is installed later
|
||||
} catch (e) {
|
||||
if (GLib.find_program_in_path('canberra-gtk-play') !== null) {
|
||||
this._canberra = new Gio.SubprocessLauncher({
|
||||
flags: Gio.SubprocessFlags.NONE,
|
||||
});
|
||||
this._backend = 'libcanberra';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._backend;
|
||||
}
|
||||
|
||||
_canberraPlaySound(name, cancellable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = this._canberra.spawnv(['canberra-gtk-play', '-i', name]);
|
||||
|
||||
proc.wait_check_async(cancellable, (proc, res) => {
|
||||
try {
|
||||
resolve(proc.wait_check_finish(res));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _canberraLoopSound(name, cancellable) {
|
||||
while (!cancellable.is_cancelled())
|
||||
await this._canberraPlaySound(name, cancellable);
|
||||
}
|
||||
|
||||
_gsoundPlaySound(name, cancellable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._gsound.play_full(
|
||||
{'event.id': name},
|
||||
cancellable,
|
||||
(source, res) => {
|
||||
try {
|
||||
resolve(source.play_full_finish(res));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async _gsoundLoopSound(name, cancellable) {
|
||||
while (!cancellable.is_cancelled())
|
||||
await this._gsoundPlaySound(name, cancellable);
|
||||
}
|
||||
|
||||
_gdkPlaySound(name, cancellable) {
|
||||
if (this._display === undefined)
|
||||
this._display = Gdk.Display.get_default();
|
||||
|
||||
let count = 0;
|
||||
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => {
|
||||
try {
|
||||
if (count++ < 4 && !cancellable.is_cancelled()) {
|
||||
this._display.beep();
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
});
|
||||
|
||||
return !cancellable.is_cancelled();
|
||||
}
|
||||
|
||||
_gdkLoopSound(name, cancellable) {
|
||||
this._gdkPlaySound(name, cancellable);
|
||||
GLib.timeout_add(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
1500,
|
||||
this._gdkPlaySound.bind(this, name, cancellable)
|
||||
);
|
||||
}
|
||||
|
||||
async playSound(name, cancellable) {
|
||||
try {
|
||||
if (!(cancellable instanceof Gio.Cancellable))
|
||||
cancellable = new Gio.Cancellable();
|
||||
|
||||
this._playing.add(cancellable);
|
||||
|
||||
switch (this.backend) {
|
||||
case 'gsound':
|
||||
await this._gsoundPlaySound(name, cancellable);
|
||||
break;
|
||||
|
||||
case 'canberra':
|
||||
await this._canberraPlaySound(name, cancellable);
|
||||
break;
|
||||
|
||||
default:
|
||||
await this._gdkPlaySound(name, cancellable);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
} finally {
|
||||
this._playing.delete(cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
async loopSound(name, cancellable) {
|
||||
try {
|
||||
if (!(cancellable instanceof Gio.Cancellable))
|
||||
cancellable = new Gio.Cancellable();
|
||||
|
||||
this._playing.add(cancellable);
|
||||
|
||||
switch (this.backend) {
|
||||
case 'gsound':
|
||||
await this._gsoundLoopSound(name, cancellable);
|
||||
break;
|
||||
|
||||
case 'canberra':
|
||||
await this._canberraLoopSound(name, cancellable);
|
||||
break;
|
||||
|
||||
default:
|
||||
await this._gdkLoopSound(name, cancellable);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
} finally {
|
||||
this._playing.delete(cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
for (const cancellable of this._playing)
|
||||
cancellable.cancel();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The service class for this component
|
||||
*/
|
||||
var Component = Player;
|
||||
|
@@ -0,0 +1,226 @@
|
||||
'use strict';
|
||||
|
||||
const Gio = imports.gi.Gio;
|
||||
const GLib = imports.gi.GLib;
|
||||
const GObject = imports.gi.GObject;
|
||||
|
||||
|
||||
/**
|
||||
* The warning level of a battery.
|
||||
*
|
||||
* @readonly
|
||||
* @enum {number}
|
||||
*/
|
||||
const DeviceLevel = {
|
||||
UNKNOWN: 0,
|
||||
NONE: 1,
|
||||
DISCHARGING: 2,
|
||||
LOW: 3,
|
||||
CRITICAL: 4,
|
||||
ACTION: 5,
|
||||
NORMAL: 6,
|
||||
HIGH: 7,
|
||||
FULL: 8,
|
||||
LAST: 9,
|
||||
};
|
||||
|
||||
/**
|
||||
* The device state.
|
||||
*
|
||||
* @readonly
|
||||
* @enum {number}
|
||||
*/
|
||||
const DeviceState = {
|
||||
UNKNOWN: 0,
|
||||
CHARGING: 1,
|
||||
DISCHARGING: 2,
|
||||
EMPTY: 3,
|
||||
FULLY_CHARGED: 4,
|
||||
PENDING_CHARGE: 5,
|
||||
PENDING_DISCHARGE: 6,
|
||||
LAST: 7,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A class representing the system battery.
|
||||
*/
|
||||
var Battery = GObject.registerClass({
|
||||
GTypeName: 'GSConnectSystemBattery',
|
||||
Signals: {
|
||||
'changed': {
|
||||
flags: GObject.SignalFlags.RUN_FIRST,
|
||||
},
|
||||
},
|
||||
Properties: {
|
||||
'charging': GObject.ParamSpec.boolean(
|
||||
'charging',
|
||||
'Charging',
|
||||
'The current charging state.',
|
||||
GObject.ParamFlags.READABLE,
|
||||
false
|
||||
),
|
||||
'level': GObject.ParamSpec.int(
|
||||
'level',
|
||||
'Level',
|
||||
'The current power level.',
|
||||
GObject.ParamFlags.READABLE,
|
||||
-1, 100,
|
||||
-1
|
||||
),
|
||||
'threshold': GObject.ParamSpec.uint(
|
||||
'threshold',
|
||||
'Threshold',
|
||||
'The current threshold state.',
|
||||
GObject.ParamFlags.READABLE,
|
||||
0, 1,
|
||||
0
|
||||
),
|
||||
},
|
||||
}, class Battery extends GObject.Object {
|
||||
|
||||
_init() {
|
||||
super._init();
|
||||
|
||||
this._cancellable = new Gio.Cancellable();
|
||||
this._proxy = null;
|
||||
this._propertiesChangedId = 0;
|
||||
|
||||
this._loadUPower();
|
||||
}
|
||||
|
||||
async _loadUPower() {
|
||||
try {
|
||||
this._proxy = new Gio.DBusProxy({
|
||||
g_bus_type: Gio.BusType.SYSTEM,
|
||||
g_name: 'org.freedesktop.UPower',
|
||||
g_object_path: '/org/freedesktop/UPower/devices/DisplayDevice',
|
||||
g_interface_name: 'org.freedesktop.UPower.Device',
|
||||
g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START,
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
this._proxy.init_async(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
this._cancellable,
|
||||
(proxy, res) => {
|
||||
try {
|
||||
resolve(proxy.init_finish(res));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
this._propertiesChangedId = this._proxy.connect(
|
||||
'g-properties-changed',
|
||||
this._onPropertiesChanged.bind(this)
|
||||
);
|
||||
|
||||
this._initProperties(this._proxy);
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
|
||||
const service = Gio.Application.get_default();
|
||||
|
||||
if (service !== null)
|
||||
service.notify_error(e);
|
||||
else
|
||||
logError(e);
|
||||
}
|
||||
|
||||
this._proxy = null;
|
||||
}
|
||||
}
|
||||
|
||||
_initProperties(proxy) {
|
||||
if (proxy.g_name_owner === null)
|
||||
return;
|
||||
|
||||
const percentage = proxy.get_cached_property('Percentage').unpack();
|
||||
const state = proxy.get_cached_property('State').unpack();
|
||||
const level = proxy.get_cached_property('WarningLevel').unpack();
|
||||
|
||||
this._level = Math.floor(percentage);
|
||||
this._charging = (state !== DeviceState.DISCHARGING);
|
||||
this._threshold = (!this.charging && level >= DeviceLevel.LOW);
|
||||
|
||||
this.emit('changed');
|
||||
}
|
||||
|
||||
_onPropertiesChanged(proxy, changed, invalidated) {
|
||||
let emitChanged = false;
|
||||
const properties = changed.deepUnpack();
|
||||
|
||||
if (properties.hasOwnProperty('Percentage')) {
|
||||
emitChanged = true;
|
||||
|
||||
const value = proxy.get_cached_property('Percentage').unpack();
|
||||
this._level = Math.floor(value);
|
||||
this.notify('level');
|
||||
}
|
||||
|
||||
if (properties.hasOwnProperty('State')) {
|
||||
emitChanged = true;
|
||||
|
||||
const value = proxy.get_cached_property('State').unpack();
|
||||
this._charging = (value !== DeviceState.DISCHARGING);
|
||||
this.notify('charging');
|
||||
}
|
||||
|
||||
if (properties.hasOwnProperty('WarningLevel')) {
|
||||
emitChanged = true;
|
||||
|
||||
const value = proxy.get_cached_property('WarningLevel').unpack();
|
||||
this._threshold = (!this.charging && value >= DeviceLevel.LOW);
|
||||
this.notify('threshold');
|
||||
}
|
||||
|
||||
if (emitChanged)
|
||||
this.emit('changed');
|
||||
}
|
||||
|
||||
get charging() {
|
||||
if (this._charging === undefined)
|
||||
this._charging = false;
|
||||
|
||||
return this._charging;
|
||||
}
|
||||
|
||||
get is_present() {
|
||||
return (this._proxy && this._proxy.g_name_owner);
|
||||
}
|
||||
|
||||
get level() {
|
||||
if (this._level === undefined)
|
||||
this._level = -1;
|
||||
|
||||
return this._level;
|
||||
}
|
||||
|
||||
get threshold() {
|
||||
if (this._threshold === undefined)
|
||||
this._threshold = 0;
|
||||
|
||||
return this._threshold;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._cancellable.is_cancelled())
|
||||
return;
|
||||
|
||||
this._cancellable.cancel();
|
||||
|
||||
if (this._proxy && this._propertiesChangedId > 0) {
|
||||
this._proxy.disconnect(this._propertiesChangedId);
|
||||
this._propertiesChangedId = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* The service class for this component
|
||||
*/
|
||||
var Component = Battery;
|
||||
|
Reference in New Issue
Block a user