Hey Kasyan, Since I don't write "native ScriptUI" code anymore, you force me to disclose a part of my secret libraries 😉 That's OK, here is a sample: // testScroller.js // Demonstrates ScriptUI Scrollbar usage //------------------------------------------------- //------------------------------------------------ // THE LIBRARIES //------------------------------------------------ var DOMEX = DOMEX||(function() // Extended DOM //------------------------------------------------ { Window.prototype.focus = (function() //---------------------------- // ScriptUI Window focus method { var getActivableControls = function() { // this: UI object var r = [], i, c; for ( i=0 ; i<this.children.length ; ++i ) { c = this.children; if (c.constructor == StaticText) continue; // exclude StaticText focus if (c.constructor == Scrollbar) continue; // exclude Scrollbar focus if (c.noTabFocus ) continue; // exclude explicit noTabFocus ctrl if ('active' in c) {r.push(c);continue;} r = r.concat(arguments.callee.call(c)); } return r; }; return function(/*int*/step, /*?ctrl*/ac) { // this: Window this.controls = this.controls || getActivableControls.call(this); ac = ac||false; var sz = this.controls.length, i, c; for( i=0 ; i<sz ; ++i ) { c = this.controls; if( ac && c==ac ) break; if( (!ac) && c.active ) {ac=c; break;} } if( !ac ) return; if( !step ) return ac; i = (i+step+sz)%sz; step = (step<0)?-1:1; while( ac !=(c=this.controls) ) { if( c.visible && c.enabled ) break; i = (i+step+sz)%sz; } c.active = true; return c; }; })(); Window.prototype.setTabKey = function() //---------------------------- // ScriptUI Window set tab key { // this: Window this.addEventListener('keydown', function(kev) { if( kev.keyName != 'Tab' ) return; if( kev.ctrlKey || kev.altKey || kev.metaKey ) return; kev.preventDefault(); this.focus( ((kev.shiftKey)?-1:1 ) ); },false/*==bubble*/); }; /*STATIC*/ Window.UI = function(ui) { // constructor var flat = function(container) { // this: Window.UI var p,c; for( p in container ) { c = container ; if( ( !c ) || ( typeof c != 'object' ) || ( c.propertyIsEnumerable(p) ) || ( !('window' in c) ) || ( p.substr(0,8) == 'private_' ) ) continue; this = this ||c; flat.call(this,c); } }; var connect = function() { // this: UI container var cs = this.children, i = cs.length, ci; while( i-- ) { ci = cs; if( 'alignChildren' in ci ) connect.call(ci); if( XW && ('xType' in ci) ) XW.connector(ci.xType).call(ci); } }; this.window = new Window(ui.toSource(). slice(1,-1). replace(RegExp('(?:\\{_([^:]+):)','g'),'$1'). replace(RegExp('}}','g'),'}') ); this.window.layout.layout(); flat.call(this,this.window); connect.call(this.window); this.window.setTabKey(); }; return true; })(); var XW = XW||(function() // Extended Widgets [v.10-06-14] //------------------------------------------------ { var MAC_OS = (File.fs == "Macintosh"); var widgets = {}; // IconButton // -------- widgets.PngButton = { ICON_PLUS: String("\u0089PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\b\x06\x00\x00\x00\x1F\u00F3\u00FFa\x00\x00\x00\x04gAMA\x00\x00\u00AF\u00C87\x05\u008A\u00E9\x00\x00\x00\x19tEXtSoftware\x00Adobe ImageReadyq\u00C9e<\x00\x00\x02oIDAT8\u00CB\u00A5\u0093\u00EBK\u0093a\x18\u0087\u00FD[\u00B6/\x05\u0083\u00A4\u0086Y(\u00A8)%X(o\u00D9l\u008A\u008AN\u00DB\u0096sk\u008A\u00CE\u00F6n.\u009D\u00BA\u00CD\x03-\u009D\x13\x0F\x15\u00B5\u00A1\u00DBh\u00A4;8\x0F\u00C3\x03f\u00E2\u00D4\u00D2E\u00EA\u00EBP\u00A2\u008D\"\bj\u00C3\u00AF\u00BF\u00DE\x15MG\u00CB\u0088\x1E\u00F8}y\u00E0\u00BA\u009E\u00E7\u00BE\u00B9\u00EF$\x00I\u00FF\u0093\u00DF.t\u00D3u\u00CC\x0E\u008F\u0084lu\u0089\u00A8\u00E6\tAX\u00FE\u00A2:\u00DC\u00F0\u00BC\u0082\u0092Z\u00CBH\u00D1h1\u00F3D\u0081nZJ\u00B4OJB\u00CF\u00D6{\u00B1\x1AZ\u0084\u00FF\u00F3\x06\u00D6?\u00AD`2`\u0087\u00D2S\x03\u00BE\u0089\x13\u00E2=\u00B9N$\x14\u00FC\u0084\u00C5\u0091\u00E9=;\x0E\u00BE\u00EDa\u00F6\u0083\x0B&j\x10\u008Fw\f\u00B0\u00EF\u009B\u00B0\u00F4q\x0E\u00DA\x05\x19JG\u00F2#\u00DC\u00C1<\"N\u00A0\u009D\u00922h8\u00E8\u00DE\u00B5`\u00EF\u00EB6\u0086\u00B7\u00F5x\u00B8\u00D6\u0081n_+\f~\x1D\u00FA\u00FCZto\u00B6\u00C0}`\x07\u00E9\x11\u00A2\u00D0x%X\u00D0\u009B\u00CD\u0088\thX\u00D1\u00BF\u00AC\u00C6\u00BB/\u009B\u00F4\u008B}\u00E8\u00DD\u00D2B\u00F3J\u0089_G\u00BD&\u0083|Q\b\u00C5r-\u009C\u00FB6\u00DC\x1C\u00C9A\u00DE\u0083\fEL\u00D0\u00E2\u00AC\u00A1\\\x01\x1B\u00FDU3:WUh[\u0091C6+\u008A\t\x046.\x1Af\u00CA \u009D*\u00C6\u00C0\x1B\x1D\u00F4K\u00CD\u00B8\u00DC\u009DF\u00C5\x04\u008Aq\u00FE\u00A1\u00F7\u00BD\x0B\u00FDou4\u00DC\u0084?\x1D\u00F1d\x11\x14\u00F3|X\u00FC\u00C3\u00C8\u00D2\u00A5\x1E\u00C6\x04Mv\u00DE\u00E1D`\f\u00DA\r\x12*_\u00FD\u0089\x02\u00D2[\r\u00AB\x7F\b\u00E9\x1D\u00EC#A\u00BD\u00AD\u009C2\u00FA\u00B40li\u00D0\u00F4R\b\u00F1|\x05x\u00D6\x1Bq`4w=\\\u00F4\u00FB\u00D4\u00E8\u00F2\u00DE\u00C3\x05u\u00F2Q\t\u00E2\u00B1\x12\u00C5m+\x01G\u00C0\x02\u00D9|%$\u00DE\u00D2\x1F5\x1F\x17\u0088\u009C\x1C\u00D4\u00B9\u008B\u00E1\u00D85\u00E3RO*\u00D8\u00F7YGM\x14\u009A\u008B\x18UO\x0B\u0083\u00A4G\u0080qj4\u00D6\u00B0(X\u00EB\u008C&\ns1\u00B1c\u00C2\x1D\u00CB\u00AD(\x1CLV\u009Ef\u00C4\rR\u00F9\u00A3\x02\u00A2d\u00E8j\u00A4\u00D1Q\t'-1\u00FA\u00DAA\u00CETA>U\t\u00E3j\x1B\x1C4,\u00B4p\u00C0V\u00B1\"4L$\x1Ce\u00CE@.A\x18rB\u00F9\x03\u0099\u00E8Y a~m\u0080y\u00C3\x00\u008D\u00B7\x11Y])Q8t\x1CN\u00B8L\u00D7\u00F4\u0099\u00CC\u00DC\u009Et2\u00BB\u00F3\"\u0095\u00A1I\t\u00A7\u00B5\u009F\r\u009Fo=C\u009DS\u00B1\u00C8d\u00E5)\u00E6_\u00B7\u00F1_\u00F3\x1DAF\u00CB\x1F\x00(\u00D3\u00C1\x00\x00\x00\x00IEND\u00AEB`\u0082"), ICON_MINUS: String("\u0089PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\b\x03\x00\x00\x00(-\x0FS\x00\x00\x00\x19tEXtSoftware\x00Adobe ImageReadyq\u00C9e<\x00\x00\x000PLTE\u00D4\u00A7\u0096\u00E2\u00B5\u00A4\u00E4\u00C5\u00BA\u00CAz[\u00A7cH\u00EF\u00D7\u00CE\u0093Q7\u00F4\u00E8\u00E4\u00CE\u0084h\u00BFdA\u00D3\u0090v\u00D8\u009D\u0086\u00C8uU\u00C5nL\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00A53\x0E}\x00\x00\x00\x10tRNS\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\x00\u00E0#]\x19\x00\x00\x00\u0087IDATx\u00DA\\\u008F[\x12\u00C4 \b\x04y\u00898\u00E8\u00E6\u00FE\u00B7\r&\u00BB\u00A9\u00CA\u00F6\u0087U\u00B4:\x00\x1D\x7F\u00D0>\u00BA@5\u00B8\u00FFDCJk\u00ACSn\u00D1\u00EAN\u0088Xb\u00C8\x16=\u00B8e\"3Y\u00AD\u0097\x104J\x04P)F%\u00C0\f\u00CC\u008B\u0084\u0095\u0098\u0092\u00F8\\,%\u00DF\u00A2\x1E\u00BCDd\u00C6\\\x17\u0088\u00FD\u0085GMp\x1B\u00F6\x1D\u00DA\u0097\u00F2\u00B7\x1E\u00BE\u00DB\x1Eb\x15\u00A3\x13U\u00CB=\u00BA\u0098\x05e\u00B8\u00CB\u00B3\x1C\u0099\u00BB\u00D3\u00B3\u00DC\u008BS\u0080\x01\x005\u00C3\t\u00EA\u00DEA\u00F0S\x00\x00\x00\x00IEND\u00AEB`\u0082"), ui: function(ss) { var r = {_IconButton:{xType:'PngButton', settings:ss, noTabFocus:1, preferredSize:[20,20], helpTip:ss.helpTip||'', properties:{style:ss.style||'toolbutton'}, }}; return r; }, connect: function() { // <this> IconButton (function(/*str*/pngStr) { // Set image var f = new File(Folder.temp.absoluteURI + '/ib' + (+new Date()) + '.png' ); f.encoding = 'BINARY'; f.open('w'); f.write(pngStr); f.close(); this.image = ScriptUI.newImage(f.fsName); f.remove(); }).call(this, widgets[this.xType][this.settings.icon]); } }; // PanelListBox // -------- // Sample: // XW.PanelListBox({clusterType:'{_EditText:{characters:5}}', viewCount:5, defaultItem:{text:'default'}, items:['{text:"aaa"}','{text:"bbb"}']}); widgets.PanelListBox = { ui: function(ss) { ss.viewCount = ss.viewCount||5; var r = {_Panel:{ xType: 'PanelListBox', settings:ss, orientation:'row', alignment:'left', alignChildren: 'fill', margins:0, spacing:0, private_gClusters:{_Group:{orientation:'column', alignChildren:'fill', margins:0, spacing:0}}, private_gScroll:{_Group:{orientation: 'row',alignChildren: 'fill', margins:0, spacing:0, private_sbStepper:{_Scrollbar:{preferredSize:[20, -1]}} }}, }}; var i, iMax = ss.viewCount-1, cs = r._Panel.private_gClusters._Group, buildCluster = function(){ var ret = {_Group:{orientation:'row', alignChildren:'fill', margins:1, spacing:5, private_xClusterIn: false, private_xTrash: XW.PngButton({icon:'ICON_MINUS',action:'remove', helpTip:ss.trashTip||"Remove the item."}), private_xPlus: XW.PngButton({icon:'ICON_PLUS',action:'add', helpTip:ss.addTip||"Add an item."}), }}; ret._Group.private_xClusterIn = (new Function('return '+ss.clusterType+';'))(); return ret; }; for( i=0 ; i <= iMax ; i++ ) cs['private_g'+i] = {_Group:{orientation:'column', alignChildren:'fill', margins:0, spacing:0, private_xClusterOut: buildCluster(), private_pLine: (i==iMax)?false:{_Panel:{alignment:'fill'}} }}; return r; }, connect: function() { // <this> UI Panel var plb = this; var evChangeCount = ScriptUI.events.createEvent('UIEvent'); evChangeCount.initEvent('changeCount',true,false,true); evChangeCount.value = 0; var changeCount = function() { evChangeCount.value = scroller.itemCount(); plb.dispatchEvent(evChangeCount); }; (function() { this.offset = this.offset||0; this.items = this.items||[]; this.maxCount = Math.max(this.items.length, this.maxCount||this.viewCount); }).call(this.settings); var scroller = (function(ss, sb) { // this: Group (private_gClusters) var dModel, iSize, vSize, oMax, ofs; dModel = (new Function('return '+ss.defaultItem+';'))(); var setItem = function(/*?obj*/v, /*index*/i, /*max*/mx, /*bool*/ ADDING) { // this: {obj} var p,s; var N_VAR = i+1, A_VAR = !!ADDING, M_VAR = (+mx)||1; v = v||{}; for( p in dModel ) { if( p in v ) this = v ; if( !(p in this) ) { this =dModel ; if( (typeof i != 'undefined') && (typeof this == 'string' ) ) { s = this . replace(/%N%/g,N_VAR). replace(/%A%/g,A_VAR). replace(/%M%/g,M_VAR); if( s[0] == '{' ) { try { s = (new Function('return '+s.substr(1)+';'))(); } catch(_){} } this = s; } } } }; var cloneItem = function() { // this: {obj} var p, r = {}; for( p in dModel ) r =this ; return r; }; var data = (function(/*str[]*/strItems, /*int*/maxCount) { var items = [], i = Math.min(strItems.length, maxCount), v; while( i-- ) { v = (new Function('return '+strItems+';'))(); setItem.call(v,undefined,i,i+1); items.unshift(v); } return { length: function(){return items.length;}, get: function(i){return items;}, set: function(i,v){setItem.call(items,v,i,items.length);}, remove: function(i) {items.splice(i,1); return items.length;}, add: function(i){var v={};setItem.call(v,undefined,i+1,1+items.length,true);items.splice(i+1,0,v);return items.length;}, getItems: function(/*bool*/NO_CLONE) { if( NO_CLONE ) return items; var i = items.length, r = []; while( i-- ) r.unshift(cloneItem.call(items)); return r; } }; })(ss.items, ss.maxCount); iSize = data.length(); vSize = Math.min(ss.viewCount,iSize); oMax = +((iSize>vSize)&&(iSize-vSize)); ofs = ss.offset; var rawOffset = function(/*int*/v, /*bool*/SET_DATA, /*bool*/SET_VIEW) { var s = (v<ofs)?1:0, kLim = (vSize*s)-(1-s), k = (1-s)*(vSize-1), s = 2*s-1; while( k != kLim ) { if( SET_DATA ) data.set(k+ofs, view.get(k)); if( SET_VIEW ) view.set(k, data.get(k+v)); k+=s; } return ofs=v; }; var view = (function() { var clusters = [], i = ss.viewCount, cOut, reActive; while( i-- ) { cOut = this['private_g'+i]['private_xClusterOut']; cOut.index = i; cOut.addEventListener('keydown', function(ev) { // up/down keys reActive = false; var iFoc = 0; var locs, loc, t, chi; var ii = ((ev.keyName == 'Up')&&-(ofs > 0 || this.index > 0 )) || ((ev.keyName == 'Down')&&+( ofs < oMax || this.index < vSize-1 )) || ((ev.keyName == 'Tab')&&((ev.shiftKey)?-( ofs > 0 && this.index == 0 ):+( ofs < oMax && this.index == vSize-1 ))); if(! ii ) return; ii += this.index; if( iFoc = (-(ii==-1)+(ii==vSize)) ) { if( ev.target.constructor == EditText ) ev.target.notify(); // force onChange sb.safeSet(rawOffset(ofs+iFoc, /*set_data*/true, /*set_view*/true)); reActive = true; if( ev.keyName != 'Tab' ) return; else ii = this.index-iFoc; } // searching the corresponding widget up/down locs = [(t=ev.target).location]; while( (t=t.parent) != this ) { locs.push(t.location); } t = clusters[ii].parent; while( loc=locs.pop() ) { t = t.children; for( ii = t.length-1 ; ii>=0 ; ii-- ) { chi = t[ii]; if( chi.location.x != loc.x ) continue; if( chi.location.y != loc.y ) continue; t = chi; break; } if( !chi ) {t=false; break;} } if( t ) t.active = true; }); cOut.addEventListener('keyup', function(ev) { if( reActive && (ev.keyName == 'Up' || ev.keyName == 'Down') ) { ev.target.active = false; ev.target.active = true; reActive = false; } }); cOut.visible = i < vSize; cOut.private_xTrash.enabled = iSize>0; cOut.private_xPlus.enabled = iSize<ss.maxCount; clusters.unshift(cOut['private_xClusterIn']); } return { get: function(i){return clusters;}, set: function(i,v){setItem.call(clusters,v,i,data.length());}, }; }).call(this); return { maxOffset: function(){return oMax;}, viewSize: function(){return vSize;}, itemCount: function(){return iSize;}, offset: function(/*?int*/v, /*?bool*/NO_DATA_SET) { if( typeof v == 'undefined' || v > oMax) return ofs; return rawOffset(v,/*set data*/!NO_DATA_SET,/*set_view*/true); }, remove: function(/*int*/cid) { if( iSize <= 1 ) return; rawOffset(ofs,/*set_data*/true,/*set_view*/false); // backup every data, don't touch ofs // cid is the cluster-id of the DEL button var iData = cid + ofs; // iData is the data-id from which DEL is called iSize = data.remove(iData); // remove the data and update iSize if( iData == iSize ) iData--; // now iData is the data-id to select finally // if necessary, dec vSize and hide the last cluster if( vSize > iSize ) { --vSize; view.get(vSize).parent.visible = false; } // vSize now reflects the number of visible clusters (updated) oMax = +((iSize>vSize)&&(iSize-vSize)); // oMax is the new max offset // we need to un-scroll(-1) only if ofs>oMax if( ofs > oMax ) --ofs; // now update the view (from ofs) and update the scrollbar rawOffset(ofs,/*set_data*/false,/*set_view*/true); sb.safeSet(ofs, oMax); // update TRASH / ADD button availability view.get(0).parent.private_xTrash.enabled = ( iSize > 1 ); if( iSize == ss.maxCount-1 ) { cid = ss.viewCount; while( cid-- ) view.get(cid).parent.private_xPlus.enabled = true; } // then activate the remaining cluster cid = iData-ofs; if( 'activate' in view.get(cid) ) view.get(cid).activate(); changeCount(); }, add: function(/*int*/cid) { if( iSize >= ss.maxCount ) return; rawOffset(ofs,/*set_data*/true,/*set_view*/false); // backup every data, don't touch ofs // cid is the cluster-id of the ADD button var iData = cid + ofs; // iData is the data-id from which ADD is called iSize = data.add(iData); // add a new default data AFTER iData, update iSize ++iData; // iData is now the added data-id // if necessary, avail a new visible cluster and inc vSize if( vSize < ss.viewCount ) { view.get(vSize).parent.visible = true; ++vSize; } // vSize now reflects the number of available clusters (updated) oMax = +((iSize>vSize)&&(iSize-vSize)); // oMax is the new max offset // we need to scroll(+1) only if cid is the last cluster in scroll mode ( cid== ss.viewCount-1 ) if( cid == ss.viewCount-1 ) ++ofs; // now update the view (from ofs) and update the scrollbar rawOffset(ofs,/*set_data*/false,/*set_view*/true); sb.safeSet(ofs,oMax); // update TRASH / ADD button availability view.get(0).parent.private_xTrash.enabled = ( iSize > 1 ); if( iSize == ss.maxCount ) { cid = ss.viewCount; while( cid-- ) view.get(cid).parent.private_xPlus.enabled = false; } // then activate the added data line cid = iData-ofs; if( 'activate' in view.get(cid) ) view.get(cid).activate(); changeCount(); }, getItems: function(/*?bool*/NO_CLONE) { rawOffset(ofs,/*set_data*/true,/*set_view*/false); // backup data return data.getItems(!!NO_CLONE); }, updateView: function() { rawOffset(ofs,/*set_data*/false,/*set_view*/true); // backup data }, activate: function(cid) { if( (cid < vSize) && 'activate' in view.get(cid) ) view.get(cid).activate(); }, }; }).call(this.private_gClusters, this.settings, this.private_gScroll.private_sbStepper); (function() { // this: scrollbar var SAFE_SETTING = false; this.safeSet = function(v,mx) { SAFE_SETTING = true; if( typeof mx == 'undefined' ) { this.value = v; SAFE_SETTING = false; return; } if( mx > 0 && !this.enabled ) this.enabled = true; if( v >= this.maxvalue ) { this.maxvalue = mx; this.value = v; } else { this.value = v; this.maxvalue = mx; } if( mx <= 0 && this.enabled ) this.enabled = false; SAFE_SETTING = false; }; var elChange = function(ev) { // this: scrollbar if( SAFE_SETTING ) return; var v = ~~this.value; if( v!=scroller.offset() ) scroller.offset(v); }; this.minvalue = 0; this.maxvalue = scroller.maxOffset(); this.value = scroller.offset(); this.stepdelta = 1; this.jumpdelta = 20; // !!important!! this.enabled = this.maxvalue>0; this.addEventListener('change', elChange); this.addEventListener('changing', elChange); scroller.offset(this.value, /*NO_DATA_SET*/true); }).call(this.private_gScroll.private_sbStepper); (function() { // this: Group (private_gClusters) <- trash/add button this.addEventListener('click', function(ev) { var tg = ev.target; if( !tg.xType ) return; if( tg.xType != 'PngButton' ) return; if( !tg.settings.action ) return; scroller[tg.settings.action](tg.parent.index); }); }).call(this.private_gClusters); this.getItems = scroller.getItems; this.updateView = scroller.updateView; this.activate = scroller.activate; this.itemCount = scroller.itemCount; }, }; // XW INTERFACE // -------- var xwItf = {}, xt; for( xt in widgets ) xwItf[xt] = widgets[xt].ui; xwItf.connector = function(/*str*/t) { if(! (t in widgets) ) return false; return widgets .connect; }; xwItf.addWidget = function(/*str*/_xt, /*obj*/_wg) { widgets[_xt] = _wg; xwItf[_xt] = _wg.ui; }; return xwItf; })(); //------------------------------------------------ // THE SCRIPT //------------------------------------------------ var UserInterface = (function() { XW.addWidget('TestRack', { ui: function(ss) { var r = {_Group:{ xType: 'TestRack', settings:ss, orientation:'row', margins:[10,4,2,4], spacing:1, alignChildren: 'center', helpTip: ss.helpTip||"", private_sTest:{_StaticText:{text:'', characters:10, helpTip:"Group..."}}, private_eTest:{_EditText:{text:'',characters:20, helpTip:"Enter the Label of this group."}}, private_bTest:{_Button:{text:'',size:[100,20], helpTip:"Click here to add the label."}}, }}; return r; }, connect: function() { // <this> UI Group // 'static text' linking (function(root,sTest) { root.sTestValue = ''; root.watch('sTestValue',function(p,ov,nv) { sTest.text = nv; return nv; }); })(this, this.private_sTest); // 'edittext' linking (function(root,eTest) { root.eTestValue = ''; eTest.addEventListener('change', function(ev) { root.eTestValue = this.text; }); root.watch('eTestValue',function(p,ov,nv) { eTest.text = nv; return nv; }); })(this, this.private_eTest); // 'button' linking (function(root,bTest) { root.bTestValue = ''; bTest.addEventListener('click',function() { root.bTestValue = bTest.text; root.private_eTest.active = true; // focus on label }); root.watch('bTestValue',function(p,ov,nv) { bTest.text = nv; return nv; }); })(this, this.private_bTest); this.activate = function() { this.private_eTest.active = true; }; }, }); var ui = new Window.UI({_dialog:{ text: 'Set Group Label', properties: {closeOnKey: 'OSCmnd+W'}, orientation: 'column', plTestScroller: XW.PanelListBox({clusterType:'XW.TestRack({})', viewCount:4, maxCount:20, trashTip: "Delete this line", addTip: "Insert a new line below", defaultItem: '{sTestValue:"Group %N%", eTestValue:"Label %N%", bTestValue:"Button %N%"}', items: ['{}','{}','{}'], }), gValid: {_Group:{ margins:10, spacing:10, orientation: 'row', alignChildren: ['center','top'], bOK: {_Button: {text:"OK"}}, bCancel: {_Button: {text:"Cancel"}}, }}, }}); var w = ui.window; // tweak // --------------- w.cancelElement = ui.bCancel; w.defaultElement = ui.bOK; var r = ui.window.show(); if( r==2 ) return 'cancel'; return 'ok'; })(); The important scrolling stuff is in the widgets.PanelListBox component. @+ Marc
... View more