Cabbage Logo
Back to Cabbage Site

New proposal for modular GUI, part 2

I’ve been thinking about an effective way to re-use GUI abstractions, and based on our previous discussion I thought i would float an updated idea by you all. This is just a rough draft but my thinking is that we could use xml files to declare plants, and corresponding Csound code. Each custom plant would need its own unique namespace to avoid conflicts. The xml file might look like this:

<plant xmlns="killerWidgets">
<name>radioValueButtonGroup</name>
<cabbagecode>
	image bounds(0, 0, 160, 50) colour("red"){
	button bounds(10, 40, 30, 30) channel("killerWidgets_button1") radiogroup(99), text(""), colour:1("red")
	button bounds(40, 40, 30, 30) channel("killerWidgets_button2") radiogroup(99), text(""), colour:1("red")
	button bounds(70, 40, 30, 30) channel("killerWidgets_button3") radiogroup(99), text(""), colour:1("red")
	button bounds(100, 40, 30, 30) channel("killerWidgets_button4") radiogroup(99), text(""), colour:1("red"), value(1)
	}
</cabbagecode>
<csoundcode>
	opcode radioValueButtonGroup,k
	    kValue init 0
	    kIndex = 1
	    kTrig changed, chnget:k("killerWidgets_button1"), chnget:k("killerWidgets_button2"), chnget:k("killerWidgets_button3"), chnget:k("killerWidgets_button4")
	    if kTrig == 1 then
	        while kIndex<5 do
	        SChannel sprintfk "killerWidgets_button%d", kIndex
	        kValue = (chnget:k(SChannel)==1 ? kIndex : kValue)
	        kIndex+=1
	        od
	    endif
		xout kValue
	endop
</csoundcode>
</plant>

So the .csd file might then look like this:

<Cabbage>
import "plants.xml"
form caption("Presets") size(670, 580), colour(58, 110, 182), pluginID("def1")
killerWidgets.radioValueButtonGroup bounds(10, 10, 200, 100)
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d -+rtmidi=NULL -M0 -m0d --midi-key-cps=4 --midi-velocity-amp=5 ;--midi-key-cps=4
</CsOptions>
<CsInstruments>
; Initialize the global variables. 
sr = 44100
;kr = 4410
ksmps =32
nchnls = 2
0dbfs = 1

instr 1
kValue killerWidgets_radioValueButtonGroup
printk2 kValue
endin

The UDOs when called in Csound will have the plant namespace prefixed to them so there are no conflicts with other UDOs, or plants. I’d have to look into some way of making the UDO code visible to Csound, but that shouldn’t be too tricky. Ideas? Suggestions?

Note that I’d like to make this system as simple as possible to implement and maintain. And also simple for the end user to use. I don’t want to have to do lots of parsing and text replacement each time a plant is imported.

This is a great idea. I totally support it.
I think for this there should be some strong guidelines for programming reusable snippets.
For instance, I feel more comfortable with separating cabbage GUI management in one specific instrument from what is related to csound audio stuff.
Do you think this is good idea and then could this be possible?

Also, there will have a need for explaining difference between groupbox, image and plants. Should they all be same level or should be plant a container for other widget including groupbox and image?

You might, but others may not? I think this can be left to the user? Each user has their own style. The success of the system will largely depend on how people code their abstractions.

I’m not sure I follow. The proposed GUI system will only work with plants. You can use a groupbox or an image as the container but it must be a plant.

This is a very promising direction, but…

It’s like you already knew where a lot of my questions are going to be headed :innocent:

I know this is separate from, but are basic “cabbageincludes” still on the table? Something to allow for a bunch of common definitions between files etc.

How does this work as far as parsing with relation to other definitions? For example, if I define a BUTTON_COLOR prior to including the plant.xml, will that macro be expandable in the included file? Assuming yes, if the macros are redfined between a first and second instance creation, would the macros expand to the new definitions, or are they expanded at time of inclusion? (I assume they’re expanded at inclusion, but just asking). A way to have multiple instances with different properties would be ideal, but understandably would add some complexity…

