OK, here's the code from the classes, lightly annotated. Reviewing the code I can see a couple of loose ends

but I won't call attention to them

// Midi Trigger -- corresponds to a socket in the MIDI hierarchy
// contains a dictionary of note nums -> processes
MT : AbstractChuckNewDict {
classvar <>default; // used in ChuckBrowserKeyController
classvar <>readyThreshold = 5; // how long to hold a process in ready state before clearing
classvar <>defaultMinNote = 48, <>defaultMaxNote = 72; // integer note numbers
var <lastBP, noteAllocator;
var <socket, <>minNote, <>maxNote; // socket is midi responder, calls my noteOn method
// specific to the chucklib framework, not really relevant to the topic
//
// all indices have to be converted to channel objects
*prNew { |index|
var temp;
this.put(index = index.asChannelIndex, temp = super.prNew(index));
^temp
}
*new { |index|
var collTemp;
^collection[this.name][index = index.asChannelIndex] ?? { this.prNew(index) }
}
// needed b/c MIDIChannelIndices can be == but not ===
*collectionType { ^Dictionary }
//
//////
init {
value = IdentityDictionary.new; // note num -> MTNoteInfo
minNote = defaultMinNote;
maxNote = defaultMaxNote;
socket = MTSocket(collIndex, this);
noteAllocator = ContiguousBlockAllocator(maxNote+1, minNote);
default = this;
}
free {
this.changed(\free);
socket.free;
value.do(_.free);
this.class.collection.removeAt(collIndex);
collIndex = value = socket = noteAllocator = minNote = maxNote = nil;
this.releaseDependants; // remove from dependants dictionary
this.removeFromCollection;
}
// chucklib methods to populate slots
bindPR { |pr, adverb|
BP(pr.collIndex).free;
this.add(pr => BP(pr.collIndex), adverb);
}
bindBP { |bp, adverb|
this.add(bp, adverb);
}
bindFact { |fact, adverb|
(fact.isBP).if({
this.add(fact.makev, adverb)
}, {
"%'s subtype is %; must be 'bp' to chuck into MT.".format(fact, fact.v[\type]).warn;
});
}
// support for BP([\pr1, \pr2, \pr3, \pr4]) => MT(0) syntax
// you give up control over where they go
bindArray { |ar|
ar.do({ |bp| bp => this });
}
// this does the work of filling a slot
add { |bp, adverb, updateGUI = true|
var new, nextAddNote;
lastBP = bp;
nextAddNote = this.convertAdverb(adverb); // get the number to use
this.adverbIsValidNote(adverb).if({
noteAllocator.reserve(adverb);
});
value[nextAddNote].notNil.if({ value[nextAddNote].free });
// create the MTNoteInfo object and add to the collection
value.put(nextAddNote, new = MTNoteInfo(bp, false, nextAddNote, this));
// .changed calls .update on all my dependents -- see MTGui.update
updateGUI.if({ this.changed(new) }); // tell the gui
}
// release a slot
removeAt { |notenum|
noteAllocator.free(notenum);
this.changed(notenum); // tells the dependents this slot is empty
^value.removeAt(notenum).free;
}
adverbIsValidNote { |adverb|
(adverb = adverb.tryPerform(\asInteger)).notNil
and: { adverb.inclusivelyBetween(minNote, maxNote) }
}
convertAdverb { |adverb|
(this.adverbIsValidNote(adverb)).if({
^adverb.asInteger
}, {
^noteAllocator.alloc(1)
});
}
// handle an incoming midi message
// .ready is an insurance policy -- you have to hit the midi key twice within 5 secs
// to get the action -- it protects against accidentally starting or stopping the wrong process
noteOn { |num|
var entry, clock;
// entry must exist
(entry = value[num]).notNil.if({
// if ready to fire, do play
entry.ready.if({
entry.ready = false;
entry.bp.isPlaying.if({
entry.bp.stop;
}, {
entry.bp.play;
});
this.changed(entry);
}, { // else make ready and schedule check for non-ready
entry.ready = true;
AppClock.sched(readyThreshold, {
entry.ready = false;
this.changed(entry);
});
this.changed(entry);
});
});
}
guiClass { ^MTGui }
}
MTNoteInfo {
var <>bp, <>ready, <>noteNum, <owner;
*new { |bp, ready, noteNum, owner|
var new;
new = super.newCopyArgs(bp, ready, noteNum, owner);
// part of the dependency chain: the BP object can notify any MTNoteInfo objects
// corresponding to it using .changed
bp.addDependant(new);
^new
}
asString { ^(noteNum.asMIDINote ++ ": " ++ bp.collIndex) }
// MTGui uses this to determine the color of the slot
playState {
^case { ready == true } { \ready } // ready takes precedence
{ bp.isDriven } { \driven }
{ bp.isPlaying } { \playing }
{ \idle }
}
free { bp.removeDependant(this) }
// respond to notifications from BP
update { |obj, changer|
(changer == \free).if({
owner.removeAt(noteNum);
});
#[\play, \stop, \driven].includes(changer).if({ // ignore other messages
owner.changed(this); // tell the owner I changed--owner's dependent is the MTGui
});
}
}
MTGui : HJHObjectGui {
classvar <>dragWidth = 80, <>dragHeight = 15, <>rows = 12,
<>gap = 2;
classvar <>font, <backColors, blackKeys;
var <mainView,
<chanText, // which channel?
<dragViews, // one per key
minNoteFloor, maxNoteCeil; // to position views correctly
*initClass {
font = Font("Helvetica", 10); // nice and small
backColors = (
ready: [Color.new255(181, 196, 255), Color.new255(136, 147, 191)], // baby blue
playing: [Color.new255(69, 255, 23), Color.new255(52, 191, 17)], // green
late: [Color.yellow, Color.yellow(0.75)], // not currently used
driven: [Color.new255(214, 191, 255), Color.new255(161, 143, 191)], // lavender
idle: [Color.gray(0.75), Color.gray(0.5)]
);
// so that c#, d# etc. are shaded slightly darker: see populateView
blackKeys = #[1, 3, 6, 8, 10];
}
// this initializes the views
guiBody { |lay|
var size, cols, myRows, width, height, temp;
layout = lay;
// compute bounds
minNoteFloor = model.minNote.trunc(rows);
((maxNoteCeil = model.maxNote.trunc(rows)) != model.maxNote).if({
maxNoteCeil = maxNoteCeil + rows;
});
size = maxNoteCeil - minNoteFloor + 1;
myRows = rows;
cols = (size / rows).floor; // one column per octave
// octave + 1 should not create a new column, but it needs an extra row
// if > octave+1, need an extra column
temp = (size - (cols*rows + 1)).sign;
// case here ignores temp == -1
case { temp == 0 } { myRows = myRows + 1 }
{ temp == 1 } { cols = cols + 1 };
width = dragWidth * cols + gap + (gap*cols);
// include an extra row for the header
height = dragHeight * (myRows+1) + (gap*2) + (gap*myRows);
mainView.isNil.if({ // if this gui is already open, don't recreate
{ mainView = SCCompositeView(layout, argBounds ?? { Rect(0, 0, width, height) })
.onClose_({ this.remove });
chanText = SCStaticText(mainView,
Rect(gap+mainView.bounds.left, gap+mainView.bounds.top, width-gap, dragHeight))
.string_("MT(" ++ model.collIndex.asShortString ++ ")") // midi channel index
.font_(font).align_(\center);
// like mixingboard -- create them and index them, then place them
dragViews = Array.fill(model.maxNote - model.minNote + 1, { |i|
this.emptyView(this.makeDragView, i);
});
this.positionViews(myRows, cols)
.populateViews;
}.defer;
});
}
// create the slot's view onscreen -- SCDragBoth allows dragging both in and out
makeDragView {
^SCDragBoth(mainView, Rect(0, 0, dragWidth, dragHeight))
.font_(font)
.beginDragAction_({ |drag| drag.object })
.action_({ |sink|
var index;
// dragging an object from a dragboth into itself is like clicking on it
// this should trigger the model
index = dragViews.indexOf(sink) + model.minNote;
Here's the trick for clickable drag sinks. Because the model holds the object to which the slot refers, I can check for identity:
model.v[index].bp === sink.object -- if you click on the dragboth without dragging, this condition will be true!
(model.v[index].notNil and: { model.v[index].bp === sink.object }).if({
model.v[index].ready = true;
model.noteOn(index); // trigger
}, {
// else reassign
I use polymorphism to handle dragging a new object in. Instead of doing complex and ugly type checking here, I see if the object understands how to get dragged into the gui. If it's an invalid object, tryPerform does nothing (correctly).
(sink.object.tryPerform(\draggedIntoMTGui, this, index) != true).if({
// if inappropriate class, reset view
model.v[index].isNil.if({
this.emptyView(sink, index - model.minNote);
}, {
this.populateView(model.v[index])
});
});
});
})
}
Sometimes it's easier to separate the logic for creating views from that for positioning them. I create the views with an origin of (0, 0), then use Rect-moveTo to apply the right origin later.
positionViews { |r, c|
var x, y, i, j, top, left;
top = mainView.bounds.top;
left = mainView.bounds.left;
i = 0;
j = model.minNote - minNoteFloor;
x = gap;
// if minNote is not a multiple of 12, first y position needs to be adjusted
y = gap*2 + dragHeight + ((dragHeight + gap) * j);
{
dragViews.do({ |view|
view.bounds_(view.bounds.moveTo(x + left, y + top));
((y = y + dragHeight + gap; j = j+1) >= rows).if({
((i = i+1) < c).if({
// advance to next col, if we haven't hit the end of the cols
// otherwise, leave position as is (oct+1)
y = gap*2 + dragHeight; x = x + dragWidth + gap; j = 0;
});
});
});
}.defer;
}
// methods to update views
populateViews {
model.value.keysValuesDo({ |k, v|
this.populateView(v);
});
}
populateView { |entry|
{ dragViews[entry.noteNum - model.minNote].object_(entry.bp)
.string_(" " ++ entry.asString)
.background_(backColors[entry.playState]
[blackKeys.includes(entry.noteNum % 12).binaryValue])
}.defer;
}
emptyView { |view, index|
^view.string_(" " ++ (index + model.minNote).asMIDINote)
.background_(backColors[\idle]
[blackKeys.includes(index % 12).binaryValue])
}
// handles changes from the model (the MT object)
update { |mt, mtEntry|
case { mtEntry == \free } { this.remove; }
{ mtEntry.isNumber }
{ this.emptyView(dragViews[mtEntry-model.minNote], mtEntry-model.minNote) }
{ this.populateView(mtEntry); };
}
writeName {} // from cruxxial: don't want it, don't need it
// drop all the views on close
remove {
mainView.notNil.if({
mainView.notClosed.if({ // check for window closed
{ mainView.remove; }.defer;
});
mainView = chanText = dragViews = nil;
model.removeDependant(this);
});
}
}
Vesaflex, I'll answer your question tomorrow...
hjh