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.

note: this quark now requires sc version 3.3.1 or newer.

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.

<>sortFunc

sorts the dictionary in other ways than in the default alphabetic order.

Instance Methods

currentPath

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

value_(path)

set the value. path should be an array of symbols.

valueAction_(path)

set the value and evaluate the action. path should be an array of symbols.

//--simple example

(

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

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};

)

a.value_(['bass', 'funky'])

a.value_(['melo'])

//--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;

a.sortFunc= {|a, b| a>b}

a.sortFunc= nil //the default

//--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 buildTreeFunc;

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

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};

)