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,49 @@
|
||||
'use strict';
|
||||
|
||||
const Gdk = imports.gi.Gdk;
|
||||
const Gio = imports.gi.Gio;
|
||||
const GLib = imports.gi.GLib;
|
||||
const Gtk = imports.gi.Gtk;
|
||||
|
||||
const Config = imports.config;
|
||||
|
||||
|
||||
/*
|
||||
* Window State
|
||||
*/
|
||||
Gtk.Window.prototype.restoreGeometry = function (context = 'default') {
|
||||
this._windowState = new Gio.Settings({
|
||||
settings_schema: Config.GSCHEMA.lookup(
|
||||
'org.gnome.Shell.Extensions.GSConnect.WindowState',
|
||||
true
|
||||
),
|
||||
path: `/org/gnome/shell/extensions/gsconnect/${context}/`,
|
||||
});
|
||||
|
||||
// Size
|
||||
const [width, height] = this._windowState.get_value('window-size').deepUnpack();
|
||||
|
||||
if (width && height)
|
||||
this.set_default_size(width, height);
|
||||
|
||||
// Maximized State
|
||||
if (this._windowState.get_boolean('window-maximized'))
|
||||
this.maximize();
|
||||
};
|
||||
|
||||
Gtk.Window.prototype.saveGeometry = function () {
|
||||
const state = this.get_window().get_state();
|
||||
|
||||
// Maximized State
|
||||
const maximized = (state & Gdk.WindowState.MAXIMIZED);
|
||||
this._windowState.set_boolean('window-maximized', maximized);
|
||||
|
||||
// Leave the size at the value before maximizing
|
||||
if (maximized || (state & Gdk.WindowState.FULLSCREEN))
|
||||
return;
|
||||
|
||||
// Size
|
||||
const size = this.get_size();
|
||||
this._windowState.set_value('window-size', new GLib.Variant('(ii)', size));
|
||||
};
|
||||
|
@@ -0,0 +1,638 @@
|
||||
'use strict';
|
||||
|
||||
const Gdk = imports.gi.Gdk;
|
||||
const GdkPixbuf = imports.gi.GdkPixbuf;
|
||||
const GLib = imports.gi.GLib;
|
||||
const GObject = imports.gi.GObject;
|
||||
const Gtk = imports.gi.Gtk;
|
||||
|
||||
|
||||
/**
|
||||
* Return a random color
|
||||
*
|
||||
* @param {*} [salt] - If not %null, will be used as salt for generating a color
|
||||
* @param {number} alpha - A value in the [0...1] range for the alpha channel
|
||||
* @return {Gdk.RGBA} A new Gdk.RGBA object generated from the input
|
||||
*/
|
||||
function randomRGBA(salt = null, alpha = 1.0) {
|
||||
let red, green, blue;
|
||||
|
||||
if (salt !== null) {
|
||||
const hash = new GLib.Variant('s', `${salt}`).hash();
|
||||
red = ((hash & 0xFF0000) >> 16) / 255;
|
||||
green = ((hash & 0x00FF00) >> 8) / 255;
|
||||
blue = (hash & 0x0000FF) / 255;
|
||||
} else {
|
||||
red = Math.random();
|
||||
green = Math.random();
|
||||
blue = Math.random();
|
||||
}
|
||||
|
||||
return new Gdk.RGBA({red: red, green: green, blue: blue, alpha: alpha});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the relative luminance of a RGB set
|
||||
* See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
||||
*
|
||||
* @param {Gdk.RGBA} rgba - A GdkRGBA object
|
||||
* @return {number} The relative luminance of the color
|
||||
*/
|
||||
function relativeLuminance(rgba) {
|
||||
const {red, green, blue} = rgba;
|
||||
|
||||
const R = (red > 0.03928) ? red / 12.92 : Math.pow(((red + 0.055) / 1.055), 2.4);
|
||||
const G = (green > 0.03928) ? green / 12.92 : Math.pow(((green + 0.055) / 1.055), 2.4);
|
||||
const B = (blue > 0.03928) ? blue / 12.92 : Math.pow(((blue + 0.055) / 1.055), 2.4);
|
||||
|
||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a GdkRGBA contrasted for the input
|
||||
* See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
|
||||
*
|
||||
* @param {Gdk.RGBA} rgba - A GdkRGBA object for the background color
|
||||
* @return {Gdk.RGBA} A GdkRGBA object for the foreground color
|
||||
*/
|
||||
function getFgRGBA(rgba) {
|
||||
const bgLuminance = relativeLuminance(rgba);
|
||||
const lightContrast = (0.07275541795665634 + 0.05) / (bgLuminance + 0.05);
|
||||
const darkContrast = (bgLuminance + 0.05) / (0.0046439628482972135 + 0.05);
|
||||
|
||||
const value = (darkContrast > lightContrast) ? 0.06 : 0.94;
|
||||
return new Gdk.RGBA({red: value, green: value, blue: value, alpha: 0.5});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a GdkPixbuf for @path, allowing the corrupt JPEG's KDE Connect sometimes
|
||||
* sends. This function is synchronous.
|
||||
*
|
||||
* @param {string} path - A local file path
|
||||
* @param {number} size - Size in pixels
|
||||
* @param {scale} [scale] - Scale factor for the size
|
||||
* @return {Gdk.Pixbuf} A pixbuf
|
||||
*/
|
||||
function getPixbufForPath(path, size, scale = 1.0) {
|
||||
let data, loader;
|
||||
|
||||
// Catch missing avatar files
|
||||
try {
|
||||
data = GLib.file_get_contents(path)[1];
|
||||
} catch (e) {
|
||||
debug(e, path);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Consider errors from partially corrupt JPEGs to be warnings
|
||||
try {
|
||||
loader = new GdkPixbuf.PixbufLoader();
|
||||
loader.write(data);
|
||||
loader.close();
|
||||
} catch (e) {
|
||||
debug(e, path);
|
||||
}
|
||||
|
||||
const pixbuf = loader.get_pixbuf();
|
||||
|
||||
// Scale to monitor
|
||||
size = Math.floor(size * scale);
|
||||
return pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.HYPER);
|
||||
}
|
||||
|
||||
function getPixbufForIcon(name, size, scale, bgColor) {
|
||||
const color = getFgRGBA(bgColor);
|
||||
const theme = Gtk.IconTheme.get_default();
|
||||
const info = theme.lookup_icon_for_scale(
|
||||
name,
|
||||
size,
|
||||
scale,
|
||||
Gtk.IconLookupFlags.FORCE_SYMBOLIC
|
||||
);
|
||||
|
||||
return info.load_symbolic(color, null, null, null)[0];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a localized string for a phone number type
|
||||
* See: http://www.ietf.org/rfc/rfc2426.txt
|
||||
*
|
||||
* @param {string} type - An RFC2426 phone number type
|
||||
* @return {string} A localized string like 'Mobile'
|
||||
*/
|
||||
function getNumberTypeLabel(type) {
|
||||
if (type.includes('fax'))
|
||||
// TRANSLATORS: A fax number
|
||||
return _('Fax');
|
||||
|
||||
if (type.includes('work'))
|
||||
// TRANSLATORS: A work or office phone number
|
||||
return _('Work');
|
||||
|
||||
if (type.includes('cell'))
|
||||
// TRANSLATORS: A mobile or cellular phone number
|
||||
return _('Mobile');
|
||||
|
||||
if (type.includes('home'))
|
||||
// TRANSLATORS: A home phone number
|
||||
return _('Home');
|
||||
|
||||
// TRANSLATORS: All other phone number types
|
||||
return _('Other');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a display number from @contact for @address.
|
||||
*
|
||||
* @param {Object} contact - A contact object
|
||||
* @param {string} address - A phone number
|
||||
* @return {string} A (possibly) better display number for the address
|
||||
*/
|
||||
function getDisplayNumber(contact, address) {
|
||||
const number = address.toPhoneNumber();
|
||||
|
||||
for (const contactNumber of contact.numbers) {
|
||||
const cnumber = contactNumber.value.toPhoneNumber();
|
||||
|
||||
if (number.endsWith(cnumber) || cnumber.endsWith(number))
|
||||
return GLib.markup_escape_text(contactNumber.value, -1);
|
||||
}
|
||||
|
||||
return GLib.markup_escape_text(address, -1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contact Avatar
|
||||
*/
|
||||
const AvatarCache = new WeakMap();
|
||||
|
||||
var Avatar = GObject.registerClass({
|
||||
GTypeName: 'GSConnectContactAvatar',
|
||||
}, class ContactAvatar extends Gtk.DrawingArea {
|
||||
|
||||
_init(contact = null) {
|
||||
super._init({
|
||||
height_request: 32,
|
||||
width_request: 32,
|
||||
valign: Gtk.Align.CENTER,
|
||||
visible: true,
|
||||
});
|
||||
|
||||
this.contact = contact;
|
||||
}
|
||||
|
||||
get rgba() {
|
||||
if (this._rgba === undefined) {
|
||||
if (this.contact)
|
||||
this._rgba = randomRGBA(this.contact.name);
|
||||
else
|
||||
this._rgba = randomRGBA(GLib.uuid_string_random());
|
||||
}
|
||||
|
||||
return this._rgba;
|
||||
}
|
||||
|
||||
get contact() {
|
||||
if (this._contact === undefined)
|
||||
this._contact = null;
|
||||
|
||||
return this._contact;
|
||||
}
|
||||
|
||||
set contact(contact) {
|
||||
if (this.contact === contact)
|
||||
return;
|
||||
|
||||
this._contact = contact;
|
||||
this._surface = undefined;
|
||||
this._rgba = undefined;
|
||||
this._offset = 0;
|
||||
}
|
||||
|
||||
_loadSurface() {
|
||||
// Get the monitor scale
|
||||
const display = Gdk.Display.get_default();
|
||||
const monitor = display.get_monitor_at_window(this.get_window());
|
||||
const scale = monitor.get_scale_factor();
|
||||
|
||||
// If there's a contact with an avatar, try to load it
|
||||
if (this.contact && this.contact.avatar) {
|
||||
// Check the cache
|
||||
this._surface = AvatarCache.get(this.contact);
|
||||
|
||||
// Try loading the pixbuf
|
||||
if (!this._surface) {
|
||||
const pixbuf = getPixbufForPath(
|
||||
this.contact.avatar,
|
||||
this.width_request,
|
||||
scale
|
||||
);
|
||||
|
||||
if (pixbuf) {
|
||||
this._surface = Gdk.cairo_surface_create_from_pixbuf(
|
||||
pixbuf,
|
||||
0,
|
||||
this.get_window()
|
||||
);
|
||||
AvatarCache.set(this.contact, this._surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have a surface, load a fallback
|
||||
if (!this._surface) {
|
||||
let iconName;
|
||||
|
||||
// If we were given a contact, it's direct message otherwise group
|
||||
if (this.contact)
|
||||
iconName = 'avatar-default-symbolic';
|
||||
else
|
||||
iconName = 'group-avatar-symbolic';
|
||||
|
||||
// Center the icon
|
||||
this._offset = (this.width_request - 24) / 2;
|
||||
|
||||
// Load the fallback
|
||||
const pixbuf = getPixbufForIcon(iconName, 24, scale, this.rgba);
|
||||
|
||||
this._surface = Gdk.cairo_surface_create_from_pixbuf(
|
||||
pixbuf,
|
||||
0,
|
||||
this.get_window()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
vfunc_draw(cr) {
|
||||
if (!this._surface)
|
||||
this._loadSurface();
|
||||
|
||||
// Clip to a circle
|
||||
const rad = this.width_request / 2;
|
||||
cr.arc(rad, rad, rad, 0, 2 * Math.PI);
|
||||
cr.clipPreserve();
|
||||
|
||||
// Fill the background if the the surface is offset
|
||||
if (this._offset > 0) {
|
||||
Gdk.cairo_set_source_rgba(cr, this.rgba);
|
||||
cr.fill();
|
||||
}
|
||||
|
||||
// Draw the avatar/icon
|
||||
cr.setSourceSurface(this._surface, this._offset, this._offset);
|
||||
cr.paint();
|
||||
|
||||
cr.$dispose();
|
||||
return Gdk.EVENT_PROPAGATE;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* A row for a contact address (usually a phone number).
|
||||
*/
|
||||
const AddressRow = GObject.registerClass({
|
||||
GTypeName: 'GSConnectContactsAddressRow',
|
||||
Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/contacts-address-row.ui',
|
||||
Children: ['avatar', 'name-label', 'address-label', 'type-label'],
|
||||
}, class AddressRow extends Gtk.ListBoxRow {
|
||||
|
||||
_init(contact, index = 0) {
|
||||
super._init();
|
||||
|
||||
this._index = index;
|
||||
this._number = contact.numbers[index];
|
||||
this.contact = contact;
|
||||
}
|
||||
|
||||
get contact() {
|
||||
if (this._contact === undefined)
|
||||
this._contact = null;
|
||||
|
||||
return this._contact;
|
||||
}
|
||||
|
||||
set contact(contact) {
|
||||
if (this.contact === contact)
|
||||
return;
|
||||
|
||||
this._contact = contact;
|
||||
|
||||
if (this._index === 0) {
|
||||
this.avatar.contact = contact;
|
||||
this.avatar.visible = true;
|
||||
|
||||
this.name_label.label = GLib.markup_escape_text(contact.name, -1);
|
||||
this.name_label.visible = true;
|
||||
|
||||
this.address_label.margin_start = 0;
|
||||
this.address_label.margin_end = 0;
|
||||
} else {
|
||||
this.avatar.visible = false;
|
||||
this.name_label.visible = false;
|
||||
|
||||
// TODO: rtl inverts margin-start so the number don't align
|
||||
this.address_label.margin_start = 38;
|
||||
this.address_label.margin_end = 38;
|
||||
}
|
||||
|
||||
this.address_label.label = GLib.markup_escape_text(this.number.value, -1);
|
||||
|
||||
if (this.number.type !== undefined)
|
||||
this.type_label.label = getNumberTypeLabel(this.number.type);
|
||||
}
|
||||
|
||||
get number() {
|
||||
if (this._number === undefined)
|
||||
return {value: 'unknown', type: 'unknown'};
|
||||
|
||||
return this._number;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* A widget for selecting contact addresses (usually phone numbers)
|
||||
*/
|
||||
var ContactChooser = GObject.registerClass({
|
||||
GTypeName: 'GSConnectContactChooser',
|
||||
Properties: {
|
||||
'device': GObject.ParamSpec.object(
|
||||
'device',
|
||||
'Device',
|
||||
'The device associated with this window',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
GObject.Object
|
||||
),
|
||||
'store': GObject.ParamSpec.object(
|
||||
'store',
|
||||
'Store',
|
||||
'The contacts store',
|
||||
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
|
||||
GObject.Object
|
||||
),
|
||||
},
|
||||
Signals: {
|
||||
'number-selected': {
|
||||
flags: GObject.SignalFlags.RUN_FIRST,
|
||||
param_types: [GObject.TYPE_STRING],
|
||||
},
|
||||
},
|
||||
Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/contact-chooser.ui',
|
||||
Children: ['entry', 'list', 'scrolled'],
|
||||
}, class ContactChooser extends Gtk.Grid {
|
||||
|
||||
_init(params) {
|
||||
super._init(params);
|
||||
|
||||
// Setup the contact list
|
||||
this.list._entry = this.entry.text;
|
||||
this.list.set_filter_func(this._filter);
|
||||
this.list.set_sort_func(this._sort);
|
||||
|
||||
// Make sure we're using the correct contacts store
|
||||
this.device.bind_property(
|
||||
'contacts',
|
||||
this,
|
||||
'store',
|
||||
GObject.BindingFlags.SYNC_CREATE
|
||||
);
|
||||
|
||||
// Cleanup on ::destroy
|
||||
this.connect('destroy', this._onDestroy);
|
||||
}
|
||||
|
||||
get store() {
|
||||
if (this._store === undefined)
|
||||
this._store = null;
|
||||
|
||||
return this._store;
|
||||
}
|
||||
|
||||
set store(store) {
|
||||
if (this.store === store)
|
||||
return;
|
||||
|
||||
// Unbind the old store
|
||||
if (this._store) {
|
||||
// Disconnect from the store
|
||||
this._store.disconnect(this._contactAddedId);
|
||||
this._store.disconnect(this._contactRemovedId);
|
||||
this._store.disconnect(this._contactChangedId);
|
||||
|
||||
// Clear the contact list
|
||||
const rows = this.list.get_children();
|
||||
|
||||
for (let i = 0, len = rows.length; i < len; i++) {
|
||||
rows[i].destroy();
|
||||
// HACK: temporary mitigator for mysterious GtkListBox leak
|
||||
imports.system.gc();
|
||||
}
|
||||
}
|
||||
|
||||
// Set the store
|
||||
this._store = store;
|
||||
|
||||
// Bind the new store
|
||||
if (this._store) {
|
||||
// Connect to the new store
|
||||
this._contactAddedId = store.connect(
|
||||
'contact-added',
|
||||
this._onContactAdded.bind(this)
|
||||
);
|
||||
|
||||
this._contactRemovedId = store.connect(
|
||||
'contact-removed',
|
||||
this._onContactRemoved.bind(this)
|
||||
);
|
||||
|
||||
this._contactChangedId = store.connect(
|
||||
'contact-changed',
|
||||
this._onContactChanged.bind(this)
|
||||
);
|
||||
|
||||
// Populate the list
|
||||
this._populate();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* ContactStore Callbacks
|
||||
*/
|
||||
_onContactAdded(store, id) {
|
||||
const contact = this.store.get_contact(id);
|
||||
this._addContact(contact);
|
||||
}
|
||||
|
||||
_onContactRemoved(store, id) {
|
||||
const rows = this.list.get_children();
|
||||
|
||||
for (let i = 0, len = rows.length; i < len; i++) {
|
||||
const row = rows[i];
|
||||
|
||||
if (row.contact.id === id) {
|
||||
row.destroy();
|
||||
// HACK: temporary mitigator for mysterious GtkListBox leak
|
||||
imports.system.gc();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onContactChanged(store, id) {
|
||||
this._onContactRemoved(store, id);
|
||||
this._onContactAdded(store, id);
|
||||
}
|
||||
|
||||
_onDestroy(chooser) {
|
||||
chooser.store = null;
|
||||
}
|
||||
|
||||
_onSearchChanged(entry) {
|
||||
this.list._entry = entry.text;
|
||||
let dynamic = this.list.get_row_at_index(0);
|
||||
|
||||
// If the entry contains string with 2 or more digits...
|
||||
if (entry.text.replace(/\D/g, '').length >= 2) {
|
||||
// ...ensure we have a dynamic contact for it
|
||||
if (!dynamic || !dynamic.__tmp) {
|
||||
dynamic = new AddressRow({
|
||||
// TRANSLATORS: A phone number (eg. "Send to 555-5555")
|
||||
name: _('Send to %s').format(entry.text),
|
||||
numbers: [{type: 'unknown', value: entry.text}],
|
||||
});
|
||||
dynamic.__tmp = true;
|
||||
this.list.add(dynamic);
|
||||
|
||||
// ...or if we already do, then update it
|
||||
} else {
|
||||
const address = entry.text;
|
||||
|
||||
// Update contact object
|
||||
dynamic.contact.name = address;
|
||||
dynamic.contact.numbers[0].value = address;
|
||||
|
||||
// Update UI
|
||||
dynamic.name_label.label = _('Send to %s').format(address);
|
||||
dynamic.address_label.label = address;
|
||||
}
|
||||
|
||||
// ...otherwise remove any dynamic contact that's been created
|
||||
} else if (dynamic && dynamic.__tmp) {
|
||||
dynamic.destroy();
|
||||
}
|
||||
|
||||
this.list.invalidate_filter();
|
||||
this.list.invalidate_sort();
|
||||
}
|
||||
|
||||
// GtkListBox::row-activated
|
||||
_onNumberSelected(box, row) {
|
||||
if (row === null)
|
||||
return;
|
||||
|
||||
// Emit the number
|
||||
const address = row.number.value;
|
||||
this.emit('number-selected', address);
|
||||
|
||||
// Reset the contact list
|
||||
this.entry.text = '';
|
||||
this.list.select_row(null);
|
||||
this.scrolled.vadjustment.value = 0;
|
||||
}
|
||||
|
||||
_filter(row) {
|
||||
// Dynamic contact always shown
|
||||
if (row.__tmp)
|
||||
return true;
|
||||
|
||||
const query = row.get_parent()._entry;
|
||||
|
||||
// Show contact if text is substring of name
|
||||
const queryName = query.toLocaleLowerCase();
|
||||
|
||||
if (row.contact.name.toLocaleLowerCase().includes(queryName))
|
||||
return true;
|
||||
|
||||
// Show contact if text is substring of number
|
||||
const queryNumber = query.toPhoneNumber();
|
||||
|
||||
if (queryNumber.length) {
|
||||
for (const number of row.contact.numbers) {
|
||||
if (number.value.toPhoneNumber().includes(queryNumber))
|
||||
return true;
|
||||
}
|
||||
|
||||
// Query is effectively empty
|
||||
} else if (/^0+/.test(query)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_sort(row1, row2) {
|
||||
if (row1.__tmp)
|
||||
return -1;
|
||||
|
||||
if (row2.__tmp)
|
||||
return 1;
|
||||
|
||||
return row1.contact.name.localeCompare(row2.contact.name);
|
||||
}
|
||||
|
||||
_populate() {
|
||||
// Add each contact
|
||||
const contacts = this.store.contacts;
|
||||
|
||||
for (let i = 0, len = contacts.length; i < len; i++)
|
||||
this._addContact(contacts[i]);
|
||||
}
|
||||
|
||||
_addContactNumber(contact, index) {
|
||||
const row = new AddressRow(contact, index);
|
||||
this.list.add(row);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
_addContact(contact) {
|
||||
try {
|
||||
// HACK: fix missing contact names
|
||||
if (contact.name === undefined)
|
||||
contact.name = _('Unknown Contact');
|
||||
|
||||
if (contact.numbers.length === 1)
|
||||
return this._addContactNumber(contact, 0);
|
||||
|
||||
for (let i = 0, len = contact.numbers.length; i < len; i++)
|
||||
this._addContactNumber(contact, i);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dictionary of number-contact pairs for each selected phone number.
|
||||
*
|
||||
* @return {Object[]} A dictionary of contacts
|
||||
*/
|
||||
getSelected() {
|
||||
try {
|
||||
const selected = {};
|
||||
|
||||
for (const row of this.list.get_selected_rows())
|
||||
selected[row.number.value] = row.contact;
|
||||
|
||||
return selected;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -0,0 +1,223 @@
|
||||
'use strict';
|
||||
|
||||
const Gio = imports.gi.Gio;
|
||||
const GObject = imports.gi.GObject;
|
||||
const Gtk = imports.gi.Gtk;
|
||||
|
||||
const Contacts = imports.service.ui.contacts;
|
||||
const Messaging = imports.service.ui.messaging;
|
||||
const URI = imports.service.utils.uri;
|
||||
|
||||
|
||||
var Dialog = GObject.registerClass({
|
||||
GTypeName: 'GSConnectLegacyMessagingDialog',
|
||||
Properties: {
|
||||
'device': GObject.ParamSpec.object(
|
||||
'device',
|
||||
'Device',
|
||||
'The device associated with this window',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
GObject.Object
|
||||
),
|
||||
'plugin': GObject.ParamSpec.object(
|
||||
'plugin',
|
||||
'Plugin',
|
||||
'The plugin providing messages',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
GObject.Object
|
||||
),
|
||||
},
|
||||
Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/legacy-messaging-dialog.ui',
|
||||
Children: [
|
||||
'infobar', 'stack',
|
||||
'message-box', 'message-avatar', 'message-label', 'entry',
|
||||
],
|
||||
}, class Dialog extends Gtk.Dialog {
|
||||
|
||||
_init(params) {
|
||||
super._init({
|
||||
application: Gio.Application.get_default(),
|
||||
device: params.device,
|
||||
plugin: params.plugin,
|
||||
use_header_bar: true,
|
||||
});
|
||||
|
||||
this.set_response_sensitive(Gtk.ResponseType.OK, false);
|
||||
|
||||
// Dup some functions
|
||||
this.headerbar = this.get_titlebar();
|
||||
this._setHeaderBar = Messaging.Window.prototype._setHeaderBar;
|
||||
|
||||
// Info bar
|
||||
this.device.bind_property(
|
||||
'connected',
|
||||
this.infobar,
|
||||
'reveal-child',
|
||||
GObject.BindingFlags.INVERT_BOOLEAN
|
||||
);
|
||||
|
||||
// Message Entry/Send Button
|
||||
this.device.bind_property(
|
||||
'connected',
|
||||
this.entry,
|
||||
'sensitive',
|
||||
GObject.BindingFlags.DEFAULT
|
||||
);
|
||||
|
||||
this._connectedId = this.device.connect(
|
||||
'notify::connected',
|
||||
this._onStateChanged.bind(this)
|
||||
);
|
||||
|
||||
this._entryChangedId = this.entry.buffer.connect(
|
||||
'changed',
|
||||
this._onStateChanged.bind(this)
|
||||
);
|
||||
|
||||
// Set the message if given
|
||||
if (params.message) {
|
||||
this.message = params.message;
|
||||
this.addresses = params.message.addresses;
|
||||
|
||||
this.message_avatar.contact = this.device.contacts.query({
|
||||
number: this.addresses[0].address,
|
||||
});
|
||||
this.message_label.label = URI.linkify(this.message.body);
|
||||
this.message_box.visible = true;
|
||||
|
||||
// Otherwise set the address(es) if we were passed those
|
||||
} else if (params.addresses) {
|
||||
this.addresses = params.addresses;
|
||||
}
|
||||
|
||||
// Load the contact list if we weren't supplied with an address
|
||||
if (this.addresses.length === 0) {
|
||||
this.contact_chooser = new Contacts.ContactChooser({
|
||||
device: this.device,
|
||||
});
|
||||
this.stack.add_named(this.contact_chooser, 'contact-chooser');
|
||||
this.stack.child_set_property(this.contact_chooser, 'position', 0);
|
||||
|
||||
this._numberSelectedId = this.contact_chooser.connect(
|
||||
'number-selected',
|
||||
this._onNumberSelected.bind(this)
|
||||
);
|
||||
|
||||
this.stack.visible_child_name = 'contact-chooser';
|
||||
}
|
||||
|
||||
this.restoreGeometry('legacy-messaging-dialog');
|
||||
|
||||
this.connect('destroy', this._onDestroy);
|
||||
}
|
||||
|
||||
_onDestroy(dialog) {
|
||||
if (dialog._numberSelectedId !== undefined) {
|
||||
dialog.contact_chooser.disconnect(dialog._numberSelectedId);
|
||||
dialog.contact_chooser.destroy();
|
||||
}
|
||||
|
||||
dialog.entry.buffer.disconnect(dialog._entryChangedId);
|
||||
dialog.device.disconnect(dialog._connectedId);
|
||||
}
|
||||
|
||||
vfunc_delete_event() {
|
||||
this.saveGeometry();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
vfunc_response(response_id) {
|
||||
if (response_id === Gtk.ResponseType.OK) {
|
||||
// Refuse to send empty or whitespace only texts
|
||||
if (!this.entry.buffer.text.trim())
|
||||
return;
|
||||
|
||||
this.plugin.sendMessage(
|
||||
this.addresses,
|
||||
this.entry.buffer.text,
|
||||
1,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
get addresses() {
|
||||
if (this._addresses === undefined)
|
||||
this._addresses = [];
|
||||
|
||||
return this._addresses;
|
||||
}
|
||||
|
||||
set addresses(addresses = []) {
|
||||
this._addresses = addresses;
|
||||
|
||||
// Set the headerbar
|
||||
this._setHeaderBar(this._addresses);
|
||||
|
||||
// Show the message editor
|
||||
this.stack.visible_child_name = 'message-editor';
|
||||
this._onStateChanged();
|
||||
}
|
||||
|
||||
get device() {
|
||||
if (this._device === undefined)
|
||||
this._device = null;
|
||||
|
||||
return this._device;
|
||||
}
|
||||
|
||||
set device(device) {
|
||||
this._device = device;
|
||||
}
|
||||
|
||||
get plugin() {
|
||||
if (this._plugin === undefined)
|
||||
this._plugin = null;
|
||||
|
||||
return this._plugin;
|
||||
}
|
||||
|
||||
set plugin(plugin) {
|
||||
this._plugin = plugin;
|
||||
}
|
||||
|
||||
_onActivateLink(label, uri) {
|
||||
Gtk.show_uri_on_window(
|
||||
this.get_toplevel(),
|
||||
uri.includes('://') ? uri : `https://${uri}`,
|
||||
Gtk.get_current_event_time()
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_onNumberSelected(chooser, number) {
|
||||
const contacts = chooser.getSelected();
|
||||
|
||||
this.addresses = Object.keys(contacts).map(address => {
|
||||
return {address: address};
|
||||
});
|
||||
}
|
||||
|
||||
_onStateChanged() {
|
||||
if (this.device.connected &&
|
||||
this.entry.buffer.text.trim() &&
|
||||
this.stack.visible_child_name === 'message-editor')
|
||||
this.set_response_sensitive(Gtk.ResponseType.OK, true);
|
||||
else
|
||||
this.set_response_sensitive(Gtk.ResponseType.OK, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the contents of the message entry
|
||||
*
|
||||
* @param {string} text - The message to place in the entry
|
||||
*/
|
||||
setMessage(text) {
|
||||
this.entry.buffer.text = text;
|
||||
}
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,299 @@
|
||||
'use strict';
|
||||
|
||||
const Gdk = imports.gi.Gdk;
|
||||
const GObject = imports.gi.GObject;
|
||||
const Gtk = imports.gi.Gtk;
|
||||
|
||||
|
||||
/**
|
||||
* A map of Gdk to "KDE Connect" keyvals
|
||||
*/
|
||||
const ReverseKeyMap = new Map([
|
||||
[Gdk.KEY_BackSpace, 1],
|
||||
[Gdk.KEY_Tab, 2],
|
||||
[Gdk.KEY_Linefeed, 3],
|
||||
[Gdk.KEY_Left, 4],
|
||||
[Gdk.KEY_Up, 5],
|
||||
[Gdk.KEY_Right, 6],
|
||||
[Gdk.KEY_Down, 7],
|
||||
[Gdk.KEY_Page_Up, 8],
|
||||
[Gdk.KEY_Page_Down, 9],
|
||||
[Gdk.KEY_Home, 10],
|
||||
[Gdk.KEY_End, 11],
|
||||
[Gdk.KEY_Return, 12],
|
||||
[Gdk.KEY_Delete, 13],
|
||||
[Gdk.KEY_Escape, 14],
|
||||
[Gdk.KEY_Sys_Req, 15],
|
||||
[Gdk.KEY_Scroll_Lock, 16],
|
||||
[Gdk.KEY_F1, 21],
|
||||
[Gdk.KEY_F2, 22],
|
||||
[Gdk.KEY_F3, 23],
|
||||
[Gdk.KEY_F4, 24],
|
||||
[Gdk.KEY_F5, 25],
|
||||
[Gdk.KEY_F6, 26],
|
||||
[Gdk.KEY_F7, 27],
|
||||
[Gdk.KEY_F8, 28],
|
||||
[Gdk.KEY_F9, 29],
|
||||
[Gdk.KEY_F10, 30],
|
||||
[Gdk.KEY_F11, 31],
|
||||
[Gdk.KEY_F12, 32],
|
||||
]);
|
||||
|
||||
|
||||
/*
|
||||
* A list of keyvals we consider modifiers
|
||||
*/
|
||||
const MOD_KEYS = [
|
||||
Gdk.KEY_Alt_L,
|
||||
Gdk.KEY_Alt_R,
|
||||
Gdk.KEY_Caps_Lock,
|
||||
Gdk.KEY_Control_L,
|
||||
Gdk.KEY_Control_R,
|
||||
Gdk.KEY_Meta_L,
|
||||
Gdk.KEY_Meta_R,
|
||||
Gdk.KEY_Num_Lock,
|
||||
Gdk.KEY_Shift_L,
|
||||
Gdk.KEY_Shift_R,
|
||||
Gdk.KEY_Super_L,
|
||||
Gdk.KEY_Super_R,
|
||||
];
|
||||
|
||||
|
||||
/*
|
||||
* Some convenience functions for checking keyvals for modifiers
|
||||
*/
|
||||
const isAlt = (key) => [Gdk.KEY_Alt_L, Gdk.KEY_Alt_R].includes(key);
|
||||
const isCtrl = (key) => [Gdk.KEY_Control_L, Gdk.KEY_Control_R].includes(key);
|
||||
const isShift = (key) => [Gdk.KEY_Shift_L, Gdk.KEY_Shift_R].includes(key);
|
||||
const isSuper = (key) => [Gdk.KEY_Super_L, Gdk.KEY_Super_R].includes(key);
|
||||
|
||||
|
||||
var InputDialog = GObject.registerClass({
|
||||
GTypeName: 'GSConnectMousepadInputDialog',
|
||||
Properties: {
|
||||
'device': GObject.ParamSpec.object(
|
||||
'device',
|
||||
'Device',
|
||||
'The device associated with this window',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
GObject.Object
|
||||
),
|
||||
'plugin': GObject.ParamSpec.object(
|
||||
'plugin',
|
||||
'Plugin',
|
||||
'The mousepad plugin associated with this window',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
GObject.Object
|
||||
),
|
||||
},
|
||||
Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/mousepad-input-dialog.ui',
|
||||
Children: [
|
||||
'infobar', 'infobar-label',
|
||||
'shift-label', 'ctrl-label', 'alt-label', 'super-label', 'entry',
|
||||
],
|
||||
}, class InputDialog extends Gtk.Dialog {
|
||||
|
||||
_init(params) {
|
||||
super._init(Object.assign({
|
||||
use_header_bar: true,
|
||||
}, params));
|
||||
|
||||
const headerbar = this.get_titlebar();
|
||||
headerbar.title = _('Keyboard');
|
||||
headerbar.subtitle = this.device.name;
|
||||
|
||||
// Main Box
|
||||
const content = this.get_content_area();
|
||||
content.border_width = 0;
|
||||
|
||||
// TRANSLATORS: Displayed when the remote keyboard is not ready to accept input
|
||||
this.infobar_label.label = _('Remote keyboard on %s is not active').format(this.device.name);
|
||||
|
||||
// Text Input
|
||||
this.entry.buffer.connect(
|
||||
'insert-text',
|
||||
this._onInsertText.bind(this)
|
||||
);
|
||||
|
||||
this.infobar.connect('notify::reveal-child', this._onState.bind(this));
|
||||
this.plugin.bind_property('state', this.infobar, 'reveal-child', 6);
|
||||
|
||||
this.show_all();
|
||||
}
|
||||
|
||||
vfunc_delete_event(event) {
|
||||
this._ungrab();
|
||||
return this.hide_on_delete();
|
||||
}
|
||||
|
||||
vfunc_grab_broken_event(event) {
|
||||
if (event.keyboard)
|
||||
this._ungrab();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
vfunc_key_release_event(event) {
|
||||
if (!this.plugin.state)
|
||||
debug('ignoring remote keyboard state');
|
||||
|
||||
const keyvalLower = Gdk.keyval_to_lower(event.keyval);
|
||||
const realMask = event.state & Gtk.accelerator_get_default_mod_mask();
|
||||
|
||||
this.alt_label.sensitive = !isAlt(keyvalLower) && (realMask & Gdk.ModifierType.MOD1_MASK);
|
||||
this.ctrl_label.sensitive = !isCtrl(keyvalLower) && (realMask & Gdk.ModifierType.CONTROL_MASK);
|
||||
this.shift_label.sensitive = !isShift(keyvalLower) && (realMask & Gdk.ModifierType.SHIFT_MASK);
|
||||
this.super_label.sensitive = !isSuper(keyvalLower) && (realMask & Gdk.ModifierType.SUPER_MASK);
|
||||
|
||||
return super.vfunc_key_release_event(event);
|
||||
}
|
||||
|
||||
vfunc_key_press_event(event) {
|
||||
if (!this.plugin.state)
|
||||
debug('ignoring remote keyboard state');
|
||||
|
||||
let keyvalLower = Gdk.keyval_to_lower(event.keyval);
|
||||
let realMask = event.state & Gtk.accelerator_get_default_mod_mask();
|
||||
|
||||
this.alt_label.sensitive = isAlt(keyvalLower) || (realMask & Gdk.ModifierType.MOD1_MASK);
|
||||
this.ctrl_label.sensitive = isCtrl(keyvalLower) || (realMask & Gdk.ModifierType.CONTROL_MASK);
|
||||
this.shift_label.sensitive = isShift(keyvalLower) || (realMask & Gdk.ModifierType.SHIFT_MASK);
|
||||
this.super_label.sensitive = isSuper(keyvalLower) || (realMask & Gdk.ModifierType.SUPER_MASK);
|
||||
|
||||
// Wait for a real key before sending
|
||||
if (MOD_KEYS.includes(keyvalLower))
|
||||
return false;
|
||||
|
||||
// Normalize Tab
|
||||
if (keyvalLower === Gdk.KEY_ISO_Left_Tab)
|
||||
keyvalLower = Gdk.KEY_Tab;
|
||||
|
||||
// Put shift back if it changed the case of the key, not otherwise.
|
||||
if (keyvalLower !== event.keyval)
|
||||
realMask |= Gdk.ModifierType.SHIFT_MASK;
|
||||
|
||||
// HACK: we don't want to use SysRq as a keybinding (but we do want
|
||||
// Alt+Print), so we avoid translation from Alt+Print to SysRq
|
||||
if (keyvalLower === Gdk.KEY_Sys_Req && (realMask & Gdk.ModifierType.MOD1_MASK) !== 0)
|
||||
keyvalLower = Gdk.KEY_Print;
|
||||
|
||||
// CapsLock isn't supported as a keybinding modifier, so keep it from
|
||||
// confusing us
|
||||
realMask &= ~Gdk.ModifierType.LOCK_MASK;
|
||||
|
||||
if (keyvalLower === 0)
|
||||
return false;
|
||||
|
||||
debug(`keyval: ${event.keyval}, mask: ${realMask}`);
|
||||
|
||||
const request = {
|
||||
alt: !!(realMask & Gdk.ModifierType.MOD1_MASK),
|
||||
ctrl: !!(realMask & Gdk.ModifierType.CONTROL_MASK),
|
||||
shift: !!(realMask & Gdk.ModifierType.SHIFT_MASK),
|
||||
super: !!(realMask & Gdk.ModifierType.SUPER_MASK),
|
||||
sendAck: true,
|
||||
};
|
||||
|
||||
// specialKey
|
||||
if (ReverseKeyMap.has(event.keyval)) {
|
||||
request.specialKey = ReverseKeyMap.get(event.keyval);
|
||||
|
||||
// key
|
||||
} else {
|
||||
const codePoint = Gdk.keyval_to_unicode(event.keyval);
|
||||
request.key = String.fromCodePoint(codePoint);
|
||||
}
|
||||
|
||||
this.device.sendPacket({
|
||||
type: 'kdeconnect.mousepad.request',
|
||||
body: request,
|
||||
});
|
||||
|
||||
// Pass these key combinations rather than using the echo reply
|
||||
if (request.alt || request.ctrl || request.super)
|
||||
return super.vfunc_key_press_event(event);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
vfunc_window_state_event(event) {
|
||||
if (!this.plugin.state)
|
||||
debug('ignoring remote keyboard state');
|
||||
|
||||
if (event.new_window_state & Gdk.WindowState.FOCUSED)
|
||||
this._grab();
|
||||
else
|
||||
this._ungrab();
|
||||
|
||||
return super.vfunc_window_state_event(event);
|
||||
}
|
||||
|
||||
_onInsertText(buffer, location, text, len) {
|
||||
if (this._isAck)
|
||||
return;
|
||||
|
||||
debug(`insert-text: ${text} (chars ${[...text].length})`);
|
||||
|
||||
for (const char of [...text]) {
|
||||
if (!char)
|
||||
continue;
|
||||
|
||||
// TODO: modifiers?
|
||||
this.device.sendPacket({
|
||||
type: 'kdeconnect.mousepad.request',
|
||||
body: {
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
shift: false,
|
||||
super: false,
|
||||
sendAck: false,
|
||||
key: char,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onState(widget) {
|
||||
if (!this.plugin.state)
|
||||
debug('ignoring remote keyboard state');
|
||||
|
||||
if (this.is_active)
|
||||
this._grab();
|
||||
else
|
||||
this._ungrab();
|
||||
}
|
||||
|
||||
_grab() {
|
||||
if (!this.visible || this._keyboard)
|
||||
return;
|
||||
|
||||
const seat = Gdk.Display.get_default().get_default_seat();
|
||||
const status = seat.grab(
|
||||
this.get_window(),
|
||||
Gdk.SeatCapabilities.KEYBOARD,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
if (status !== Gdk.GrabStatus.SUCCESS) {
|
||||
logError(new Error('Grabbing keyboard failed'));
|
||||
return;
|
||||
}
|
||||
|
||||
this._keyboard = seat.get_keyboard();
|
||||
this.grab_add();
|
||||
this.entry.has_focus = true;
|
||||
}
|
||||
|
||||
_ungrab() {
|
||||
if (this._keyboard) {
|
||||
this._keyboard.get_seat().ungrab();
|
||||
this._keyboard = null;
|
||||
this.grab_remove();
|
||||
}
|
||||
|
||||
this.entry.buffer.text = '';
|
||||
}
|
||||
});
|
@@ -0,0 +1,174 @@
|
||||
'use strict';
|
||||
|
||||
const Gio = imports.gi.Gio;
|
||||
const GObject = imports.gi.GObject;
|
||||
const Gtk = imports.gi.Gtk;
|
||||
|
||||
const URI = imports.service.utils.uri;
|
||||
|
||||
|
||||
/**
|
||||
* A dialog for repliable notifications.
|
||||
*/
|
||||
var ReplyDialog = GObject.registerClass({
|
||||
GTypeName: 'GSConnectNotificationReplyDialog',
|
||||
Properties: {
|
||||
'device': GObject.ParamSpec.object(
|
||||
'device',
|
||||
'Device',
|
||||
'The device associated with this window',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
GObject.Object
|
||||
),
|
||||
'plugin': GObject.ParamSpec.object(
|
||||
'plugin',
|
||||
'Plugin',
|
||||
'The plugin that owns this notification',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
GObject.Object
|
||||
),
|
||||
'uuid': GObject.ParamSpec.string(
|
||||
'uuid',
|
||||
'UUID',
|
||||
'The notification reply UUID',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
null
|
||||
),
|
||||
},
|
||||
Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/notification-reply-dialog.ui',
|
||||
Children: ['infobar', 'notification-title', 'notification-body', 'entry'],
|
||||
}, class ReplyDialog extends Gtk.Dialog {
|
||||
|
||||
_init(params) {
|
||||
super._init({
|
||||
application: Gio.Application.get_default(),
|
||||
device: params.device,
|
||||
plugin: params.plugin,
|
||||
uuid: params.uuid,
|
||||
use_header_bar: true,
|
||||
});
|
||||
|
||||
this.set_response_sensitive(Gtk.ResponseType.OK, false);
|
||||
|
||||
// Info bar
|
||||
this.device.bind_property(
|
||||
'connected',
|
||||
this.infobar,
|
||||
'reveal-child',
|
||||
GObject.BindingFlags.INVERT_BOOLEAN
|
||||
);
|
||||
|
||||
// Notification Data
|
||||
const headerbar = this.get_titlebar();
|
||||
headerbar.title = params.notification.appName;
|
||||
headerbar.subtitle = this.device.name;
|
||||
|
||||
this.notification_title.label = params.notification.title;
|
||||
this.notification_body.label = URI.linkify(params.notification.text);
|
||||
|
||||
// Message Entry/Send Button
|
||||
this.device.bind_property(
|
||||
'connected',
|
||||
this.entry,
|
||||
'sensitive',
|
||||
GObject.BindingFlags.DEFAULT
|
||||
);
|
||||
|
||||
this._connectedId = this.device.connect(
|
||||
'notify::connected',
|
||||
this._onStateChanged.bind(this)
|
||||
);
|
||||
|
||||
this._entryChangedId = this.entry.buffer.connect(
|
||||
'changed',
|
||||
this._onStateChanged.bind(this)
|
||||
);
|
||||
|
||||
this.restoreGeometry('notification-reply-dialog');
|
||||
|
||||
this.connect('destroy', this._onDestroy);
|
||||
}
|
||||
|
||||
_onDestroy(dialog) {
|
||||
dialog.entry.buffer.disconnect(dialog._entryChangedId);
|
||||
dialog.device.disconnect(dialog._connectedId);
|
||||
}
|
||||
|
||||
vfunc_delete_event() {
|
||||
this.saveGeometry();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
vfunc_response(response_id) {
|
||||
if (response_id === Gtk.ResponseType.OK) {
|
||||
// Refuse to send empty or whitespace only messages
|
||||
if (!this.entry.buffer.text.trim())
|
||||
return;
|
||||
|
||||
this.plugin.replyNotification(
|
||||
this.uuid,
|
||||
this.entry.buffer.text
|
||||
);
|
||||
}
|
||||
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
get device() {
|
||||
if (this._device === undefined)
|
||||
this._device = null;
|
||||
|
||||
return this._device;
|
||||
}
|
||||
|
||||
set device(device) {
|
||||
this._device = device;
|
||||
}
|
||||
|
||||
get plugin() {
|
||||
if (this._plugin === undefined)
|
||||
this._plugin = null;
|
||||
|
||||
return this._plugin;
|
||||
}
|
||||
|
||||
set plugin(plugin) {
|
||||
this._plugin = plugin;
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
if (this._uuid === undefined)
|
||||
this._uuid = null;
|
||||
|
||||
return this._uuid;
|
||||
}
|
||||
|
||||
set uuid(uuid) {
|
||||
this._uuid = uuid;
|
||||
|
||||
// We must have a UUID
|
||||
if (!uuid) {
|
||||
this.destroy();
|
||||
debug('no uuid for repliable notification');
|
||||
}
|
||||
}
|
||||
|
||||
_onActivateLink(label, uri) {
|
||||
Gtk.show_uri_on_window(
|
||||
this.get_toplevel(),
|
||||
uri.includes('://') ? uri : `https://${uri}`,
|
||||
Gtk.get_current_event_time()
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_onStateChanged() {
|
||||
if (this.device.connected && this.entry.buffer.text.trim())
|
||||
this.set_response_sensitive(Gtk.ResponseType.OK, true);
|
||||
else
|
||||
this.set_response_sensitive(Gtk.ResponseType.OK, false);
|
||||
}
|
||||
});
|
||||
|
@@ -0,0 +1,248 @@
|
||||
'use strict';
|
||||
|
||||
const GLib = imports.gi.GLib;
|
||||
const Gio = imports.gi.Gio;
|
||||
const GObject = imports.gi.GObject;
|
||||
const Gtk = imports.gi.Gtk;
|
||||
|
||||
const Config = imports.config;
|
||||
|
||||
|
||||
/*
|
||||
* Issue Header
|
||||
*/
|
||||
const ISSUE_HEADER = `
|
||||
GSConnect: ${Config.PACKAGE_VERSION} (${Config.IS_USER ? 'user' : 'system'})
|
||||
GJS: ${imports.system.version}
|
||||
Session: ${GLib.getenv('XDG_SESSION_TYPE')}
|
||||
OS: ${GLib.get_os_info('PRETTY_NAME')}
|
||||
`;
|
||||
|
||||
|
||||
/**
|
||||
* A dialog for selecting a device
|
||||
*/
|
||||
var DeviceChooser = GObject.registerClass({
|
||||
GTypeName: 'GSConnectServiceDeviceChooser',
|
||||
Properties: {
|
||||
'action-name': GObject.ParamSpec.string(
|
||||
'action-name',
|
||||
'Action Name',
|
||||
'The name of the associated action, like "sendFile"',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
null
|
||||
),
|
||||
'action-target': GObject.param_spec_variant(
|
||||
'action-target',
|
||||
'Action Target',
|
||||
'The parameter for action invocations',
|
||||
new GLib.VariantType('*'),
|
||||
null,
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
},
|
||||
Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/service-device-chooser.ui',
|
||||
Children: ['device-list', 'cancel-button', 'select-button'],
|
||||
}, class DeviceChooser extends Gtk.Dialog {
|
||||
|
||||
_init(params = {}) {
|
||||
super._init({
|
||||
use_header_bar: true,
|
||||
application: Gio.Application.get_default(),
|
||||
});
|
||||
this.set_keep_above(true);
|
||||
|
||||
// HeaderBar
|
||||
this.get_header_bar().subtitle = params.title;
|
||||
|
||||
// Dialog Action
|
||||
this.action_name = params.action_name;
|
||||
this.action_target = params.action_target;
|
||||
|
||||
// Device List
|
||||
this.device_list.set_sort_func(this._sortDevices);
|
||||
|
||||
this._devicesChangedId = this.application.settings.connect(
|
||||
'changed::devices',
|
||||
this._onDevicesChanged.bind(this)
|
||||
);
|
||||
this._onDevicesChanged();
|
||||
}
|
||||
|
||||
vfunc_response(response_id) {
|
||||
if (response_id === Gtk.ResponseType.OK) {
|
||||
try {
|
||||
const device = this.device_list.get_selected_row().device;
|
||||
device.activate_action(this.action_name, this.action_target);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
get action_name() {
|
||||
if (this._action_name === undefined)
|
||||
this._action_name = null;
|
||||
|
||||
return this._action_name;
|
||||
}
|
||||
|
||||
set action_name(name) {
|
||||
this._action_name = name;
|
||||
}
|
||||
|
||||
get action_target() {
|
||||
if (this._action_target === undefined)
|
||||
this._action_target = null;
|
||||
|
||||
return this._action_target;
|
||||
}
|
||||
|
||||
set action_target(variant) {
|
||||
this._action_target = variant;
|
||||
}
|
||||
|
||||
_onDeviceActivated(box, row) {
|
||||
this.response(Gtk.ResponseType.OK);
|
||||
}
|
||||
|
||||
_onDeviceSelected(box) {
|
||||
this.set_response_sensitive(
|
||||
Gtk.ResponseType.OK,
|
||||
(box.get_selected_row())
|
||||
);
|
||||
}
|
||||
|
||||
_onDevicesChanged() {
|
||||
// Collect known devices
|
||||
const devices = {};
|
||||
|
||||
for (const [id, device] of this.application.manager.devices.entries())
|
||||
devices[id] = device;
|
||||
|
||||
// Prune device rows
|
||||
this.device_list.foreach(row => {
|
||||
if (!devices.hasOwnProperty(row.name))
|
||||
row.destroy();
|
||||
else
|
||||
delete devices[row.name];
|
||||
});
|
||||
|
||||
// Add new devices
|
||||
for (const device of Object.values(devices)) {
|
||||
const action = device.lookup_action(this.action_name);
|
||||
|
||||
if (action === null)
|
||||
continue;
|
||||
|
||||
const row = new Gtk.ListBoxRow({
|
||||
visible: action.enabled,
|
||||
});
|
||||
row.set_name(device.id);
|
||||
row.device = device;
|
||||
|
||||
action.bind_property(
|
||||
'enabled',
|
||||
row,
|
||||
'visible',
|
||||
Gio.SettingsBindFlags.DEFAULT
|
||||
);
|
||||
|
||||
const grid = new Gtk.Grid({
|
||||
column_spacing: 12,
|
||||
margin: 6,
|
||||
visible: true,
|
||||
});
|
||||
row.add(grid);
|
||||
|
||||
const icon = new Gtk.Image({
|
||||
icon_name: device.icon_name,
|
||||
pixel_size: 32,
|
||||
visible: true,
|
||||
});
|
||||
grid.attach(icon, 0, 0, 1, 1);
|
||||
|
||||
const name = new Gtk.Label({
|
||||
label: device.name,
|
||||
halign: Gtk.Align.START,
|
||||
hexpand: true,
|
||||
visible: true,
|
||||
});
|
||||
grid.attach(name, 1, 0, 1, 1);
|
||||
|
||||
this.device_list.add(row);
|
||||
}
|
||||
|
||||
if (this.device_list.get_selected_row() === null)
|
||||
this.device_list.select_row(this.device_list.get_row_at_index(0));
|
||||
}
|
||||
|
||||
_sortDevices(row1, row2) {
|
||||
return row1.device.name.localeCompare(row2.device.name);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* A dialog for reporting an error.
|
||||
*/
|
||||
var ErrorDialog = GObject.registerClass({
|
||||
GTypeName: 'GSConnectServiceErrorDialog',
|
||||
Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/service-error-dialog.ui',
|
||||
Children: [
|
||||
'error-stack',
|
||||
'expander-arrow',
|
||||
'gesture',
|
||||
'report-button',
|
||||
'revealer',
|
||||
],
|
||||
}, class ErrorDialog extends Gtk.Window {
|
||||
|
||||
_init(error) {
|
||||
super._init({
|
||||
application: Gio.Application.get_default(),
|
||||
title: `GSConnect: ${error.name}`,
|
||||
});
|
||||
this.set_keep_above(true);
|
||||
|
||||
this.error = error;
|
||||
this.error_stack.buffer.text = `${error.message}\n\n${error.stack}`;
|
||||
this.gesture.connect('released', this._onReleased.bind(this));
|
||||
}
|
||||
|
||||
_onClicked(button) {
|
||||
if (this.report_button === button) {
|
||||
const uri = this._buildUri(this.error.message, this.error.stack);
|
||||
Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null);
|
||||
}
|
||||
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
_onReleased(gesture, n_press) {
|
||||
if (n_press === 1)
|
||||
this.revealer.reveal_child = !this.revealer.reveal_child;
|
||||
}
|
||||
|
||||
_onRevealChild(revealer, pspec) {
|
||||
this.expander_arrow.icon_name = this.revealer.reveal_child
|
||||
? 'pan-down-symbolic'
|
||||
: 'pan-end-symbolic';
|
||||
}
|
||||
|
||||
_buildUri(message, stack) {
|
||||
const body = `\`\`\`${ISSUE_HEADER}\n${stack}\n\`\`\``;
|
||||
const titleQuery = encodeURIComponent(message).replace('%20', '+');
|
||||
const bodyQuery = encodeURIComponent(body).replace('%20', '+');
|
||||
const uri = `${Config.PACKAGE_BUGREPORT}?title=${titleQuery}&body=${bodyQuery}`;
|
||||
|
||||
// Reasonable URI length limit
|
||||
if (uri.length > 2000)
|
||||
return uri.substr(0, 2000);
|
||||
|
||||
return uri;
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user