PopUpTreeMenu hierarchical PopUp menu


Superclass: SCViewHolder


known problems / todo:

* the tree must must follow the structure described below.  later it might be possible to use e.g. Help.tree

* screen width and height are not respected.  menus with many entries may be cut off at the borders.

* click once and menu should stay open so one can browse without keeping the mouse pressed.

difficult due to mouseOverAction bug - it does not fire when already clicked somewhere else.

* swingosc: scroll handles appear.  difficult to solve - something with font size calculation.



Instance Variables


<>tree


dictionary of dictionaries.  branches terminate if they have an empty dictionary as a value (= leaf).

everything have to be symbols.


( 'symbol1': (), 'symbol2': () ) //same level

( 'symbol': ( 'symbol': () ) ) //nested

( '1' : ('11': () ), '2.2': ('22': () ) ) //numbers also have to be symbols


<value


array of symbols pointing to the last selected node.  nil if nothing selected.


<currentLeaf


array of symbols pointing to current selection (leaves only).  empty if nothing selected.

see currentPath for reporting both nodes and leaves.


<>action


function that gets evaluated when some leaf is selected.  functions is passed: this, value


<>openAction


function that gets evaluated when the popup menu is clicked.  function is passed: this, mouseX, mouseY.

can be used to automatically populate the tree to keep it in sync with files in a folder.  see example below.


<>closeAction


function that gets evaluated when the popup menu closes.  function is passed: this, mouseX, mouseY.


Instance Methods


currentPath


array of symbols pointing to current selection.  array is empty if nothing selected.



//--simple example

(

var w= GUI.window.new("popuptreemenu - simple", Rect(200, 400, 300, 100)).front;

var a= PopUpTreeMenu.new(w, Rect(50, 30, 100, 20))

.tree_(

(

'a drum': (),

'bass': (

'funky': (),

unhip: (

'umpahTuba': (),

'umpahUpright': ()

)

),

'melo': ()

)

);

a.action_{|view, val| ("selected:"+val).postln};

)



//--polling state of the menu in use - example also show custom color/font settings

(

var w= GUI.window.new("popuptreemenu - state", Rect(400, 400, 300, 100)).front;

a= PopUpTreeMenu.new(w, Rect(10, 20, 200, 34))

.tree_(('aaa': (), 'bbb': (), 'ccc': ('123': (), '456': (), '789': ('hier': ()))))

.font_(GUI.font.new("Arial", 24))

.background_(Color.red(1))

.hiliteColor_(Color.red(0.75))

.stringColor_(Color.red(0.5));

Routine.run{while({w.isClosed.not}, {{

("path:"++a.currentPath).post;

(" leaf:"++a.currentLeaf).post;

(" value:"++a.value).postln;

}.defer; 0.4.wait})}

)

//--changing position and font while active

a.bounds_(Rect(30, 30, 100, 20));

a.font_(GUI.font.new("Geneva", 10));

a.refresh

a.value;



//--scanning sound file folder each time menu is pressed.

//--slightly inefficient but keeps menu/folder in sync

(

var w= GUI.window.new("popuptreemenu - scan", Rect(200, 400, 300, 100)).front;

var a= PopUpTreeMenu.new(w, Rect(10, 20, 200, 20)).items_(["sounds"]);

var buildTreeFunc= {|path|

var deepPathMatch, syms, tree;

deepPathMatch= {|pn|

var arr= [];

pn.pathMatch.do{|x|

if(x.last.isPathSeparator, {

arr= arr++deepPathMatch.value(PathName(x++"*"));

}, {

arr= arr++PathName(x);

})

};

arr

};

syms= deepPathMatch.value(PathName(path)).collect{|x|

x.fullPath.split($/).collect{|y| y.asSymbol}

};

tree= ();

syms.do{|x|

var parent= tree;

x.do{|y, i|

var node= parent[y];

if(node.isNil, {

parent.put(y, ());

parent= parent[y];

}, {

parent= node;

});

};

};

tree;

};

a.openAction_{|view, x, y|

"rebuilding tree of /sounds directory".postln;

view.tree= buildTreeFunc.value("sounds"); //build and replace tree

};

a.closeAction_{|view, x, y| "closing".postln};

a.action_{|view, val| ("selected:"+val).postln};

)