Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JACK configuration interface #9

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions gsjackctl/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const Local = imports.ui.main.extensionManager.lookup('[email protected]');

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;
Expand Down Expand Up @@ -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();
Expand All @@ -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);

Expand Down Expand Up @@ -87,6 +91,10 @@ var Extension = class Extension {
}
});

this._prefs.connect('activate', () => {
ExtensionUtils.openPrefs();
});

this._control.connect('start-jack', () => {
try {
this._jackctl.StartServerSync();
Expand Down
206 changes: 206 additions & 0 deletions gsjackctl/prefsWidget.js
Original file line number Diff line number Diff line change
@@ -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('[email protected]');
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 :
53 changes: 53 additions & 0 deletions jack/jackdbus.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}
};

Expand Down
20 changes: 20 additions & 0 deletions prefs.js
Original file line number Diff line number Diff line change
@@ -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 :
57 changes: 57 additions & 0 deletions test/cfg_widget.js
Original file line number Diff line number Diff line change
@@ -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 :
Loading