-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
prefs: Update to libadwaita and add GUI for app specific gesture sett…
…ings
- Loading branch information
1 parent
f518026
commit 0c4b8a3
Showing
15 changed files
with
750 additions
and
457 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,342 @@ | ||
import Gtk from '@gi-types/gtk4'; | ||
import Gio from '@gi-types/gio2'; | ||
import GLib from '@gi-types/glib2'; | ||
import GObject from '@gi-types/gobject2'; | ||
import Adw from '@gi-types/adw1'; | ||
import { ForwardBackKeyBinds, GioSettings } from './settings'; | ||
import { registerClass } from './utils/gobject'; | ||
|
||
/** return icon name for give app */ | ||
function getAppIconName(app: Gio.AppInfoPrototype) { | ||
return app.get_icon()?.to_string() ?? 'icon-missing'; | ||
} | ||
|
||
/** Dialog window used for selecting application from given list of apps | ||
* Emits `app-selected` signal with application id | ||
*/ | ||
const AppChooserDialog = registerClass( | ||
{ | ||
Properties: {}, | ||
Signals: { 'app-selected': { param_types: [GObject.TYPE_STRING] } }, | ||
}, | ||
class GIE_AppChooserDialog extends Adw.PreferencesWindow { | ||
private _group: Adw.PreferencesGroup; | ||
|
||
/** | ||
* @param apps list of apps to display in dialog | ||
* @param parent parent window, dialog will be transient for parent | ||
*/ | ||
constructor(apps: Gio.AppInfoPrototype[], parent: Adw.Window) { | ||
super({ | ||
modal: true, | ||
transientFor: parent, | ||
// destroyWithParent: false, | ||
}); | ||
|
||
this.set_default_size( | ||
0.5 * parent.defaultWidth, | ||
0.6 * parent.defaultHeight, | ||
); | ||
|
||
this._group = new Adw.PreferencesGroup(); | ||
const page = new Adw.PreferencesPage(); | ||
page.add(this._group); | ||
this.add(page); | ||
|
||
apps.forEach(app => this._addAppRow(app)); | ||
} | ||
|
||
/** for given app add row to selectable list */ | ||
private _addAppRow(app: Gio.AppInfoPrototype) { | ||
const row = new Adw.ActionRow({ | ||
title: app.get_display_name() ?? '', | ||
subtitle: app.get_description() ?? '', | ||
activatable: true, | ||
iconName: getAppIconName(app), | ||
}); | ||
this._group.add(row); | ||
|
||
row.connect('activated', () => { | ||
this.emit('app-selected', app.get_id()); | ||
this.close(); | ||
}); | ||
} | ||
}, | ||
); | ||
|
||
/** type definition for gesture setting(keybind and reverse flag) for app */ | ||
declare type AppGestureSettings = [ForwardBackKeyBinds, boolean]; | ||
|
||
/** | ||
* Class to create row for application in list to display gesture settings of app | ||
* Emits 'value-updated' when any of settings changes | ||
* Emits 'remove-request' when remove button is clicked | ||
*/ | ||
const AppGestureSettingsRow = registerClass( | ||
{ | ||
Properties: {}, | ||
Signals: { | ||
'value-updated': { param_types: [GObject.TYPE_UINT, GObject.TYPE_BOOLEAN] }, | ||
'remove-request': {}, | ||
}, | ||
}, | ||
class GIE_AppGestureSettingsRow extends Adw.ExpanderRow { | ||
private _keyBindCombo: Adw.ComboRow; | ||
private _reverseButton: Gtk.Switch; | ||
|
||
/** | ||
* @param appGestureSettings value of current settings for app | ||
* @param model list of choices of keybings for setting | ||
*/ | ||
constructor(app: Gio.AppInfoPrototype, appGestureSettings: AppGestureSettings, model: Gio.ListModel) { | ||
super({ | ||
title: app.get_display_name() ?? '', | ||
iconName: getAppIconName(app), | ||
}); | ||
|
||
const [keyBind, reverse] = appGestureSettings; | ||
|
||
// keybinding combo row | ||
this._keyBindCombo = new Adw.ComboRow({ | ||
title: 'Keybinding', | ||
subtitle: 'Keyboard shortcut to emit after gesture is completed', | ||
model, | ||
}); | ||
this._keyBindCombo.set_selected(keyBind); | ||
this.add_row(this._keyBindCombo); | ||
|
||
// reverse switch row | ||
this._reverseButton = new Gtk.Switch({ | ||
active: reverse, | ||
valign: Gtk.Align.CENTER, | ||
}); | ||
let actionRow = new Adw.ActionRow({ title: 'Reverse gesture direction' }); | ||
actionRow.add_suffix(this._reverseButton); | ||
this.add_row(actionRow); | ||
|
||
// remove setting row | ||
const removeButton = new Gtk.Button({ | ||
label: 'Remove...', | ||
valign: Gtk.Align.CENTER, | ||
halign: Gtk.Align.END, | ||
cssClasses: ['raised'], | ||
}); | ||
actionRow = new Adw.ActionRow(); | ||
actionRow.add_suffix(removeButton); | ||
this.add_row(actionRow); | ||
|
||
// remove request signal emitted when remove button is clicked | ||
removeButton.connect('clicked', () => this.emit('remove-request')); | ||
this._keyBindCombo.connect('notify::selected', this._onValueUpdated.bind(this)); | ||
this._reverseButton.connect('notify::active', this._onValueUpdated.bind(this)); | ||
|
||
} | ||
|
||
/** function called internally whenever some setting is changed, emits external signal */ | ||
private _onValueUpdated() { | ||
this.emit('value-updated', this._keyBindCombo.selected, this._reverseButton.active); | ||
} | ||
}, | ||
); | ||
|
||
/** | ||
* Class to display list of applications and their gesture settings | ||
*/ | ||
const AppKeybindingGesturePrefsGroup = registerClass( | ||
class GIE_AppKeybindingGesturePrefsGroup extends Adw.PreferencesGroup { | ||
private _settings: GioSettings; | ||
private _prefsWindow: Adw.PreferencesWindow; | ||
private _appRows: Map<string, typeof AppGestureSettingsRow.prototype>; | ||
private _cachedSettings: Record<string, AppGestureSettings>; | ||
private _addAppButtonRow: Adw.PreferencesRow; | ||
private _appGestureModel: Gtk.StringList<GObject.Object>; | ||
|
||
/** | ||
* @param prefsWindow parent preferences window | ||
* @param settings extension settings object | ||
*/ | ||
constructor(prefsWindow: Adw.PreferencesWindow, settings: GioSettings) { | ||
super({ | ||
title: 'Enable application specific gestures', | ||
description: 'Window switching gesture needs to be disabled', | ||
}); | ||
|
||
this._prefsWindow = prefsWindow; | ||
this._settings = settings; | ||
this._appRows = new Map(); | ||
|
||
this._cachedSettings = this._settings.get_value('forward-back-application-keyboard-shortcuts').deepUnpack(); | ||
this._appGestureModel = this._getAppGestureModelForComboBox(); | ||
|
||
// build ui widgets | ||
this._addAppButtonRow = this._buildAddAppButtonRow(); | ||
this.add(this._addAppButtonRow); | ||
|
||
Object.keys(this._cachedSettings).sort().reverse().forEach(appId => this._addAppGestureRow(appId)); | ||
|
||
// bind switch to setting value | ||
const toggleSwitch = new Gtk.Switch({ valign: Gtk.Align.CENTER }); | ||
this._settings.bind('enable-forward-back-gesture', toggleSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); | ||
this.set_header_suffix(toggleSwitch); | ||
} | ||
|
||
/** | ||
* Handler function, called when add button is clicked | ||
* Displays dialog to select application to add | ||
*/ | ||
private _onAddAppButtonClicked() { | ||
// find list of new apps that can be selected | ||
const allApps = Gio.app_info_get_all(); | ||
const selectableApps = allApps | ||
.filter(app => { | ||
const appId = app.get_id(); | ||
return app.should_show() && appId && !this._appRows.has(appId); | ||
}) | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
.sort((a, b) => a.get_id()!.localeCompare(b.get_id()!)); | ||
|
||
const appChooserDialog = new AppChooserDialog(selectableApps, this._prefsWindow); | ||
appChooserDialog.connect('app-selected', (_source, appId) => this._addAppGestureRow(appId)); | ||
appChooserDialog.present(); | ||
} | ||
|
||
/** | ||
* @returns row for add app button | ||
*/ | ||
private _buildAddAppButtonRow() { | ||
const addButton = new Gtk.Button({ | ||
iconName: 'list-add-symbolic', | ||
cssName: 'card', | ||
heightRequest: 40, | ||
}); | ||
|
||
const addButtonRow = new Adw.PreferencesRow({ child: addButton }); | ||
addButton.connect('clicked', this._onAddAppButtonClicked.bind(this)); | ||
|
||
return addButtonRow; | ||
} | ||
|
||
/** | ||
* Adds application specific gesture settings row for given app id | ||
* Does nothing if app doesn't exist | ||
*/ | ||
private _addAppGestureRow(appId: string) { | ||
const app = Gio.DesktopAppInfo.new(appId); | ||
if (!app) return; | ||
|
||
const appRow = new AppGestureSettingsRow( | ||
app, | ||
this._getAppGestureSetting(appId), // this function updates extension settings | ||
this._appGestureModel, | ||
); | ||
this._appRows.set(appId, appRow); | ||
this.add(appRow); | ||
|
||
// callbacks for setting updates and remove request | ||
appRow.connect('remove-request', () => this._requestRemoveAppGestureRow(appId)); | ||
appRow.connect('value-updated', (_source, keyBind, reverse) => { | ||
this._setAppGestureSetting(appId, [keyBind, reverse]); | ||
}); | ||
|
||
// re-add add-appbutton at the end | ||
this.remove(this._addAppButtonRow); | ||
this.add(this._addAppButtonRow); | ||
} | ||
|
||
/** | ||
* Removes application specific gesture settings row for given app | ||
* Does nothing if row for app was not added | ||
* Updates extension settings | ||
*/ | ||
private _removeAppGestureRow(appId: string) { | ||
const appRow = this._appRows.get(appId); | ||
if (!appRow) return; | ||
|
||
this.remove(appRow); | ||
this._appRows.delete(appId); | ||
delete this._cachedSettings[appId]; | ||
this._updateExtensionSettings(); | ||
} | ||
|
||
/** | ||
* Signal handler called when removal of app gesture settings is requested | ||
* Displays confirmation dialog and removes app row if confirmed | ||
*/ | ||
private _requestRemoveAppGestureRow(appId: string) { | ||
const app = Gio.DesktopAppInfo.new(appId); | ||
|
||
const dialog = new Gtk.MessageDialog({ | ||
transient_for: this._prefsWindow, | ||
modal: true, | ||
text: `Remove application specific gesture for ${app.get_display_name()}?`, | ||
}); | ||
|
||
dialog.add_button('Cancel', Gtk.ResponseType.CANCEL); | ||
dialog | ||
.add_button('Remove', Gtk.ResponseType.ACCEPT) | ||
.get_style_context() | ||
.add_class('destructive-action'); | ||
|
||
dialog.connect('response', (_dlg, response) => { | ||
if (response === Gtk.ResponseType.ACCEPT) { | ||
this._removeAppGestureRow(appId); | ||
} | ||
dialog.destroy(); | ||
}); | ||
|
||
dialog.present(); | ||
} | ||
|
||
/** | ||
* Returns application specific gesture setting | ||
* if setting is not set, returns default value and saves extension settings | ||
*/ | ||
private _getAppGestureSetting(appId: string): AppGestureSettings { | ||
let val = this._cachedSettings[appId]; | ||
|
||
if (!val) { | ||
// this is case when new app was selected for gesture | ||
val = [ForwardBackKeyBinds.Default, false]; | ||
this._setAppGestureSetting(appId, val); | ||
} | ||
|
||
return val; | ||
} | ||
|
||
/** Saves application specific gesture setting into extension settings */ | ||
private _setAppGestureSetting(appId: string, appGestureSettings: AppGestureSettings) { | ||
this._cachedSettings[appId] = appGestureSettings; | ||
this._updateExtensionSettings(); | ||
} | ||
|
||
/** Updates extension settings */ | ||
private _updateExtensionSettings() { | ||
const glibVariant = new GLib.Variant('a{s(ib)}', this._cachedSettings); | ||
this._settings.set_value('forward-back-application-keyboard-shortcuts', glibVariant); | ||
} | ||
|
||
/** Returns model which contains all possible choices for keybinding setting for app-gesture */ | ||
private _getAppGestureModelForComboBox() { | ||
const appGestureModel = new Gtk.StringList(); | ||
Object.values(ForwardBackKeyBinds).forEach(val => { | ||
if (typeof val !== 'number') return; | ||
appGestureModel.append(ForwardBackKeyBinds[val]); | ||
}); | ||
|
||
return appGestureModel; | ||
} | ||
}, | ||
); | ||
|
||
/** | ||
* @returns preference page for application gestures | ||
*/ | ||
export function getAppKeybindingGesturePrefsPage(prefsWindow: Adw.PreferencesWindow, settings: GioSettings) { | ||
const page = new Adw.PreferencesPage({ | ||
title: 'App Gestures', | ||
iconName: 'org.gnome.Settings-applications-symbolic', | ||
}); | ||
|
||
page.add(new AppKeybindingGesturePrefsGroup(prefsWindow, settings)); | ||
return page; | ||
} |
Oops, something went wrong.