diff --git a/gsjackctl/extension.js b/gsjackctl/extension.js index 6350c93..4694254 100644 --- a/gsjackctl/extension.js +++ b/gsjackctl/extension.js @@ -11,6 +11,8 @@ const Local = imports.ui.main.extensionManager.lookup('gsjackctl@cbix.de'); const {GLib, GObject} = imports.gi; const Main = imports.ui.main; +const ExtensionUtils = imports.misc.extensionUtils; +const PopupMenu = imports.ui.popupMenu; const {Indicator} = Local.imports.gsjackctl.indicator; const {Status} = Local.imports.gsjackctl.status; @@ -40,6 +42,7 @@ var Extension = class Extension { this._indicator = new Indicator(); this._status = new Status(); this._control = new Control(); + this._prefs = new PopupMenu.PopupMenuItem('Settings'); this._indicator.menu.addMenuItem(this._status); try { this._a2jControl = new A2jControl(); @@ -48,6 +51,7 @@ var Extension = class Extension { this._a2jControl = false; log('gsjackctl: a2jmidid not available'); } + this._indicator.menu.addMenuItem(this._prefs); this._indicator.menu.addMenuItem(this._control); Main.panel.addToStatusArea(this._uuid, this._indicator); @@ -87,6 +91,10 @@ var Extension = class Extension { } }); + this._prefs.connect('activate', () => { + ExtensionUtils.openPrefs(); + }); + this._control.connect('start-jack', () => { try { this._jackctl.StartServerSync(); diff --git a/gsjackctl/prefsWidget.js b/gsjackctl/prefsWidget.js new file mode 100644 index 0000000..40ea958 --- /dev/null +++ b/gsjackctl/prefsWidget.js @@ -0,0 +1,206 @@ +'use strict'; + +imports.gi.versions.Gtk = '3.0'; + +const {GLib, GObject, Gtk} = imports.gi; + +let JackConfigure; + +try { + // const Local = imports.ui.main.extensionManager.lookup('gsjackctl@cbix.de'); + const ExtensionUtils = imports.misc.extensionUtils; + const Local = ExtensionUtils.getCurrentExtension(); + JackConfigure = Local.imports.jack.jackdbus.JackConfigure; +} catch (e) { + logError(e, 'gsjackctl.prefsWidget'); + JackConfigure = imports.jack.jackdbus.JackConfigure; +} + +const COL_KEY = 0, + COL_VALUE = 1, + COL_DEFAULT = 2, + COL_DESC = 3, + COL_LEAF = 4, + COL_ISRANGE = 5, + COL_ISSTRICT = 6, + COL_ISFAKEVALUE = 7, + COL_VALUES = 8, + COL_VALUESTRING = 9, + COL_DEFAULTSTRING = 10; + +/** + * CellRendererVariant is a custom Gtk.CellRenderer wrapping different cell + * renderers and rendering one based on the parameter value and constraint. + * bool → CellRendererToggle + * numeric → CellRendererSpin + * constraint values → CellRendererCombo + * string → CellRendererText + */ +var CellRendererVariant = GObject.registerClass({ + GTypeName: 'CellRendererVariant', + Properties: { + 'value': GObject.param_spec_variant('value', 'Value', 'Configuration Value', new GLib.VariantType('v'), null, GObject.ParamFlags.READWRITE), + 'default': GObject.param_spec_variant('default', 'Default', 'Default Configuration Value', new GLib.VariantType('v'), null, GObject.ParamFlags.READWRITE), + }, +}, class CellRendererVariant extends Gtk.CellRenderer { + constructor(params) { + super(params); + this._textRenderer = new Gtk.CellRendererText(); + this._toggleRenderer = new Gtk.CellRendererToggle(); + this._comboRenderer = new Gtk.CellRendererCombo(); + this._spinRenderer = new Gtk.CellRendererSpin(); + } + + set value(value) { + log(`set value: ${value}`); + this._value = value; + if (!value) + return; + + log(`value type: ${value.type_string}`); + } + + set default(value) { + log(`set default: ${value}`); + this._default = value; + if (!value) + return; + + log(`default type: ${value.type_string}`); + } +} +); + +var PrefsWidget = GObject.registerClass( +class PrefsWidget extends Gtk.ScrolledWindow { + _init(params) { + super._init(params); + + this._jackcfg = new JackConfigure(); + + log('building prefs widget...'); + + this._treeStore = new Gtk.TreeStore(); + const colTypes = []; + colTypes[COL_KEY] = GObject.TYPE_STRING; + colTypes[COL_VALUE] = GObject.TYPE_VARIANT; + colTypes[COL_DEFAULT] = GObject.TYPE_VARIANT; + colTypes[COL_DESC] = GObject.TYPE_STRING; + colTypes[COL_LEAF] = GObject.TYPE_BOOLEAN; + colTypes[COL_ISFAKEVALUE] = GObject.TYPE_BOOLEAN; + colTypes[COL_ISRANGE] = GObject.TYPE_BOOLEAN; + colTypes[COL_ISSTRICT] = GObject.TYPE_BOOLEAN; + colTypes[COL_VALUES] = GObject.TYPE_VARIANT; + colTypes[COL_VALUESTRING] = GObject.TYPE_STRING; + colTypes[COL_DEFAULTSTRING] = GObject.TYPE_STRING; + + this._treeStore.set_column_types(colTypes); + + // adaptation of _fullConfigurationTree() traversing for TreeStore + const pathStack = [[]], + iterStack = [null]; + while (pathStack.length > 0) { + const path = pathStack.pop(); + const iter = iterStack.pop() || null; + const [isLeaf, nodes] = this._jackcfg.ReadContainerSync(path); + + // set leaf flag in store, except for root node + if (iter) + this._treeStore.set(iter, [COL_LEAF], [isLeaf]); + + if (!isLeaf) { + nodes.forEach(node => { + const newPath = path.concat(node); + pathStack.push(newPath); + const newIter = this._treeStore.append(iter); + iterStack.push(newIter); + this._treeStore.set(newIter, [COL_KEY], [node]); + }); + } else { + nodes.forEach(node => { + const newPath = path.concat(node); + const [paramInfo] = this._jackcfg.GetParameterInfoSync(newPath); + const paramValue = this._jackcfg.GetParameterValueSync(newPath); + const paramConstraint = this._jackcfg.GetParameterConstraintSync(newPath); + const newIter = this._treeStore.append(iter); + this._treeStore.set(newIter, [ + COL_KEY, + COL_VALUE, + COL_DEFAULT, + COL_VALUESTRING, + COL_DEFAULTSTRING, + COL_DESC, + COL_ISRANGE, + COL_ISRANGE, + COL_ISFAKEVALUE, + ], [ + paramInfo[1], + paramValue[2], + paramValue[1], + `${paramValue[2].get_type_string()}:${paramValue[2].print(false)}`, + `${paramValue[1].get_type_string()}:${paramValue[1].print(false)}`, + paramInfo[2], + paramConstraint[0], + paramConstraint[1], + paramConstraint[2], + ]); + }); + } + } + + this._treeView = new Gtk.TreeView({ + expand: true, + model: this._treeStore, + }); + + const colKey = new Gtk.TreeViewColumn({title: 'Key'}); + const colValue = new Gtk.TreeViewColumn({title: 'Value'}); + const colDefault = new Gtk.TreeViewColumn({title: 'Default'}); + const colDescription = new Gtk.TreeViewColumn({title: 'Description'}); + const colTest = new Gtk.TreeViewColumn({ + title: 'Test', + }); + + // dynamic renderer + const variantRenderer = new CellRendererVariant(); + + const normalRenderer = new Gtk.CellRendererText(); + const toggleRenderer = new Gtk.CellRendererToggle({ + activatable: true, + }); + toggleRenderer.connect('toggled', (rend, path) => { + const [ok, it] = this._treeStore.get_iter_from_string(path); + print(`toggled: ${rend.active}, ${ok}`); + if (ok) + // test + this._treeStore.set(it, [COL_ISSTRICT], [!rend.active]); + + // const currentState = this._treeStore. + }); + + colKey.pack_start(normalRenderer, true); + colValue.pack_start(normalRenderer, true); + colDefault.pack_start(normalRenderer, true); + colDescription.pack_start(normalRenderer, true); + colTest.pack_start(variantRenderer, true); + + colKey.add_attribute(normalRenderer, 'text', COL_KEY); + colValue.add_attribute(normalRenderer, 'text', COL_VALUESTRING); + colDefault.add_attribute(normalRenderer, 'text', COL_DEFAULTSTRING); + colDescription.add_attribute(normalRenderer, 'text', COL_DESC); + colTest.add_attribute(variantRenderer, 'value', COL_VALUE); + colTest.add_attribute(variantRenderer, 'default', COL_DEFAULT); + + this._treeView.insert_column(colKey, 0); + this._treeView.insert_column(colValue, 1); + this._treeView.insert_column(colDefault, 2); + this._treeView.insert_column(colDescription, 3); + this._treeView.insert_column(colTest, 4); + + this.add(this._treeView); + this.show_all(); + } +} +); + +// vim: set sw=4 ts=4 : diff --git a/jack/jackdbus.js b/jack/jackdbus.js index ace3169..0749a7b 100644 --- a/jack/jackdbus.js +++ b/jack/jackdbus.js @@ -278,6 +278,59 @@ var JackConfigure = class JackConfigure extends _JackdbusProxyConfigure { 'org.jackaudio.service', '/org/jackaudio/Controller' ); + + // stupid Gio.DBusProxy doesn't make this an actual class so we can't use regular class methods + this._fullConfigurationTree = () => { + const _traverseObjReducer = (acc, curr) => acc ? acc[curr] : undefined; + + const pathStack = [[]], + tree = {}; + try { + // basically a DFS through the configuration tree + // probably quite heavy so maybe don't run this too often + while (pathStack.length > 0) { + const path = pathStack.pop(); + const [isLeaf, nodes] = this.ReadContainerSync(path); + const currentNode = path.reduce(_traverseObjReducer, tree); + if (!isLeaf) { + nodes.forEach(node => { + const newPath = path.concat(node); + // log(`push ${JSON.stringify(newPath)}`); + pathStack.push(newPath); + currentNode[node] = {}; + }); + } else { + nodes.forEach(node => { + const newPath = path.concat(node); + // TODO refactor this please + // TODO use real values instead of VariantType.print() + const [paramInfo] = this.GetParameterInfoSync(newPath); + const paramValue = this.GetParameterValueSync(newPath); + const paramConstraint = this.GetParameterConstraintSync(newPath); + + currentNode[node] = { + name: paramInfo[1], + desc: paramInfo[2], + default: paramValue[2].print(true), + value: paramValue[2].print(true), + constraint: { + isRange: paramConstraint[0], + isStrict: paramConstraint[1], + isFakeValue: paramConstraint[2], + values: paramConstraint[3].map(v => ({ + key: v[0].print(true), + name: v[1], + })), + }, + }; + }); + } + } + } catch (e) { + logError(e, 'gsjackctl JackConfigure.fullConfigurationTree'); + } + return tree; + }; } }; diff --git a/prefs.js b/prefs.js new file mode 100644 index 0000000..50e18b9 --- /dev/null +++ b/prefs.js @@ -0,0 +1,20 @@ +'use strict'; + +imports.gi.versions.Gtk = '3.0'; + +const ExtensionUtils = imports.misc.extensionUtils; +const Local = ExtensionUtils.getCurrentExtension(); + +const {PrefsWidget} = Local.imports.gsjackctl.prefsWidget; + +function init() { + log('initializing gsjackctl Preferences'); +} + +function buildPrefsWidget() { + const widget = new PrefsWidget(); + widget.show_all(); + return widget; +} + +// vim: set sw=4 ts=4 : diff --git a/test/cfg_widget.js b/test/cfg_widget.js new file mode 100755 index 0000000..3c4dc1a --- /dev/null +++ b/test/cfg_widget.js @@ -0,0 +1,57 @@ +#!/usr/bin/env -S gjs -I .. + +imports.gi.versions.Gtk = '3.0'; + +const {Gtk} = imports.gi; + +const {PrefsWidget} = imports.gsjackctl.prefsWidget; + +class TestWindow { + constructor() { + this.application = new Gtk.Application({ + application_id: 'de.cbix.gsjackctl.prefswindow', + }); + this.application.connect('activate', this._onActivate.bind(this)); + this.application.connect('startup', this._onStartup.bind(this)); + } + + _onActivate() { + this._window.present(); + } + + _onStartup() { + this._buildUI(); + } + + _buildUI() { + this._window = new Gtk.ApplicationWindow({ + application: this.application, + window_position: Gtk.WindowPosition.CENTER, + default_height: 250, + default_width: 100, + title: 'gsjackctl prefs window test', + }); + + const widget = new PrefsWidget(); + widget.show_all(); + + this._window.add(widget); + this._window.show_all(); + } +} + +try { + // const Prefs = imports.prefs; + try { + print('test widget window'); + + const app = new TestWindow(); + app.application.run(ARGV); + } catch (e) { + logError(e); + } +} catch (e) { + print(e.message); +} + +// vim: set sw=4 ts=4 : diff --git a/test/dump_cfg.js b/test/dump_cfg.js new file mode 100755 index 0000000..8562fc7 --- /dev/null +++ b/test/dump_cfg.js @@ -0,0 +1,20 @@ +#!/usr/bin/env -S gjs -I .. + +// prints the full configuration tree as json. It's recommended to be run through a json pretty-printer, like +// ./dump_cfg.js | jsonpp + +try { + const {JackConfigure} = imports.jack.jackdbus; + + try { + const jackcfg = new JackConfigure(); + const configtree = jackcfg._fullConfigurationTree(); + print(JSON.stringify(configtree)); + } catch (e) { + print(`Error: ${e}`); + } +} catch (e) { + print(e.message); +} + +// vim: set sw=4 ts=4 : diff --git a/test/widget.js b/test/widget.js new file mode 100755 index 0000000..e77cd17 --- /dev/null +++ b/test/widget.js @@ -0,0 +1,136 @@ +#!/usr/bin/env -S gjs -I .. + +imports.gi.versions.Gtk = '3.0'; + +const {GObject, Gtk, Pango} = imports.gi; + +const COL_KEY = 0, + COL_VALUE = 1, + COL_DEFAULT = 2, + COL_DESC = 3, + COL_STATE = 4; + +class TestWindow { + constructor() { + this.application = new Gtk.Application({ + application_id: 'de.cbix.gsjackctl.prefswindow', + }); + this.application.connect('activate', this._onActivate.bind(this)); + this.application.connect('startup', this._onStartup.bind(this)); + } + + _onActivate() { + this._window.present(); + } + + _onStartup() { + this._buildUI(); + } + + _buildUI() { + this._window = new Gtk.ApplicationWindow({ + application: this.application, + window_position: Gtk.WindowPosition.CENTER, + default_height: 250, + default_width: 100, + title: 'gsjackctl prefs window test', + }); + + this._treeStore = new Gtk.TreeStore(); + const colTypes = []; + colTypes[COL_KEY] = GObject.TYPE_STRING; + colTypes[COL_VALUE] = GObject.TYPE_VARIANT; + colTypes[COL_DEFAULT] = GObject.TYPE_VARIANT; + colTypes[COL_DESC] = GObject.TYPE_STRING; + colTypes[COL_STATE] = GObject.TYPE_BOOLEAN; + + this._treeStore.set_column_types(colTypes); + + const itA = this._treeStore.append(null); + const itB = this._treeStore.append(null); + const itFoo = this._treeStore.append(itA); + const itBar = this._treeStore.append(itB); + const itBaz = this._treeStore.append(itBar); + + this._treeStore.set(itA, [COL_KEY, COL_DESC], ['a', 'A']); + this._treeStore.set(itB, [COL_KEY, COL_DESC], ['b', 'B']); + this._treeStore.set(itFoo, [COL_KEY, COL_DESC], ['foo', 'Foo']); + this._treeStore.set(itBar, [COL_KEY, COL_DESC], ['bar', 'Bar']); + this._treeStore.set(itBaz, [COL_KEY, COL_DESC], ['baz', 'Baz']); + + this._comboStore = new Gtk.ListStore(); + this._comboStore.set_column_types([ + GObject.TYPE_STRING, + GObject.TYPE_STRING, + ]); + this._comboStore.set(this._comboStore.append(), [0, 1], ['es', 'Spain']); + this._comboStore.set(this._comboStore.append(), [0, 1], ['pt', 'Portugal']); + this._comboStore.set(this._comboStore.append(), [0, 1], ['de', 'Germany']); + this._comboStore.set(this._comboStore.append(), [0, 1], ['pl', 'Poland']); + this._comboStore.set(this._comboStore.append(), [0, 1], ['sv', 'Sweden']); + + this._treeView = new Gtk.TreeView({ + expand: true, + model: this._treeStore, + }); + + const colKey = new Gtk.TreeViewColumn({title: 'Key'}); + const colValue = new Gtk.TreeViewColumn({title: 'Value'}); + const colDefault = new Gtk.TreeViewColumn({title: 'Default'}); + const colDescription = new Gtk.TreeViewColumn({title: 'Description'}); + const colState = new Gtk.TreeViewColumn({ + title: 'State', + }); + + const normalRenderer = new Gtk.CellRendererText(); + const comboRenderer = new Gtk.CellRendererCombo({ + model: this._comboStore, + }); + const toggleRenderer = new Gtk.CellRendererToggle({ + activatable: true, + }); + toggleRenderer.connect('toggled', (rend, path) => { + const [ok, it] = this._treeStore.get_iter_from_string(path); + print(`toggled: ${rend.active}, ${ok}`); + if (ok) + this._treeStore.set(it, [COL_STATE], [!rend.active]); + + // const currentState = this._treeStore. + }); + + colKey.pack_start(normalRenderer, true); + colValue.pack_start(comboRenderer, true); + colDefault.pack_start(comboRenderer, true); + colDescription.pack_start(normalRenderer, true); + colState.pack_start(toggleRenderer, true); + + colKey.add_attribute(normalRenderer, 'text', COL_KEY); + colDescription.add_attribute(normalRenderer, 'text', COL_DESC); + colState.add_attribute(toggleRenderer, 'active', COL_STATE); + + this._treeView.insert_column(colKey, COL_KEY); + this._treeView.insert_column(colValue, COL_VALUE); + this._treeView.insert_column(colDefault, COL_DEFAULT); + this._treeView.insert_column(colDescription, COL_DESC); + this._treeView.insert_column(colState, COL_STATE); + + this._window.add(this._treeView); + this._window.show_all(); + } +} + +try { + // const Prefs = imports.prefs; + try { + print('test widget window'); + + const app = new TestWindow(); + app.application.run(ARGV); + } catch (e) { + logError(e); + } +} catch (e) { + print(e.message); +} + +// vim: set sw=4 ts=4 :