How does this handle/work with radiogroups? Multiple instances would need to have multiple groups… and if that work is done behind the user’s back, how do they know they’re not reusing the same group outside of the inclusion?

Just my first thoughts thinking out loud, will think on the idea some more. :+1:

If you mean the Cabbage1 system of having Cabbage bundle everything defined in the <CabbageIncludes> tag then yes. I’ll add support for that shortly.

This is something that will need to be considered. It certainly would be nice to be able to pass definitions to the xml. It’s important too that we can have multiple unique instances of a plant. My basic proposal doesn’t allow for this. We could potentially assign each instance a unique ID, and append/prepend it to each channel name behind the scenes.

Radiogrouped buttons in a plant are only valid to that plant. As with any custom plants, we’d need to make sure they’re unique. I’ll have a think about this and trash out some ideas. If I can avoid macros I will, but they may be a necessary evil.

Sorry if that wasn’t clear. I meant the ability to include a file or multiple files that contain macro defines for the cabbage section, such as grouping all of the color definitions in to one file and including it. This was discussed before in reference to be a way to allow quick “skinning”.

Sure, this is basically what I’ve been doing manually with some of my new work. However it’s important to mention that it’s not safe to assume all channel names get this treatment. Some work within the UDO may be calling to “global” style widgets that aren’t instance specific.

Looking back at your original example… should X/Y size be inferred by the object definition? Things contained within it won’t be scaled, so it’s size is predetermined… right? So seems like when calling a new instance the bounds should only need X/Y position.

Yes, that shouldn’t be tricky at all.

It would need to be in the guidelines that UDO should not refer to any kind of global data.

This is true, but I already have a method that resizes components, it wouldn’t be tricky to call it here so users can also resize their custom GUIs.

I gave a little more thought to this. Still lots to consider. But my latest attempt provides UDOs to update the colour of the buttons using unique identifier channels. It means you can easily do it from the instrument level. Each UDO takes a plant name that is used as a prefix to the channels listed in the xml file. In this way users can ensure that the channels are unique. And they can also get their values using chnget. The plant name is set in the Cabbage code at the top, so it’s clear how the plant and one’s Csound code are connected. When Cabbage creates the GUI, it will update the channels so they match the ones specified by the user when they instantiated the plant. This way we can avoid macros and any heavy text expansion when Cabbage reads the xml file. All it needs to do is update the channels.

<plant xmlns="killerWidgets">
<name>radioValueButtonGroup</name>
<cabbagecode>
	image bounds(0, 0, 160, 50) colour("red"){
	button bounds(10, 40, 30, 30) channel("button1") radiogroup(99), text(""), colour:1("red"), identchannel("button1Ident")
	button bounds(40, 40, 30, 30) channel("button2") radiogroup(99), text(""), colour:1("red"), identchannel("button2Ident")
	button bounds(70, 40, 30, 30) channel("button3") radiogroup(99), text(""), colour:1("red")identchannel("button3Ident")
	button bounds(100, 40, 30, 30) channel("button4") radiogroup(99), text(""), colour:1("red"), value(1), identchannel("button4Ident")
	}
</cabbagecode>
<csoundcode>
 	opcode killerWidgets_setRadioValueButtonButtonColour,0,Skkkk
	SName, kR, kG, kB, kA xin
	iCnt = 1
 	while iCnt<5 do
 		SMessage sprintf "colour(%d, %d, %d, %d)", kR, kG, kB, kA
 		SChannel sprintf "%s_button%d", SName, iCnt
 		chnset SMessage, SChannel
 		iCnt+=1
 	od
 	Schannel sprintf "colour("
 	endop

	opcode killerWidgets_radioValueButtonGroup,k, S
	    Schannel xin 
	    kValue init 0
	    kIndex = 1
	    kTrig changed, chnget:k("button1"), chnget:k("button2"), chnget:k("button3"), chnget:k("button4")
	    if kTrig == 1 then
	        while kIndex<5 do
	        SChannel sprintfk "%sbutton%d", SChannel, kIndex
	        kValue = (chnget:k(SChannel)==1 ? kIndex : kValue)
	        kIndex+=1
	        od
	    endif
		xout kValue
	endop

</csoundcode>
</plant>

