HelpBrowser: path box becomes a more conventional search box
[supercollider.git] / SCClassLibrary / JITLib / GUI / NodeProxyEditor.sc
blob2e9dcffb04dfbffc7d9881a29f2ddc609a274450
1 // document for non-current proxyspaces is not correct yet.
3 NodeProxyEditor {
5         classvar <>menuHeight=18;
7         var     <proxy;
8         var <skin, <font;
10         var <parent, <zone, <nameView, <typeChanView, <monitor;
11         var <nSliders, <edits, <sinks, <scrolly, skipjack;
12         var buttonFuncs, pauseBut, sendBut;
14         var     <currentSettings=#[],   <prevSettings=#[], <editKeys=#[], <>ignoreKeys=#[];
15         var <>replaceKeys;              // a dict for slider names to be replaced
16         var     <tooManyKeys = false, <keysRotation = 0;
17         var oldType = "_";
18         var <ownWindow;
20         *initClass {
21                 StartUp.add{
22                         Spec.add(\ampx4, [0, 4, \amp]);
23                         Spec.add(\fadePx, [0, 100, \amp, 0, 0.02]);
24                 };
25         }
27         *new { arg proxy, nSliders=16, parent,
28                 extras=[\CLR, \reset, \scope, \doc, \end, \fade],
29                 monitor=true, sinks=true, morph=false;
31                 ^super.new
32                         .init(parent, nSliders, extras, monitor, sinks, morph)
33                         .proxy_(proxy);
34         }
36         proxy_ { arg px;
37                 var name, type;
38                 if (px.isNil) {
39                         proxy = nil;
40                         name = type = '-';
41                 } {
42                         if (px.isKindOf(NodeProxy)) {
43                                 proxy = px;
44                                 name = px.key ? 'anon proxy';
45                                 type = proxy.typeStr;
46                                 if (monitor.notNil) { monitor.proxy_(proxy) };
47                         }
48                 };
50                 nameView.object_(proxy);
51                 this.name_(name);
52                 typeChanView.string_(type);
53                 this.fullUpdate;
54         }
56         name { ^nameView.string.asSymbol }
57         name_ { |key|
58                 nameView.string_(key.asString);
59                         // if I have my own window, put proxy name there too
60                 if (ownWindow) { parent.name = "edit" + key };
61         }
63                 // backwards compatibility
64         pxKey { ^this.name }
65         pxKey_ { |key| ^this.name_(key) }
67         clear {
68                 proxy = nil;
69                 { nameView.object_(nil).string_("-"); }.defer;
70                 this.fullUpdate;
71         }
73         init { arg inParent, numSliders, extras, monitor, sinks, morph;
75                 var bounds;
76                 ownWindow = inParent.isNil;
77                 skin = GUI.skins[\jitSmall];
78                 font = Font.new(*skin.fontSpecs);
79                 nSliders = numSliders;
81                 bounds = Rect(0, 0, 340, nSliders + 2 * skin.buttonHeight + 16);
82                                 // if building inside an existing window, w is nil.
83                                 // w only is notNil if it belongs to the editor.
84                 parent = inParent ?? {
85                         // "NodeProxyEditor: making internal win.".postln;
86                         parent = Window(this.class.name, bounds.resizeBy(4, 4));
87                         parent.view.background_(skin.background);
88                         parent.front;
89                 };
91                 zone = CompositeView(parent, bounds);
93                 zone.decorator = zone.decorator ??  { FlowLayout(zone.bounds).gap_(0@0) };
94                 zone.background = skin.foreground;
96                 replaceKeys = replaceKeys ?? { () };
98                 this.makeButtonFuncs;
100                 this.makeTopLine(extras);
101                 if (monitor, { this.makePxMon; zone.decorator.nextLine.shift(0, 4); });
102                 if (morph, { this.makeMorph; zone.decorator.nextLine.shift(0, 4); });
104                 this.makeSinksSliders(sinks);
106                         // if my own window, sliders should resize
107                         // - not working yet!
108                 if (ownWindow) {
109                         zone.resize_(2);
110                         edits.do { |ez| ez.view.resize_(2) }
111                 };
113                 this.makeSkipJack;
114         }
116         makePxMon {
117                 monitor = ProxyMonitorGui(proxy, zone,
118                         zone.bounds.width - 4 @ (skin.buttonHeight),
119                         showName: false, makeWatcher: false);
120         }
122         makeMorph {
123                 inform("NodeProxyEditor: preset/morph not finished yet.");
124 //              zone.decorator.shift(0, 2);
125 //              StaticText(zone, Rect(0,0, 330, 1)).background_(Color.gray(0.2));
126 //              zone.decorator.shift(0, 2);
127 //              PxPreset(proxy, zone);
128         }
130         makeTopLine { |extras|
132                 nameView = DragBoth(zone, 90@menuHeight)
133                         .font_(font).align_(\center)
134                         .background_(Color.white);
135                 nameView.setBoth_(false)
136                         .mouseDownAction_ { |drag, x, y, mod|
137                                 if (mod.isAlt) {
138                                         try { ProxySpace.findSpace(proxy).document(proxy.key) };
139                                 } {
140                                         proxy.document;
141                                 }
142                         }
143                         .receiveDragHandler = { this.proxy_(View.implClass.currentDrag) };
145                 typeChanView = StaticText(zone, 30@menuHeight).string_("-").align_(0).font_(font);
147                 extras.do { |butkey|
148                         buttonFuncs[butkey].value;
149                 };
151                 zone.decorator.nextLine.shift(0, 4);
152         }
154         makeSinksSliders { |bigSinks|
155                 var sinkWidth, lay;
157                 sinkWidth = if (bigSinks, 40, 0);       // invisibly small
158                 #edits, sinks = Array.fill(nSliders, { arg i;
159                         var ez, sink;
160                         zone.decorator.nextLine;
162                         sink = DragBoth(zone, Rect(0,0, sinkWidth, skin.buttonHeight))
163                                 .string_("-").align_(\center).visible_(false)
164                                 .font_(font);
166                         sink.action_({ arg drag;
167                                 var key = editKeys[i];
168                                 var dragged = drag.object;
169                                 if (dragged.isKindOf(String)) { dragged = dragged.interpret };
170                                 if (dragged.notNil) {
171                                         if(dragged.isKindOf(NodeProxy)) {
172                                                 drag.string = "->" + dragged.key;
173                                                 proxy.map(key, dragged);
174                                                 this.checkUpdate;
175                                         } {
176                                                 if (dragged.isKindOf(SimpleNumber)) {
177                                                         proxy.set(key, dragged);
178                                                 };
179                                         }
180                                 }
181                         });
183                         ez = EZSlider(zone, (330 - sinkWidth)@(skin.buttonHeight), "", \unipolar.asSpec,                                labelWidth: 60, numberWidth: 42, unitWidth: 20);
184                         ez.visible_(false);
185                         ez.labelView.font_(font).align_(\center);
187                         ez.sliderView.keyDownAction = { |view, char,modifiers,unicode,keycode|
188                                 if (unicode == 127) { // delete key
189                                         try { proxy.unset(ez.labelView.string.asSymbol) }
190                                 } {
191                                         view.defaultKeyDownAction(char,modifiers,unicode,keycode);
192                                 };
193                         };
194                         [ez, sink]
195                 }).flop;
197                 lay = zone.decorator;
198                 lay.left_(lay.bounds.right - 20).top_(lay.bounds.top + 48);
200                 scrolly = EZScroller(zone,
201                         Rect(0, 0, 14, nSliders * skin.buttonHeight),
202                         nSliders, nSliders,
203                         { |sc| keysRotation = sc.value.asInteger; }
204                 ).visible_(false);
205                 scrolly.slider.resize_(3);
206                 [\scrolly, scrolly.slider.bounds];
207         }
209         highlightParams { |parOffset, num|
210                 var onCol = Color(1, 0.5, 0.5);
211                 var offCol = Color.clear;
212                 { edits.do { |edi, i|
213                         var col = if (i >= parOffset and: (i < (parOffset + num).max(0)), onCol, offCol);
214                         edi.labelView.background_(col.green_([0.5, 0.7].wrapAt(i - parOffset div: 2)));
215                 } }.defer;
216         }
218         makeButtonFuncs {
219                 buttonFuncs = (
220                         CLR: { Button(zone, 30@20).font_(font)
221                                         .states_([[\CLR, skin.fontColor, Color.clear]])
222                                         .action_({ arg btn, mod;
223                                                 if (mod.isAlt) { proxy.clear } {
224                                                         "use alt-click to clear proxy.".postln;
225                                                 }
226                                         })
227                         },
229                         reset: { Button(zone, 30@20).font_(font)
230                                         .states_([[\reset, skin.fontColor, Color.clear]])
231                                         .action_({ proxy !? { proxy.nodeMap = ProxyNodeMap.new; this.fullUpdate; } })
232                         },
234                         pausR: { pauseBut = Button(zone, 30@20).font_(font)
235                                         .states_([
236                                                         ["paus", skin.fontColor, skin.onColor],
237                                                         ["rsum", skin.fontColor, skin.offColor]
238                                                 ])
239                                         .action_({ arg btn; proxy !? {
240                                                                 [ { proxy.resume; }, { proxy.pause; }  ].at(btn.value).value;
241                                                         } });
242                                 },
244                         sendR:  { sendBut = Button(zone, 30@20).font_(font)
245                                                 .states_([
246                                                         ["send", skin.fontColor, skin.offColor],
247                                                         ["send", skin.fontColor, skin.onColor]
248                                                 ])
249                                                 .action_({ arg btn, mod;
250                                                         if(proxy.notNil and: (btn.value == 0)) {
251                                                                 // alt-click osx, swingosc
252                                                                 if (mod.isAlt) { proxy.rebuild } { proxy.send }
253                                                         };
254                                                         btn.value_(1 - btn.value)
255                                                 })
256                         },
258                         scope:  { Button(zone, 36@20).font_(font)
259                                                 .states_([[\scope, skin.fontColor, Color.clear]])
260                                                 .action_({ proxy !? { proxy.scope } })
261                         },
264                         doc:    { Button(zone, 30@20).font_(font)
265                                                 .states_([[\doc, skin.fontColor, Color.clear]])
266                                                 .action_({ |but, mod|
267                                                         if (mod.isAlt) {
268                                                                 try { ProxySpace.findSpace(proxy).document(proxy.key) };
269                                                         } {
270                                                                 proxy.document;
271                                                         }
272                                                 })
273                         },
275                         end:    { Button(zone, 24@20).font_(font)
276                                         .states_([[\end, skin.fontColor, Color.clear]])
277                                         .action_({ proxy !? {  proxy.end } })
278                         },
280                         fade:   { var nb = EZNumber(zone, 60@20, \fade, \fadePx,
281                                                                 { |num| proxy.fadeTime_(num.value) },
282                                                                 try { proxy.fadeTime } ? 0.02,
283                                                                 labelWidth: 24,
284                                                                 numberWidth: 32);
285                                                 nb.labelView.font_(font).background_(Color.clear);
286                                                 nb.numberView.font_(font).background_(Color.clear);
287                         },
289                                 // extras:
291                         rip:    { Button(zone, 15@20).font_(font)
292                                                 .states_([['^', skin.fontColor, Color.clear]])
293                                                 .action_({ this.class.new(proxy, nSliders) })
294                         },
297                         wake:   { Button(zone, 30@20).font_(font)
298                                                 .states_([[\wake, skin.fontColor, Color.clear]])
299                                                 .action_({  proxy !? { proxy.wakeUp } })
300                         },
302                         send:   { Button(zone, 30@20).font_(font)
303                                                 .states_([[\send, skin.fontColor, Color.clear]])
304                                                 .action_({  proxy !? { proxy.send } })
305                         },
307                         rebuild:        { Button(zone, 30@20).font_(font)
308                                                 .states_([[\rbld, skin.fontColor, Color.clear]])
309                                                 .action_({  proxy !? { proxy.rebuild } })
310                         }
312 //                      poll:   { Button(zone, 30@20).font_(font)
313 //                              .states_([[\poll, skin.fontColor, Color.clear]])
314 //                              .action_({  proxy !? { proxy.poll } }) },
316 //                                              // show a little amp view?
317 //                      amp:    { Button(zone, 30@20).font_(font)
318 //                              .states_([[\amp, skin.fontColor, Color.clear]])
319 //                              .action_({ "// show a little amp view?".postln }) }
321                 );
322         }
324         makeSkipJack {
325                 skipjack = SkipJack(
326                         { this.checkUpdate },
327                         0.2,
328                         { zone.isClosed },
329                         this.class.name
330                 );
331                 // w.onClose_({ skipjack.stop; });
332                 this.runUpdate;
333         }
335         getCurrentKeysValues {
336                 if (proxy.isNil, {^[] });
337                 currentSettings = proxy.getKeysValues(except: ignoreKeys);
338                 editKeys = currentSettings.collect({ |list| list.first.asSymbol });
339         }
340         checkTooMany {
341                 var oversize = (editKeys.size - nSliders).max(0);
342                 tooManyKeys = oversize > 0;
343                 keysRotation = keysRotation.clip(0, oversize);
345                 if (tooManyKeys) {
346                 //      "tooManyKeys...".postln;
348                         scrolly.visible_(true);
349                         scrolly.numItems_(editKeys.size);
350                         editKeys = editKeys.drop(keysRotation).keep(nSliders);
351                         currentSettings = currentSettings.drop(keysRotation).keep(nSliders);
352                 } {
353                 //      "plenty of space.".postln;
354                         scrolly.numItems_(editKeys.size);
355                         scrolly.visible_(false);
356                 };
357                 scrolly.value_( keysRotation);
358         }
360         checkUpdate {
361                 var oldKeys, newType;
362                 oldKeys = editKeys;
364                 this.getCurrentKeysValues;
365                 this.checkTooMany;
367                 // [\oldKeys, oldKeys, \editKeys, editKeys].printAll;
369                 newType = if (proxy.notNil) { proxy.typeStr };
370                 if (newType != oldType) {
371                         oldType = newType;
372                         typeChanView.string_(newType);
373                 };
375                 if (monitor.notNil) { monitor.updateAll };
377                         if (pauseBut.notNil) {
378                                 pauseBut.value_((proxy.notNil and: { proxy.paused }).binaryValue)
379                         };
380                         if (sendBut.notNil) {
381                                 sendBut.value_((proxy.notNil and: { proxy.objects.notEmpty }).binaryValue)
382                         };
384                 if ( (editKeys != oldKeys), { this.updateAllEdits }, {  this.updateVals });
385         }
387         fullUpdate {
388                 this.getCurrentKeysValues;
389                 this.checkTooMany;
390                 this.updateAllEdits;
391         }
393         updateVals {
394                 var sl, val, mapKey;
396         //      "updateVals : ".postln;
397                 if (currentSettings == prevSettings) {
398                  //     "no change.".postln;
399                         ^this;
400                 };
402         //      "values have changed - updating edits.".postln;
404                 editKeys.do { arg key, i;
405                         sl = edits[i];
406                         // editKeys and currentSettings are in sync.
407                         val = currentSettings[i][1].unbubble;
408                         if (val != try { prevSettings[i][1] }) {
409                                         // disable for arrayed controls
410                                 sl.enabled_(val.size <= 1);
411                                 // when in doubt, use this:
412                                 //      val = (currentSettings.detect { |set| set[0] == key } ? [])[1];
413                                 if(sl.numberView.hasFocus.not) {
414                                         if (val.isKindOf(SimpleNumber), {
415                                                 sl.value_(val.value);
416                                                 sl.labelView.string_(this.replaceName(key));
417                                                 sinks[i].string_("-");
418                                         }, {
419                                                 if (val.isKindOf(BusPlug), {
420                                                         mapKey = val.key;
421                                                         sinks[i].object_(val).string_(mapKey);
422                                                         sl.labelView.string = "->" + key;
423                                                 });
424                                         });
425                                 };
426                         };
427                 };
428                 prevSettings = currentSettings;
429         }
430         replaceName { |key|
431                 var replaced;
432                 if (replaceKeys.isNil) { ^key };
433                 replaced = replaceKeys[key.asSymbol];
434                 if (replaced.isNil) { ^key };
435         //      "NPXE: replacing % with %.".format(key.asCompileString, replaced.asCompileString).postln;
436                 ^replaced;
438         }
440         updateAllEdits {
441         //      var keyPressed = false;
442                 if (proxy.isNil) {
443                         [sinks, edits].do(_.do(_.visible_(false)));
444                         ^this;
445                 };
447                 edits.do { arg sl, i;
448                         var key, val, mappx, spec, slider, number, sink, mapKey;
449                         var keyString, labelKey, isWet, isMix;
451                         key = editKeys[i];
452                         sink = sinks[i];
454                         if(key.notNil) {
455                                 // editKeys and currentSettings are in sync.
456                                 val = currentSettings[i][1];
458                                 labelKey = key;
459                                 keyString = key.asString;
460                                 spec = key.asSpec;
462                                 isWet = keyString.beginsWith("wet"); // a filtered slot
463                                 isMix = keyString.beginsWith("mix");    // an additive source
465                                 if (isWet or: isMix) {
466                                         if (isWet) { spec = \amp.asSpec };
467                                         if (isMix) { spec = \amp4.asSpec };
468                                         sl.sliderView.background_(Color.green(1.0, 0.5)).refresh;
469                                         sl.labelView.background_(Color.green(1.0, 0.5)).refresh;
470                                 } {
471                                         sl.sliderView.background_(skin.foreground).refresh;
472                                         sl.labelView.background_(skin.foreground).refresh;
473                                 };
474                                 sl.visible_(true);
476                                 sl.labelView.string = this.replaceName(key);
477                                 sl.sliderView.enabled = spec.notNil;
478                                 sl.sliderView.visible = spec.notNil;
479                                 sl.numberView.enabled = true;
480                                 sl.numberView.visible = true;
481                                 sl.labelView.visible = true;
482                                 sink.visible = true;
483         //                      sl.sliderView.keyDownAction = { keyPressed = true }; // doesn't work yet.
484         //                      sl.sliderView.keyUpAction = {  proxy.xset(key, sl.value); keyPressed = false };
486                                 sl.action_({ arg nu; proxy.set(key, nu.value) });
488                                 if(spec.notNil) { sl.controlSpec = spec } {
489                                         sl.controlSpec = ControlSpec(-1e8, 1e8);
490                                 };
492                                 mappx = val.value ? 0;
493                                 if(mappx.isNumber) {
494                                         sl.value_(mappx ? 0);
495                                         sink.object_(nil).string_("-");
496                                 } {
497                                                 // assume mappx is a proxy:
498                                         if (val.isKindOf(BusPlug), {
499                                                 mapKey = currentEnvironment.findKeyForValue(mappx) ? "???";
500                                                 sink.object_(mapKey).string_(mapKey);
501                                                 sl.labelView.string = "->" + key;
502                                         });
503                                 }
504                         }  {
505                                 // "// hide unused edits".postln;
506                                 sink.object_(nil).string_("-").visible_(false);
507                                 sl.labelView.background_(skin.foreground).string_("");
508                                 sl.sliderView.background_(skin.foreground);
509                                 sl.visible_(false);
510                         };
511                 };
512                 // this.adjustWindowSize;
513         }
515         runUpdate {  skipjack.start }
516         stopUpdate { skipjack.stop }
521 RecordProxyMixer {
522         var <>proxymixer, bounds;
523         var <recorder, <recType=\mix;
524         var <>recHeaderFormat, <>recSampleFormat, <preparedForRecording=false;
525         var skipjack, <display;
527         *new { arg proxymixer, bounds;
528                 ^super.newCopyArgs(proxymixer, bounds).initDefaults.makeWindow
529         }
530         initDefaults {
531                 recHeaderFormat = recHeaderFormat ?? { this.server.recHeaderFormat };
532                 recSampleFormat = recSampleFormat ?? { this.server.recSampleFormat };
533         }
535         prepareForRecord {
536                 var numChannels=0, path, proxies, func;
537                 proxies = this.selectedKeys.collect { arg key; this.at(key) };
538                 if(proxies.isEmpty) { "NodeProxyEditor: no proxies.".postln; ^nil };
541                 if(recType == \multichannel)  {
542                         proxies.do {  |el| numChannels = numChannels + el.numChannels };
543                         func = { proxies.collect(_.ar).flat }; // no vol for now!
544                 } {
545                         proxies.do {  |el| numChannels = max(numChannels, el.numChannels) };
546                         func = { var sum=0;
547                                 proxies.do { |el| sum = sum +.s el.ar };
548                                 sum
549                         }
550                 };
552                 path = thisProcess.platform.recordingsDir +/+ "SC_PX_" ++ numChannels ++ "_" ++ Date.localtime.stamp ++ "." ++ recHeaderFormat;
553                 recorder = RecNodeProxy.audio(this.server, numChannels);
554                 recorder.source = func;
555                 recorder.open(path, recHeaderFormat, recSampleFormat);
556                 preparedForRecording = true;
557                 ^numChannels
558         }
560         font { ^GUI.font.new(*GUI.skins[\jitSmall].fontSpecs) }
561         skin { ^GUI.skins[\jitSmall] }
563         server { ^proxymixer.proxyspace.server }
565         removeRecorder {
566                 recorder !? {
567                         recorder.close;
568                         recorder.clear;
569                         recorder = nil;
570                 };
571                 preparedForRecording = false;
572         }
574         record { arg paused=false;
575                 if(recorder.notNil) {recorder.record(paused) };
576         }
578         pauseRecorder { recorder !? {recorder.pause} }
579         unpauseRecorder {
580                 recorder !? { if(recorder.isRecording) { recorder.unpause } { "not recording".postln } }
581         }
583         selectedKeys {  ^proxymixer.selectedKeys }
584         at { arg key; ^proxymixer.proxyspace.envir.at(key) }
585         selectedKeysValues {
586                 ^this.selectedKeys.collect { |key|
587                                         var proxy = this.at(key);
588                                         [ key, proxy.numChannels ]
589                 }.flat;
590         }
592         makeWindow {
593                 var rw, recbut, pbut, numChannels, recTypeChoice;
594                 var font = this.font;
596                 rw = Window("recording:" + proxymixer.title, bounds);
597                 rw.view.decorator = FlowLayout(rw.view.bounds.insetBy(20, 20));
598                 rw.onClose = { this.removeRecorder; this.stopUpdate; };
599                 rw.view.background = this.skin.background;
601                 recbut = Button(rw, Rect(0, 0, 80, 20)).states_([
602                         ["prepare rec", this.skin.fontcolor, Color.clear],
603                         ["record >", Color.red, Color.gray(0.1)],
604                         ["stop []", this.skin.fontcolor, Color.red]
605                 ]).action_({|b|
606                         var list;
607                         switch(b.value,
608                                 1, {
609                                         numChannels = this.prepareForRecord;
610                                         if(numChannels.isNil) { b.value = 0; pbut.value = 0 }
611                                 },
612                                 2, { this.record(pbut.value == 1) },
613                                 0, { this.removeRecorder  }
614                         );
615                         if(b.value == 1 and: { numChannels.notNil }) {
616                                 list = this.selectedKeysValues;
617                                 this.displayString = format("recording % channels: %", numChannels, list.join(" "));
618                         };
620                 }).font_(font);
622                 pbut = Button(rw, Rect(0, 0, 80, 20)).states_([
623                         ["pause", this.skin.fontcolor, Color.clear],
624                         [">", Color.red, Color.gray(0.1)]
625                 ]).action_({|b|
626                         if(b.value == 1) {  this.pauseRecorder } { this.unpauseRecorder }
628                 }).font_(font);
630                 recTypeChoice = PopUpMenu(rw, Rect(0, 0, 110, 20))
631                                 .items_([ \mix, \multichannel ])
632                                 .action_({ arg view;
633                                         recType = view.items[view.value];
634                                         if(recbut.value != 0) { recbut.valueAction = 0 }
635                                 })
636                                 .font_(font);
637                 recTypeChoice.value = 1;
638                 Button(rw, Rect(0, 0, 60, 20))
639                                 .states_([["cancel", this.skin.fontcolor, Color.clear]])
640                                 .action_({ if(recbut.value != 0) { recbut.valueAction = 0 } })
641                                 .font_(font);
644                 rw.view.decorator.nextLine;
645                 display = StaticText(rw, Rect(30, 40, 300, 20)).font_(font);
646                 rw.front;
647                 this.makeSkipJack;
648                 this.runUpdate;
650                 ^rw
651         }
653         displayString_ { arg str;
654                 display.string = str;
655         }
657         updateZones {
658                 if(preparedForRecording.not) {
659                         this.displayString = format("proxies: %", this.selectedKeysValues.join(" "));
660                 }
661         }
663         title {  ^"recorder for" + proxymixer.title }
665         runUpdate { skipjack.start; }
666         stopUpdate { skipjack.stop }
667         makeSkipJack {                          // stoptest should check window.
668                 skipjack = SkipJack({ this.updateZones }, 0.5, name: this.title);
669         }