Good practice with repetitious GUI design

Hello all,

I am currently migrating a lot of my thinking from Pure Data / MAXMSP. In my SC GUI design I am thinking about what is the equivalent of abstractions and sub patches would be, where an edit to one can represent an edit to all, rather than monotonously making the same edit to multiple objects.

Eli begins to touch on an approach to this using arrays at right the very end of this tutorial.

I struggling to find good documentation/video/tutorial on best practice in this way of design.

Any tips or pointers in the right direction would be greatly appreciated. Thank you :slight_smile:

This is a very interesting question!

“an edit to one can represent an edit to all” – I think there isn’t exactly such a thing in SC.

In Pd/Max (I’ll just say Pd from here on out b/c 1/ FLOSS and 2/ tbh I prefer it over Max), when you save changes to an abstraction that’s being used in an open patch, the existing instances get re-created while preserving all the inlet/outlet connections. The connections are independent of the specific instance.

In SC, “connections” are object references, and these are bound to specific instances. If you delete/re-create a GUI object, any other objects that hold references to the old GUI object will not automatically pick up the new object. Because there isn’t a concept of a patch cable representing the connection, there may be any number of objects, anywhere, holding an outdated reference! There’s no automatic way to find them.

So IMO the design strategy has to be different.

My opinion is that model-view-controller design gets you the closest to what you want to achieve. This separates the GUI away from program logic in a way that allows the GUI to be replaced, wholesale, without disrupting program logic.

“Model” maintains a value and broadcasts changes. We used to have a class called Model but now it’s either gone, or it changed to another name and I don’t remember the new name. It’s possible to hack a Model using event-style prototypes. If you find that you like this way of working, you could create your own class (which would allow a neater programming interface – prototypes have a restriction that they can’t use any method names that already exist for Event – hence getValue instead of value, and destroy instead of free – a real class wouldn’t be subject to that restriction).

(
~modelPrototype = (
	value: 0,
	// because '.value' gets eaten by the 'value' method
	getValue: { |self| self[\value] },
	value_: { |self, newValue|
		self[\value] = newValue;
		self.changed(\value, newValue);
	},
	destroy: { |self| self.changed(\didFree) }
);
)

// an array of 10 values
a = Array.fill(10, { ~modelPrototype.copy.put(\value, 1.0.rand) });

a[0].getValue
-> 0.92148387432098

The View is the GUI object. It’s OK for the view’s action function to talk directly to its specific model.

The Controller receives updates from the model and forwards them to the View.

(
var sliders, numbers, controllers;

w = Window("MVC", Rect(800, 200, 500, 400));

// I'm ignoring spec mapping, let's assume 0.0-1.0 for now
sliders = a.collect { |model, i|
	Slider()
	.orientation_(\vertical)
	.value_(model.getValue)
	.action_({ |view|
		model.value = view.value;
		numbers[i].value = view.value;
	})
	.onClose_({ controllers[i].remove });
};

numbers = a.collect { |model, i|
	NumberBox()
	.value_(model.getValue)
	.action_({ |view|
		model.value = view.value;
		sliders[i].value = view.value;
	})
	.onClose_({ controllers[i].remove });
};

controllers = a.collect { |model, i|
	SimpleController(model)
	.put(\value, { |object, whatChanged, value|
		defer {
			sliders[i].value = value;
			numbers[i].value = value;
		}
	})
	.put(\didFree, {
		defer {
			sliders[i].remove;
			numbers[i].remove;
			controllers[i].remove;
		}
	})
};

w.layout = HLayout(
	*Array.fill(a.size, { |i|
		VLayout(
			sliders[i],
			numbers[i]
		)
	})
);

w.front;
)


// change one value programmatically, GUI updates
a.choose.value = 1.0.rand;

You might think this is a long way to go around for a simple GUI. But the advantage is that you can close and re-create the GUI as many times as you need. onClose makes sure that the SimpleController connection goes away when the view disappears, so there’s no permanent linkage between the working data and the GUI.

// the "glue"
a[0].dependants  // "a SimpleController"

// close the GUI
w.close;

a[0].dependants  // empty, good!

// now you can rerun the block to create the window

So if you change the GUI-building code, then you’re very very close to “edit one, edit all.”

(For some extra fun, run the GUI block twice, and move one window so that you can see both of them, and play with the sliders.)

hjh

8 Likes

Hi Jamshark,

I can’t thank you enough for this detailed response and your code. It’s a very elegant solution, although I am still working to understand every dimension of it.

It’s funny, I assumed there would be a very straightforward way to do this, but I guess that is the ¨lock in¨ effect of thinking in terms of one (PD and Max being so similar) language.

Thanks again - I’m going to read and re-read this code many times, as it’s a great foundation for sophisticated GUI design :slight_smile:

Where Pd and Max fail is that they don’t have a good mechanism for querying stateful objects from multiple locations.

p = Point(1, 2);

// here's one place in the code
p.x

// here's another
p.x

Those are two separate calls, returning the value to two different places.

The best I’ve been able to think of in Pd is like this:

pd-multi-return

… where you have to tell it where to “return” the value to, explicitly, at the time of querying (which strikes me as a bit fragile).

Likewise, I had assumed there would be a very straightforward way to do this in Pd but nope…

hjh

Hows this for transcribing the prototype above to a Class:

Model {
  var <value=0;

  *new {|i| ^super.new.init(i) }

  init {|i| value = i }

  value_{ |newValue|
    value = newValue;
    this.changed(\value, newValue)
  }

  free {
    this.changed(\didFree)
  }
}

The Connection quark has a maxed out version, so to speak, notably with a spec parameter:

(This quark has all kinds of nice tools for this sort of thing…)

2 Likes