<Cabbage>
import "plants.xml"
form caption("Presets") size(670, 580), colour(58, 110, 182), pluginID("def1")
killerWidgets.radioValueButtonGroup bounds(10, 10, 200, 100), plant("radioGroup")
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d -+rtmidi=NULL -M0 -m0d --midi-key-cps=4 --midi-velocity-amp=5 ;--midi-key-cps=4
</CsOptions>
<CsInstruments>
; Initialize the global variables. 
sr = 44100
;kr = 4410
ksmps =32
nchnls = 2
0dbfs = 1

instr 1
killerWidgets_setRadioValueButtonButtonColour "radioGroup", 255, 0, 0, 128
kValue killerWidgets_radioValueButtonGroup "radioGroup"
printk2 kValue
endin

Sweet, that’s been pretty high on my wish list for a while!

I need to think on it some more, but I’m not sure if I like the idea of completely disallowing global data access… everything should be compartmentalized as much as possible, but what if someone is working with things that logically would be handled globally in an instrument, for example tempo comes to mind?

Two what-if ideas to toss around… what if either:

  • Channels that are instance specific are written to have a parsed prefix, like “%INSTANCE%”, signaling that it it should get parsed, where other channels operate normally?
  • Or instead global variables have a prefix, like %GLOBAL% allowing you to NOT parse a variable if specifically desired.

Btw, is your example an implementation of a button group that reports back it’s individual mode similar to how I had requested? That’s TOO funny and awesome :smile:

Nothing is disallowed. People can abuse the system is any way they like. But if you’re preparing custom GUIs for others to easily use, you should probably avoid global space. If you’re just working on your own personal system you can use any mechanisms you like.

That’s it. As god an example as any to get the ball rolling!

Ok. I guess how I had interpreted that was that every channel would be parsed with the instance prefix for ease of parsing… so global channels would essentially be an impossibility. A good reason this might be an issue is with reserved channels, like I mentioned tempo. Even if every instance reads in it’s own copy of the tempo from the DAW rather than referring to a global variable, they would still need to read “HOST_BPM” without the channel name being parsed out. That was really the core of my concern.

I have a basic working draft of this system in place, and it seems to work fairly well. Here’s a sample xml file:

<?xml version="1.0" encoding="UTF-8"?>

<plant>
<namespace>kw</namespace>
<name>radioValueButtonGroup</name>
<cabbagecode>
	image bounds(10, 10, 140, 50) colour(22, 22, 22), corners(10){
	button bounds(10, 10, 30, 30) channel("button1") radiogroup(99), text(""), colour:1("red"), identchannel("button1Ident")
	button bounds(40, 10, 30, 30) channel("button2") radiogroup(99), text(""), colour:1("red"), identchannel("button2Ident")
	button bounds(70, 10, 30, 30) channel("button3") radiogroup(99), text(""), colour:1("red")identchannel("button3Ident")
	button bounds(100, 10, 30, 30) channel("button4") radiogroup(99), text(""), colour:1("red"), value(1), identchannel("button4Ident")
	}
</cabbagecode>
<csoundcode>
 	opcode kw_setRadioValueButtonButtonColour,0,Siiii
	SName, iR, iG, iB, iA xin
	iCnt = 1
 	while iCnt &lt; 5 do
 		SMessage sprintf "colour(%d, %d, %d, %d)", iR, iG, iB, iA
 		SChannel sprintf "%s_button%dIdent", SName, iCnt
 		chnset SMessage, SChannel
 		iCnt+=1
 	od
 	endop

	opcode rw_radioValueButtonGroup,k, S
	    SChannel xin 
	    kValue init 0
	    kIndex = 1
	    kTrig changed, chnget:k(sprintfk("%sbutton1", SChannel)), chnget:k(sprintfk("%sbutton12", SChannel)), chnget:k(sprintfk("%sbutton3", SChannel)), chnget:k(sprintfk("%sbutton4", SChannel))
	    if kTrig == 1 then
	        while kIndex &lt; 5 do
	        SCustomChannel sprintfk "%sbutton%d", SChannel, kIndex
	        kValue = (chnget:k(SCustomChannel)==1 ? kIndex : kValue)
	        kIndex+=1
	        od
	    endif
		xout kValue
	endop

