├── image └── sample1.png ├── test ├── customButton.jsx ├── common.jsx └── nativeControl.jsx ├── LICENSE ├── demo └── Labels.jsx ├── tree.min.jsx ├── README.md └── tree.jsx /image/sample1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaymondClr/Tree/HEAD/image/sample1.png -------------------------------------------------------------------------------- /test/customButton.jsx: -------------------------------------------------------------------------------- 1 | //@include "../tree.min.jsx" 2 | 3 | var elements = Tree.parse({ 4 | rectbutton: [], 5 | roundbutton: [], 6 | }); 7 | -------------------------------------------------------------------------------- /test/common.jsx: -------------------------------------------------------------------------------- 1 | //@include "../tree.jsx" 2 | 3 | var elements = Tree.parse({ 4 | button1: ['a1'], 5 | group1: { 6 | param: ['G1'], 7 | group1: ['G1-1'], 8 | slider1: ['a1'], 9 | button2: ['a2'], 10 | button3: ['a3'], 11 | }, 12 | group2: { 13 | param: ['G2'], 14 | group1: ['G2-1'], 15 | button1: ['b1'], 16 | button2: ['b2'], 17 | button3: ['b3'], 18 | }, 19 | group3: { 20 | param: ['G3'], 21 | group1: ['G3-1'], 22 | button1: ['c1'], 23 | button2: ['c2'], 24 | button3: ['c3'], 25 | }, 26 | }); 27 | 28 | var myButton1 = elements.getElementsByType('slider'); 29 | $.writeln(myButton1); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Raymond Yan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/Labels.jsx: -------------------------------------------------------------------------------- 1 | //@include "../tree.min.jsx" 2 | 3 | Tree.parse({ 4 | param: [, , , { resizeable: true }], 5 | style: { margins: 0, spacing: 0, alignChildren: ['fill', 'fill'] }, 6 | rectbutton01: [, , , { enableStroke: false, fillColor: '#000000', fillOpacity: [0.8, 1, 0.6, 1] }], 7 | rectbutton02: [, , , { enableStroke: false, fillColor: '#b53838', fillOpacity: [0.8, 1, 0.6, 1] }], 8 | rectbutton03: [, , , { enableStroke: false, fillColor: '#e4d84c', fillOpacity: [0.8, 1, 0.6, 1] }], 9 | rectbutton04: [, , , { enableStroke: false, fillColor: '#a9cbc7', fillOpacity: [0.8, 1, 0.6, 1] }], 10 | rectbutton05: [, , , { enableStroke: false, fillColor: '#e5bcc9', fillOpacity: [0.8, 1, 0.6, 1] }], 11 | rectbutton06: [, , , { enableStroke: false, fillColor: '#a9a9ca', fillOpacity: [0.8, 1, 0.6, 1] }], 12 | rectbutton07: [, , , { enableStroke: false, fillColor: '#e7c19e', fillOpacity: [0.8, 1, 0.6, 1] }], 13 | rectbutton08: [, , , { enableStroke: false, fillColor: '#b3c7b3', fillOpacity: [0.8, 1, 0.6, 1] }], 14 | rectbutton09: [, , , { enableStroke: false, fillColor: '#677de0', fillOpacity: [0.8, 1, 0.6, 1] }], 15 | rectbutton10: [, , , { enableStroke: false, fillColor: '#4aa44c', fillOpacity: [0.8, 1, 0.6, 1] }], 16 | rectbutton11: [, , , { enableStroke: false, fillColor: '#8e2c9a', fillOpacity: [0.8, 1, 0.6, 1] }], 17 | rectbutton12: [, , , { enableStroke: false, fillColor: '#e8920d', fillOpacity: [0.8, 1, 0.6, 1] }], 18 | rectbutton13: [, , , { enableStroke: false, fillColor: '#7f452a', fillOpacity: [0.8, 1, 0.6, 1] }], 19 | rectbutton14: [, , , { enableStroke: false, fillColor: '#f46dd6', fillOpacity: [0.8, 1, 0.6, 1] }], 20 | rectbutton15: [, , , { enableStroke: false, fillColor: '#3da2a5', fillOpacity: [0.8, 1, 0.6, 1] }], 21 | rectbutton16: [, , , { enableStroke: false, fillColor: '#a89677', fillOpacity: [0.8, 1, 0.6, 1] }], 22 | rectbutton17: [, , , { enableStroke: false, fillColor: '#1e401e', fillOpacity: [0.8, 1, 0.6, 1] }], 23 | rectbutton18: [, , 'F', { enableStroke: false }], 24 | rectbutton19: [, , 'X', { enableStroke: false }], 25 | rectbutton20: [, , 'S', { enableStroke: false }], 26 | }); 27 | -------------------------------------------------------------------------------- /tree.min.jsx: -------------------------------------------------------------------------------- 1 | var Tree=function(){function t(){for(var t=-1,n=arguments.length;++t0}t=String(t);var e=[];return tt([this],e,n,function(n){var e=ot(n);if(S(e))return!1;return t===e}),0===e.length?null:e[0]}function $t(){function t(){return targetNames.length===n.length}targetNames=Rn(arguments);var n=[],e=[];return tt([this],e,t,function(t){var e=ot(t);if(S(e))return!1;return b(targetNames,e)&&!b(n,e)&&n.push(e)}),0===e.length?null:e}function zt(){var t=Rn(arguments),n=[];return tt([this],n,R,function(n){return b(t,n.type)}),0===n.length?null:n}function At(t){return x(t)?{}:ft(t)}function Rt(t){return en(t)&&nn(t.global)&&t.global}function Ut(t){var n=T(t)?it(t):t;return String(n)}function Vt(t,n){return hn(n)?n:new Window(t[0],t[1],t[2],t[3])}function jt(t){return r(Gt(t),function(t){return t/255})}function Gt(t){var n;return t=t.replace(oe,"$1$1$2$2$3$3"),t.replace(ie,function(t,e,r,o){n=[parseInt(e,16),parseInt(r,16),parseInt(o,16)]}),n}function Kt(t,n){return jt(t).concat(n)}function Xt(t,n){t.layout.layout(n),t.layout.resize()}function Yt(t,n){var e=Vt(Yn(ut(t)),n);return e.onResizing=e.onResize=Dn,i(e,ft(t))}function Mt(t){return parseInt(Jn.appVersion)===t}function Ht(t,n){return function(e){return L(e)&&e>=t&&e<=n}}function Jt(t){return null!=t&&"[object BridgeTalk]"===f(t)}function Qt(t){return Ne.test(Ie[t])}function _t(t){return Fe.test(Ie[t])}function Zt(t){return De.test(Ie[t])}function qt(t){return F(Ht(0,3)(t),B(t)&&re.test(t))}function tn(t){return t===Hn}function nn(t){return null!=t&&"[object global]"===f(t)}function en(t){return null!=t&&"[object $]"===f(t)}function rn(t){return B(t)&&ie.test(t)}function on(t){return ce===t}function un(t){return null===t||!t.visible}function fn(t){return t.button===ze}function an(t){return Pe.test(Ie[t])}function cn(t){return $e.test(Ie[t])}function ln(t){return We.test(Ie[t])}function sn(t){return"node"===t}function hn(t){return t instanceof Panel}function pn(t){return hn(t)}function dn(t){return t.button===Ae}function gn(t){return Ee.test(Ie[t])}function bn(t){return"tabbedpanel"===t}function vn(t,n){var e=Ie[t]+Ie[n];return Be.test(e)}function yn(t){return tn(t)||pn(t)}function mn(t){return m(Ie,t)}function xn(t,n){return!!t&&b(n,t.appName)}function kn(t){return t instanceof Window}function wn(t){return Ke[t]}function On(t){return a(t,Ue)}function Sn(t){return B(t)?ScriptUI.FontStyle[t.toUpperCase()]:t}function Cn(t){return P(t,function(t,n){return b(Ge,n)&&!x(t)?U(4,g(t)):t})}function Ln(t){return r(t,function(t){return C(t)?void 0:t})}function Tn(t){return r(n(Y,null,X(a(t,Ve))),function(t){var e=h(t,3),o=Y(e[0],e[1]),i=r(o,function(t){return n(Kt,null,t)});return i.concat(e[2])})}function In(t,n,e){return t.add(n,e[1],e[2],e[3])}function Bn(t,n,e){return t.add(n,e[1],e[2],e[3],e[4],e[5])}function Nn(t,n){return t.add("item",n)}function Pn(t,n){return t.add("node",n)}function En(t,n,e,r){return{text:this.newPen(this.PenType.SOLID_COLOR,t,1),fill:this.newBrush(this.BrushType.SOLID_COLOR,n),stroke:this.newPen(this.PenType.SOLID_COLOR,e,r)}}function Dn(){this.layout.resize()}function Fn(t){if(!T(t))return null;var e=n(st,null,$n(t,_n));return ve.push(e),e}function Wn(t){e(t,function(t){var n=t.container,e=t.itemIndex;if(bn(n.type))return n.selection=t.itemIndex;var o=r(x(e)?e:[e],function(t){return n.items[t]});n.selection=o})}function $n(t,n){var e=i(p(cr),rt(t)),r=!!e.show,o=!!e.dockable,u=!!e.singleton,f=zn(n.context,o,u),a=Vn(n.layoutMode,e.layoutMode);return[u,t,f,r,a]}function zn(t,n,e){if(e||!n)return Window;if(yn(Mn))return Mn;return yn(t)?t:Window}function An(t,n){return Gn(Ft(t),n)}function Rn(t){return G(r([].concat.apply([],t),String))}function Un(t){e(t,function(t){var n=t.prototype;n.getElementById=Wt,n.getElementsByName=$t,n.getElementsByType=zt,Nt(n,"getElementById"),Nt(n,"getElementsByName"),Nt(n,"getElementsByType")})}function Vn(t,n){if(Ht(0,2)(n))return n;if(Ht(0,2)(t))return t;return 0}function jn(t,n){e(Re,function(e){t.addEventListener(e,n)})}function Gn(t,n){var e=t[0];if(S(e))return t;var r=Dt(n),o=t[r];if(T(o)||(t[r]={}),m(o,"name"))return t;return t[r].name=e,t}function Kn(t){return t.addEventListener("mouseup",function(n){n.stopPropagation(),w(t.onClick)&&fn(n)&&t.onClick()}),t}function Xn(t,n){return jn(t,function(e){if(e.stopPropagation(),dn(e))return;t.graphics.pen=n[e.type],t.notify("onDraw")}),t}function Yn(t){var n=String(t[0]);return t[0]=b(we,n)?n:Se,t}var Mn=this,Hn=Rt($),Jn=Pt(Hn),Qn=["aftereffects","photoshop","illustrator","indesign","estoolkit"];if(!xn(Jn,Qn))return null;var _n={},Zn="0.3.6",qn=1/0,te=/\d/g,ne=/\\(\\)?/g,ee=/\)$|^\(/g,re=/REGULAR|BOLD|ITALIC|BOLDITALIC/i,oe=/^#?([a-f\d])([a-f\d])([a-f\d])$/gi,ie=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/gi,ue=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,fe=/^\w*$/,ae=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,ce=Jn.appName,le=on("aftereffects"),se=on("photoshop"),he=on("illustrator"),pe=on("indesign"),de=on("estoolkit"),ge=le&&Mt(16),be=he||pe,ve=[],ye=se?1:0,me=he?4.5:5,xe=2.4,ke={aftereffects:["dialog","palette","window"],photoshop:"dialog",illustrator:["dialog","palette","window"],indesign:"dialog",estoolkit:["dialog","palette","window"]},we=ke[ce],Oe={aftereffects:"palette",photoshop:"dialog",illustrator:"palette",indesign:"dialog",estoolkit:"palette"},Se=Oe[ce],Ce={button:3,checkbox:3,dropdownlist:3,edittext:3,iconbutton:3,image:3,listbox:3,progressbar:4,radiobutton:3,scrollbar:5,slider:5,statictext:3,treeview:3},Le={group:2,panel:3,tab:3,tabbedpanel:3},Te={rectbutton:3,roundbutton:3,angle:3},Ie={button:"A",checkbox:"B",dialog:"C",dropdownlist:"D",edittext:"E",group:"G",iconbutton:"H",image:"I",item:"J",listbox:"K",node:"L",palette:"M",panel:"N",progressbar:"O",radiobutton:"P",scrollbar:"Q",slider:"R",statictext:"S",tab:"T",tabbedpanel:"U",treeview:"V",window:"W",rectbutton:"X",roundbutton:"Y"};de&&i(Ie,{flashplayer:"F"}),se&&K(Ie,"treeview");var Be=/[CGMNTW][ABDEFGHIKNOPQRSUVXYZ]|[DK]J|[VL][LJ]|UT/,Ne=/[DGKLNTUV]/,Pe=/[DKLV]/,Ee=/[DKUV]/,De=/[XYZ]/,Fe=/[XY]/,We=/[GNTU]/,$e=/[ABDEFHIKOPQRSV]/,ze=0,Ae=2,Re=["mouseover","mouseout","mousedown","mouseup"],Ue=["fontName","fontStyle","fontSize"],Ve=["fontColor","fillColor","strokeColor","fontOpacity","fillOpacity","strokeOpacity","strokeWidth"],je=["enableText","enableFill","enableStroke","fontOffset"],Ge=["fontColor","fillColor","strokeColor","fontOpacity","fillOpacity","strokeOpacity","strokeWidth"],Ke={rectbutton:"customView",roundbutton:"customView",angle:"customBoundedValue"},Xe=["enableText","enableFill","enableStroke","fontName","fontStyle","fontSize","fontOffset","fontColor","fillColor","strokeColor","fontOpacity","fillOpacity","strokeOpacity","strokeWidth"],Ye=Xe,Me={aftereffects:2,photoshop:1,illustrator:2,indesign:2,estoolkit:2},He=U(4,g(Me[ce])),Je={aftereffects:[0,0],photoshop:[0,0],illustrator:[0,2],indesign:[0,3],estoolkit:[0,-1]},Qe=Je[ce],_e={aftereffects:["#161616","#8a8a8a","#161616","#ffffff"],photoshop:["#f0f0f0","#f0f0f0","#f0f0f0","#ffffff"],illustrator:["#4b4b4b","#ffffff","#ffffff","#ffffff"],indesign:["#4b4b4b","#ffffff","#ffffff","#ffffff"],estoolkit:["#000000","#000000","#000000","#000000"]},Ze=_e[ce],qe={aftereffects:["#8a8a8a","#232323","#636363","#2d8ceb"],photoshop:["#454545","#454545","#363636","#454545"],illustrator:["#ffffff","#535353","#46a0f5","#46a0f5"],indesign:["#ffffff","#535353","#46a0f5","#46a0f5"],estoolkit:["#e5f1fb","#e1e1e1","#cce4f7","#e5f1fb"]},tr=qe[ce],nr={aftereffects:["#8a8a8a","#8a8a8a","#636363","#2d8ceb"],photoshop:["#666666","#666666","#636363","#1473e7"],illustrator:["#ffffff","#ffffff","#46a0f5","#46a0f5"],indesign:["#ffffff","#ffffff","#46a0f5","#46a0f5"],estoolkit:["#0078d7","#adadad","#005499","#0078d7"]},er=nr[ce],rr={aftereffects:[0,0,80,28],photoshop:[0,0,65,25],illustrator:[0,0,80,26],indesign:[0,0,80,28],estoolkit:[0,0,78,23]},or=rr[ce],ir={aftereffects:[0,0,30,30],photoshop:[0,0,28,28],illustrator:[0,0,28,28],indesign:[0,0,30,30],estoolkit:[0,0,23,23]},ur=ir[ce],fr={text:"",bounds:or},ar={text:"",bounds:ur},cr={dockable:!0,show:!0,singleton:!1},lr={enableText:!0,enableFill:!0,enableStroke:!0,fontName:"Tahoma",fontStyle:"REGULAR",fontSize:12,fontOffset:[0,0],fontColor:Ze,fillColor:tr,strokeColor:er,fontOpacity:[1,1,1,1],fillOpacity:[1,1,1,1],strokeOpacity:[1,1,1,1],strokeWidth:He},sr=lr,hr={enableText:k,enableFill:k,enableStroke:k,fontName:B,fontStyle:qt,fontSize:Ht(1,1e3),fontOffset:[L,L],fontColor:[rn,rn,rn,rn],fillColor:[rn,rn,rn,rn],strokeColor:[rn,rn,rn,rn],fontOpacity:[Ht(0,1),Ht(0,1),Ht(0,1),Ht(0,1)],fillOpacity:[Ht(0,1),Ht(0,1),Ht(0,1),Ht(0,1)],strokeOpacity:[Ht(0,1),Ht(0,1),Ht(0,1),Ht(0,1)],strokeWidth:[Ht(0,10),Ht(0,10),Ht(0,10),Ht(0,10)]},pr=hr;return Un([Window,Panel,Group]),_n.parse=Fn,_n.version=Zn,_n}.call(this); -------------------------------------------------------------------------------- /test/nativeControl.jsx: -------------------------------------------------------------------------------- 1 | //@include "../tree.min.jsx" 2 | // function _arrayEach(array, iteratee) { 3 | // var index = -1; 4 | // var length = array.length; 5 | // while (++index < length) iteratee(array[index], index, array); 6 | // } 7 | 8 | var configWindow = Tree.parse({ 9 | config: { singleton: true }, 10 | checkbox: [undefined, undefined, '启用'], 11 | }); 12 | 13 | var mainWindow = Tree.parse({ 14 | button: { 15 | style: { onClick: configWindow }, 16 | param: [undefined, undefined, '设置'], 17 | }, 18 | }); 19 | 20 | var elements = Tree.parse({ 21 | config: { show: false }, 22 | group: { 23 | style: { orientation: 'row' }, 24 | param: ['myGroup'], 25 | button1: ['myButton1'], 26 | button2: ['myButton2'], 27 | }, 28 | }); 29 | var myButton1 = elements.findElement('myButton1'); 30 | myButton1.text = 'YOOO'; 31 | 32 | elements.show(); 33 | 34 | // var found = new Array(50); 35 | 36 | // var elements = Tree.parse({ 37 | // // param: ['dialog'], 38 | // progressbar: ['pbar', undefined, 0, found.length], 39 | // }); 40 | 41 | // var pbar = elements.getElementById('pbar'); 42 | 43 | // _arrayEach(found, function (value, index) { 44 | // pbar.value = index + 1; 45 | // $.sleep(200); 46 | // }); 47 | 48 | // var elements = Tree.parse({ 49 | // group: { 50 | // param: ['AHH'], 51 | // button: ['AHH'], 52 | // checkbox: [], 53 | // dropdownlist: { 54 | // item1: 1, 55 | // item2: 2, 56 | // item3: 3, 57 | // }, 58 | // edittext: [], 59 | // flashplayer: [], 60 | // group: ['YOO'], 61 | // iconbutton: [], 62 | // image: [], 63 | // listbox: ['HEI'], 64 | // panel: ['WOO'], 65 | // progressbar: [], 66 | // radiobutton: [], 67 | // scrollbar: [], 68 | // slider: ['POO'], 69 | // statictext: [], 70 | // tabbedpanel: ['EMM'], 71 | // treeview: ['YAA'], 72 | // }, 73 | // }); 74 | 75 | // var elements = Tree.parse({ 76 | // config: { show: false }, 77 | // style: { alignChildren: ['fill', ''] }, 78 | // param: [, , , { resizeable: true }], 79 | // button1: [], 80 | // edittext1: [], 81 | // button2: [], 82 | // edittext2: [], 83 | // button3: [], 84 | // edittext3: [], 85 | // button4: [], 86 | // edittext4: [], 87 | // button5: [], 88 | // edittext5: [], 89 | // button6: [], 90 | // edittext6: [], 91 | // button7: [], 92 | // edittext7: [], 93 | // button8: [], 94 | // edittext8: [], 95 | // button9: [], 96 | // edittext9: [], 97 | // }); 98 | 99 | // _arrayEach(elements.getElementsByType('button'), function (element, index) { 100 | // element.text = index; 101 | // element.onClick = function () { 102 | // alert(this.text); 103 | // }; 104 | // }); 105 | 106 | // _arrayEach(elements.getElementsByType('edittext'), function (element, index) { 107 | // element.text = index; 108 | // }); 109 | 110 | // elements.show(); 111 | 112 | // var elements = Tree.parse({ 113 | // tabbedpanel: { 114 | // param: ['TAB'], 115 | // style: { selection: 0 }, 116 | // tab1: { 117 | // param: [, , 'A'], 118 | // tabbedpanel: { 119 | // param: ['TAD'], 120 | // style: { selection: 1 }, 121 | // tab1: { 122 | // param: [, , 'C'], 123 | // tabbedpanel: ['TAD'], 124 | // }, 125 | // tab2: [, , 'D'], 126 | // }, 127 | // }, 128 | // tab2: [, , 'B'], 129 | // }, 130 | // }); 131 | 132 | // var elements = Tree.parse({ 133 | // config: { show: true }, 134 | // panel: { 135 | // button: ['AHH'], 136 | // tabbedpanel: { 137 | // param: ['KKK'], 138 | // tab: { 139 | // group: { 140 | // group: { 141 | // param: ['TARGETGROUP'], 142 | // group: { 143 | // group: { 144 | // group: { 145 | // param: ['AHHh'], 146 | // button: ['HOO'], 147 | // checkbox: [], 148 | // dropdownlist: ['HOO'], 149 | // edittext: [], 150 | // flashplayer: [], 151 | // group: { 152 | // group: ['YOOO'], 153 | // }, 154 | // iconbutton: [], 155 | 156 | // scrollbar: [], 157 | // slider: ['KAA'], 158 | // statictext: [], 159 | // tabbedpanel: ['222'], 160 | // button2: ['BAAA'], 161 | // }, 162 | // }, 163 | // }, 164 | // }, 165 | // }, 166 | // }, 167 | // }, 168 | // iconbutton: [], 169 | // listbox: ['HEI'], 170 | // slider: ['HEI'], 171 | // statictext: [], 172 | // tabbedpanel2: ['EMM'], 173 | // treeview: ['YAA'], 174 | // }, 175 | // }); 176 | 177 | // elements.getElementById('AHH').text = 'tes'; 178 | 179 | // _arrayEach(elements.getElementById('TARGETGROUP').getElementsByType('button'), function (element, index) { 180 | // element.text = index; 181 | // }); 182 | 183 | // function foo() { 184 | // alert('Yoooooo!!!'); 185 | // } 186 | 187 | // var elements = Tree.parse({ 188 | // style: { margins: 5, alignChildren: ['fill', 'fill'] }, 189 | // param: ['palette', '', undefined, { resizeable: true }], 190 | // group1: { 191 | // style: { margins: 0, spacing: 0, orientation: 'column', alignChildren: ['fill', 'fill'] }, 192 | // param: ['mainGroup1', undefined, undefined], 193 | // edittext1: { 194 | // style: { preferredSize: [180, 230] }, 195 | // param: ['console', undefined, 'console', { multiline: true, scrolling: true }], 196 | // }, 197 | // }, 198 | // group2: { 199 | // style: { orientation: 'column', alignChildren: ['fill', 'fill'], alignment: ['fill', 'bottom'] }, 200 | // param: ['paramGroup1', undefined, undefined], 201 | // group1: { 202 | // style: { orientation: 'row', alignment: ['fill', 'bottom'] }, 203 | // param: ['dropdownlistGroup'], 204 | // statictext1: [undefined, [0, 0, 30, 25], '方向'], 205 | // dropdownlist1: { 206 | // style: { alignment: ['fill', ''], selection: 3 }, //定义下拉列表的默认选项 207 | // param: ['direction', [0, 0, 170, 25], ['+x', '-x', '+y', '-y']], 208 | // }, 209 | // }, 210 | // group2: { 211 | // style: { spacing: 5, orientation: 'row', alignChildren: ['fill', 'fill'], alignment: ['fill', 'bottom'] }, 212 | // param: ['settingGroup'], 213 | // group1: { 214 | // style: { orientation: 'column', alignment: ['left', 'bottom'] }, 215 | // param: ['mainGroup'], 216 | // statictext1: ['time', [0, 0, 30, 25], '时间'], 217 | // statictext2: ['transition', [0, 0, 30, 25], '过渡'], 218 | // statictext3: ['distance', [0, 0, 30, 25], '距离'], 219 | // }, 220 | // group2: { 221 | // style: { orientation: 'column', alignChildren: ['fill', 'fill'] }, 222 | // param: ['mainGroup'], 223 | // slider1: ['time', [0, 0, 140, 25], 1, 0, 3], 224 | // slider2: ['transition', [0, 0, 140, 25], 1, 0, 3], 225 | // slider3: ['distance', [0, 0, 140, 25], 1, 0, 3], 226 | // }, 227 | // group3: { 228 | // style: { orientation: 'column', alignment: ['right', 'bottom'] }, 229 | // param: ['mainGroup'], 230 | // edittext1: ['time', [0, 0, 45, 25], '10'], 231 | // edittext2: ['transition', [0, 0, 45, 25], '10'], 232 | // edittext3: ['distance', [0, 0, 45, 25], '10'], 233 | // }, 234 | // }, 235 | // }, 236 | // button1: { 237 | // style: { onClick: foo }, //添加事件侦听 238 | // param: ['button', undefined, '添加'], 239 | // }, 240 | // }); 241 | 242 | // _arrayEach(elements.getElementsByType('edittext', 'statictext', 'button'), function (element) { 243 | // element.text = 'YOOOO'; 244 | // }); 245 | 246 | // _arrayEach(elements.getElementsByType('slider'), function (element) { 247 | // element.value = 0; 248 | // }); 249 | 250 | // elements.getElementById('direction').selection = 1; 251 | 252 | // Tree.parse({ 253 | // param: ['', , , { resizeable: false }], 254 | // style: { margins: 5, spacing: 10, alignChildren: undefined }, 255 | // rectbutton01: [null, , , { enableStroke: false, fillColor: '#000000', fillOpacity: [0.8, 1, 0.6, 1] }], 256 | // rectbutton02: [, , , { enableStroke: false, fillColor: '#b53838', fillOpacity: [0.8, 1, 0.6, 1] }], 257 | // rectbutton03: [, , , { enableStroke: false, fillColor: '#e4d84c', fillOpacity: [0.8, 1, 0.6, 1] }], 258 | // rectbutton04: [, , , { enableStroke: false, fillColor: '#a9cbc7', fillOpacity: [0.8, 1, 0.6, 1] }], 259 | // rectbutton05: [, , , { enableStroke: false, fillColor: '#e5bcc9', fillOpacity: [0.8, 1, 0.6, 1] }], 260 | // rectbutton06: [, , , { enableStroke: false, fillColor: '#a9a9ca', fillOpacity: [0.8, 1, 0.6, 1] }], 261 | // rectbutton07: [, , , { enableStroke: false, fillColor: '#e7c19e', fillOpacity: [0.8, 1, 0.6, 1] }], 262 | // rectbutton08: [, , , { enableStroke: false, fillColor: '#b3c7b3', fillOpacity: [0.8, 1, 0.6, 1] }], 263 | // rectbutton09: [, , , { enableStroke: false, fillColor: '#677de0', fillOpacity: [0.8, 1, 0.6, 1] }], 264 | // rectbutton10: [, , , { enableStroke: false, fillColor: '#4aa44c', fillOpacity: [0.8, 1, 0.6, 1] }], 265 | // rectbutton11: [, , , { enableStroke: false, fillColor: '#8e2c9a', fillOpacity: [0.8, 1, 0.6, 1] }], 266 | // rectbutton12: [, , , { enableStroke: false, fillColor: '#e8920d', fillOpacity: [0.8, 1, 0.6, 1] }], 267 | // rectbutton13: [, , , { enableStroke: false, fillColor: '#7f452a', fillOpacity: [0.8, 1, 0.6, 1] }], 268 | // rectbutton14: [, , , { enableStroke: false, fillColor: '#f46dd6', fillOpacity: [0.8, 1, 0.6, 1] }], 269 | // rectbutton15: [, , , { enableStroke: false, fillColor: '#3da2a5', fillOpacity: [0.8, 1, 0.6, 1] }], 270 | // rectbutton16: [, , , { enableStroke: false, fillColor: '#a89677', fillOpacity: [0.8, 1, 0.6, 1] }], 271 | // rectbutton17: [, , , { enableStroke: false, fillColor: '#1e401e', fillOpacity: [0.8, 1, 0.6, 1] }], 272 | // rectbutton18: [, , '运行', { enableStroke: false }], 273 | // rectbutton19: [, , '菜单', { enableStroke: false }], 274 | // rectbutton20: [, , '设置', { enableStroke: false }], 275 | // }); 276 | 277 | // var elements = Tree.parse({ 278 | // treeview: { 279 | // param: [{}, [0, 0, 500, 500]], 280 | // node2: { param: 1 }, 281 | // node: { 282 | // style: { image: File('/D/icon.png'), expanded: true }, 283 | // param: {}, 284 | // item: { 285 | // style: { image: File('/D/icon.png') }, 286 | // param: '你好世界', 287 | // }, 288 | // node: { 289 | // style: { image: File('/D/icon.png'), expanded: true }, 290 | // param: 'node', 291 | // item: { 292 | // style: { image: File('/D/icon.png') }, 293 | // param: '你好世界', 294 | // }, 295 | // }, 296 | // }, 297 | // }, 298 | // }); 299 | 300 | // var elements = Tree.parse({ 301 | // treeview: { 302 | // param: ['A', [0, 0, 500, 500], [1, 2, 3, 4, 5, 6]], 303 | // item: { 304 | // style: { image: new File('/d/icon.png') }, 305 | // param: 'ssss', 306 | // }, 307 | // node: { 308 | // style: { expanded: true, image: new File('/d/icon.png') }, 309 | // param: '一级', 310 | // item: '1', 311 | // node: { 312 | // param: '二级', 313 | // node: { 314 | // param: '三级', 315 | // node: { 316 | // param: '四级', 317 | // node: '2', 318 | // }, 319 | // }, 320 | // }, 321 | // }, 322 | // node1: { 323 | // param: '一级', 324 | // node: { 325 | // param: '二级', 326 | // node: { 327 | // param: '三级', 328 | // node: { 329 | // param: '四级', 330 | // node: '2', 331 | // }, 332 | // }, 333 | // }, 334 | // }, 335 | // }, 336 | // }); 337 | 338 | // var elements = Tree.parse({ 339 | // param: ['dialog', '', undefined, { resizeable: true }], 340 | // style: { margins: 5, spacing: 5, alignChildren: ['center', 'top'] }, 341 | // button1: [], 342 | // panel: { 343 | // style: { margins: [5, 5, 0, 5], orientation: 'row', size: [189, 29] }, 344 | // radiobutton1: [, [0, 0, 50, 15], '应用'], 345 | // radiobutton2: [, [0, 0, 50, 15], '参数'], 346 | // }, 347 | // panel1: { 348 | // style: { margins: [5, 0, 0, 0] }, 349 | // statictext: { 350 | // param: [, [0, 0, 180, 25], '© 2022 Raymond Yan', 1], 351 | // style: { alignment: ['left', 'center'] }, 352 | // }, 353 | // }, 354 | // panel2: { 355 | // style: { margins: 5, spacing: 10, alignChildren: ['fill', 'top'] }, 356 | // group1: { 357 | // style: { margins: 0, alignment: 'left' }, 358 | // radiobutton: [, [0, 0, 170, 15], '帧指示器'], 359 | // }, 360 | // group2: { 361 | // style: { margins: 0, spacing: 10, alignChildren: ['fill', ''], orientation: 'column' }, 362 | // radiobutton: [, [0, 0, 170, 15], '外部数据'], 363 | // group1: { 364 | // style: { margins: 0, spacing: 5, alignChildren: ['fill', ''] }, 365 | // rectbutton1: [, [0, 0, 145, 26], 'Final Cut Pro XML'], 366 | // rectbutton2: { 367 | // param: [, [0, 0, 25, 26], '?'], 368 | // style: { alignment: ['right', ''] }, 369 | // }, 370 | // }, 371 | // }, 372 | // group3: { 373 | // style: { margins: 0, spacing: 5, alignChildren: ['fill', ''], orientation: 'column' }, 374 | // radiobutton: [, , '自定义时间'], 375 | // group1: { 376 | // style: { spacing: 5, alignChildren: ['fill', ''] }, 377 | // edittext1: [, [0, 0, 40, 26], '0'], 378 | // edittext2: [, [0, 0, 40, 26], '0'], 379 | // edittext3: [, [0, 0, 40, 26], '0'], 380 | // edittext4: [, [0, 0, 40, 26], '0'], 381 | // }, 382 | // group2: { 383 | // style: { spacing: 5, alignChildren: ['fill', ''] }, 384 | // edittext1: [, [0, 0, 85, 26], '0:00:00:000'], 385 | // edittext2: [, [0, 0, 85, 26], '00000'], 386 | // }, 387 | // }, 388 | // }, 389 | // panel3: { 390 | // style: { margins: 5, spacing: 5, alignChildren: ['fill', ''], orientation: 'row' }, 391 | // rectbutton1: ['fo', [0, 0, 55, 26], '缩短'], 392 | // rectbutton2: [, , '匹配'], 393 | // rectbutton3: [, , '延长'], 394 | // }, 395 | // treeview: { 396 | // param: [, [0, 0, 500, 500]], 397 | // node: { 398 | // style: { expanded: true }, 399 | // node: { 400 | // node: { 401 | // style: { expanded: true }, 402 | // item1: 1, 403 | // item2: 2, 404 | // item3: 3, 405 | // item4: 4, 406 | // item5: 5, 407 | // }, 408 | // item2: 2, 409 | // item3: 3, 410 | // item4: 4, 411 | // node2: { 412 | // item1: 1, 413 | // item2: 2, 414 | // item3: 3, 415 | // item4: 4, 416 | // item5: 5, 417 | // }, 418 | // }, 419 | // item2: 2, 420 | // item3: 3, 421 | // item4: 4, 422 | // item5: 5, 423 | // }, 424 | // }, 425 | // }); 426 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | TreeUI (简称 Tree)是一款支持 `Ae` `Ps` `Ai` `Id` 的 `ScriptUI` 解析库。它基于 `Extend Script` 语言(ECMAScript 3标准的一个实现)构建。它提供了一种介于 ScriptUI 自身实现的 `add` 方法和 `Resource specifications` 构建形式之间的脚本 UI 构建方式,即使用多级树结构对象去描述并构建UI。可以帮助脚本开发者高效的开发脚本 UI。 4 | 5 | 6 | 7 | 下面是一个运行在 Ae 中的基本示例: 8 | 9 | ```javascript 10 | //@include "tree.min.js" 11 | 12 | Tree.parse({ button: [] }); 13 | ``` 14 | 15 | 结果展示 16 | 17 | ![image](image/sample1.png) 18 | 19 | # 导入 20 | 21 | 根据你的使用场景和个人偏好,在使用 TreeUI 时,可以选择不同的导入方式 22 | 23 | 24 | 25 | ## 通过在脚本头部粘贴源码进行导入(推荐) 26 | 27 | 脚本通常具有绿色、轻量的优点,如果通过多文件管理代码,将失去脚本的这些优点,这是推荐该导入方式的原因。 28 | 29 | ```javascript 30 | var Tree=function(){/*....*/}.call(this) 31 | ``` 32 | 33 | ✏️ 推荐在生产环境中使用 `tree.min.js` ,即经过压缩的版本,这样可以有效减少脚本文件的体积。 34 | 35 | 36 | 37 | ## 通过 Extend Script 预处理指令导入 38 | 39 | ```javascript 40 | //@include "tree.min.js" 41 | ``` 42 | 43 | 44 | 45 | ## 上下文配置 46 | 47 | 如果你将 TreeUI 源码放置于全局环境下(即脚本的顶部,通常为第一行),或者不为 Ae 开发脚本,则可以跳过此环节,直接进入后面的章节: 48 | 49 | ```javascript 50 | var Tree = function () {/*....*/}.call(this); 51 | 52 | Tree.parse({ button: [] }); 53 | ``` 54 | 55 | TreeUI 特别针对 Ae 封装了可停靠面板的逻辑代码,即运行在 `ScriptUI Panels` 目录下的脚本 UI 可以在 Ae 中进行停靠的特性。受限于可停靠的底层检测逻辑,必须向解析器传递全局环境作为判断依据,否则解析后的 UI 将无法正常在 Ae 中停靠。 56 | 57 | 下面为配置方法: 58 | 59 | ```javascript 60 | (function (global) { 61 | var Tree = function () {/*....*/}.call(this); 62 | 63 | Tree.context = global; 64 | 65 | Tree.parse({ button: [] }); 66 | })(this); 67 | ``` 68 | 69 | 70 | 71 | ## 全局变量 72 | 73 | TreeUI 默认使用标识符 `Tree` 作为全局变量,TreeUI 的所有属性和方法都添加到这个变量。 74 | 75 | 如果你需要将 `Tree` 改为其他变量名,可以通过以下方式实现: 76 | 77 | ```javascript 78 | var Foo = function () {/*....*/}.call(this); 79 | Foo.parse({ button: [] }); 80 | ``` 81 | 82 | 83 | 84 | # 兼容性 85 | 86 | TreeUI 对 ScriptUI 有着完全的兼容性,并在此基础之上简化 UI 构建流程。兼容性主要体现在以下方面: 87 | 88 | - 元素兼容:TreeUI 支持解析 ScriptUI 中的所有元素。 89 | - 参数兼容:TreeUI 支持解析 ScriptUI 中所有元素的创建参数,且未对原有传参顺序进行调整。 90 | - 样式兼容:TreeUI 支持解析 ScriptUI 中所有元素的样式属性。 91 | - 组合兼容:TreeUI 完全同步 ScriptUI 中所有可能的元素组合方式。 92 | 93 | 当你已经熟悉 ScriptUI 传统的构建方式时,完全不需要花费更多额外的时间去学习一套新的理论,一切都是平滑过渡的。 94 | 95 | 96 | 97 | # ScriptUI 基础 98 | 99 | 如果你从来没有了解过 ScriptUI,或知之甚少,那么阅读本章将会帮你快速建立 ScriptUI 知识框架,以便之后快速上手 TreeUI。 100 | 101 | 因为这不是一份描述 ScriptUI 的文档,所以本章不会过多描述 ScriptUI 的各种细节,如需获取完整信息,请阅读[官方文档](https://extendscript.docsforadobe.dev/user-interface-tools/index.html)。 102 | 103 | ✏️ 为了保证示例阅读的简洁性,本章及之后的示例代码中,将不会出现导入及配置 TreeUI 部分的代码。 104 | 105 | 106 | 107 | ## ScriptUI 108 | 109 | ScriptUI 是 Adobe 提供的内置组件,它与 ExtendScript JavaScript 解释器一起工作,为 JavaScript 脚本提供创建用户界面元素并与之交互的能力。 110 | 111 | 112 | 113 | ## ESTK 114 | 115 | 全称 Adobe ExtendScript Toolkit CC,是 Adobe 专门为脚本编写设计的集成开发环境(IDE),包含 ScriptUI 的完整实现。它可以非常方便的让脚本在指定 Adobe 应用程序中运行,同时具备一些基本的调试功能。虽然官方早已停止维护该软件,但相对所有 Adobe 应用来说,它依然是目前最便捷的脚本调试工具。TreeUI 中的所有特性可以完整的在 ESTK 中展现,如果你当前处于学习阶段,那么推荐你直接在 ESTK 中运行下文中的案例。 116 | 117 | 118 | 119 | ## 宿主 120 | 121 | 宿主又称宿主应用,我们可以通俗的将宿主理解为 Adobe 公司开发的软件,一款软件就是一个宿主,比如 Ae 就是一个宿主。脚本依赖于宿主运行。不同的宿主在渲染 ScriptUI 元素外观时会有不同的策略,所以你会发现在 Ae 中运行良好的 UI,放到 Ps 中就会出现错误,甚至外观完全不同,这是正常的,如果你同时为多款 Adobe 应用程序开发脚本,处理这种差异,也是编写脚本的工作之一。在后面的章节中,将使用“宿主应用”指代“Adobe 应用程序”。 122 | 123 | 124 | 125 | ## 元素 126 | 127 | 元素是 ScriptUI 中对于容器与控件的统称,它们通常被“装”在操作系统的窗口中,属于窗口的 UI 元素可以是容器或控件。 128 | 129 | 130 | 131 | ## 容器与控件 132 | 133 | ScriptUI 中的元素分为两大类,即容器与控件。 134 | 135 | 区分容器与控件是一件很基础却又很重要的事情。容器通常可以包含其它容器或控件,所谓包含,即我们可以向容器中添加容器和控件,但通常无法向控件中添加容器和控件。容器是我们组织 UI 布局的的基本框架和单元,我们可以把容器和控件分别比作计算机中的文件夹和文件,而主容器,即下文中将提到的 `Window` 就相当于计算机中的 C 盘、D盘。有些控件同时具有容器和控件的特征,继续阅读下面的内容,你将了解这些特征。 136 | 137 | 138 | 139 | ### 父子级 140 | 141 | 从容器度的角度说,装在容器中的容器或控件通常称作它的子级(child element),反之则为父级(parent element)。 142 | 143 | 144 | 145 | ### 元素名称与元素类型名称 146 | 147 | 元素名称是描述一个元素叫什么的书面用语,而元素类型名称则是在编程过程中作为方法参数传递的字符串,它是区分大小写的,所以之后涉及元素详情的表格中会相应的列出元素类型名称,以供参照。我们以元素类型名称 `button` 为例,以下示例展示了它被使用时的样子: 148 | 149 | ```javascript 150 | var button = container.add('button'); 151 | ``` 152 | 153 | 154 | 155 | ### 容器 156 | 157 | 容器是组织 UI 中其它元素和布局的基本工具,它具有给元素“打组”并进行统一控制的能力。容器可以包含其它容器,也就是说容器可以进行多级嵌套。 158 | 159 | 容器有以下三种: 160 | 161 | | 容器名称 | 类型名称 | 简述 | 162 | | -------- | -------- | ------------------------------------------------------------ | 163 | | Window | window | 窗口容器,可以添加不包括自身在内的所有容器和控件,它是所有元素的主容器,所以任何容器都不能“装”它。 | 164 | | Group | group | 组容器,可以添加包括自身在内的所有子容器和子控件。 | 165 | | Panel | panel | 面板容器,可以添加包括自身在内的所有子容器和子控件。 | 166 | 167 | 一般最常使用的容器只有 `Group` 和 `Panel` 。 168 | 169 | Window 容器有三种类型,且具有特定于操作系统的不同窗口外观: 170 | 171 | | Window 类型 | 类型名称 | 简述 | 172 | | ----------- | -------- | ------------------------------------------------------------ | 173 | | Dialog | dialog | 会生成一个学名叫“模态窗口”的窗口,这个窗口弹出时,你不能与窗口外的其它元素进行交互。 | 174 | | Palette | palette | 俗称调色板,这个窗口弹出时,你可以与窗口外的其它元素进行交互,所以它是目前最受欢迎的窗口类型。 | 175 | | Window | window | 与 dialog 无本质区别,在形式上(仅仅是形式上)可以用作应用程序的主窗口。事实上它很少在脚本开发中被使用。 | 176 | 177 | ✏️ TreeUI 会在内部生成一个默认的 Window 容器,所以你很少有机会和它打交道。但了解它的存在是必要的。 178 | 179 | 不同宿主应用对于 Window 类型的支持是存在差异的,下表展示了这些差异: 180 | 181 | | Window 类型 | 类型名称 | Ae | Ps | Ai | Id | ESTK | 182 | | ----------- | -------- | :--: | :--: | :--: | :--: | :--: | 183 | | Dialog | dialog | √ | √ | √ | √ | √ | 184 | | Palette | palette | √ | × | √ | × | √ | 185 | | Window | window | √ | × | √ | × | √ | 186 | 187 | `√` 代表支持,`×` 代表不支持。 188 | 189 | ⚠️ 在没有显式定义主容器类型的情况下,TreeUI 会针对不同宿主提供相应的默认主容器类型,并以 `palette` 类型优先,其次是 `dialog` 。 190 | 191 | ⚠️ **如果向宿主应用提供了不支持的 Window 类型参数,TreeUI 会自动将类型转换为默认类型。** 192 | 193 | 194 | 195 | ### 控件 196 | 197 | 控件一般是 UI 中直接可见与可交互的元素,同时也是实现 UI 功能的主要载体。 198 | 199 | 下表简述了不同控件的功能: 200 | 201 | | 控件名称 | 类型名称 | 简述 | 202 | | ------------- | ------------ | ---------------------------------------------- | 203 | | Button | button | 按钮 | 204 | | Checkbox | checkbox | 复选框 | 205 | | Dropdown List | dropdownlist | 下拉列表 | 206 | | Edit Text | edittext | 输入框 | 207 | | Flash Player | flashplayer | Flash 播放器。已被官方弃用,仅在 estk 中可用。 | 208 | | Icon Button | iconbutton | 图标按钮 | 209 | | Image | image | 图像 | 210 | | Listbox | listbox | 列表 | 211 | | Progress Bar | progressbar | 进度条 | 212 | | Radio Button | radiobutton | 单选按钮 | 213 | | Scrollbar | scrollbar | 滚动条 | 214 | | Slider | slider | 滑杆 | 215 | | Static Text | statictext | 静态文本 | 216 | | Tabbed Panel | tabbedpanel | 选项卡 | 217 | | Treeview | treeview | 树视图 | 218 | 219 | 220 | 221 | ### 特殊的控件 222 | 223 | 有些控件对父级存在特定的类型要求,所以它们不算常规意义上的控件,下表描述了这些特殊控件的特征: 224 | 225 | | 控件名称 | 类型名称 | 特征 | 226 | | -------- | -------- | ---------------------------------------------------- | 227 | | Item | item | 父级只能是 Dropdown List、Listbox、Tree View 或 node | 228 | | Node | node | 父级只能是 Node (Node可以装Node)或 Tree View 控件 | 229 | | Tab | tab | 父级只能是 Tabbed Panel | 230 | 231 | 232 | 233 | ### 特殊的容器 234 | 235 | 有些控件表现出容器的特征,即可以添加其它控件或容器,但可添加的子级类型通常是单一的,下表描述了这些特殊容器的特征: 236 | 237 | | 控件名称 | 类型名称 | 特征 | 238 | | ------------- | ------------ | -------------------------------------------- | 239 | | Dropdown List | dropdownlist | 子级只能是 Item | 240 | | Listbox | listbox | 子级只能是 Item | 241 | | Tabbed Panel | tabbedpanel | 子级只能是 Tab | 242 | | Tab | tab | 子级可以是不包括它自身在内的所有容器或控件。 | 243 | | Treeview | treeview | 子级只能是 Node 或 Item | 244 | 245 | 246 | 247 | ## 元素组合 248 | 249 | 下表完整展示了 ScriptUI 中可能的父子元素组合方式,分隔线上方为父级,下方为子级,所有元素皆以类型名称表示: 250 | 251 | | 组合一 | 组合二 | 组合三 | 组合四 | 组合五 | 组合六 | 252 | | :-----------: | :-----------: | :-----------: | :-----------: | :-----------: | :-----------: | 253 | | window | | | | | | 254 | | tab | | | | | | 255 | | group | | | | | | 256 | | panel | dropdownlist | treeview | node | tabbedpanel | listbox | 257 | | ============= | ============= | ============= | ============= | ============= | ============= | 258 | | button | item | item | node | tab | item | 259 | | checkbox | | node | item | | | 260 | | dropdownlist | | | | | | 261 | | edittext | | | | | | 262 | | flashplayer | | | | | | 263 | | group | | | | | | 264 | | iconbutton | | | | | | 265 | | image | | | | | | 266 | | listbox | | | | | | 267 | | panel | | | | | | 268 | | progressbar | | | | | | 269 | | radiobutton | | | | | | 270 | | scrollbar | | | | | | 271 | | slider | | | | | | 272 | | statictext | | | | | | 273 | | tabbedpanel | | | | | | 274 | | treeview | | | | | | 275 | 276 | 277 | 278 | # TreeUI 快速入门 279 | 280 | 281 | 282 | ## 构建逻辑 283 | 284 | TreeUI 使用多级树结构对象去描述并构建 UI。ScriptUI 的 `Resource specifications` ,即资源字符串构建就是该方式的一种实现,但由于它完全是字符串形态,所以非常难以调试,相应的,使用 ScriptUI 容器的 `add` 方法是典型的过程式开发,需要借助大量的中间变量,同时 UI 元素之间的层级关系非常模糊,这同样让调试变得困难。而 TreeUI 在这两种方法之间寻求平衡,它直接使用对象字面量表示方式描述 UI,你将同时拥有当前主流编辑器(如 VsCode)的语法高亮、代码提示和符合直觉的 UI 构建方式,元素之间的层级关系将变得一目了然,且非常易于调试,这些也正是 TreeUI 被开发的原因和价值所在。 285 | 286 | 如果现在有这样一个需求:构建一个 UI,UI中有一个面板,面板中有一个输入框和按钮,不考虑元素样式。下面的示例就分别展示了使用不同方式实现该需求的方法: 287 | 288 | 使用 add 方法 289 | 290 | ```javascript 291 | var window = new Window('palette'); 292 | var panel = window.add('panel'); 293 | var editText = panel.add('edittext'); 294 | var button = panel.add('button'); 295 | window.show(); 296 | ``` 297 | 298 | 使用资源字符串 299 | 300 | ```javascript 301 | var window = new Window('palette { \ 302 | panel: Panel { \ 303 | editText: EditText {}, \ 304 | button: Button {} \ 305 | }, \ 306 | }'); 307 | window.show(); 308 | ``` 309 | 310 | 使用 TreeUI 311 | 312 | ```javascript 313 | var window = Tree.parse({ 314 | panel: { 315 | editText: [], 316 | button: [], 317 | }, 318 | }); 319 | ``` 320 | 321 | 322 | 323 | ## 元素创建 324 | 325 | TreeUI 始终使用对象键值对(我们通常叫对象的属性与值)描述 UI,即 key-value 形式。 326 | 327 | ``` 328 | { name: 'Raymond Yan' } 329 | ``` 330 | 331 | 以上代码使用一个对象描述一个人的姓名,那么在 TreeUI 中描述一个按钮,就是这样的: 332 | 333 | ``` 334 | { button: [] } 335 | ``` 336 | 337 | 其中 button 为[元素类型名称](#Container-Control),`[]` 是一个定义按钮参数的数组,示例中是一个空数组,即代表不添加任何参数,而使用该元素的默认参数。 338 | 339 | 同样,你可以使用这种方法同时描述一个输入框和一个滑杆: 340 | 341 | ``` 342 | { 343 | edittext: [], 344 | slider: [], 345 | } 346 | ``` 347 | 348 | 然后使用 `Tree.parse` 方法解析它们,就像这样: 349 | 350 | ```javascript 351 | var elements = Tree.parse({ 352 | button: [], 353 | edittext: [], 354 | slider: [], 355 | }); 356 | ``` 357 | 358 | 在 estk 中运行上面的代码,你就会看到一个包含按钮、输入框和滑杆的窗口,一切就是这么简单! 359 | 360 | 你甚至可以一次创建 ScriptUI 中所有的元素,看看它们的样子: 361 | 362 | ```javascript 363 | var elements = Tree.parse({ 364 | button: [], 365 | checkbox: [], 366 | dropdownlist: [], 367 | edittext: [], 368 | flashplayer: [], 369 | group: [], 370 | iconbutton: [], 371 | image: [], 372 | listbox: [], 373 | panel: [], 374 | progressbar: [], 375 | radiobutton: [], 376 | scrollbar: [], 377 | slider: [], 378 | statictext: [], 379 | tabbedpanel: [], 380 | treeview: [], 381 | }); 382 | 383 | ``` 384 | 385 | ⚠️ 由于不同宿主对于 ScriptUI 控件类型的支持存在差异,以上示例中的元素,在个别宿主中会无法显示,具体支持情况参考下表: 386 | 387 | 为了保证阅读体验,下表仅对不支持的控件使用 `×` 进行标识。 388 | 389 | | 控件类型 | Ae | Ps | Ai | Id | ESTK | 390 | | ------------ | :--: | :--: | :--: | :--: | :--: | 391 | | button | | | | | | 392 | | checkbox | | | | | | 393 | | dropdownlist | | | | | | 394 | | edittext | | | | | | 395 | | flashplayer | × | × | × | × | | 396 | | group | | | | | | 397 | | iconbutton | | | | | | 398 | | image | | | | | | 399 | | listbox | | | | | | 400 | | panel | | | | | | 401 | | progressbar | | | | | | 402 | | radiobutton | | | | | | 403 | | scrollbar | | | | | | 404 | | slider | | | | | | 405 | | statictext | | | | | | 406 | | tabbedpanel | | | | | | 407 | | treeview | | × | | | | 408 | 409 | 410 | 411 | ## 元素嵌套 412 | 413 | 如果要将控件装入某个容器,形成一种组结构,只需定义一个容器类型名称作为键名、新对象作为值的对象,并将要装入容器的控件类型名称以键值对的方式依次表示在新对象中即可,它非常符合直觉,我们通过一个示例来说明这种直觉匹配。例如现在我需要一个组,组里套个组,组里再装个面板,面板中才是按钮,那自然可以这么写: 414 | 415 | ```javascript 416 | var window = Tree.parse({ 417 | group: { 418 | group: { 419 | panel: { 420 | button: [], 421 | }, 422 | }, 423 | }, 424 | }); 425 | ``` 426 | 427 | ⚠️ 嵌套中的父子级类型名称自然是要满足组合要求的,例如,你不能将一个组嵌套至一个按钮内,这显然是不合常理的。你可以在[元素组合](#Element-combination)一节中查阅所有支持的父子级元素类型名称的组合方式。 428 | 429 | ⚠️ **TreeUI 会跳过不满足父子级搭配的子级控件,且存在错误组合的子级的所有子级都不会被解析。** 430 | 431 | 432 | 433 | ## 冲突处理 434 | 435 | 由于 TreeUI 使用字面量对象作为解析源,当在对象同一层级中同时指定多个同类型控件或容器的类型名称时,将会不可避免的发生对象键值覆盖,下面的示例展示了这种冲突: 436 | 437 | ```javascript 438 | var window = Tree.parse({ 439 | button: [], 440 | button: [], 441 | }); 442 | ``` 443 | 444 | 以上代码被执行后,你只会看到最后一个按钮被渲染,因为 TreeUI 只能解析到最后指定的 `button` ,而第一次指定则会因为对象不能具有同名键这一特性被覆盖了。 445 | 446 | 为了解决这种冲突,TreeUI 采取了一种通用的解决方案,即在元素类型名称后添加数字编号。 447 | 448 | 这种元素类型名称和数字编号的组合方式总是要求紧凑的,你不能在元素类型名称与数字之间加入其它符号,否则该元素将不被解析。 449 | 450 | 你可以这么做(推荐): 451 | 452 | ```javascript 453 | var window = Tree.parse({ 454 | button1: [], 455 | button2: [], 456 | }); 457 | ``` 458 | 459 | 也可以这么做: 460 | 461 | ```javascript 462 | var window = Tree.parse({ 463 | '01button': [], 464 | '02button': [], 465 | }); 466 | ``` 467 | 468 | 但不能这么做: 469 | 470 | ```javascript 471 | var window = Tree.parse({ 472 | button_1: [], 473 | button_2: [], 474 | }); 475 | ``` 476 | 477 | 编号的作用仅为避免覆盖冲突,所以连续的编号并不是必须的,你完全可以使用 button1、button3、button12 这种排列方式,这对结果不会有任何影响。 478 | 479 | 采用这种方式的一个考量是,使用数字编号可以得到一个整洁的代码排版,当批量创建一组同类型元素时,不会因为元素类型名称长短不一而损失可读性。 480 | 481 | 482 | 483 | ## 解析顺序 484 | 485 | TreeUI 对于元素的解析顺序是自上而下、由外向内的,这就意味着,同一层级的元素,对象中的键值对越靠上的越先被解析,不同层级的,嵌套越浅的,越先被解析。 486 | 487 | 488 | 489 | ## 返回值 490 | 491 | **如果脚本在 Ae 宿主的 `ScriptUI Panels` 目录下运行,那么 TreeUI 始终返回一个 `Panel` 对象,否则返回一个 `Window` 对象。** 492 | 493 | 你可以使用一个变量存储返回值: 494 | 495 | ```javascript 496 | var elements = Tree.parse({ /*...*/ }); 497 | ``` 498 | 499 | 如果向 `Tree.parse` 方法传递一个非对象类型的值,TreeUI 返回 `null` : 500 | 501 | ```javascript 502 | var elements = Tree.parse(''); // return null 503 | ``` 504 | 505 | 如果 TreeUI 在不支持的宿主环境中运行,会返回 `null` 。 506 | 507 | 508 | 509 | ## 元素 ID 510 | 511 | 在元素创建一节中我们提到,`[]` 是一个定义元素参数的数组,那么对于所有常规的控件和容器来说,该数组的第一个参数永远是元素 ID ,它是一个字符串。 512 | 513 | 如果把解析后的 UI 比作一棵树,那么容器就是一根根枝干,控件就是枝干上的叶子。 514 | 515 | 那元素 ID 是什么呢,元素 ID 就是枝干和叶子的身份证号码——一个唯一的、不重复的字符串。有了它,我们就可以在 TreeUI 解析完所有元素之后精准地读取它们,换言之,如果你从未定义过元素的 ID ,那么在解析完成后,你将无法读取这些元素,虽然可以看到它们。 516 | 517 | 元素的 ID 这么定义: 518 | 519 | ```javascript 520 | var elements = Tree.parse({ button: ['myButton'] }); 521 | ``` 522 | 523 | 524 | 525 | ## 参数拆分 526 | 527 | 在 ScriptUI 中,定义一个元素的外观,通常需要定义元素的两个部分,即创建参数和元素属性。创建参数是元素在解析前,向解释器预先提供的一组参数,通常包含:元素类型、元素尺寸、元素内容和个别特定于元素的创建属性等,并以此预定义元素的外观。元素属性是元素在解析后,通过写入对象属性的方式二次改变元素的外观。 528 | 529 | 而 TreeUI 可以一次性的在元素描述阶段,通过参数拆分的方式,同时向解释器提供创建参数和元素属性。 530 | 531 | 我们还是以创建一个按钮为例,这是参数被拆分前的样子: 532 | 533 | ```javascript 534 | var elements = Tree.parse({ 535 | button: ['myButton'], 536 | }); 537 | ``` 538 | 539 | 这是拆分后的样子: 540 | 541 | ```javascript 542 | var elements = Tree.parse({ 543 | button: { 544 | style: { enabled: false }, 545 | param: ['myButton'], 546 | }, 547 | }); 548 | ``` 549 | 550 | 你会发现,此时并没有向 `button` 提供一个数组类型的参数列表,而是提供了一个对象,这个对象包含两个属性,分别是 `style` 和 `param` 。原本定义元素参数的数组被挪到了 `param` 的后面,而 `style` 后面是一个对象,对象中写着 `enabled: false` 。如果你在 estk 中运行这段代码,你将会得到一个无法点击的按钮,这是因为 enabled 在 ScriptUI 中是按钮元素的一个属性,用于控制元素是否可以被交互。 551 | 552 | 不过,在 TreeUI 中我们将属性称作样式,也就是 `style`,明确这种称呼的变化,是非常必要的,因为在 TreeUI 中,每个元素都可以按照这种方式进行参数拆分。同时也意味着,`param` 和 `style` 是两个固定属性,如果你写成下面这样,相信我,它们一定不会生效: 553 | 554 | ```javascript 555 | var elements = Tree.parse({ 556 | button: { 557 | Style: { enabled: false }, 558 | parameter: ['myButton'], 559 | }, 560 | }); 561 | ``` 562 | 563 | ✏️ 以上代码的参数部分不会被 TreeUI 解析,但你会得到一个默认外观且可点击的按钮。 564 | 565 | 说到这,我们就可以补充一个在元素 ID 一节中没有明确的点——容器的元素 ID 怎么定义呢?因为之前的示例中,它的类型名称后从来都是对象,对象中定义的都是容器包含的其它元素。 566 | 567 | 显然,现在可以很好的回答这个问题了,容器的元素 ID 使用参数拆分的方式定义(当然,容器的参数可以不进行拆分,但这么做估计不会有什么太大的意义,毕竟容器就是用来装其它东西的。),而且还可以顺便定义容器的样式: 568 | 569 | ```javascript 570 | var elements = Tree.parse({ 571 | group: { 572 | style: { orientation: 'row' }, 573 | param: ['myGroup'], 574 | button1: ['myButton1'], 575 | button2: ['myButton2'], 576 | }, 577 | }); 578 | ``` 579 | 580 | 在 estk 中运行以上示例,你将得到两个横向排列的按钮。这显然是样式(没错,叫样式,不叫属性) `orientation` 的功劳,你看,`orientation` 的值是字符串 `row` ,也就是将子元素排成一行的意思。另外我们还发现,button1 和 button2 的参数还是以未拆分的形式定义的,这说明参数拆分的另一个特点——拆分对于当前定义的任何元素来说,都是可选的,拆与不拆,完全看你的需求。 581 | 582 | 583 | 584 | ## 元素读取 585 | 586 | ⚠️ 只有在定义了元素 ID 的情况下,元素才能够被读取。 587 | 588 | ### 使用 TreeUI 实现的 getElement 方法(推荐) 589 | 590 | TreeUI 在 ScriptUI 所有容器构造器的原型上实现了一组获取元素的方法,一共有 3 种: 591 | 592 | 我们通过一组已经被解析完成的元素,分别演示它们的用法: 593 | 594 | ```javascript 595 | var elements = Tree.parse({ 596 | group: { 597 | style: { orientation: 'row' }, 598 | param: ['myGroup'], 599 | button1: ['myButton1'], 600 | button2: ['myButton2'], 601 | }, 602 | }); 603 | ``` 604 | 605 | 1、**`getElementById`** 606 | 607 | 该方法始终返回第一次通过元素 ID 匹配到的元素。 608 | 609 | ```javascript 610 | var myGroup = elements.getElementById('myGroup'); // 返回一个 group 元素 611 | var myButton1 = myGroup.getElementById('myButton1'); // 返回一个按钮元素 612 | var myButton2 = elements.getElementById('myButton2'); // 返回一个按钮元素 613 | ``` 614 | 615 | 616 | 617 | 2、**`getElementsByName`** 618 | 619 | 该方法始终返回一个数组,其中是所有通过元素 ID 匹配到的元素。当元素 ID 存在重复时,仅收集第一次匹配到的元素。收集顺序与 TreeUI 解析顺序一致,所以返回值中的元素顺序和传参顺序无关。 620 | 621 | ```javascript 622 | var myElements1 = elements.getElementsByName('myGroup'); // 返回一个包含组元素的数组 623 | var myElements2 = elements.getElementsByName('myButton1', 'myButton2'); // 返回一个包含两个 button 元素的数组 624 | ``` 625 | 626 | 也可以按以下形式获取: 627 | 628 | ```javascript 629 | var myElements1 = elements.getElementsByName(['myGroup']); // 返回一个包含 group 元素的数组 630 | var myElements2 = elements.getElementsByName(['myGroup', 'myButton2']); // 返回一个包含 group 和 button 元素的数组 631 | ``` 632 | 633 | 3、**`getElementsByType`** 634 | 635 | 该方法始终返回一个数组,其中是所有通过元素类型名称匹配到的元素。 636 | 637 | ```javascript 638 | var myElements1 = elements.getElementsByType('button'); // 返回一个包含两个 button 元素的数组 639 | var myElements2 = elements.getElementsByType('group'); // 返回一个包含 group 元素的数组 640 | var myElements3 = elements.getElementsByType('group', 'button'); // 返回一个包含 group 元素和 2 个 button 元素的数组 641 | ``` 642 | 643 | 也可以按以下形式获取: 644 | 645 | ```javascript 646 | var myElements = elements.getElementsByType(['group', 'button']); // 返回一个包含 group元素和 2 个 button 元素的数组 647 | ``` 648 | 649 | 使用链式调用: 650 | 651 | ```javascript 652 | var myButtons = elements.getElementById('group').getElementsByType('button'); // 返回一个包含 2 个 button 元素的数组 653 | ``` 654 | 655 | 656 | 657 | ### 使用对象点语法读取元素 658 | 659 | 我们知道,TreeUI 解析后的返回值是一个对象,既然是对象,那必然可以按对象的方式进行读取: 660 | 661 | ```javascript 662 | var elements = Tree.parse({ 663 | group: { 664 | style: { orientation: 'row' }, 665 | param: ['myGroup'], 666 | button1: ['myButton1'], 667 | button2: ['myButton2'], 668 | }, 669 | }); 670 | 671 | var myGroup = elements.myGroup; 672 | var myButton1 = myGroup.myButton1; 673 | var myButton2 = elements.myGroup.myButton2; 674 | ``` 675 | 676 | 当然,也可以使用中括号,这在元素 ID 是个变量的时候特别有用: 677 | 678 | ```javascript 679 | var index = [1, 2]; 680 | var myGroup = elements['myGroup']; 681 | var myButton1 = myGroup['myButton' + index[0]]; 682 | var myButton1 = myGroup['myButton' + index[1]]; 683 | ``` 684 | 685 | 686 | 687 | ### 使用 ScriptUI 自有的 findElement() 方法 688 | 689 | ⚠️ 当脚本在 Ae 宿主的 `ScriptUI Panels` 目录下运行时,TreeIU 返回一个 `Panel` 对象,该对象不存在 ` findElement()` 方法,故此方法无效。 690 | 691 | 当获取的元素嵌套较深时,使用该方法显然是最好的选择。 692 | 693 | ```javascript 694 | var elements = Tree.parse({ 695 | group: { 696 | style: { orientation: 'row' }, 697 | param: ['myGroup'], 698 | button1: ['myButton1'], 699 | button2: ['myButton2'], 700 | }, 701 | }); 702 | 703 | var myGroup = elements.findElement('myGroup'); 704 | var myButton1 = elements.findElement('myButton1'); 705 | var myButton2 = elements.findElement('myButton2'); 706 | ``` 707 | 708 | ⚠️ `findElement` 方法始终返回第一次匹配到的元素 ID 对应的元素,而不是全部,所以,请始终保持元素 ID 的唯一性。 709 | 710 | ### 注意事项 711 | 712 | ⚠️ 当主容器窗口类型不是 palette 时,或者像 `Ps` 和 `Id` 这种完全不支持 palette 的宿主,在元素被渲染后,也就是在使用了 show() 方法后,会发生阻塞,这将导致在 show() 之后对元素的所有操作都不会得到处理。 713 | 714 | 我们通过一个示例来说明这个问题: 715 | 716 | ```javascript 717 | var elements = Tree.parse({ 718 | button: ['myButton1'], 719 | }); 720 | var myButton1 = elements.getElementById('myButton1'); 721 | myButton1.text = 'YOOO'; 722 | ``` 723 | 724 | 以上代码在主容器窗口类型为 dialog 或 window时 ,执行后会弹出一个模态窗口,但后续对于元素 text 属性的修改将不会生效,你只能看到一个空白的按钮。 725 | 726 | 但我们可以通过延迟应用 show() 方法,即通过 TreeUI 的窗口配置属性 show 来解决这个问题: 727 | 728 | ```javascript 729 | var elements = Tree.parse({ 730 | config: { show: false }, 731 | button: ['myButton1'], 732 | }); 733 | var myButton1 = elements.getElementById('myButton1'); 734 | myButton1.text = 'YOOO'; 735 | 736 | elements.show(); 737 | ``` 738 | 739 | 740 | 741 | ## 事件添加 742 | 743 | 事件是可以被 JavaScript 侦测到的行为。 每个元素都可以产生某些可以触发的事件。比如说,我们可以在用户点击某按钮时产生一个 onClick 事件来触发某个函数。你可以在官方文档中了解关于 [ScriptUI 事件](https://extendscript.docsforadobe.dev/user-interface-tools/event-handling.html) 的全部内容。 744 | 745 | 以下示例演示了如何使用传统方式给一个按钮添加事件: 746 | 747 | ```javascript 748 | var elements = Tree.parse({ 749 | button1: ['run'], 750 | }); 751 | 752 | elements.run.onClick = function () { 753 | alert('Yoooooo!'); 754 | }; 755 | ``` 756 | 757 | 以上代码运行后,如果使用鼠标点击按钮,会弹出一个内容为“Yoooooo!”的窗口。 758 | 759 | 但在 TreeUI 中,你可以直接通过元素样式添加事件,它可以产生同样的效果: 760 | 761 | ```javascript 762 | function foo() { 763 | alert('Yoooooo!'); 764 | } 765 | 766 | Tree.parse({ button1: { style: { onClick: foo } } }); 767 | ``` 768 | 769 | 不仅是 onClick 事件,所有 ScriptUI 中预定义的事件类型都可以通过样式写入,你可以从[这里](https://extendscript.docsforadobe.dev/user-interface-tools/control-objects.html?highlight=onChange#control-event-handling-callbacks)查阅不同元素对应可用的事件类型。 770 | 771 | 772 | 773 | ## 程序容错 774 | 775 | 作者将 TreeUI 的一般受众定义为非科班的开发人员,所以在程序容错方面做了很多工作。因为相比因个别参数错误导致整个程序无法运行而言,先有一个能跑但不完美的程序,再去调试,会对新手更加友好。你可以缺省绝大多数参数,甚至传入一些非法值,这意味着,你将很少有机会看到因传参不当导致的报错信息,程序会自动忽略哪些非法传参,并使用默认参数进行替补。如果你在开发过程中传入某些参数后,并没有显示预期的结果,恳请仔细阅读该文档中涉及参数的细节,确保你已完全了解参数的类型和边界。这是权衡的结果,也请专业开发者理解。 776 | 777 | 778 | 779 | # 参数 780 | 781 | TreeUI 直接映射了 ScriptUI 的参数,**仅将第一个参数由原本的元素类型名称改为了元素 ID**。 782 | 783 | 以 button 控件在官方文档中的签名为例: 784 | 785 | ``` 786 | w.add ("button" [, bounds, text, creation_properties}]); 787 | ``` 788 | 789 | ⚠️ 字符串 `"button"` 就是元素类型名称,在 TreeUI 中,**这个位置的参数将被用于定义元素元素 ID**,而不再是元素类型名称。 790 | 791 | | 参数名称 | 描述 | 792 | | -------------- | ------------------------------------------------------------ | 793 | | bounds | 可选的。控件的位置和大小。 | 794 | | text | 可选的。控件中显示的文本。 | 795 | | creation_props | name:控件的唯一名称。对于模态对话框,特殊名称 ok 使其为 defaultElement,特殊名称 cancel 使其为父对话框的 cancelElement。 | 796 | 797 | 在 ScriptUI 中,参数这么用: 798 | 799 | ```javascript 800 | var window = new Window('palette'); 801 | var button = window.add('button', [0, 0, 100, 25], '按钮', { name: 'myButton' }); 802 | window.show(); 803 | ``` 804 | 805 | 而 TreeUI 中,参数这么用: 806 | 807 | ```javascript 808 | var window = Tree.parse({ button: ['myButton', [0, 0, 100, 25], '按钮'] }); 809 | ``` 810 | 811 | 你会发现 最后一个叫 `creation_props` 的参数,在以上示例中并没有被使用,那是因为 button 的 `creation_props` 只有一个叫 name 的可配置属性,而这个 name 就是元素的 ID ,所以,只需要按照 TreeUI 的规则,在数组的第一个元素位置定义它即可,无需重复定义,虽然以下的写法是可以的,但没必要: 812 | 813 | ```javascript 814 | var window = Tree.parse({ button: ['myButton', [0, 0, 100, 25], '按钮', { name: 'my_button' }] }); 815 | ``` 816 | 817 | ⚠️ 以上操作会导致先定义的元素 ID `myButton` 被后定义的 `my_button` 覆盖。 818 | 819 | 如果一个元素的 creation_props 具有 name 之外的可配置属性,就可以专门用上它了,下面以 edittext 为例,通过[查阅](https://extendscript.docsforadobe.dev/user-interface-tools/control-objects.html#edittext)官方文档,我们得知 edittext 除了 name ,一共有 6 个可配置属性,如果在 TreeUI 中把它们全都用上,就是这样: 820 | 821 | ```javascript 822 | var window = Tree.parse({ 823 | edittext: [ 824 | 'myEditText', 825 | [0, 0, 100, 25], 826 | '按钮', 827 | { 828 | readonly: false, 829 | noecho: false, 830 | enterKeySignalsOnChange: false, 831 | borderless: false, 832 | multiline: false, 833 | scrollable: true, 834 | }, 835 | ], 836 | }); 837 | ``` 838 | 839 | 840 | 841 | 可以通过以下入口查阅元素的所有可用参数: 842 | 843 | - [窗口](https://extendscript.docsforadobe.dev/user-interface-tools/window-object.html#window-object-constructor) 844 | 845 | - [控件](https://extendscript.docsforadobe.dev/user-interface-tools/control-objects.html#control-types-and-creation-parameters) 846 | - [容器](https://extendscript.docsforadobe.dev/user-interface-tools/control-objects.html#control-types-and-creation-parameters) 847 | 848 | 849 | 850 | # 样式 851 | 852 | TreeUI 直接映射了 ScriptUI 的属性,并称作样式,通过[参数拆分](#Split-Parameters)的方式提供给元素。 853 | 854 | 以下示例展示了如何使用样式定义按钮的文字和大小: 855 | 856 | ```javascript 857 | var elements = Tree.parse({ 858 | button: { style: { text: '按钮', size: [100, 100] } }, 859 | }); 860 | ``` 861 | 862 | ✏️ 按钮的文字和大小其实可以直接通过参数定义,像这样: 863 | 864 | ```javascript 865 | var elements = Tree.parse({ 866 | button: { param: ['myButton', [0, 0, 100, 100], '按钮'] }, 867 | }); 868 | ``` 869 | 870 | ⚠️ 如果对元素同时使用参数和样式,并且功能存在重叠,则参数会被样式覆盖: 871 | 872 | ```javascript 873 | var elements = Tree.parse({ 874 | button: { 875 | param: ['myButton', [0, 0, 100, 100], '按钮'], 876 | style: { text: 'Ok', size: [50, 50] }, 877 | }, 878 | }); 879 | ``` 880 | 881 | 以上代码执行后,你会得到一个 50 × 50 的按钮,且按钮上的文字是“Ok”,所以请避免同时使用功能重叠的参数和样式。 882 | 883 | 884 | 885 | 你可以在[这里](https://extendscript.docsforadobe.dev/user-interface-tools/common-properties.html)找到元素所有可以配置的样式。 886 | 887 | ✏️ 通过查阅官方文档你会发现,不是所有样式在任何元素上都是可用的。 888 | 889 | 如果需要进行针对性的查找,可以使用以下入口: 890 | 891 | - [窗口](https://extendscript.docsforadobe.dev/user-interface-tools/window-object.html#window-object-properties) 892 | 893 | - [控件](https://extendscript.docsforadobe.dev/user-interface-tools/control-objects.html#control-object-properties) 894 | - [容器](https://extendscript.docsforadobe.dev/user-interface-tools/window-object.html#container-properties) 895 | 896 | 897 | 898 | # 自定义控件 899 | 900 | TreeUI 在 ScriptUI 原有控件之外,封装了一组自定义控件,以满足开发者对控件外观及功能的更多需求。 901 | 902 | 自定义控件是元素控件类型的一种。控件适用的特性对自定义控件依旧适用。 903 | 904 | ✏️ TreeUI 分别为不同的宿主提供与宿主默认主题一致的外观,所以在不同宿主中,自定义控件的默认外观并不相同。 905 | 906 | 907 | 908 | ## rectbutton 和 roundbutton 909 | 910 | 一个可以对外观进行自定义的矩形和圆形按钮,单击可触发 onClick 回调。它们有着完全一致的可配置属性,仅在类型名称和外观方面存在差异。 911 | 912 | 创建一个空白的矩形按钮: 913 | 914 | ```javascript 915 | Tree.parse({ rectbutton: [] }); 916 | ``` 917 | 918 | 创建一个空白的圆形按钮: 919 | 920 | ```javascript 921 | Tree.parse({ roundbutton: [] }); 922 | ``` 923 | 924 | 与 ScriptUI 现有的按钮一样,自定义按钮的传参顺序依次为: 925 | 926 | ```javaScript 927 | rectbutton: [元素 ID , 按钮大小, 按钮文字, {创建属性}] 928 | roundbutton: [元素 ID , 按钮大小, 按钮文字, {创建属性}] 929 | ``` 930 | 931 | 按钮的外观配置主要指对于`{创建属性}`的配置,它是一个对象,并具有以下可配置属性: 932 | 933 | | 属性名称 | 参数类型 | 参数限制 | 参数作用 | 934 | | ------------- | -------------------------- | ------------------------------------------------------------ | ------------ | 935 | | enableText | `Boolean` | true 或 false | 是否显示文字 | 936 | | enableFill | `Boolean` | true 或 false | 是否启用填充 | 937 | | enableStroke | `Boolean` | true 或 false | 是否启用描边 | 938 | | fontName | `String` | 字体名称字符串 | 字体 | 939 | | fontStyle | `Boolean` 或 `Number` | 0 ≤ value ≤ 3 或[特定字符串](https://extendscript.docsforadobe.dev/user-interface-tools/scriptui-class.html?highlight=ITALIC#scriptui-fontstyle) | 字体样式 | 940 | | fontSize | `Number` | 1 ≤ value ≤ 1000 | 文字大小 | 941 | | fontOffset | `2 Item Array` | -INFINITY ≤ value ≤ INFINITY | 文字偏移量 | 942 | | fontColor | `4 Item Array` 或 `String` | Hex 字符串 | 文字颜色 | 943 | | fillColor | `4 Item Array` 或 `String` | Hex 字符串 | 填充颜色 | 944 | | strokeColor | `4 Item Array` 或 `String` | Hex 字符串 | 描边颜色 | 945 | | fontOpacity | `4 Item Array` 或 `Number` | 0 ≤ value ≤ 1 | 文字透明度 | 946 | | fillOpacity | `4 Item Array` 或 `Number` | 0 ≤ value ≤ 1 | 填充透明度 | 947 | | strokeOpacity | `4 Item Array` 或 `Number` | 0 ≤ value ≤ 1 | 描边透明度 | 948 | | strokeWidth | `Number` | 0 ≤ value ≤ 10 | 描边宽度 | 949 | 950 | 个别参数详解: 951 | 952 | | 属性名称 | 详解 | 953 | | ---------- | ------------------------------------------------------------ | 954 | | fontName | 打开AE,点开字符面板右上角菜单,勾选“显示英文字体名称”,此时字体列表中显示的名称即可以字符串的形式作为 fontName 参数。 | 955 | | fontStyle | 可以是字符串 `REGULAR` `BOLD` `ITALIC` `BOLDITALIC` 之一,或数字 0 至 3,分别对应前面的 4 种样式。 | 956 | | fontOffset | 用于偏移文字在按钮上的位置,以修正 ScriptUI 中的计算 Bug 。参数是一个2元素数组,分别对应 x 和 y 方向的偏移量。 | 957 | 958 | ✏️ fontColor、fillColor、strokeColor 属性皆使用一个 4 元素数组作为参数,它们分别对应鼠标状态的四个阶段,即悬停、移出、点击、抬起。 959 | 960 | 以下示例使用了按钮在 Ae 中的默认参数来演示如何配置这些属性: 961 | 962 | ```javaScript 963 | Tree.parse({ 964 | rectbutton: [ 965 | 'button', 966 | undefined, 967 | '按钮', 968 | { 969 | enableText: true, 970 | enableFill: true, 971 | enableStroke: true, 972 | fontName: 'Tahoma', 973 | fontStyle: 'REGULAR', 974 | fontSize: 12, 975 | fontOffset: [0, 0], 976 | fontColor: ['#161616', '#8a8a8a', '#161616', '#ffffff'], 977 | fillColor: ['#8a8a8a', '#232323', '#636363', '#2d8ceb'], 978 | strokeColor: ['#8a8a8a', '#8a8a8a', '#636363', '#2d8ceb'], 979 | fontOpacity: [1, 1, 1, 1], 980 | fillOpacity: [1, 1, 1, 1], 981 | strokeOpacity: [1, 1, 1, 1], 982 | strokeWidth: [2, 2, 2, 2], 983 | }, 984 | ], 985 | }); 986 | ``` 987 | 988 | 所有的属性都是可以缺省的,这意味着,你只需配置你关注的属性,而不必把所有属性都罗列出来。 989 | 990 | 例如,你只是想将按钮上文字的字体换成楷体: 991 | 992 | ```javaScript 993 | Tree.parse({ rectbutton: ['button', undefined, '按钮', { fontName: 'KaiTi' }] }); 994 | ``` 995 | 996 | 又或者,你想把默认状态下按钮的填充色改成红色,并把描边去掉,可以这么写: 997 | 998 | ```javaScript 999 | Tree.parse({ rectbutton: ['button', undefined, '按钮', { enableStroke: false, fillColor: [, '#e81123'] }] }); 1000 | ``` 1001 | 1002 | 细心的你可能已经发现了,fillColor是一个四个元素的数组,但示例中只传了一个参数。的确是这样,你完全可以在数组对应位置(显然逗号还是不能省的)只传入一个参数,解析器知道你想做什么。 1003 | 1004 | 不仅如此,颜色、透明度和描边属性还可以直接按以下方式传参,他表示四种状态下都使用同一个参数: 1005 | 1006 | ```javaScript 1007 | Tree.parse({ rectbutton: ['button', undefined, '按钮', { enableStroke: false, fillColor: '#e81123' }] }); 1008 | ``` 1009 | 1010 | 这时候你就拥有了一个纯红色的按钮,并且任何鼠标状态下都是这个颜色。 1011 | 1012 | ✏️ 所有参数都是支持缺省的,如果你发现配置了参数之后,按钮的外观并没有发生改变,那么只有两种可能,一是参数是无效的,二是参数超出了取值边界。所有非法输入被检测到后,都会使用默认参数进行替补。 1013 | 1014 | 1015 | 1016 | # 窗口配置 1017 | 1018 | 每个 `TreeUI.parse` 方法的源中,都可以通过标识符 `config` 在根位置同时写入一个或多个配置参数。 1019 | 1020 | 1021 | 1022 | ## dockable 1023 | 1024 | 类型:`Boolean` 1025 | 1026 | 默认为 true。配置窗口是否可以在AE中停靠。 1027 | 1028 | 示例: 1029 | 1030 | ```javascript 1031 | var Window = Tree.parse({ 1032 | config: { dockable: false }, 1033 | button: [], 1034 | }); 1035 | ``` 1036 | 1037 | 以上示例运行后窗口将始终无法在 Ae 中停靠。 1038 | 1039 | 1040 | 1041 | ## show 1042 | 1043 | 类型:`Boolean` 1044 | 1045 | 默认为 true。配置窗口是否在创建后显示。 1046 | 1047 | 示例: 1048 | 1049 | ```javascript 1050 | var Window = Tree.parse({ 1051 | config: { show: false }, 1052 | button: [], 1053 | }); 1054 | ``` 1055 | 1056 | 以上示例运行后将不会在宿主中显示窗口,必须对 window 使用 ScriptUI 自有的 `show()` 方法才能够显示: 1057 | 1058 | ```javascript 1059 | var Window = Tree.parse({ 1060 | config: { show: false }, 1061 | button: [], 1062 | }); 1063 | window.show() 1064 | ``` 1065 | 1066 | 1067 | 1068 | ## singleton 1069 | 1070 | 类型:`Boolean` 1071 | 1072 | 默认为 false。单例模式。启用后 TreeUI 返回一个方法,调用该方法可创建单例窗口,无论调用该方法多少次,窗口只会被创建一次,特别适用于构建脚本的参数配置面板。 1073 | 1074 | ⚠ PhotoShop 不支持真正意义上的单例模式,当宿主为 PhotoShop 时,TreeUI 每次都会重新渲染一次窗口,这是 PhotoShop 自身导致的问题。 1075 | 1076 | 以下示例展示了单例模式的用法: 1077 | 1078 | ```javascript 1079 | var configWindow = Tree.parse({ 1080 | config: { singleton: true }, 1081 | checkbox: [undefined, undefined, '启用'], 1082 | }); 1083 | 1084 | var mainWindow = Tree.parse({ 1085 | button: { 1086 | style: { onClick: configWindow }, 1087 | param: [undefined, undefined, '设置'], 1088 | }, 1089 | }); 1090 | ``` 1091 | 1092 | 运行以上代码,你首先会看到一个叫“设置”的按钮,点击它,会弹出一个新的窗口,窗口中有个可以点击的复选框。 1093 | 1094 | 1095 | 1096 | # 属性 1097 | 1098 | ## Tree.version 1099 | 1100 | 类型:`String` 1101 | 1102 | 只读。一个包含 TreeUI 版本信息的字符串。 1103 | 1104 | ## Tree.context 1105 | 1106 | 类型:`Global Object` 1107 | 1108 | 只写。一个指向当前脚本运行上下文的对象。 1109 | 1110 | ## Tree.layoutMode 1111 | 1112 | 类型:`Number` 1113 | 1114 | 只写。默认为 0,用于配置全局 layout 模式,只可写入以下值: 1115 | 1116 | | 值 | 描述 | 1117 | | ---- | ------------------------------------------------------------ | 1118 | | 0 | 如果容器边界(即大小+位置)已经设置,则不执行任何操作。否则,让布局管理器对该容器和任何子容器进行操作。 | 1119 | | 1 | 强制布局管理器重新计算子容器的大小。 | 1120 | | 2 | 内部使用。当子容器有“填充”对齐时涉及。 | 1121 | 1122 | 1123 | 1124 | # 方法 1125 | 1126 | ## Tree.parse() 1127 | 1128 | `Tree.parse(source)` 1129 | 1130 | | 参数 | 类型 | 描述 | 1131 | | ------ | ------ | ---------------------- | 1132 | | source | Object | 一个用于描述 UI 的对象 | 1133 | 1134 | 返回一个 `window` 对象。当 source 不是对象时,返回 null 。 1135 | 1136 | ## Container.prototype.getElementById() 1137 | 1138 | `container.getElementById(id)` 1139 | 1140 | | 参数 | 类型 | 描述 | 1141 | | ---- | ------ | -------------------------------------------------------- | 1142 | | id | String | id 是大小写敏感的字符串,代表了所要查找的元素的唯一 ID。 | 1143 | 1144 | 返回一个匹配到 ID 的 SciptUI `Element` 对象。若在当前容器下没有找到,则返回 null。 1145 | 1146 | ## Container.prototype.getElementsByName() 1147 | 1148 | `container.getElementsByName(names)` 1149 | 1150 | | 参数 | 类型 | 描述 | 1151 | | ----- | --------------- | ------------------------------------------------------------ | 1152 | | names | String 或 Array | names 可以是多个用逗号分隔的字符串或数组,代表了索要查找的元素的多个唯一ID。 | 1153 | 1154 | 返回匹配到 ID 的 SciptUI `Element` 对象集合。若在当前容器下没有找到,则返回 null。 1155 | 1156 | ## Container.prototype.getElementsByType() 1157 | 1158 | `container.getElementsByType(types)` 1159 | 1160 | | 参数 | 类型 | 描述 | 1161 | | ----- | --------------- | ------------------------------------------------------------ | 1162 | | types | String 或 Array | type 可以是多个用逗号分隔的字符串或数组,代表了索要查找的元素的多个类型 | 1163 | 1164 | 返回类型匹配的 SciptUI `Element` 对象集合。若在当前容器下没有找到,则返回 null。 1165 | 1166 | # 代码示例 1167 | 1168 | ## 创建一个空白按钮 1169 | 1170 | ```javascript 1171 | Tree.parse({ button: '' }); 1172 | ``` 1173 | 1174 | 1175 | 1176 | ## 添加元素 ID 并设置事件 1177 | 1178 | ```javascript 1179 | var elements = Tree.parse({ 1180 | button1: ['run'], 1181 | }); 1182 | 1183 | elements.run.onClick = function () { 1184 | alert('Yoooooo!'); 1185 | }; 1186 | ``` 1187 | 1188 | 1189 | 1190 | ## 将事件通过样式写入 1191 | 1192 | ```javascript 1193 | function foo() { 1194 | alert('Yoooooo!'); 1195 | } 1196 | 1197 | var elements = Tree.parse({ 1198 | button1: { 1199 | style: { onClick: foo }, 1200 | param: ['run'], 1201 | }, 1202 | }); 1203 | ``` 1204 | 1205 | 1206 | 1207 | ## 为按钮添加更多样式 1208 | 1209 | ```javascript 1210 | function foo() { 1211 | alert('Yoooooo!'); 1212 | } 1213 | 1214 | var elements = Tree.parse({ 1215 | button1: { 1216 | style: { onClick: foo }, 1217 | param: ['run', [0, 0, 100, 30], '按钮'], 1218 | }, 1219 | }); 1220 | ``` 1221 | 1222 | 1223 | 1224 | ## 配置全局主容器的样式 1225 | 1226 | ```javascript 1227 | function foo() { 1228 | alert('Yoooooo!'); 1229 | } 1230 | 1231 | var elements = Tree.parse({ 1232 | style: { margins: 5, alignChildren: ['fill', 'fill'] }, 1233 | param: ['palette', '', undefined, { resizeable: true }], 1234 | button1: { 1235 | style: { onClick: foo }, 1236 | param: ['run', [0, 0, 100, 30], '按钮'], 1237 | }, 1238 | }); 1239 | ``` 1240 | 1241 | 1242 | 1243 | ## 单例面板 1244 | 1245 | ```javascript 1246 | var configWindow = Tree.parse({ 1247 | config: { singleton: true }, 1248 | checkbox: [undefined, undefined, '启用'], 1249 | }); 1250 | 1251 | var mainWindow = Tree.parse({ 1252 | button: { 1253 | style: { onClick: configWindow }, 1254 | param: [undefined, undefined, '设置'], 1255 | }, 1256 | }); 1257 | ``` 1258 | 1259 | 1260 | 1261 | ## 构建更复杂的 UI 1262 | 1263 | ```javascript 1264 | function foo() { 1265 | alert('Yoooooo!!!'); 1266 | } 1267 | 1268 | var elements = Tree.parse({ 1269 | style: { margins: 5, alignChildren: ['fill', 'fill'] }, 1270 | param: ['palette', '', undefined, { resizeable: true }], 1271 | group1: { 1272 | style: { margins: 0, spacing: 0, orientation: 'column', alignChildren: ['fill', 'fill'] }, 1273 | param: ['mainGroup1', undefined, undefined], 1274 | edittext1: { 1275 | style: { preferredSize: [180, 230] }, 1276 | param: ['console', undefined, 'console', { multiline: true, scrolling: true }], 1277 | }, 1278 | }, 1279 | group2: { 1280 | style: { orientation: 'column', alignChildren: ['fill', 'fill'], alignment: ['fill', 'bottom'] }, 1281 | param: ['paramGroup1', undefined, undefined], 1282 | group1: { 1283 | style: { orientation: 'row', alignment: ['fill', 'bottom'] }, 1284 | param: ['dropdownlistGroup'], 1285 | statictext1: [undefined, [0, 0, 30, 25], '方向'], 1286 | dropdownlist1: { 1287 | style: { alignment: ['fill', ''], selection: 3 }, //定义下拉列表的默认选项 1288 | param: ['direction', [0, 0, 170, 25], ['+x', '-x', '+y', '-y']], 1289 | }, 1290 | }, 1291 | group2: { 1292 | style: { spacing: 5, orientation: 'row', alignChildren: ['fill', 'fill'], alignment: ['fill', 'bottom'] }, 1293 | param: ['settingGroup'], 1294 | group1: { 1295 | style: { orientation: 'column', alignment: ['left', 'bottom'] }, 1296 | param: ['mainGroup'], 1297 | statictext1: ['time', [0, 0, 30, 25], '时间'], 1298 | statictext2: ['transition', [0, 0, 30, 25], '过渡'], 1299 | statictext3: ['distance', [0, 0, 30, 25], '距离'], 1300 | }, 1301 | group2: { 1302 | style: { orientation: 'column', alignChildren: ['fill', 'fill'] }, 1303 | param: ['mainGroup'], 1304 | slider1: ['time', [0, 0, 140, 25], 1, 0, 3], 1305 | slider2: ['transition', [0, 0, 140, 25], 1, 0, 3], 1306 | slider3: ['distance', [0, 0, 140, 25], 1, 0, 3], 1307 | }, 1308 | group3: { 1309 | style: { orientation: 'column', alignment: ['right', 'bottom'] }, 1310 | param: ['mainGroup'], 1311 | edittext1: ['time', [0, 0, 45, 25], '10'], 1312 | edittext2: ['transition', [0, 0, 45, 25], '10'], 1313 | edittext3: ['distance', [0, 0, 45, 25], '10'], 1314 | }, 1315 | }, 1316 | }, 1317 | button1: { 1318 | style: { onClick: foo }, //添加事件侦听 1319 | param: ['button', undefined, '添加'], 1320 | }, 1321 | }); 1322 | ``` 1323 | 1324 | 1325 | 1326 | -------------------------------------------------------------------------------- /tree.jsx: -------------------------------------------------------------------------------- 1 | var Tree = function () { 2 | var ROOT = this; 3 | var GLOBAL = getGlobal($); 4 | var BRIDGETALK = getBridgeTalk(GLOBAL); 5 | var SUPPORT_HOST = ['aftereffects', 'photoshop', 'illustrator', 'indesign', 'estoolkit']; 6 | 7 | if (!isValidHost(BRIDGETALK, SUPPORT_HOST)) return null; 8 | 9 | var TREE = {}; 10 | 11 | var VERSION = '0.3.6'; 12 | 13 | var INFINITY = 1 / 0; 14 | 15 | var reEscapeNumber = /\d/g; 16 | 17 | var reEscapeChar = /\\(\\)?/g; 18 | 19 | var reEscapeParentheses = /\)$|^\(/g; 20 | 21 | var reIsFontStyleFlag = /REGULAR|BOLD|ITALIC|BOLDITALIC/i; 22 | 23 | var reMapFullHex = /^#?([a-f\d])([a-f\d])([a-f\d])$/gi, 24 | reIsHex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/gi; 25 | 26 | var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, 27 | reIsPlainProp = /^\w*$/, 28 | rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g; 29 | 30 | var HOST_NAME = BRIDGETALK.appName, 31 | IS_AE_HOST = isHost('aftereffects'), 32 | IS_PS_HOST = isHost('photoshop'), 33 | IS_AI_HOST = isHost('illustrator'), 34 | IS_ID_HOST = isHost('indesign'), 35 | IS_TK_HOST = isHost('estoolkit'), 36 | IS_AE_2019 = IS_AE_HOST && isAppVersion(16), 37 | IS_NO_FONT_SIZE_HOST = IS_AI_HOST || IS_ID_HOST; 38 | 39 | var MAIN_CONTAINER_COLLECTION = []; 40 | 41 | var FIX_PS_STROKE_ISSUE = IS_PS_HOST ? 1 : 0, 42 | FIX_HEIGHT_MEASURE_ISSUE = IS_AI_HOST ? 4.5 : 5, 43 | PS_WIDTH_MEASURE_FACTOR = 2.4; 44 | 45 | var SUPPORT_CONTAINER_TYPE_REFERENCES = { aftereffects: ['dialog', 'palette', 'window'], photoshop: 'dialog', illustrator: ['dialog', 'palette', 'window'], indesign: 'dialog', estoolkit: ['dialog', 'palette', 'window'] }, 46 | SUPPORT_CONTAINER_TYPE = SUPPORT_CONTAINER_TYPE_REFERENCES[HOST_NAME]; 47 | 48 | var MAIN_CONTAINER_TYPE_REFERENCES = { aftereffects: 'palette', photoshop: 'dialog', illustrator: 'palette', indesign: 'dialog', estoolkit: 'palette' }, 49 | MAIN_CONTAINER_TYPE_DEFAULT = MAIN_CONTAINER_TYPE_REFERENCES[HOST_NAME]; 50 | 51 | var CONTROL_PARAM_REFERENCES = { button: 3, checkbox: 3, dropdownlist: 3, edittext: 3, iconbutton: 3, image: 3, listbox: 3, progressbar: 4, radiobutton: 3, scrollbar: 5, slider: 5, statictext: 3, treeview: 3 }, 52 | CONTAINER_PARAM_REFERENCES = { group: 2, panel: 3, tab: 3, tabbedpanel: 3 }, 53 | CUSTOM_CONTROL_PARAM_REFERENCES = { rectbutton: 3, roundbutton: 3, angle: 3 }; 54 | 55 | var CONTROL_TYPE_FLAG = { 56 | button: 'A', 57 | checkbox: 'B', 58 | dialog: 'C', 59 | dropdownlist: 'D', 60 | edittext: 'E', 61 | group: 'G', 62 | iconbutton: 'H', 63 | image: 'I', 64 | item: 'J', 65 | listbox: 'K', 66 | node: 'L', 67 | palette: 'M', 68 | panel: 'N', 69 | progressbar: 'O', 70 | radiobutton: 'P', 71 | scrollbar: 'Q', 72 | slider: 'R', 73 | statictext: 'S', 74 | tab: 'T', 75 | tabbedpanel: 'U', 76 | treeview: 'V', 77 | window: 'W', 78 | rectbutton: 'X', 79 | roundbutton: 'Y', 80 | // angle: 'Z', 81 | }; 82 | 83 | if (IS_TK_HOST) _assign(CONTROL_TYPE_FLAG, { flashplayer: 'F' }); 84 | if (IS_PS_HOST) _unset(CONTROL_TYPE_FLAG, 'treeview'); 85 | 86 | var reCombination = /[CGMNTW][ABDEFGHIKNOPQRSUVXYZ]|[DK]J|[VL][LJ]|UT/, 87 | reIsContainer = /[DGKLNTUV]/, 88 | reIsListItemContainer = /[DKLV]/, 89 | reIsSelectableElement = /[DKUV]/, 90 | reIsCustomControl = /[XYZ]/, 91 | reIsCustomButton = /[XY]/, 92 | reIsNativeContainer = /[GNTU]/, 93 | reIsNativeControl = /[ABDEFHIKOPQRSV]/; 94 | 95 | var MOUSE_LEFT_CLICK_FLAG = 0, 96 | MOUSE_RIGHT_CLICK_FLAG = 2; 97 | 98 | var MOUSE_EVENT_REFERENCES = ['mouseover', 'mouseout', 'mousedown', 'mouseup']; 99 | 100 | var CUSTOM_BUTTON_FONT_PROPERTIES = ['fontName', 'fontStyle', 'fontSize'], 101 | CUSTOM_BUTTON_PENS_PROPERTIES = ['fontColor', 'fillColor', 'strokeColor', 'fontOpacity', 'fillOpacity', 'strokeOpacity', 'strokeWidth'], 102 | CUSTOM_BUTTON_CONFIG_PROPERTIES = ['enableText', 'enableFill', 'enableStroke', 'fontOffset'], 103 | CUSTOM_BUTTON_AUTO_FILL_PROPERTIES = ['fontColor', 'fillColor', 'strokeColor', 'fontOpacity', 'fillOpacity', 'strokeOpacity', 'strokeWidth']; 104 | 105 | var CUSTOM_ELEMENT_TYPE_REFERENCES = { rectbutton: 'customView', roundbutton: 'customView', angle: 'customBoundedValue' }; 106 | 107 | var RECT_BUTTON_VALID_PROPERTIES = ['enableText', 'enableFill', 'enableStroke', 'fontName', 'fontStyle', 'fontSize', 'fontOffset', 'fontColor', 'fillColor', 'strokeColor', 'fontOpacity', 'fillOpacity', 'strokeOpacity', 'strokeWidth'], 108 | ROUND_BUTTON_VALID_PROPERTIES = RECT_BUTTON_VALID_PROPERTIES; 109 | 110 | var STROKE_WIDTH_REFERENCES = { aftereffects: 2, photoshop: 1, illustrator: 2, indesign: 2, estoolkit: 2 }, 111 | STROKE_WIDTH_DEFAULT = _times(4, _constant(STROKE_WIDTH_REFERENCES[HOST_NAME])); 112 | 113 | var TEXT_LOCATION_REFERENCES = { aftereffects: [0, 0], photoshop: [0, 0], illustrator: [0, 2], indesign: [0, 3], estoolkit: [0, -1] }, 114 | FIX_TEXT_LOCATION = TEXT_LOCATION_REFERENCES[HOST_NAME]; 115 | 116 | var FONT_COLOR_REFERENCES = { 117 | aftereffects: ['#161616', '#8a8a8a', '#161616', '#ffffff'], 118 | photoshop: ['#f0f0f0', '#f0f0f0', '#f0f0f0', '#ffffff'], 119 | illustrator: ['#4b4b4b', '#ffffff', '#ffffff', '#ffffff'], 120 | indesign: ['#4b4b4b', '#ffffff', '#ffffff', '#ffffff'], 121 | estoolkit: ['#000000', '#000000', '#000000', '#000000'], 122 | }; 123 | var FONT_COLOR_DEFAULT = FONT_COLOR_REFERENCES[HOST_NAME]; 124 | 125 | var FILL_COLOR_REFERENCES = { 126 | aftereffects: ['#8a8a8a', '#232323', '#636363', '#2d8ceb'], 127 | photoshop: ['#454545', '#454545', '#363636', '#454545'], 128 | illustrator: ['#ffffff', '#535353', '#46a0f5', '#46a0f5'], 129 | indesign: ['#ffffff', '#535353', '#46a0f5', '#46a0f5'], 130 | estoolkit: ['#e5f1fb', '#e1e1e1', '#cce4f7', '#e5f1fb'], 131 | }; 132 | var FILL_COLOR_DEFAULT = FILL_COLOR_REFERENCES[HOST_NAME]; 133 | 134 | var STROKE_COLOR_REFERENCES = { 135 | aftereffects: ['#8a8a8a', '#8a8a8a', '#636363', '#2d8ceb'], 136 | photoshop: ['#666666', '#666666', '#636363', '#1473e7'], 137 | illustrator: ['#ffffff', '#ffffff', '#46a0f5', '#46a0f5'], 138 | indesign: ['#ffffff', '#ffffff', '#46a0f5', '#46a0f5'], 139 | estoolkit: ['#0078d7', '#adadad', '#005499', '#0078d7'], 140 | }; 141 | var STROKE_COLOR_DEFAULT = STROKE_COLOR_REFERENCES[HOST_NAME]; 142 | 143 | var RECT_BOUNDS_REFERENCES = { 144 | aftereffects: [0, 0, 80, 28], 145 | photoshop: [0, 0, 65, 25], 146 | illustrator: [0, 0, 80, 26], 147 | indesign: [0, 0, 80, 28], 148 | estoolkit: [0, 0, 78, 23], 149 | }; 150 | var RECT_BOUNDS_DEFAULT = RECT_BOUNDS_REFERENCES[HOST_NAME]; 151 | 152 | var ROUND_BOUNDS_REFERENCES = { 153 | aftereffects: [0, 0, 30, 30], 154 | photoshop: [0, 0, 28, 28], 155 | illustrator: [0, 0, 28, 28], 156 | indesign: [0, 0, 30, 30], 157 | estoolkit: [0, 0, 23, 23], 158 | }; 159 | var ROUND_BOUNDS_DEFAULT = ROUND_BOUNDS_REFERENCES[HOST_NAME]; 160 | 161 | var RECT_STYLE_DEFAULT = { text: '', bounds: RECT_BOUNDS_DEFAULT }, 162 | ROUND_STYLE_DEFAULT = { text: '', bounds: ROUND_BOUNDS_DEFAULT }; 163 | 164 | var MAIN_CONTAINER_DEFAULT = { 165 | dockable: true, 166 | show: true, 167 | singleton: false, 168 | }; 169 | 170 | var RECT_CREATION_DEFAULT = { 171 | enableText: true, 172 | enableFill: true, 173 | enableStroke: true, 174 | fontName: 'Tahoma', 175 | fontStyle: 'REGULAR', 176 | fontSize: 12, 177 | fontOffset: [0, 0], 178 | fontColor: FONT_COLOR_DEFAULT, 179 | fillColor: FILL_COLOR_DEFAULT, 180 | strokeColor: STROKE_COLOR_DEFAULT, 181 | fontOpacity: [1, 1, 1, 1], 182 | fillOpacity: [1, 1, 1, 1], 183 | strokeOpacity: [1, 1, 1, 1], 184 | strokeWidth: STROKE_WIDTH_DEFAULT, 185 | }; 186 | var ROUND_CREATION_DEFAULT = RECT_CREATION_DEFAULT; 187 | 188 | var RECT_BUTTON_PARAM_VALIDATOR = { 189 | enableText: _isBoolean, 190 | enableFill: _isBoolean, 191 | enableStroke: _isBoolean, 192 | fontName: _isString, 193 | fontStyle: isFontStyleFlag, 194 | fontSize: isBetween(1, 1000), 195 | fontOffset: [_isNumber, _isNumber], 196 | fontColor: [isHex, isHex, isHex, isHex], 197 | fillColor: [isHex, isHex, isHex, isHex], 198 | strokeColor: [isHex, isHex, isHex, isHex], 199 | fontOpacity: [isBetween(0, 1), isBetween(0, 1), isBetween(0, 1), isBetween(0, 1)], 200 | fillOpacity: [isBetween(0, 1), isBetween(0, 1), isBetween(0, 1), isBetween(0, 1)], 201 | strokeOpacity: [isBetween(0, 1), isBetween(0, 1), isBetween(0, 1), isBetween(0, 1)], 202 | strokeWidth: [isBetween(0, 10), isBetween(0, 10), isBetween(0, 10), isBetween(0, 10)], 203 | }; 204 | var ROUND_BUTTON_PARAM_VALIDATOR = RECT_BUTTON_PARAM_VALIDATOR; 205 | 206 | function _and() { 207 | var index = -1, 208 | length = arguments.length; 209 | while (++index < length) if (!arguments[index]) return false; 210 | return true; 211 | } 212 | 213 | function _apply(func, thisArg, args) { 214 | switch (args.length) { 215 | case 0: 216 | return func.call(thisArg); 217 | case 1: 218 | return func.call(thisArg, args[0]); 219 | case 2: 220 | return func.call(thisArg, args[0], args[1]); 221 | case 3: 222 | return func.call(thisArg, args[0], args[1], args[2]); 223 | default: 224 | return func.apply(thisArg, args); 225 | } 226 | } 227 | 228 | function _arrayEach(array, iteratee) { 229 | var index = -1; 230 | var length = array.length; 231 | while (++index < length) iteratee(array[index], index, array); 232 | } 233 | 234 | function _arrayMap(array, iteratee) { 235 | var index = -1; 236 | var length = array.length; 237 | var result = Array(length); 238 | while (++index < length) result[index] = iteratee(array[index], index, array); 239 | return result; 240 | } 241 | 242 | function _arraySome(array, predicate) { 243 | var index = -1; 244 | var length = array.length; 245 | while (++index < length) { 246 | if (predicate(array[index], index, array)) return true; 247 | } 248 | return false; 249 | } 250 | 251 | function _assign(object) { 252 | _objectEach([].slice.call(arguments, 1), function (source) { 253 | if (_isObject(source)) { 254 | for (var property in source) if (_has(source, property)) object[property] = source[property]; 255 | } 256 | }); 257 | return object; 258 | } 259 | 260 | function _baseGet(object, path) { 261 | path = _castPath(path, object); 262 | var index = 0; 263 | var length = path.length; 264 | while (object != null && index < length) object = object[_toKey(path[index++])]; 265 | return index && index == length ? object : undefined; 266 | } 267 | 268 | function _baseObjectToString(value) { 269 | return Object.prototype.toString.call(value); 270 | } 271 | 272 | function _basePick(object, keys) { 273 | return _basePickBy(object, keys, function (value, key) { 274 | return _has(object, key); 275 | }); 276 | } 277 | 278 | function _basePickBy(object, keys, predicate) { 279 | var result = {}; 280 | _arrayEach(keys, function (key) { 281 | var value = object[key]; 282 | if (predicate(value, key)) result[key] = value; 283 | }); 284 | return result; 285 | } 286 | 287 | function _baseToString(value) { 288 | if (typeof value == 'string') return value; 289 | if (_isArray(value)) return _arrayMap(value, _baseToString) + ''; 290 | var result = value + ''; 291 | return result == '0' && 1 / value == -INFINITY ? '-0' : result; 292 | } 293 | 294 | function _castPath(value, object) { 295 | if (_isArray(value)) return value; 296 | return _isKey(value, object) ? [value] : _stringToPath(_toString(value)); 297 | } 298 | 299 | function _chunk(array, count) { 300 | if (count == null || count < 1) return []; 301 | var result = []; 302 | var i = 0; 303 | var length = array.length; 304 | while (i < length) result.push([].slice.call(array, i, (i += count))); 305 | return result; 306 | } 307 | 308 | function _clone(object) { 309 | if (!_isObjectLike(object)) return object; 310 | return _isArray(object) ? object.slice() : _assign({}, object); 311 | } 312 | 313 | function _cloneDeep(object) { 314 | var result = _isArray(object) ? [] : {}; 315 | if (_isObjectLike(object)) { 316 | _each(object, function (value, key, object) { 317 | if (_has(object, key)) result[key] = _isObjectLike(value) ? _cloneDeep(value) : value; 318 | }); 319 | } 320 | return result; 321 | } 322 | 323 | function _constant(value) { 324 | return function () { 325 | return value; 326 | }; 327 | } 328 | 329 | function _contains(array, value) { 330 | var index = -1; 331 | var length = array.length; 332 | while (++index < length) if (array[index] === value) return true; 333 | return false; 334 | } 335 | 336 | function _each(object, iterator) { 337 | var func = _isArray(object) ? _arrayEach : _objectEach; 338 | return func(object, iterator); 339 | } 340 | 341 | function _get(object, key, defaultValue) { 342 | var result = object == null ? undefined : object[key]; 343 | return result === undefined ? defaultValue : result; 344 | } 345 | 346 | function _has(object, key) { 347 | return object != null && Object.prototype.hasOwnProperty.call(object, key); 348 | } 349 | 350 | function _isArray(object) { 351 | return _baseObjectToString(object) === '[object Array]'; 352 | } 353 | 354 | function _isBoolean(value) { 355 | return value === true || value === false; 356 | } 357 | 358 | function _isFunction(object) { 359 | return typeof object === 'function'; 360 | } 361 | 362 | function _isKey(value, object) { 363 | if (_isArray(value)) return false; 364 | var type = typeof value; 365 | if (type == 'number' || type == 'boolean' || value == null) return true; 366 | return _or(reIsPlainProp.test(value), !reIsDeepProp.test(value), object != null && value in Object(object)); 367 | } 368 | 369 | function _isNil(value) { 370 | return value == null; 371 | } 372 | 373 | function _isNull(value) { 374 | return value === null; 375 | } 376 | 377 | function _isNumber(value) { 378 | return typeof value == 'number'; 379 | } 380 | 381 | function _isObject(value) { 382 | var type = typeof value; 383 | return _and(value != null, type == 'object' || type == 'function'); 384 | } 385 | 386 | function _isObjectLike(value) { 387 | return value != null && typeof value == 'object'; 388 | } 389 | 390 | function _isString(value) { 391 | return typeof value == 'string'; 392 | } 393 | 394 | function _keys(object) { 395 | var keys = []; 396 | for (var key in object) { 397 | if (object.hasOwnProperty(key)) keys.push(key); 398 | } 399 | return keys; 400 | } 401 | 402 | function _mapValues(object, iteratee) { 403 | var result = {}; 404 | _objectEach(object, function (value, key, object) { 405 | result[key] = iteratee(value, key, object); 406 | }); 407 | return result; 408 | } 409 | 410 | function _object(list, values) { 411 | if (list == null) return {}; 412 | var result = {}; 413 | for (var i = 0, length = list.length; i < length; i++) { 414 | values ? (result[list[i]] = values[i]) : (result[list[i][0]] = list[i][1]); 415 | } 416 | return result; 417 | } 418 | 419 | function _objectEach(object, iteratee) { 420 | _arrayEach(_keys(object), function (key) { 421 | iteratee(object[key], key, object); 422 | }); 423 | } 424 | 425 | function _or() { 426 | var index = -1, 427 | length = arguments.length; 428 | while (++index < length) if (arguments[index]) return true; 429 | return false; 430 | } 431 | 432 | function _pluck(object, key) { 433 | return _arrayMap(object, _property(key)); 434 | } 435 | 436 | function _property(key) { 437 | return function (object) { 438 | return object[key]; 439 | }; 440 | } 441 | 442 | function _stringToPath(string) { 443 | var result = []; 444 | if (string.charCodeAt(0) === 46) result.push(''); 445 | string.replace(rePropName, function (match, number, quote, subString) { 446 | result.push(quote ? subString.replace(reEscapeChar, '$1') : number || match); 447 | }); 448 | return result; 449 | } 450 | 451 | function _stubFalse() { 452 | return false; 453 | } 454 | 455 | function _times(n, iteratee) { 456 | var index = -1; 457 | var result = Array(n); 458 | while (++index < n) result[index] = iteratee(index); 459 | return result; 460 | } 461 | 462 | function _toKey(value) { 463 | if (typeof value == 'string') return value; 464 | var result = value + ''; 465 | return result == '0' && 1 / value == -INFINITY ? '-0' : result; 466 | } 467 | 468 | function _toString(value) { 469 | return value == null ? '' : _baseToString(value); 470 | } 471 | 472 | function _uniq() { 473 | var result = []; 474 | var array = [].concat.apply([], arguments); 475 | _arrayEach(array, function (value) { 476 | if (!_contains(result, value)) result.push(value); 477 | }); 478 | return result; 479 | } 480 | 481 | function _unset(object, key) { 482 | delete object[key]; 483 | return object; 484 | } 485 | 486 | function _values(object) { 487 | var keys = _keys(object); 488 | var length = keys.length; 489 | var values = new Array(length); 490 | for (var i = 0; i < length; i++) { 491 | values[i] = object[keys[i]]; 492 | } 493 | return values; 494 | } 495 | 496 | function _zip() { 497 | var length = _apply(Math.max, null, _pluck(arguments, 'length').concat(0)); 498 | var results = new Array(length); 499 | for (var i = 0; i < length; i++) { 500 | results[i] = _pluck(arguments, '' + i); 501 | } 502 | return results; 503 | } 504 | 505 | function addContainer(container, value, type, collector) { 506 | var func = isNode(type) ? addNodeContainer : addGeneralContainer; 507 | return _apply(func, null, arguments); 508 | } 509 | 510 | function addControl(container, value) { 511 | var func = isListItemContainer(container.type) ? addListItem : addGeneralControl; 512 | _assign(_apply(func, null, arguments), getElementStyle(value)); 513 | } 514 | 515 | function addGeneralContainer(container, value, type, collector) { 516 | var style = getElementStyle(value); 517 | var container = nativeAddContainer(container, type, wrapElementParam(value, type)); 518 | 519 | if (isSelectableElement(type) && _has(style, 'selection')) { 520 | collector.selectableElement.push({ container: container, itemIndex: style.selection }); 521 | return _assign(container, _unset(style, 'selection')); 522 | } 523 | 524 | return _assign(container, getElementStyle(value)); 525 | } 526 | 527 | function addGeneralControl(container, value, type, collector) { 528 | type = IS_AE_2019 && isCustomButton(type) ? 'button' : type; 529 | var func = isCustomControl(type) ? customAddControl : nativeAddControl; 530 | var control = func(container, type, wrapElementParam(value, type)); 531 | return control; 532 | } 533 | 534 | function addListItem(container, value, type, collector) { 535 | return nativeAddItem(container, getListItemParam(value)); 536 | } 537 | 538 | function addNodeContainer(container, value, type, collector) { 539 | var style = getElementStyle(value); 540 | var node = nativeAddNode(container, getListItemParam(value)); 541 | if (style.expanded) collector.nodeItems.push(node); 542 | return _assign(node, _unset(style, 'expanded')); 543 | } 544 | 545 | function assignCustomParam(customParam, defaultParam, validator) { 546 | eachPathDeep(defaultParam, function (value, key, path, object) { 547 | var _value = _baseGet(customParam, path); 548 | if (_baseGet(validator, path)(_value)) object[key] = _value; 549 | }); 550 | return defaultParam; 551 | } 552 | 553 | function baseEachElement(containers, accumulator, breaker, predicate) { 554 | return _arraySome(containers, function (container) { 555 | var _containers = []; 556 | var isDone = _arraySome(container.children, function (element) { 557 | if (isNativeContainer(element.type)) _containers.push(element); 558 | if (predicate(element)) accumulator.push(element); 559 | return breaker(accumulator); 560 | }); 561 | return isDone ? true : baseEachElement(_containers, accumulator, breaker, predicate); 562 | }); 563 | } 564 | 565 | function baseEachPath(object, accumulator, iteratee) { 566 | _each(object, function (value, key) { 567 | iteratee(value, key, accumulator.concat(key), object); 568 | if (_isObjectLike(value)) baseEachPath(value, accumulator.concat(key), iteratee); 569 | }); 570 | } 571 | 572 | function baseEachSource(object, container, containerIteratee, controlIteratee, collector) { 573 | _objectEach(object, function (value, key) { 574 | key = escapeNumber(key).toLowerCase(); 575 | if (!(isValidElement(key) && isValidCombination(container.type, key))) return; 576 | if (isContainer(key)) { 577 | var newContainer = containerIteratee(container, value, key, collector); 578 | baseEachSource(value, newContainer, containerIteratee, controlIteratee, collector); 579 | } else controlIteratee(container, value, key, collector); 580 | }); 581 | } 582 | 583 | function baseGetConfig(value) { 584 | return _get(value, 'config'); 585 | } 586 | 587 | function baseGetElementId(element) { 588 | var properties = element.properties; 589 | return properties && properties.name; 590 | } 591 | 592 | function baseGetListItemParam(value) { 593 | return _get(value, 'param'); 594 | } 595 | 596 | function baseGetParam(value) { 597 | var result = _get(value, 'param'); 598 | return _isArray(result) ? mapNullValue(result) : []; 599 | } 600 | 601 | function baseGetStyle(value) { 602 | var result = _get(value, 'style'); 603 | return _isObject(result) ? result : {}; 604 | } 605 | 606 | function bulidElements(resource, context) { 607 | var container = initMainContainer(resource, context); 608 | var collector = new ElementCollector(); 609 | 610 | baseEachSource(resource, container, addContainer, addControl, collector); 611 | 612 | selectChildItem(collector.selectableElement); 613 | expandTreeViewNodes(collector.nodeItems); 614 | 615 | return container; 616 | } 617 | 618 | function buildNativeWindow(resource, context, showWindow, layoutMode) { 619 | var container = bulidElements(resource, context); 620 | initLayout(container, layoutMode); 621 | if (isWindow(container) && showWindow) container.show(); 622 | return container; 623 | } 624 | 625 | function buildSingletonWindow(resource, context, showWindow, layoutMode) { 626 | var container = null; 627 | return function () { 628 | if (isInvisibleContainer(container) || IS_PS_HOST) container = bulidElements(resource, context); 629 | initLayout(container, layoutMode); 630 | if (isWindow(container) && showWindow) container.show(); 631 | return container; 632 | }; 633 | } 634 | 635 | function buildWindow(isSingletonWindow) { 636 | var func = isSingletonWindow ? buildSingletonWindow : buildNativeWindow; 637 | return _apply(func, null, [].slice.call(arguments, 1)); 638 | } 639 | 640 | function calculateTextLocation() { 641 | var rawSize = this.graphics.measureString(this.text, this.font); 642 | var drawSize = this.graphics.measureString(this.text, this.font, rawSize[0]); 643 | var drawWidth = IS_PS_HOST ? fixPsWidthMeasureIssue(drawSize[0], this.text) : drawSize[0]; 644 | var width = (this.width - drawWidth) / 2 + this.left + this.fontOffset[0]; 645 | var height = IS_NO_FONT_SIZE_HOST ? FIX_HEIGHT_MEASURE_ISSUE : (this.height - drawSize[1]) / 2 + this.top + this.fontOffset[1]; 646 | return [width + FIX_TEXT_LOCATION[0], height + FIX_TEXT_LOCATION[1]]; 647 | } 648 | 649 | function createFont(param) { 650 | return ScriptUI.newFont(param.fontName, mapFontStyleFlag(param.fontStyle), param.fontSize); 651 | } 652 | 653 | function createPens(graphics, pensParam) { 654 | var pens = _arrayMap(pensParam, function (param) { 655 | return _apply(newPens, graphics, param); 656 | }); 657 | return _object(MOUSE_EVENT_REFERENCES, pens); 658 | } 659 | 660 | function createRectButton(container, type, param) { 661 | var rawProperties = getCreationProperties(param, type); 662 | if (_isNil(rawProperties)) rawProperties = {}; 663 | var bounds = _get(param, 1, RECT_STYLE_DEFAULT.bounds); 664 | var text = _get(param, 2, RECT_STYLE_DEFAULT.text); 665 | var baseElementParam = { text: text, properties: { name: rawProperties.name } }; 666 | var control = container.add(createResourceSpecification(type, baseElementParam), bounds); 667 | var drawParam = _assign(_basePick(rawProperties, RECT_BUTTON_VALID_PROPERTIES), { bounds: bounds, text: text }); 668 | drawParam = assignCustomParam(mapFullParam(drawParam), _cloneDeep(RECT_CREATION_DEFAULT), RECT_BUTTON_PARAM_VALIDATOR); 669 | return drawControl(control, type, drawParam); 670 | } 671 | 672 | function createResourceSpecification(type, param) { 673 | return mapCustomElementType(type) + createStringCode(param); 674 | } 675 | 676 | function createRoundButton(container, type, param) { 677 | var rawProperties = getCreationProperties(param, type); 678 | if (_isNil(rawProperties)) rawProperties = {}; 679 | var bounds = _get(param, 1, ROUND_STYLE_DEFAULT.bounds); 680 | var text = _get(param, 2, ROUND_STYLE_DEFAULT.text); 681 | var baseElementParam = { text: text, properties: { name: rawProperties.name } }; 682 | var control = container.add(createResourceSpecification(type, baseElementParam), bounds); 683 | var drawParam = _assign(_basePick(rawProperties, ROUND_BUTTON_VALID_PROPERTIES), { bounds: bounds, text: text }); 684 | drawParam = assignCustomParam(mapFullParam(drawParam), _cloneDeep(ROUND_CREATION_DEFAULT), ROUND_BUTTON_PARAM_VALIDATOR); 685 | return drawControl(control, type, drawParam); 686 | } 687 | 688 | function createStringCode(param) { 689 | return escapeParentheses(uneval(param)); 690 | } 691 | 692 | function customAddControl(container, type, param) { 693 | var addStrategies = { rectbutton: createRectButton, roundbutton: createRoundButton }; 694 | return addStrategies[type](container, type, param); 695 | } 696 | 697 | function customDraw(control, type) { 698 | var graphics = control.graphics; 699 | var definePathmethod = type === 'rectbutton' ? 'rectPath' : 'ellipsePath'; 700 | var drawValue = type === 'rectbutton' ? DrawRectValues : DrawRoundValues; 701 | control.onDraw = function () { 702 | var param = new drawValue(control); 703 | graphics[definePathmethod](param.left, param.top, param.width, param.height); 704 | if (param.enableFill) graphics.fillPath(param.pen.fill); 705 | if (param.enableStroke) graphics.strokePath(param.pen.stroke); 706 | if (param.enableText) graphics.drawString(param.text, param.pen.text, param.textX, param.textY, param.font); 707 | }; 708 | return control; 709 | } 710 | 711 | function drawControl(control, type, param) { 712 | var graphics = control.graphics; 713 | var pens = createPens(graphics, mapPensParam(param)); 714 | var font = createFont(mapFontParam(param)); 715 | _assign(graphics, _basePick(param, CUSTOM_BUTTON_CONFIG_PROPERTIES), { pen: pens.mouseout, font: font }); 716 | return customDraw(wrapOnClickEvent(wrapPensState(control, pens)), type); 717 | } 718 | 719 | function DrawRectValues(control) { 720 | this.graphics = control.graphics; 721 | this.enableText = this.graphics.enableText; 722 | this.enableFill = this.graphics.enableFill; 723 | this.enableStroke = this.graphics.enableStroke; 724 | this.fontOffset = this.graphics.fontOffset; 725 | this.pen = this.graphics.pen; 726 | this.text = control.text; 727 | this.font = this.graphics.font; 728 | this.left = 0; 729 | this.top = 0; 730 | this.width = control.size[0] - FIX_PS_STROKE_ISSUE; 731 | this.height = control.size[1] - FIX_PS_STROKE_ISSUE; 732 | this.textLocation = calculateTextLocation.call(this); 733 | this.textX = this.textLocation[0]; 734 | this.textY = this.textLocation[1]; 735 | } 736 | 737 | function DrawRoundValues(control) { 738 | this.graphics = control.graphics; 739 | this.enableText = this.graphics.enableText; 740 | this.enableFill = this.graphics.enableFill; 741 | this.enableStroke = this.graphics.enableStroke; 742 | this.fontOffset = this.graphics.fontOffset; 743 | this.pen = this.graphics.pen; 744 | this.text = control.text; 745 | this.font = this.graphics.font; 746 | this.left = this.pen.stroke.lineWidth; 747 | this.top = this.pen.stroke.lineWidth; 748 | this.width = control.size[0] - this.left * 2 - FIX_PS_STROKE_ISSUE; 749 | this.height = control.size[1] - this.top * 2 - FIX_PS_STROKE_ISSUE; 750 | this.textLocation = calculateTextLocation.call(this); 751 | this.textX = this.textLocation[0]; 752 | this.textY = this.textLocation[1]; 753 | } 754 | 755 | function eachPathDeep(object, iteratee) { 756 | baseEachPath(object, [], function (value) { 757 | if (!_isObjectLike(value)) _apply(iteratee, null, arguments); 758 | }); 759 | } 760 | 761 | function ElementCollector() { 762 | this.nodeItems = []; 763 | this.selectableElement = []; 764 | } 765 | 766 | function escapeNumber(string) { 767 | return string.replace(reEscapeNumber, ''); 768 | } 769 | 770 | function escapeParentheses(string) { 771 | return string.replace(reEscapeParentheses, ''); 772 | } 773 | 774 | function expandTreeViewNodes(nodes) { 775 | _arrayEach(nodes, function (node) { 776 | node.expanded = true; 777 | }); 778 | } 779 | 780 | function fixPsWidthMeasureIssue(rawWidth, text) { 781 | var textNum = text.length; 782 | return rawWidth / ((textNum + PS_WIDTH_MEASURE_FACTOR) / textNum); 783 | } 784 | 785 | function freezeProperty(object, property) { 786 | object.watch(property, function (name, oldValue, newValue) { 787 | return oldValue; 788 | }); 789 | } 790 | 791 | function getBridgeTalk(object) { 792 | return !!object && isBridgeTalkObject(object.BridgeTalk) && object.BridgeTalk; 793 | } 794 | 795 | function getCreationProperties(param, key) { 796 | return param[getCreationPropertiesIndex(key)]; 797 | } 798 | 799 | function getCreationPropertiesIndex(key) { 800 | if (isNativeControl(key)) return CONTROL_PARAM_REFERENCES[key]; 801 | if (isCustomControl(key)) return CUSTOM_CONTROL_PARAM_REFERENCES[key]; 802 | if (isContainer(key)) return CONTAINER_PARAM_REFERENCES[key]; 803 | } 804 | 805 | function getElementParam(value) { 806 | return _isArray(value) ? mapNullValue(value) : baseGetParam(value); 807 | } 808 | 809 | function getElementsById(targetId) { 810 | targetId = String(targetId); 811 | var result = []; 812 | 813 | function breaker(accumulator) { 814 | return accumulator.length > 0; 815 | } 816 | 817 | baseEachElement([this], result, breaker, function (element) { 818 | var elementId = baseGetElementId(element); 819 | if (_isNil(elementId)) return false; 820 | return targetId === elementId; 821 | }); 822 | 823 | return result.length === 0 ? null : result[0]; 824 | } 825 | 826 | function getElementsByName() { 827 | targetNames = wrapFindElementInput(arguments); 828 | var seen = []; 829 | var result = []; 830 | 831 | function breaker() { 832 | return targetNames.length === seen.length; 833 | } 834 | 835 | baseEachElement([this], result, breaker, function (element) { 836 | var elementId = baseGetElementId(element); 837 | if (_isNil(elementId)) return false; 838 | return _contains(targetNames, elementId) && !_contains(seen, elementId) && seen.push(elementId); 839 | }); 840 | 841 | return result.length === 0 ? null : result; 842 | } 843 | 844 | function getElementsByType() { 845 | var targetTypes = wrapFindElementInput(arguments); 846 | var result = []; 847 | 848 | baseEachElement([this], result, _stubFalse, function (element) { 849 | return _contains(targetTypes, element.type); 850 | }); 851 | 852 | return result.length === 0 ? null : result; 853 | } 854 | 855 | function getElementStyle(value) { 856 | return _isArray(value) ? {} : baseGetStyle(value); 857 | } 858 | 859 | function getGlobal(object) { 860 | return isHelperObject(object) && isGlobalObject(object.global) && object.global; 861 | } 862 | 863 | function getListItemParam(value) { 864 | var result = _isObject(value) ? baseGetListItemParam(value) : value; 865 | return String(result); 866 | } 867 | 868 | function getMainContainer(param, context) { 869 | return isPanel(context) ? context : new Window(param[0], param[1], param[2], param[3]); 870 | } 871 | 872 | function hexToArray(hex) { 873 | return _arrayMap(hexToRgb(hex), function (value) { 874 | return value / 255; 875 | }); 876 | } 877 | 878 | function hexToRgb(hex) { 879 | var result; 880 | hex = hex.replace(reMapFullHex, '$1$1$2$2$3$3'); 881 | hex.replace(reIsHex, function (match, $1, $2, $3) { 882 | result = [parseInt($1, 16), parseInt($2, 16), parseInt($3, 16)]; 883 | }); 884 | return result; 885 | } 886 | 887 | function hexToRgba(hex, alpha) { 888 | return hexToArray(hex).concat(alpha); 889 | } 890 | 891 | function initLayout(container, layoutMode) { 892 | container.layout.layout(layoutMode); 893 | container.layout.resize(); 894 | } 895 | 896 | function initMainContainer(resource, context) { 897 | var mainContainer = getMainContainer(wrapWindowType(baseGetParam(resource)), context); 898 | mainContainer.onResizing = mainContainer.onResize = resizeHandle; 899 | return _assign(mainContainer, baseGetStyle(resource)); 900 | } 901 | 902 | function isAppVersion(number) { 903 | return parseInt(BRIDGETALK.appVersion) === number; 904 | } 905 | 906 | function isBetween(start, end) { 907 | return function (value) { 908 | return _isNumber(value) && value >= start && value <= end; 909 | }; 910 | } 911 | 912 | function isBridgeTalkObject(value) { 913 | return value != null && _baseObjectToString(value) === '[object BridgeTalk]'; 914 | } 915 | 916 | function isContainer(type) { 917 | return reIsContainer.test(CONTROL_TYPE_FLAG[type]); 918 | } 919 | 920 | function isCustomButton(type) { 921 | return reIsCustomButton.test(CONTROL_TYPE_FLAG[type]); 922 | } 923 | 924 | function isCustomControl(type) { 925 | return reIsCustomControl.test(CONTROL_TYPE_FLAG[type]); 926 | } 927 | 928 | function isFontStyleFlag(value) { 929 | return _or(isBetween(0, 3)(value), _isString(value) && reIsFontStyleFlag.test(value)); 930 | } 931 | 932 | function isGlobalContext(value) { 933 | return value === GLOBAL; 934 | } 935 | 936 | function isGlobalObject(value) { 937 | return value != null && _baseObjectToString(value) === '[object global]'; 938 | } 939 | 940 | function isHelperObject(value) { 941 | return value != null && _baseObjectToString(value) === '[object $]'; 942 | } 943 | 944 | function isHex(hex) { 945 | return _isString(hex) && reIsHex.test(hex); 946 | } 947 | 948 | function isHost(hostName) { 949 | return HOST_NAME === hostName; 950 | } 951 | 952 | function isInvisibleContainer(container) { 953 | return container === null || !container.visible; 954 | } 955 | 956 | function isLeftClick(event) { 957 | return event.button === MOUSE_LEFT_CLICK_FLAG; 958 | } 959 | 960 | function isListItemContainer(type) { 961 | return reIsListItemContainer.test(CONTROL_TYPE_FLAG[type]); 962 | } 963 | 964 | function isNativeControl(type) { 965 | return reIsNativeControl.test(CONTROL_TYPE_FLAG[type]); 966 | } 967 | 968 | function isNativeContainer(type) { 969 | return reIsNativeContainer.test(CONTROL_TYPE_FLAG[type]); 970 | } 971 | 972 | function isNode(type) { 973 | return type === 'node'; 974 | } 975 | 976 | function isPanel(container) { 977 | return container instanceof Panel; 978 | } 979 | 980 | function isPanelContext(global) { 981 | return isPanel(global); 982 | } 983 | 984 | function isRightClick(event) { 985 | return event.button === MOUSE_RIGHT_CLICK_FLAG; 986 | } 987 | 988 | function isSelectableElement(type) { 989 | return reIsSelectableElement.test(CONTROL_TYPE_FLAG[type]); 990 | } 991 | 992 | function isTabbedpanel(type) { 993 | return type === 'tabbedpanel'; 994 | } 995 | 996 | function isValidCombination(parentType, childType) { 997 | var flagCombination = CONTROL_TYPE_FLAG[parentType] + CONTROL_TYPE_FLAG[childType]; 998 | return reCombination.test(flagCombination); 999 | } 1000 | 1001 | function isValidContext(global) { 1002 | return isGlobalContext(global) || isPanelContext(global); 1003 | } 1004 | 1005 | function isValidElement(type) { 1006 | return _has(CONTROL_TYPE_FLAG, type); 1007 | } 1008 | 1009 | function isValidHost(bridgetalk, supportlist) { 1010 | return !!bridgetalk && _contains(supportlist, bridgetalk.appName); 1011 | } 1012 | 1013 | function isWindow(container) { 1014 | return container instanceof Window; 1015 | } 1016 | 1017 | function mapCustomElementType(customControlType) { 1018 | return CUSTOM_ELEMENT_TYPE_REFERENCES[customControlType]; 1019 | } 1020 | 1021 | function mapFontParam(param) { 1022 | return _basePick(param, CUSTOM_BUTTON_FONT_PROPERTIES); 1023 | } 1024 | 1025 | function mapFontStyleFlag(value) { 1026 | return _isString(value) ? ScriptUI.FontStyle[value.toUpperCase()] : value; 1027 | } 1028 | 1029 | function mapFullParam(param) { 1030 | return _mapValues(param, function (value, key) { 1031 | return _contains(CUSTOM_BUTTON_AUTO_FILL_PROPERTIES, key) && !_isArray(value) ? _times(4, _constant(value)) : value; 1032 | }); 1033 | } 1034 | 1035 | function mapNullValue(array) { 1036 | return _arrayMap(array, function (value) { 1037 | return _isNull(value) ? undefined : value; 1038 | }); 1039 | } 1040 | 1041 | function mapPensParam(param) { 1042 | return _arrayMap(_apply(_zip, null, _values(_basePick(param, CUSTOM_BUTTON_PENS_PROPERTIES))), function (values) { 1043 | var result = _chunk(values, 3); 1044 | var colorValue = _zip(result[0], result[1]); 1045 | var colorArray = _arrayMap(colorValue, function (value) { 1046 | return _apply(hexToRgba, null, value); 1047 | }); 1048 | return colorArray.concat(result[2]); 1049 | }); 1050 | } 1051 | 1052 | function nativeAddContainer(container, type, param) { 1053 | return container.add(type, param[1], param[2], param[3]); 1054 | } 1055 | 1056 | function nativeAddControl(container, type, param) { 1057 | return container.add(type, param[1], param[2], param[3], param[4], param[5]); 1058 | } 1059 | 1060 | function nativeAddItem(node, param) { 1061 | return node.add('item', param); 1062 | } 1063 | 1064 | function nativeAddNode(container, param) { 1065 | return container.add('node', param); 1066 | } 1067 | 1068 | function newPens(fontColor, fillColor, strokeColor, strokeWidth) { 1069 | return { 1070 | text: this.newPen(this.PenType.SOLID_COLOR, fontColor, 1), 1071 | fill: this.newBrush(this.BrushType.SOLID_COLOR, fillColor), 1072 | stroke: this.newPen(this.PenType.SOLID_COLOR, strokeColor, strokeWidth), 1073 | }; 1074 | } 1075 | 1076 | function resizeHandle() { 1077 | this.layout.resize(); 1078 | } 1079 | 1080 | function runInContext(resource) { 1081 | if (!_isObject(resource)) return null; 1082 | var container = _apply(buildWindow, null, WindowBuildValue(resource, TREE)); 1083 | MAIN_CONTAINER_COLLECTION.push(container); 1084 | return container; 1085 | } 1086 | 1087 | function selectChildItem(selectableElement) { 1088 | _arrayEach(selectableElement, function (value) { 1089 | var container = value.container; 1090 | var itemIndex = value.itemIndex; 1091 | if (isTabbedpanel(container.type)) return (container.selection = value.itemIndex); 1092 | var items = _arrayMap(_isArray(itemIndex) ? itemIndex : [itemIndex], function (indexValue) { 1093 | return container.items[indexValue]; 1094 | }); 1095 | container.selection = items; 1096 | }); 1097 | } 1098 | 1099 | function WindowBuildValue(resource, parserSelf) { 1100 | var config = _assign(_clone(MAIN_CONTAINER_DEFAULT), baseGetConfig(resource)); 1101 | var showWindow = !!config.show; 1102 | var dockable = !!config.dockable; 1103 | var isSingletonWindow = !!config.singleton; 1104 | var context = wrapContext(parserSelf.context, dockable, isSingletonWindow); 1105 | var layoutMode = wrapLayoutMode(parserSelf.layoutMode, config.layoutMode); 1106 | return [isSingletonWindow, resource, context, showWindow, layoutMode]; 1107 | } 1108 | 1109 | function wrapContext(global, dockable, isSingletonWindow) { 1110 | if (isSingletonWindow || !dockable) return Window; 1111 | if (isValidContext(ROOT)) return ROOT; 1112 | return isValidContext(global) ? global : Window; 1113 | } 1114 | 1115 | function wrapElementParam(value, type) { 1116 | return wrapNodeName(getElementParam(value), type); 1117 | } 1118 | 1119 | function wrapFindElementInput(input) { 1120 | return _uniq(_arrayMap([].concat.apply([], input), String)); 1121 | } 1122 | 1123 | function wrapGetElementMethods(constructors) { 1124 | _arrayEach(constructors, function (constructor) { 1125 | var prototype = constructor.prototype; 1126 | prototype.getElementById = getElementsById; 1127 | prototype.getElementsByName = getElementsByName; 1128 | prototype.getElementsByType = getElementsByType; 1129 | freezeProperty(prototype, 'getElementById'); 1130 | freezeProperty(prototype, 'getElementsByName'); 1131 | freezeProperty(prototype, 'getElementsByType'); 1132 | }); 1133 | } 1134 | 1135 | function wrapLayoutMode(setAll, setAlone) { 1136 | if (isBetween(0, 2)(setAlone)) return setAlone; 1137 | if (isBetween(0, 2)(setAll)) return setAll; 1138 | return 0; 1139 | } 1140 | 1141 | function wrapMouseClickEvent(control, handle) { 1142 | _arrayEach(MOUSE_EVENT_REFERENCES, function (eventName) { 1143 | control.addEventListener(eventName, handle); 1144 | }); 1145 | } 1146 | 1147 | function wrapNodeName(param, type) { 1148 | var nodedName = param[0]; 1149 | if (_isNil(nodedName)) return param; 1150 | var index = getCreationPropertiesIndex(type); 1151 | var creationProperties = param[index]; 1152 | if (!_isObject(creationProperties)) param[index] = {}; 1153 | if (_has(creationProperties, 'name')) return param; 1154 | param[index].name = nodedName; 1155 | return param; 1156 | } 1157 | 1158 | function wrapOnClickEvent(control) { 1159 | control.addEventListener('mouseup', function (event) { 1160 | event.stopPropagation(); 1161 | if (_isFunction(control.onClick) && isLeftClick(event)) control.onClick(); 1162 | }); 1163 | return control; 1164 | } 1165 | 1166 | function wrapPensState(control, pens) { 1167 | wrapMouseClickEvent(control, function (event) { 1168 | event.stopPropagation(); 1169 | if (isRightClick(event)) return; 1170 | control.graphics.pen = pens[event.type]; 1171 | control.notify('onDraw'); 1172 | }); 1173 | return control; 1174 | } 1175 | 1176 | function wrapWindowType(param) { 1177 | var type = String(param[0]); 1178 | param[0] = _contains(SUPPORT_CONTAINER_TYPE, type) ? type : MAIN_CONTAINER_TYPE_DEFAULT; 1179 | return param; 1180 | } 1181 | 1182 | wrapGetElementMethods([Window, Panel, Group]); 1183 | 1184 | TREE.parse = runInContext; 1185 | TREE.version = VERSION; 1186 | 1187 | return TREE; 1188 | }.call(this); 1189 | --------------------------------------------------------------------------------