</csoundcode>
</plant>

And I’m using it in Cabbage with this csd file:

<Cabbage>
form caption("RadioThings") size(840, 280), colour(58, 110, 210), import("plant.xml")
radioValueButtonGroup bounds(10, 10, 100, 50), channel("radioGroup"), namespace("kw")
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d -+rtmidi=NULL -M0 -m0d --midi-key-cps=4 --midi-velocity-amp=5
</CsOptions>
<CsInstruments>
; Initialize the global variables. 
sr = 44100
ksmps = 32
nchnls = 2
0dbfs = 1

instr 1
kValue kw_radioValueButtonGroup "radioGroup"
printk2 kValue
endin

</CsInstruments>
<CsScore>
f1 0 1024 10 1
;causes Csound to run for about 7000 years...
i1 0 z
</CsScore>
</CsoundSynthesizer>

I’m just wondering about the namespace property. Since Csound doesn’t yet have namespaces i wonder if we need to bother. In this case it forces Cabbage to load the radioValueButtonGroup plant that is contained in that namespace. If users just tagged their plants and opcodes with a unique post or prefix it might be just as handy.

So when Cabbage runs first, it expands all the import code. It modifies the plant channels by prepending a channel string, in this case “radioGroup”. This means all channels will be unique. If I needed another radioValueGroup I would just use another channel.

because everything gets expanded, we can exported entire projects, includes files and all, into a single text file when distributing larger projects. That seems like a nice side effect of this approach. You may have noticed the import() identifier. This can also be used to import macros. Multiple files can be be passed to it.

I have this working now with the GUI editor so now you can quickly resize any imported plant whilst in edit mode. It can be done manually, but it’s nice to be able to move them around with a mouse too. It’s pretty neat. I think this feature could really encourage people to start writing and sharing really great abstractions. It’s great that they can be dropped in any .csd file. Check out the gif…

@t_grey Before you ask, it’s not yet possible to pass macros to the .xml file, at least I don’t think it is. But it is possible to call UDOs that will update the look and feel of a plant. So if we encourage people to add these kinds of utility UDOs I think it leads to better abstractions.

Right, I did some more work with this. I’ve added a new <cabbagecodescript> tag. This section holds a JS script that will be executed when the import file is first imported. When executed it will create your Cabbage code. Note this is optional. One can leave this empty and use the <cabbagecode> section directly. Here is a simple JS script that will create a grid sequencer.

<cabbagecodescript>
var channelNumber = 128;
Cabbage.print("image bounds(10, 10, 360, 178) colour(0, 0, 0, 0){");
Cabbage.print("image bounds(0, 0, 23, 176), colour(100, 100, 100), identchannel(\"scrubberIdent\")");

for(var y = 0 ; y &lt; 8 ; y++)
{
  for (var x = 0 ; x &lt; 16 ; x++)
  {
  Cabbage.print("checkbox bounds("+x*22+","+y*22+", 20, 20), channel(\"gridChannel"+channelNumber+"\"), identchannel(\"gridIdent"+channelNumber+"\")");
  channelNumber--;
  }
}
Cabbage.print("}");
</cabbagecodescript>

Here’s a simple gridSequencer plant with UDOs to get the value of each checkbox, clear the grid, and randomise the grid.

How about the actual radiogroup() cabbage declaration (in this case 99)? Does this get accounted for yet, and if so how?

That’s cool, but as we start to use modular code, won’t that be counterproductive? Honestly I’d be more interested in a way to export multiple files at once in a way that keeps shared modules/files as actually shared. I understand how this could be more difficult tho.

Wow, this is all looking really cool! Obviously I’m still rooting for those macro expansions… :innocent: I definitely plan on sharing what I’ve been working on once it’s done. And I think you’ve convinced me to give up on maintaining C1 compatibility at this point :wink:

Is that really the full example? I feel like something is missing… I get the idea of the cabbagecode section tho. That’s neat!

Yes. As the check boxes belong to the plant, their radio ids are local to that plant.

It’s only a side effect I thought was worth mentioning. I’d need to add a way of actually exporting the full text files if anyone was interested. [quote=“t_grey, post:14, topic:785”]
Wow, this is all looking really cool! Obviously I’m still rooting for those macro expansions…
[/quote]

I think even now there are workarounds for this, i.e., making sure you give your plants enough utility UDOs to control how they look and behave! [quote=“t_grey, post:14, topic:785”]
Is that really the full example? I feel like something is missing… I get the idea of the cabbagecode section tho. That’s neat!
[/quote]

Yes, that’s really the full thing. I simply create the plant by running a simple JS for loop which outputs
lines of Cabbage code using Cabbage.print(). The code it generates is then used as the Cabbage code for that plant. I’ve been meaning to add a way of progamatically creating GUIs for some time. Javacript is nice because JUCE comes with its own simple JS engine.

I don’t see where there’s any handling of the stop/random/clear buttons or the tempo… are there UDOs that went with it? That’s what has me confused… I think I understand the drawing part, just expect to see more.

I have to politely but enthusiastically disagree, but I’ll admit it might be because you’re already aware of a good easy solution that I’m not. Having the ability to change them is great through internal methods, but this (at least as far as I know) doesn’t provide any way to have an unknown number of plant instances themed with common color settings unique to the instrument/instance. Plus, the larger and more complex an instrument got, the more work would be required in the initialization/skinning of the plugin, right?

For example, if I write a bunch of exported plants, it should be fairly simple to skin them all in a blue theme in one instrument, and grey in another. With macros you could just include a different set of color defines prior to including the plants files to achieve this, but without them I’m concerned that you would have to iterate through each plant unique to the instrument, sending them identchannel messages just to get an initial color set. Having the methods attached helps ease that pain, sure… but honestly, it feels like a bit of a mess to me.

Another, perhaps better example; I’m hoping to be able to change a single shared include file with some color definitions, and have it change the look of an entire group of instruments at a time without any other work within the instruments. I think the same problem/principle applies.

But again, it’s very possible I’m just missing the obvious answer to the problem.

I see your point. It’s a valid one. I don’t like macro use here because they are global, but I agree that some quick and efficient way of passing theme data to imported plants is probably a must. Let me think about it.

Oh btw, yes, there were UDOs controlling the widgets. I forgot I only posted the script part.

I see your point here. But as soon as you hard code a macro into a plant, then you need the person to define that macros in your main .csd file. If we just consider the CabbageCode section of an import xml file. You’d like to be able to do this:

<cabbagecode>
	image bounds(0, 0, 160, 50) colour("red") $image_attribs {
	button bounds(10, 40, 30, 30) channel("killerWidgets_button1") radiogroup(99), text(""), $button_attribs
	button bounds(40, 40, 30, 30) channel("killerWidgets_button2") radiogroup(99), text(""), $button_attribs
	button bounds(70, 40, 30, 30) channel("killerWidgets_button3") radiogroup(99), text(""), $button_attribs
	button bounds(100, 40, 30, 30) channel("killerWidgets_button4") radiogroup(99), text(""), $button_attribs, value(1)
	}
</cabbagecode>

In this case you use two different macros which need to be defined in your Cabbage section in the parent .csd. I guess this is fine, but I’m afraid it will get messy. Let me take a look. It would mean that others hoping to use your abstractions will have to do some work first to get the macros set up.

This sounds like a stylesheet. I have considered implemented custom global style sheets in the past. Might be worth looking at again.

A missing macro define in the cabbage section doesn’t currently fail compilation, it expands to nothing… here it should be no different. If the macro is defined as “colour(red)” rather than “red”, then the missing macro wouldn’t have a significant negative affect the widget, but would just leave all widgets in their default color states. I don’t see this as a problem at all, it’s carrying the expected behavior into the included abstraction! And IMHO this still allows very simple and easy flexibility for the author.

Perhaps… but again, other than being the default colors there’s nothing really REQUIRING it if it’s designed somewhat logically. But even so, I was already considering/expecting that many of my abstractions might have a slightly more specialized version for my own instruments with the theme macros etc, versus a more public “general purpose” widget abstraction with the extra stuff stripped out.