· 9 years ago · Jan 14, 2017, 11:20 PM
1local files = {
2["MThemeManager.ti"]="--[[\
3 The MThemeManager mixin \"should\" be used by classes that want to manage objects which are themeable, the main example being the 'Application' class.\
4]]\
5\
6local function deepCopy( source )\
7 if type( source ) == \"table\" then\
8 local copy = {}\
9 for key, value in next, source, nil do copy[ deepCopy( key ) ] = deepCopy( value ) end\
10\
11 return copy\
12 end\
13\
14 return source\
15end\
16\
17class \"MThemeManager\" abstract() {\
18 themes = {}\
19}\
20\
21--[[\
22 @instance\
23 @desc Adds the given theme into this objects `themes` table\
24 @param <Theme Instance - theme>\
25]]\
26function MThemeManager:addTheme( theme )\
27 self:removeTheme( theme )\
28 table.insert( self.themes, theme )\
29\
30 self:update()\
31end\
32\
33--[[\
34 @instance\
35 @desc Removes the given theme from this objects `themes` table. Returns true if a theme was removed, false otherwise.\
36 @param <Instance 'Theme'/string name - target>\
37 @return <boolean - success>\
38]]\
39function MThemeManager:removeTheme( target )\
40 local searchName = ( type( target ) == \"string\" and true ) or ( not Titanium.typeOf( target, \"Theme\", true ) and error \"Invalid target to remove\" )\
41 local themes = self.themes\
42 for i = 1, #themes do\
43 if ( searchName and themes[ i ].name == target ) or ( not searchName and themes[ i ] == target ) then\
44 table.remove( themes, i )\
45 self:update()\
46\
47 return true\
48 end\
49 end\
50end\
51\
52--[[\
53 @instance\
54 @desc Adds a theme instance named 'name' and imports the file contents from 'location' to this object\
55 @param <string - name>, <string - location>\
56]]\
57function MThemeManager:importTheme( name, location )\
58 self:addTheme( Theme.fromFile( name, location ) )\
59end\
60\
61--[[\
62 @instance\
63 @desc Updates the theme information bound to this object. All nodes under this parent are reset and the rules are reapplied to each.\
64 'onThemeUpdate' is called on each child node allowing for each node to update themselves after their new theme information has been applied.\
65 @param [table - target]\
66]]\
67function MThemeManager:update( target )\
68 self:cacheAllApplicableRules()\
69\
70 local target = target or self.collatedNodes\
71 for i = 1, #target do target[ i ]:onThemeUpdate() end\
72end\
73\
74--[[\
75 @instance\
76 @desc Resets the applicableRules table on all nodes of type 'type' that are under this node (collated/query)\
77 @param <string - type>\
78]]\
79function MThemeManager:resetApplicableRules( type )\
80 local nodes = type == \"*\" and self.collatedNodes or self:query( type ).result\
81 for i = 1, #nodes do\
82 nodes[ i ].applicableRules = {}\
83 end\
84end\
85\
86--[[\
87 @instance\
88 @desc Iterates over the rules given and applies them as 'applicable rules' to each eligible node (collated/query).\
89 These 'applicable rules' are then used by the node to determine theme properties.\
90 @param <table - rules>\
91]]\
92function MThemeManager:cacheApplicableRules( rules )\
93 local cachedRegistry, cachedTypes, rule, compute = {}, {}\
94\
95 for i = 1, #rules do\
96 rule = rules[ i ]\
97 compute = rule.computeType\
98\
99 self:query( rule.query ):each(function( item )\
100 local applicable = item.applicableRules\
101 if compute then\
102 local itemType = item.__type\
103 if not cachedRegistry[ itemType ] then\
104 cachedRegistry[ itemType ] = Titanium.getClass( itemType ).getRegistry()\
105 end\
106\
107 local reg = cachedRegistry[ itemType ]\
108 if not cachedTypes[ itemType ] then\
109 cachedTypes[ itemType ] = reg.constructor and reg.constructor.argumentTypes or {}\
110 end\
111\
112 local ruleClone = deepCopy( rule )\
113 ruleClone.value = XMLParser.convertArgType( ruleClone.value, cachedTypes[ itemType ][ reg.alias[ rule.property ] or rule.property ] )\
114 applicable[ #applicable + 1 ] = ruleClone\
115 else\
116 applicable[ #applicable + 1 ] = rule\
117 end\
118 end)\
119 end\
120end\
121\
122--[[\
123 @instance\
124 @desc Iterates over each theme bound to this object and caches the rules for type 'type' by passing them to 'MThemeManager.cacheApplicableRules'\
125 @param <string - type>, [boolean - manualReset]\
126]]\
127function MThemeManager:cacheApplicableRulesForType( type, manualReset )\
128 local themes = self.themes\
129 if not manualReset then self:resetApplicableRules( type ) end\
130\
131 for i = 1, #themes do\
132 self:cacheApplicableRules( themes[ i ].rules[ type ] or {} )\
133\
134 local ANY = themes[ i ].rules.ANY\
135 if ANY then self:cacheApplicableRules( ANY ) end\
136 end\
137end\
138\
139--[[\
140 @instance\
141 @desc Caches all rules from all themes bound to this object by passing them to 'MThemeManager.cacheApplicableRules'\
142 @param [boolean - manualReset]\
143]]\
144function MThemeManager:cacheAllApplicableRules( manualReset )\
145 local themes, rules = self.themes\
146 if not manualReset then self:resetApplicableRules \"*\" end\
147\
148 for i = 1, #themes do\
149 rules = themes[ i ].rules\
150 for type in pairs( rules ) do\
151 self:cacheApplicableRules( rules[ type ] )\
152 end\
153 end\
154end\
155",
156["QueryParser.ti"]="local function parseValue( val )\
157 if val == \"true\" then return true\
158 elseif val == \"false\" then return false end\
159\
160 return tonumber( val ) or error(\"Invalid value passed for parsing '\"..tostring( val )..\"'\")\
161end\
162\
163class \"QueryParser\" extends \"Parser\"\
164\
165function QueryParser:__init__( queryString )\
166 self:super( QueryLexer( queryString ).tokens )\
167end\
168\
169function QueryParser:parse()\
170 local allQueries, currentQuery, currentStep = {}, {}, {}\
171\
172 local nextStepDirect\
173 local function advanceSection()\
174 if next( currentStep ) then\
175 table.insert( currentQuery, currentStep )\
176 currentStep = { direct = nextStepDirect }\
177\
178 nextStepDirect = nil\
179 end\
180 end\
181\
182 local token = self:stepForward()\
183 while token do\
184 if token.type == \"QUERY_TYPE\" then\
185 if currentStep.type then self:throw( \"Attempted to set query type to '\"..token.value..\"' when already set as '\"..currentStep.type..\"'\" ) end\
186\
187 currentStep.type = token.value\
188 elseif token.type == \"QUERY_CLASS\" then\
189 if not currentStep.classes then currentStep.classes = {} end\
190\
191 table.insert( currentStep.classes, token.value )\
192 elseif token.type == \"QUERY_ID\" then\
193 if currentStep.id then self:throw( \"Attempted to set query id to '\"..token.value..\"' when already set as '\"..currentStep.id..\"'\" ) end\
194\
195 currentStep.id = token.value\
196 elseif token.type == \"QUERY_SEPERATOR\" then\
197 if self.tokens[ self.position + 1 ].type ~= \"QUERY_DIRECT_PREFIX\" then\
198 advanceSection()\
199 end\
200 elseif token.type == \"QUERY_END\" then\
201 advanceSection()\
202\
203 if next( currentQuery ) then\
204 table.insert( allQueries, currentQuery )\
205 currentQuery = {}\
206 else\
207 self:throw( \"Unexpected '\"..token.value..\"' found, no left hand query\" )\
208 end\
209 elseif token.type == \"QUERY_COND_OPEN\" then\
210 currentStep.condition = self:parseCondition()\
211 elseif token.type == \"QUERY_DIRECT_PREFIX\" and not nextStepDirect then\
212 nextStepDirect = true\
213 else\
214 self:throw( \"Unexpected '\"..token.value..\"' found while parsing query\" )\
215 end\
216\
217 token = self:stepForward()\
218 end\
219\
220 advanceSection()\
221 if next( currentQuery ) then\
222 table.insert( allQueries, currentQuery )\
223 end\
224\
225 self.query = allQueries\
226end\
227\
228function QueryParser:parseCondition()\
229 local conditions, condition = {}, {}\
230\
231 local token = self:stepForward()\
232 while true do\
233 if token.type == \"QUERY_COND_ENTITY\" and ( condition.symbol or not condition.property ) then\
234 condition[ condition.symbol and \"value\" or \"property\" ] = condition.symbol and parseValue( token.value ) or token.value\
235 elseif token.type == \"QUERY_COND_STRING_ENTITY\" and condition.symbol then\
236 condition.value = token.value\
237 elseif token.type == \"QUERY_COND_SYMBOL\" and not condition.property and token.value == \"#\" then\
238 condition.modifier = token.value\
239 elseif token.type == \"QUERY_COND_SYMBOL\" and ( condition.property ) then\
240 condition.symbol = token.value\
241 elseif token.type == \"QUERY_COND_SEPERATOR\" and next( condition ) then\
242 conditions[ #conditions + 1 ] = condition\
243 condition = {}\
244 elseif token.type == \"QUERY_COND_CLOSE\" and ( not condition.property or ( condition.property and condition.value ) ) then\
245 break\
246 else\
247 self:throw( \"Unexpected '\"..token.value..\"' inside of condition block\" )\
248 end\
249\
250 token = self:stepForward()\
251 end\
252\
253 if next( condition ) then\
254 conditions[ #conditions + 1 ] = condition\
255 end\
256\
257 return #conditions > 0 and conditions or nil\
258end\
259",
260["Theme.ti"]="--[[\
261 The Theme class \"is\" a basic class \"designed\" to hold styling rules.\
262\
263 Themes are added to objects using the MThemeManager mixin (or a custom implementation). These themes then dictate the appearance of objects that utilize 'MThemeable'.\
264]]\
265local function getTagDetails( rule )\
266 return ( rule.arguments.id and ( \"#\" .. rule.arguments.id ) or \"\" ) .. (function( classString ) local classes = \"\"; for className in classString:gmatch(\"%S+\") do classes = classes .. \".\"..className end; return classes end)( rule.arguments[\"class\"] or \"\" )\
267end\
268\
269local function splitXMLTheme( queue, tree )\
270 for i = 1, #tree do\
271 local children = tree[ i ].children\
272 if children then\
273 for c = 1, #children do\
274 local type = tree[ i ].type\
275 queue[ #queue + 1 ] = { ( type == \"Any\" and \"*\" or type ) .. getTagDetails( tree[ i ] ), children[ c ], tree[ i ] }\
276 end\
277 end\
278 end\
279\
280 return queue\
281end\
282\
283class \"Theme\" {\
284 name = false;\
285\
286 rules = {};\
287}\
288\
289--[[\
290 @constructor\
291 @desc Constructs the Theme by setting the name.\
292 @param <string - name>, [string - source]\
293]]\
294function Theme:__init__( name, source )\
295 self.name = type( name ) == \"string\" and name or error(\"Failed to initialise Theme. Name '\"..tostring( name )..\"' is invalid, expected string.\")\
296\
297 if source then self.rules = Theme.parse( source ) end\
298end\
299\
300--[[\
301 @static\
302 @desc Parses XML source code by lexing/parsing it into an XML tree. The XML is then parsed into theme rules\
303 @param <string - source>\
304 @return <table - rules>\
305]]\
306function Theme.static.parse( source )\
307 local queue, rawRules, q = splitXMLTheme( {}, XMLParser( source ).tree ), {}, 1\
308\
309 local function processQueueEntry( entry )\
310 local queryPrefix, rule = entry[ 1 ], entry[ 2 ]\
311 local children = rule.children\
312\
313 if children then\
314 for c = 1, #children do\
315 if not Titanium.getClass( rule.type ) and rule.type ~= \"Any\" then\
316 return error( \"Failed to generate theme data. Child target '\"..rule.type..\"' doesn't exist as a Titanium class\" )\
317 end\
318\
319 local type = rule.type\
320 queue[ #queue + 1 ] = { queryPrefix .. \" \" .. ( rule.arguments.direct and \"> \" or \"\" ) .. ( type == \"Any\" and \"*\" or type ) .. getTagDetails( rule ), children[ c ], rule }\
321 end\
322 elseif rule.content then\
323 local ownerType = entry[ 3 ].type\
324 local dynamic = rule.arguments.dynamic\
325\
326 local ruleTarget, computeType, value = ownerType, false, rule.content\
327 if ownerType == \"Any\" then\
328 ruleTarget, computeType = \"ANY\", true\
329 elseif not dynamic then\
330 local parentReg = Titanium.getClass( ownerType ).getRegistry()\
331 local argumentTypes = parentReg.constructor and parentReg.constructor.argumentTypes or {}\
332\
333 value = XMLParser.convertArgType( value, argumentTypes[ parentReg.alias[ rule.type ] or rule.type ] )\
334 end\
335\
336 if dynamic then\
337 value = DynamicEqParser( rule.content )\
338 end\
339\
340 if not rawRules[ ruleTarget ] then rawRules[ ruleTarget ] = {} end\
341 table.insert( rawRules[ ruleTarget ], {\
342 query = queryPrefix,\
343 computeType = not dynamic and computeType or nil,\
344 property = rule.type,\
345 value = value,\
346 important = rule.arguments.important,\
347 isDynamic = dynamic\
348 })\
349 else\
350 return error( \"Failed to generate theme data. Invalid theme rule found. No value (XML_CONTENT) has been set for tag '\"..rule.type..\"'\" )\
351 end\
352 end\
353\
354 while q <= #queue do\
355 processQueueEntry( queue[ q ] )\
356 q = q + 1\
357 end\
358\
359 return rawRules\
360end\
361\
362--[[\
363 @static\
364 @desc Creates a Theme instance with the name passed and the source as the contents of the file at 'path'.\
365 @param <string - name>, <string - path>\
366 @return <Theme Instance - Theme>\
367]]\
368function Theme.static.fromFile( name, path )\
369 if not fs.exists( path ) then\
370 return error( \"Path '\"..tostring( path )..\"' cannot be found\" )\
371 end\
372\
373 local h = fs.open( path, \"r\" )\
374 local content = h.readAll()\
375 h.close()\
376\
377 return Theme( name, content )\
378end\
379",
380["Pane.ti"]="--[[\
381 A pane is a very simple node that simply draws a box at 'X', 'Y' with dimensions 'width', 'height'.\
382]]\
383\
384class \"Pane\" extends \"Node\" {\
385 backgroundColour = colours.black;\
386\
387 allowMouse = true;\
388 useAnyCallbacks = true;\
389}\
390\
391--[[\
392 @instance\
393 @desc Resolves arguments and calls super constructor.\
394]]\
395function Pane:__init__( ... )\
396 self:resolve( ... )\
397 self:super()\
398end\
399\
400--[[\
401 @instance\
402 @desc Clears the canvas, the canvas background colour becomes 'backgroundColour' during the clear.\
403 @param [boolean - force]\
404]]\
405function Pane:draw( force )\
406 local raw = self.raw\
407 if raw.changed or force then\
408 raw.canvas:clear()\
409 raw.changed = false\
410 end\
411end\
412\
413--[[\
414 @instance\
415 @desc Handles any mouse events cast onto this node to prevent nodes under it being affected by them.\
416 @param <MouseEvent - event>, <boolean - handled>, <boolean - within>\
417]]\
418function Pane:onMouse( event, handled, within )\
419 if not within or handled then return end\
420\
421 event.handled = true\
422end\
423\
424configureConstructor({\
425 orderedArguments = {\"X\", \"Y\", \"width\", \"height\", \"backgroundColour\"}\
426}, true)\
427",
428["DynamicEqParser.ti"]="local TERMS = { \"NAME\", \"STRING\", \"NUMBER\", \"PAREN\" }\
429local BIN_AMBIG = { \"binary\", \"ambiguos\" }\
430local UNA_AMBIG = { \"unary\", \"ambiguos\" }\
431\
432class \"DynamicEqParser\" extends \"Parser\" {\
433 state = \"root\";\
434 stacks = {{}};\
435 output = \"local args = ...; return \";\
436}\
437\
438function DynamicEqParser:__init__( expression )\
439 self:super( DynamicEqLexer( expression ).tokens )\
440end\
441\
442function DynamicEqParser:testForOperator( beforeType, afterType, optional, beforeOffset, afterOffset )\
443 local pass, before, after = self:testAdjacent( beforeType and \"OPERATOR\", afterType and \"OPERATOR\", false, beforeOffset, afterOffset, not optional )\
444 if not pass then return false end\
445\
446 local function test( token, filter )\
447 if not token then return true elseif not filter then return false end\
448 if type( filter ) == \"table\" then\
449 for i = 1, #filter do\
450 if token[ filter[ i ] ] or filter[ i ] == \"*\" then return true end\
451 end\
452 else\
453 if type == \"*\" then return true end\
454 return token[ filter ]\
455 end\
456 end\
457\
458 local bT, aT = test( before, beforeType ), test( after, afterType )\
459 if beforeType and afterType then return bT and aT else return ( beforeType and bT ) or ( afterType and aT ) end\
460end\
461\
462function DynamicEqParser:testForTerms( pre, post, optional )\
463 return self:testAdjacent( pre and TERMS, post and TERMS, false, false, false, not optional )\
464end\
465\
466function DynamicEqParser:resolveStacks( target )\
467 local stacks, instances = self.stacks, {}\
468 for i = 1, #stacks - ( #stacks[ #stacks ] == 0 and 1 or 0 ) do\
469 local stack = stacks[ i ]\
470 if #stack <= 1 then\
471 self:throw(\"Invalid stack '\".. stack[ 1 ] ..\"'. At least 2 parts must exist to resolve\")\
472 end\
473\
474 local stackStart, instancePoint = stack[ 1 ]\
475 if stackStart == \"self\" then\
476 instancePoint = target\
477 elseif stackStart == \"parent\" then\
478 instancePoint = target.parent\
479 elseif stackStart == \"application\" then\
480 instancePoint = target.application\
481 else self:throw(\"Invalid stack start '\"..stackStart..\"'. Only self, parent and application allowed\") end\
482\
483 for p = 2, #stack - 1 do\
484 if not instancePoint then self:throw(\"Failed to resolve stacks. Index '\"..stack[ p ]..\"' could not be accessed on '\"..tostring( instancePoint )..\"'\") end\
485 instancePoint = instancePoint[ stack[ p ] ]\
486 end\
487\
488 if not instancePoint then self:throw \"Invalid instance\" elseif not stack[ #stack ] then self:throw \"Invalid property\" end\
489 instances[ #instances + 1 ] = { stack[ #stack ], instancePoint }\
490 end\
491\
492 return instances\
493end\
494\
495function DynamicEqParser:appendToOutput( str )\
496 self.output = self.output .. ( str or self:getCurrentToken().value )\
497end\
498\
499function DynamicEqParser:parseRootState( token )\
500 token = token or self:getCurrentToken()\
501 if token.type == \"NAME\" then\
502 local filter = { \"OPERATOR\", \"DOT\", \"PAREN\" }\
503 if not self:testAdjacent( filter, filter ) then self:throw(\"Unexpected name '\"..token.value..\"'\") end\
504\
505 self:appendToStack( token.value )\
506 self:setState \"name\"\
507\
508 self:appendToOutput( \"args[\"..#self.stacks..\"]\" )\
509 elseif token.type == \"PAREN\" then\
510 if token.value == \"(\" then\
511 if not ( self:testForOperator( BIN_AMBIG, false, true ) and ( self:testForTerms( false, true ) or self:testForOperator( false, UNA_AMBIG ) ) ) then\
512 self:throw(\"Unexpected parentheses '\"..token.value..\"'\")\
513 end\
514 elseif token.value == \")\" then\
515 if not ( self:testForTerms( true ) and self:testForOperator( false, BIN_AMBIG, true ) ) then\
516 self:throw(\"Unexpected parentheses '\"..token.value..\"'\")\
517 end\
518 else self:throw(\"Invalid parentheses '\"..token.value..\"'\") end\
519\
520 self:appendToOutput()\
521 elseif token.type == \"STRING\" then\
522 local unaryOffset = self:testForOperator \"unary\" and 1 or 0\
523 if not ( ( self:testForOperator( BIN_AMBIG, false, false, unaryOffset ) or self:testAdjacent( \"PAREN\", false, false, unaryOffset ) ) and ( self:testForOperator( false, BIN_AMBIG, true ) or self:testAdjacent( false, \"PAREN\" ) ) ) then\
524 self:throw(\"Unexpected string '\"..token.value..\"'\")\
525 end\
526\
527 self:appendToOutput( (\"%s%s%s\"):format( token.surroundedBy, token.value, token.surroundedBy ) )\
528 elseif token.type == \"NUMBER\" then\
529 if not self:testAdjacent( { \"OPERATOR\", \"PAREN\" }, { \"OPERATOR\", \"PAREN\" }, false, false, false ) then\
530 self:throw(\"Unexpected number '\"..token.value..\"'\")\
531 end\
532\
533 self:appendToOutput()\
534 elseif token.type == \"OPERATOR\" then\
535 if token.unary then\
536 if not ( self:testForTerms( false, true ) and ( self:testForOperator( BIN_AMBIG ) or self:testAdjacent \"PAREN\" ) ) then\
537 self:throw(\"Unexpected unary operator '\"..token.value..\"'. Operator must follow a binary operator and precede a term\")\
538 end\
539 elseif token.binary then\
540 if not ( self:testForTerms( true ) and ( self:testForOperator( false, \"unary\" ) or self:testForTerms( false, true ) ) ) then\
541 self:throw(\"Unexpected binary operator '\"..token.value..\"'. Expected terms before and after operator, or unary operator following\")\
542 end\
543 elseif token.ambiguos then\
544 local trailing = self:testForTerms( false, true )\
545\
546 if not ( ( ( trailing or ( self:testForOperator( false, UNA_AMBIG ) and self:testForTerms( true ) ) ) and self:testForTerms( true, false, true ) ) or ( self:testForOperator( BIN_AMBIG ) and trailing ) ) then\
547 self:throw(\"Unexpected ambiguos operator '\"..token.value..\"'\")\
548 end\
549 else self:throw(\"Unknown operator '\"..token.value..\"'\") end\
550\
551 self:appendToOutput( (\" %s \"):format( token.value ) )\
552 else\
553 self:throw(\"Unexpected block '\"..token.value..\"' of token type '\"..token.type..\"'.\")\
554 end\
555end\
556\
557function DynamicEqParser:parseNameState( token )\
558 token = token or self:getCurrentToken()\
559 if token.type == \"DOT\" then\
560 local trailing = self:peek()\
561 if trailing and trailing.type == \"NAME\" then\
562 self:stepForward()\
563 self:appendToStack( trailing.value )\
564 else\
565 local last = self:getStack()\
566 self:throw(\"Failed to index '\" .. table.concat( last, \".\" ) .. \"'. No name following dot.\")\
567 end\
568 else\
569 self:setState \"root\"\
570 table.insert( self.stacks, {} )\
571\
572 self:parseRootState( token )\
573 end\
574end\
575\
576function DynamicEqParser:getStack( offset )\
577 return self.stacks[ #self.stacks + ( offset or 0 ) ]\
578end\
579\
580function DynamicEqParser:appendToStack( value, stackOffset )\
581 table.insert( self:getStack( stackOffset ), value )\
582end\
583\
584function DynamicEqParser:setState( state )\
585 self.state = state\
586end\
587\
588function DynamicEqParser:parse()\
589 local token = self:stepForward()\
590 while token do\
591 if self.state == \"root\" then\
592 self:parseRootState()\
593 elseif self.state == \"name\" then\
594 self:parseNameState()\
595 else\
596 self:throw(\"Invalid parser state '\"..self.state..\"'\")\
597 end\
598\
599 token = self:stepForward()\
600 end\
601end\
602",
603["MNodeContainer.ti"]="local function resetNode( self, node )\
604 node:queueAreaReset()\
605\
606 node.parent = nil\
607 node.application = nil\
608\
609 if self.focusedNode == node then\
610 node.focused = false\
611 end\
612\
613 node:executeCallbacks \"remove\"\
614\
615 self.changed = true\
616 self:clearCollatedNodes()\
617end\
618\
619class \"MNodeContainer\" abstract() {\
620 nodes = {}\
621}\
622\
623--[[\
624 @instance\
625 @desc Adds a node to the object. This node will have its object and parent (this) set\
626 @param <Instance 'Node' - node>\
627 @return 'param1 (node)'\
628]]\
629function MNodeContainer:addNode( node )\
630 if not Titanium.typeOf( node, \"Node\", true ) then\
631 return error( \"Cannot add '\"..tostring( node )..\"' as Node on '\"..tostring( self )..\"'\" )\
632 end\
633\
634 node.parent = self\
635 if Titanium.typeOf( self, \"Application\", true ) then\
636 node.application = self\
637 self.needsThemeUpdate = true\
638 else\
639 if Titanium.typeOf( self.application, \"Application\", true ) then\
640 node.application = self.application\
641 self.application.needsThemeUpdate = true\
642 end\
643 end\
644\
645 self.changed = true\
646 self:clearCollatedNodes()\
647\
648 table.insert( self.nodes, node )\
649 node:updateProperties()\
650\
651 if node.focused then node:focus() end\
652 return node\
653end\
654\
655--[[\
656 @instance\
657 @desc Removes a node matching the name* provided OR, if a node object is passed the actual node. Returns false if not found or (true and node)\
658 @param <Instance 'Node'/string name - target>\
659 @return <boolean - success>, [node - removedNode**]\
660\
661 *Note: In order for the node to be removed its 'name' field must match the 'name' parameter.\
662 **Note: Removed node will only be returned if a node was removed (and thus success 'true')\
663]]\
664function MNodeContainer:removeNode( target )\
665 local searchName = type( target ) == \"string\"\
666\
667 if not searchName and not Titanium.typeOf( target, \"Node\", true ) then\
668 return error( \"Cannot perform search for node using target '\"..tostring( target )..\"' to remove.\" )\
669 end\
670\
671 local nodes, node, nodeName = self.nodes, nil\
672 for i = 1, #nodes do\
673 node = nodes[ i ]\
674\
675 if ( searchName and node.id == target ) or ( not searchName and node == target ) then\
676 resetNode( self, node )\
677\
678 table.remove( nodes, i )\
679 return true, node\
680 end\
681 end\
682\
683 return false\
684end\
685\
686function MNodeContainer:clearNodes()\
687 local nodes = self.nodes\
688 for i = #nodes, 1, -1 do\
689 resetNode( self, nodes[ i ] )\
690 table.remove( nodes, i )\
691 end\
692end\
693\
694--[[\
695 @instance\
696 @desc Searches for (and returns) a node with the 'id' specified. If 'recursive' is true and a node that contains others is found, the node will also be searched.\
697 @param <string - id>, [boolean - recursive]\
698 @return [Node Instance - node]\
699]]\
700function MNodeContainer:getNode( id, recursive )\
701 local nodes, node = recursive and self.collatedNodes or self.nodes\
702\
703 for i = 1, #nodes do\
704 node = nodes[ i ]\
705 if node.id == id then\
706 return node\
707 end\
708 end\
709end\
710\
711--[[\
712 @instance\
713 @desc Returns true if the mouse event passed is in bounds of a visible child node\
714 @param <MouseEvent - event>\
715 @return [boolean - insideBounds]\
716]]\
717function MNodeContainer:isMouseColliding( event )\
718 local eX, eY, nodes = event.X - self.X + 1, event.Y - self.Y + 1, self.nodes\
719 for i = 1, #nodes do\
720 local node = nodes[ i ]\
721 local nodeX, nodeY = node.X, node.Y\
722\
723 if node.visible and eX >= nodeX and eX <= nodeX + node.width - 1 and eY >= nodeY and eY <= nodeY + node.height - 1 then\
724 return true\
725 end\
726 end\
727\
728 return false\
729end\
730\
731--[[\
732 @instance\
733 @desc Returns a 'NodeQuery' instance containing the nodes that matched the query and methods to manipulate\
734 @param <string - query>\
735 @return <NodeQuery Instance - Query Result>\
736]]\
737function MNodeContainer:query( query )\
738 return NodeQuery( self, query )\
739end\
740\
741--[[\
742 @instance\
743 @desc Clears the collatedNodes of all parents forcing them to update their collatedNodes cache on next retrieval\
744]]\
745function MNodeContainer:clearCollatedNodes()\
746 self.collatedNodes = false\
747\
748 local parent = self.parent\
749 if parent then\
750 parent:clearCollatedNodes()\
751 end\
752end\
753\
754function MNodeContainer:getCollatedNodes()\
755 if not self.collatedNodes or #self.collatedNodes == 0 then\
756 self:collate()\
757 end\
758\
759 return self.collatedNodes\
760end\
761\
762--[[\
763 @instance\
764 @desc Caches all nodes under this container (and child containers) in 'collatedNodes'.\
765 This list maybe out of date if 'collate' isn't called before usage. Caching is not automatic.\
766 @param [table - collated]\
767]]\
768function MNodeContainer:collate( collated )\
769 local collated = collated or {}\
770\
771 local nodes, node = self.nodes\
772 for i = 1, #nodes do\
773 node = nodes[ i ]\
774 collated[ #collated + 1 ] = node\
775\
776 local collatedNode = node.collatedNodes\
777 if collatedNode then\
778 for i = 1, #collatedNode do\
779 collated[ #collated + 1 ] = collatedNode[ i ]\
780 end\
781 end\
782 end\
783\
784 self.collatedNodes = collated\
785end\
786\
787--[[\
788 @instance\
789 @desc Sets the enabled property of the node to 'enabled'. Sets node's 'changed' to true.\
790 @param <boolean - enabled>\
791]]\
792function MNodeContainer:setEnabled( enabled )\
793 self.super:setEnabled( enabled )\
794 if self.parentEnabled then\
795 local nodes = self.nodes\
796 for i = 1, #nodes do\
797 nodes[ i ].parentEnabled = enabled\
798 end\
799 end\
800end\
801\
802function MNodeContainer:setParentEnabled( enabled )\
803 self.super:setParentEnabled( enabled )\
804\
805 local newEnabled, nodes = self.enabled, self.nodes\
806 for i = 1, #nodes do\
807 nodes[ i ].parentEnabled = newEnabled\
808 end\
809end\
810\
811\
812--[[\
813 @instance\
814 @desc Iterates over child nodes to ensure that nodes added to this container prior to Application set are updated (with the new Application)\
815 @param <Application - app>\
816]]\
817function MNodeContainer:setApplication( app )\
818 self.application = app\
819\
820 local nodes = self.nodes\
821 for i = 1, #nodes do\
822 nodes[ i ].application = app\
823 end\
824end\
825\
826--[[\
827 @instance\
828 @desc Clears the area provided and queues a redraw for child nodes intersecting the area.\
829 The content of the child node will not be update, it's content will only be drawn to it's parent.\
830 @param <number - x>, <number - y>, <number - width>, <number - height>\
831]]\
832function MNodeContainer:redrawArea( x, y, width, height, xOffset, yOffset )\
833 if not self.canvas then return end\
834 self.canvas:clearArea( x, y, width, height )\
835\
836 local nodes, node, nodeX, nodeY = self.nodes\
837 for i = 1, #nodes do\
838 node = nodes[ i ]\
839 nodeX, nodeY = node.X + ( xOffset or 0 ), node.Y + ( yOffset or 0 )\
840\
841 if not ( nodeX + node.width - 1 < x or nodeX > x + width or nodeY + node.height - 1 < y or nodeY > y + height ) then\
842 node.needsRedraw = true\
843 end\
844 end\
845\
846 local parent = self.parent\
847 if parent then\
848 parent:redrawArea( self.X + x - 1, self.Y + y - 1, width, height )\
849 end\
850end\
851\
852--[[\
853 @instance\
854 @desc Appends nodes loaded via TML to the Applications nodes.\
855 @param <string - path>\
856]]\
857function MNodeContainer:importFromTML( path )\
858 TML.fromFile( self, path )\
859 self.changed = true\
860end\
861\
862--[[\
863 @instance\
864 @desc Removes all nodes from the Application and inserts those loaded via TML\
865 @param <string - path>\
866]]\
867function MNodeContainer:replaceWithTML( path )\
868 local nodes, node = self.nodes\
869 for i = #nodes, 1, -1 do\
870 node = nodes[ i ]\
871 node.parent = nil\
872 node.application = nil\
873\
874 table.remove( nodes, i )\
875 end\
876\
877 self:importFromTML( path )\
878end\
879",
880["KeyEvent.ti"]="class \"KeyEvent\" extends \"Event\" {\
881 main = \"KEY\";\
882\
883 sub = false;\
884\
885 keyCode = false;\
886 keyName = false;\
887}\
888\
889function KeyEvent:__init__( name, key, held, sub )\
890 self.name = name\
891 self.sub = sub or name == \"key_up\" and \"UP\" or \"DOWN\"\
892 self.held = held\
893\
894 self.keyCode = key\
895 self.keyName = keys.getName( key )\
896\
897 self.data = { name, key, held }\
898end\
899",
900["MKeyHandler.ti"]="--[[\
901 The key handler mixin \"facilitates\" common features of objects that utilize key events. The mixin \"can\" manage hotkeys and will check them for validity\
902 when a key event is caught.\
903]]\
904\
905class \"MKeyHandler\" abstract() {\
906 static = {\
907 keyAlias = {}\
908 };\
909\
910 keys = {};\
911 hotkeys = {};\
912 cooldown = false;\
913}\
914\
915--[[\
916 @instance\
917 @desc 'Handles' a key by updating its status in 'keys'. If the event was a key down, it's status will be set to false if not held and true if it is.\
918 If the event is a key down, the key's status will be set to nil (use this to detect if a key is not pressed).\
919 The registered hotkeys will be updated everytime this function is called.\
920 @param <KeyEvent - event>\
921]]\
922function MKeyHandler:handleKey( event )\
923 local keyCode = event.keyCode\
924 if event.sub == \"DOWN\" then\
925 self.keys[ keyCode ] = event.held\
926 self:checkHotkeys( keyCode )\
927 else\
928 self.keys[ keyCode ] = nil\
929 self:checkHotkeys()\
930 end\
931end\
932\
933--[[\
934 @instance\
935 @desc Returns true if a key is pressed (regardless of held state) and false otherwise\
936 @param <number - keyCode>\
937 @return <boolean - isPressed>\
938]]\
939function MKeyHandler:isPressed( keyCode )\
940 return self.keys[ keyCode ] ~= nil\
941end\
942\
943--[[\
944 @instance\
945 @desc Returns true if the key is pressed and held, or false otherwise\
946 @param <number - keyCode>\
947 @return <boolean - isHeld>\
948]]\
949function MKeyHandler:isHeld( keyCode )\
950 return self.keys[ keyCode ]\
951end\
952\
953--[[\
954 @instance\
955 @desc Breaks 'hotkey' into key names and check their status. The last element of the hotkey must be pressed last (be the active key)\
956 Hotkey format \"leftCtrl-leftShift-t\" (keyName-keyName-keyName)\
957 @param <string - hotkey>, [number - key]\
958 @return <boolean - hotkeyMatch>\
959]]\
960function MKeyHandler:matchesHotkey( hotkey, key )\
961 for segment in hotkey:gmatch \"(%w-)%-\" do\
962\9\9if self.keys[ keys[ segment ] ] == nil then\
963\9\9\9return false\
964 end\
965\9end\
966\
967\9return key == keys[ hotkey:gsub( \".+%-\", \"\" ) ]\
968end\
969\
970--[[\
971 @instance\
972 @desc Registers a hotkey by adding it's callback and hotkey string to the handlers 'hotkeys'.\
973 @param <string - name>, <string - hotkey>, <function - callback>\
974]]\
975function MKeyHandler:registerHotkey( name, hotkey, callback )\
976 if not ( type( name ) == \"string\" and type( hotkey ) == \"string\" and type( callback ) == \"function\" ) then\
977 return error \"Expected string, string, function\"\
978 end\
979\
980 self.hotkeys[ name ] = { hotkey, callback }\
981end\
982\
983--[[\
984 @instance\
985 @desc Iterates through the registered hotkeys and checks for matches using 'matchesHotkey'. If a hotkey matches it's registered callback is invoked\
986 @param [number - key]\
987]]\
988function MKeyHandler:checkHotkeys( key )\
989 for _, hotkey in pairs( self.hotkeys ) do\
990 if self:matchesHotkey( hotkey[ 1 ], key ) then\
991 hotkey[ 2 ]( self, key )\
992 end\
993 end\
994end\
995",
996["TML.ti"]="--[[\
997 @local\
998 @desc Creates a table of arguments using the classes constructor configuration. This table is then unpacked (and the result returned)\
999 @param <Class Base - class>, <table - target>\
1000 @return [var - args...]\
1001]]\
1002local function formArguments( class, target )\
1003 local reg = class:getRegistry()\
1004 local constructor, alias, args = reg.constructor, reg.alias, target.arguments\
1005 local returnArguments, trailingTable, dynamics = {}, {}, {}\
1006\
1007 if not constructor then return nil end\
1008 local argumentTypes = constructor.argumentTypes\
1009\
1010 local ordered, set, target = constructor.orderedArguments, {}\
1011 for i = 1, #ordered do\
1012 target = ordered[ i ]\
1013 local argType = argumentTypes[ alias[ target ] or target ]\
1014\
1015 local val = args[ target ]\
1016 if val then\
1017 local escaped, rest = val:match \"^(%%*)%$(.*)$\"\
1018 if not escaped or #escaped % 2 ~= 0 then\
1019 returnArguments[ i ] = XMLParser.convertArgType( val, argType )\
1020 else\
1021 returnArguments[ i ] = argType == \"string\" and \"\" or ( argType == \"number\" and 1 or ( argType == \"boolean\" ) ) or error \"invalid argument type\"\
1022 dynamics[ target ] = DynamicEqParser( rest )\
1023 end\
1024 end\
1025\
1026 set[ ordered[ i ] ] = true\
1027 end\
1028\
1029 for argName, argValue in pairs( args ) do\
1030 if not set[ argName ] then\
1031 trailingTable[ argName ] = XMLParser.convertArgType( argValue, argumentTypes[ alias[ argName ] or argName ] )\
1032 end\
1033 end\
1034\
1035 if next( trailingTable ) then\
1036 returnArguments[ #ordered + 1 ] = trailingTable\
1037 end\
1038\
1039 return class( unpack( returnArguments, 1, next(trailingTable) and #ordered + 1 or #ordered ) ), dynamics\
1040end\
1041\
1042--[[\
1043 The TML class \"is\" used to parse an XML tree into Titanium nodes.\
1044]]\
1045\
1046class \"TML\" {\
1047 tree = false;\
1048 parent = false;\
1049}\
1050\
1051--[[\
1052 @constructor\
1053 @desc Constructs the TML instance by storing the parent and tree on 'self' and then parsing the tree.\
1054 @param <Class Instance - parent>, <table - tree>\
1055]]\
1056function TML:__init__( parent, source )\
1057 self.parent = parent\
1058 self.tree = XMLParser( source ).tree\
1059\
1060 self:parseTree()\
1061end\
1062\
1063--[[\
1064 @instance\
1065 @desc Parses 'self.tree' by creating and adding node instances to their parents.\
1066]]\
1067function TML:parseTree()\
1068 local queue = { { self.parent, self.tree } }\
1069\
1070 local i, toSetup, parent, tree = 1, {}\
1071 while i <= #queue do\
1072 parent, tree = queue[ i ][ 1 ], queue[ i ][ 2 ]\
1073\
1074 local target\
1075 for t = 1, #tree do\
1076 target = tree[ t ]\
1077\
1078 if parent:can \"addTMLObject\" then\
1079 local obj, children = parent:addTMLObject( target )\
1080 if obj and children then\
1081 table.insert( queue, { obj, children } )\
1082 end\
1083 else\
1084 local classArg = target.arguments[\"class\"]\
1085 if classArg then target.arguments[\"class\"] = nil end\
1086\
1087 local itemClass = Titanium.getClass( target.type ) or error( \"Failed to spawn XML tree. Failed to find class '\"..target.type..\"'\" )\
1088 if not Titanium.typeOf( itemClass, \"Node\" ) and target.type ~= \"Page\" then --TODO: Remove this page exception when issue #28 is resolved\
1089 error(\"Failed to spawn XML tree. Class '\"..target.type..\"' is not a valid node\")\
1090 end\
1091\
1092 local itemInstance, dynamics = formArguments( itemClass, target )\
1093 if classArg then\
1094 itemInstance.classes = type( itemInstance.classes ) == \"table\" and itemInstance.classes or {}\
1095 for className in classArg:gmatch \"%S+\" do\
1096 itemInstance.classes[ className ] = true\
1097 end\
1098 end\
1099\
1100 if target.children then\
1101 table.insert( queue, { itemInstance, target.children } )\
1102 end\
1103\
1104 toSetup[ #toSetup + 1 ] = { itemInstance, dynamics }\
1105 if parent:can \"addNode\" then\
1106 parent:addNode( itemInstance )\
1107 else\
1108 return error(\"Failed to spawn XML tree. \"..tostring( parent )..\" cannot contain nodes.\")\
1109 end\
1110 end\
1111 end\
1112\
1113 i = i + 1\
1114 end\
1115\
1116 for i = 1, #toSetup do\
1117 local instance = toSetup[ i ][ 1 ]\
1118\
1119 for property, config in pairs( toSetup[ i ][ 2 ] ) do\
1120 --TODO: Resolve node queries\
1121\
1122 instance:dynamicallyLinkProperty( property, config:resolveStacks( instance ), config.output )\
1123 end\
1124 end\
1125end\
1126\
1127--[[\
1128 @static\
1129 @desc Reads the data from 'path' and creates a TML instance with the contents as the source (arg #2)\
1130 @param <Class Instance - parent>, <string - path>\
1131 @return <TML Instance - instance>\
1132]]\
1133function TML.static.fromFile( parent, path )\
1134 if not Titanium.isInstance( parent ) then\
1135 return error \"Expected Titanium instance as first argument (parent)\"\
1136 end\
1137\
1138 if not fs.exists( path ) then return error( \"Path \"..tostring( path )..\" cannot be found\" ) end\
1139\
1140 local h = fs.open( path, \"r\" )\
1141 local content = h.readAll()\
1142 h.close()\
1143\
1144 return TML( parent, content )\
1145end\
1146",
1147["MThemeable.ti"]="--[[\
1148 The MThemeable mixin \"facilitates\" the use of themes on objects.\
1149 It allows properties to be registered allowing the object to monitor property changes and apply them correctly.\
1150\
1151 The mixin \"stores\" all properties set directly on the object in `mainValues`. These values are prioritised over values from themes unless the theme rule is designated as 'important'.\
1152\
1153 This mixin \"no\" longer handles property links as this functionality has been replaced by a more robust system 'MPropertyManager'.\
1154]]\
1155\
1156class \"MThemeable\" abstract() {\
1157 isUpdating = false;\
1158 hooked = false;\
1159\
1160 properties = {};\
1161 classes = {};\
1162 applicableRules = {};\
1163\
1164 mainValues = {}; --TODO: Preserve dynamic values, instead of the dynamic value output so that they can be restored fully\
1165 defaultValues = {};\
1166}\
1167\
1168--[[\
1169 @instance\
1170 @desc Registers the properties provided. These properties are monitored for changes.\
1171 @param <string - property>, ...\
1172]]\
1173function MThemeable:register( ... )\
1174 if self.hooked then return error \"Cannot register new properties while hooked. Unhook the theme handler before registering new properties\" end\
1175\
1176 local args = { ... }\
1177 for i = 1, #args do\
1178 self.properties[ args[ i ] ] = true\
1179 end\
1180end\
1181\
1182--[[\
1183 @instance\
1184 @desc Unregisters properties provided\
1185 @param <string - property>, ...\
1186]]\
1187function MThemeable:unregister( ... )\
1188 if self.hooked then return error \"Cannot unregister properties while hooked. Unhook the theme handler before unregistering properties\" end\
1189\
1190 local args = { ... }\
1191 for i = 1, #args do\
1192 self.properties[ args[ i ] ] = nil\
1193 end\
1194end\
1195\
1196--[[\
1197 @instance\
1198 @desc Hooks into the instance by creating watch instructions that inform the mixin \"of\" property changes.\
1199]]\
1200function MThemeable:hook()\
1201 if self.hooked then return error \"Failed to hook theme handler. Already hooked\" end\
1202\
1203 for property in pairs( self.properties ) do\
1204 self:watchProperty( property, function( _, __, value )\
1205 if self.isUpdating then return end\
1206\
1207 self.mainValues[ property ] = value\
1208 return self:fetchPropertyValue( property )\
1209 end, \"THEME_HOOK_\" .. self.__ID )\
1210\
1211 self[ self.__resolved[ property ] and \"mainValues\" or \"defaultValues\" ][ property ] = self[ property ]\
1212 end\
1213\
1214 self.hooked = true\
1215end\
1216\
1217--[[\
1218 @instance\
1219 @desc Removes the watch instructions originating from this mixin (identified by 'THEME_HOOK_<ID>' name)\
1220]]\
1221function MThemeable:unhook()\
1222 if not self.hooked then return error \"Failed to unhook theme handler. Already unhooked\" end\
1223 self:unwatchProperty( \"*\", \"THEME_HOOK_\" .. self.__ID )\
1224\
1225 self.hooked = false\
1226end\
1227\
1228function MThemeable:fetchPropertyValue( property )\
1229 local newValue = self.mainValues[ property ]\
1230 local requireImportant = newValue ~= nil\
1231\
1232 local rules, r, usedRule = self.applicableRules\
1233 for i = 1, #rules do\
1234 r = rules[ i ]\
1235 if r.property == property and ( not requireImportant or r.important ) then\
1236 newValue = r.value\
1237 usedRule = r\
1238\
1239 if r.important then requireImportant = true end\
1240 end\
1241 end\
1242\
1243 return newValue, usedRule\
1244end\
1245\
1246--[[\
1247 @instance\
1248 @desc Fetches the value from the application by checking themes for valid rules. If a theme value is found it is applied directly (this does trigger the setter)\
1249 @param <string - property>\
1250]]\
1251function MThemeable:updateProperty( property )\
1252 if not self.properties[ property ] then\
1253 return error( \"Failed to update property '\"..tostring( property )..\"'. Property not registered\" )\
1254 end\
1255\
1256 --TODO: Look into removing old dynamic values when the theme rule is no longer used. Manual dynamicValues need to be distinguished from those created automatically. Ugh.\
1257 local new, rule = self:fetchPropertyValue( property )\
1258 self.isUpdating = true\
1259 if new then\
1260 if rule and rule.isDynamic then\
1261 if self.binds[ property ] then self:unlinkProperties( self, property ) end\
1262\
1263 self:dynamicallyLinkProperty( property, new:resolveStacks( self ), new.output )\
1264 else\
1265 self[ property ] = new\
1266 end\
1267 elseif self[ property ] ~= self.mainValues[ property ] then --TODO: Check if this elseif is needed, or if simply else will suffice\
1268 --TODO: Investigate the use of this statement to ensure 'false' main values are not being overriden by the default values\
1269 self[ property ] = self.mainValues[ property ] or self.defaultValues[ property ]\
1270 end\
1271\
1272 self.isUpdating = false\
1273end\
1274\
1275function MThemeable:queueUpdate()\
1276 self.needsRuleUpdate = true\
1277end\
1278\
1279--[[\
1280 @instance\
1281 @desc Updates each registered property\
1282]]\
1283function MThemeable:updateProperties()\
1284 if self.needsRuleUpdate and self.application then\
1285 self.needsRuleUpdate = false\
1286 self.application:cacheApplicableRulesForType( self.__type )\
1287 end\
1288\
1289 for property in pairs( self.properties ) do self:updateProperty( property ) end\
1290end\
1291\
1292--[[\
1293 @instance\
1294 @desc Adds class 'class' and updated TML properties\
1295 @param <string - class>\
1296]]\
1297function MThemeable:addClass( class )\
1298 self.classes[ class ] = true\
1299 self:queueUpdate()\
1300 self:updateProperties()\
1301end\
1302\
1303--[[\
1304 @instance\
1305 @desc Removes class 'class' and updated TML properties\
1306 @param <string - class>\
1307]]\
1308function MThemeable:removeClass( class )\
1309 self.classes[ class ] = nil\
1310 self:queueUpdate()\
1311 self:updateProperties()\
1312end\
1313\
1314--[[\
1315 @instance\
1316 @desc Shortcut method to set class \"if\" 'has' is truthy or remove it otherwise (updates properties too)\
1317 @param <string - class>, [var - has]\
1318]]\
1319function MThemeable:setClass( class, has )\
1320 self.classes[ class ] = has and true or nil\
1321 self:queueUpdate()\
1322 self:updateProperties()\
1323end\
1324\
1325--[[\
1326 @instance\
1327 @desc Returns true if:\
1328 - Param passed is a table and all values inside the table are set as classes on this object\
1329 - Param is string and this object has that class\
1330 @param <string|table - class>\
1331 @return <boolean - has>\
1332]]\
1333function MThemeable:hasClass( t )\
1334 if type( t ) == \"string\" then\
1335 return self.classes[ t ]\
1336 elseif type( t ) == \"table\" then\
1337 for i = 1, #t do\
1338 if not self.classes[ t[ i ] ] then\
1339 return false\
1340 end\
1341 end\
1342\
1343 return true\
1344 else\
1345 return error(\"Invalid target '\"..tostring( t )..\"' for class check\")\
1346 end\
1347end\
1348\
1349function MThemeable:onThemeUpdate()\
1350 self:updateProperties()\
1351end\
1352",
1353["RadioButton.ti"]="class \"RadioButton\" extends \"Checkbox\" {\
1354 static = {\
1355 groups = {}\
1356 };\
1357\
1358 group = false;\
1359}\
1360\
1361function RadioButton:__init__( ... )\
1362 self:super( ... )\
1363\
1364 if self.toggled then\
1365 RadioButton.deselectInGroup( self.group, self )\
1366 end\
1367end\
1368\
1369function RadioButton:select( ... )\
1370 RadioButton.deselectInGroup( self.group )\
1371\
1372 self.toggled = true\
1373 self:executeCallbacks \"select\"\
1374end\
1375\
1376function RadioButton:onMouseUp( event, handled, within )\
1377 if not handled and within and self.active then\
1378 self:select( event, handled, within )\
1379\
1380 event.handled = true\
1381 end\
1382\
1383 self.active = false\
1384end\
1385\
1386function RadioButton:onLabelClicked( label, event, handled, within )\
1387 self:select( event, handled, within, label )\
1388 event.handled = true\
1389end\
1390\
1391function RadioButton:setGroup( group )\
1392 if self.group then\
1393 RadioButton.removeFromGroup( self, self.group )\
1394 end\
1395 self.group = group\
1396\
1397 RadioButton.addToGroup( self, group )\
1398end\
1399\
1400function RadioButton.static.addToGroup( node, group )\
1401 local g = RadioButton.groups[ group ]\
1402 if type( g ) == \"table\" then\
1403 RadioButton.removeFromGroup( node, group )\
1404\
1405 table.insert( g, node )\
1406 else\
1407 RadioButton.groups[ group ] = { node }\
1408 end\
1409end\
1410\
1411function RadioButton.static.removeFromGroup( node, group )\
1412 local index = RadioButton.isInGroup( node, group )\
1413 if index then\
1414 table.remove( RadioButton.groups[ group ], index )\
1415\
1416 if #RadioButton.groups[ group ] == 0 then\
1417 RadioButton.groups[ group ] = nil\
1418 end\
1419 end\
1420end\
1421\
1422function RadioButton.static.isInGroup( node, group )\
1423 local g = RadioButton.groups[ group ]\
1424 for i = 1, #g do\
1425 if g[ i ] == node then return i end\
1426 end\
1427\
1428 return false\
1429end\
1430\
1431function RadioButton.static.deselectInGroup( group, target )\
1432 local g = RadioButton.groups[ group ]\
1433\
1434 for i = 1, #g do if ( not target or ( target and g[ i ] ~= target ) ) then g[ i ].toggled = false end end\
1435end\
1436\
1437function RadioButton.static.getValue( group )\
1438 local g = RadioButton.groups[ group ]\
1439 if g then\
1440 local radio\
1441 for i = 1, #g do\
1442 radio = g[ i ]\
1443 if radio.toggled then return radio.value end\
1444 end\
1445 end\
1446end\
1447\
1448configureConstructor({\
1449 orderedArguments = { \"X\", \"Y\", \"group\" },\
1450 requiredArguments = { \"group\" },\
1451 argumentTypes = { group = \"string\" },\
1452 useProxy = { \"group\" }\
1453}, true, true )\
1454",
1455["DynamicEqLexer.ti"]="class \"DynamicEqLexer\" extends \"Lexer\"\
1456\
1457function DynamicEqLexer:lexNumber()\
1458 local stream = self:trimStream()\
1459 local exp, following = stream:match \"^%d*%.?%d+(e)([-+]?%d*)\"\
1460\
1461 if exp and exp ~= \"\" then\
1462 if following and following ~= \"\" then\
1463 self:pushToken { type = \"NUMBER\", value = self:consumePattern \"^%d*%.?%d+e[-+]?%d*\" }\
1464 return true\
1465 else self:throw \"Invalid number. Expected digit after 'e'\" end\
1466 elseif stream:find \"^%d*%.?%d+\" then\
1467 self:pushToken { type = \"NUMBER\", value = self:consumePattern \"^%d*%.?%d+\" }\
1468 return true\
1469 end\
1470end\
1471\
1472function DynamicEqLexer:tokenize()\
1473 local stream = self:trimStream()\
1474 local first = stream:sub( 1, 1 )\
1475\
1476 if stream:find \"^%b{}\" then\
1477 self:pushToken { type = \"QUERY\", value = self:consumePattern \"^%b{}\" }\
1478 elseif not self:lexNumber() then\
1479 if first == \"'\" or first == '\"' then\
1480 self:pushToken { type = \"STRING\", value = self:consumeString( first ), surroundedBy = first }\
1481 elseif stream:find \"^and\" then\
1482 self:pushToken { type = \"OPERATOR\", value = self:consumePattern \"^and\", binary = true }\
1483 elseif stream:find \"^or\" then\
1484 self:pushToken { type = \"OPERATOR\", value = self:consumePattern \"^or\", binary = true }\
1485 elseif stream:find \"^not\" then\
1486 self:pushToken { type = \"OPERATOR\", value = self:consumePattern \"^not\", unary = true }\
1487 elseif stream:find \"^[#]\" then\
1488 self:pushToken { type = \"OPERATOR\", value = self:consumePattern \"^[#]\", unary = true }\
1489 elseif stream:find \"^[/%*%%]\" then\
1490 self:pushToken { type = \"OPERATOR\", value = self:consumePattern \"^[/%*%%]\", binary = true }\
1491 elseif stream:find \"^%.%.\" then\
1492 self:pushToken { type = \"OPERATOR\", value = self:consumePattern \"^%.%.\", binary = true }\
1493 elseif stream:find \"^%=%=\" then\
1494 self:pushToken { type = \"OPERATOR\", value = self:consumePattern \"^%=%=\", binary = true }\
1495 elseif stream:find \"^[%+%-]\" then\
1496 self:pushToken { type = \"OPERATOR\", value = self:consumePattern \"^[%+%-]\", ambiguos = true }\
1497 elseif stream:find \"^[%(%)]\" then\
1498 self:pushToken { type = \"PAREN\", value = self:consumePattern \"^[%(%)]\" }\
1499 elseif stream:find \"^%.\" then\
1500 self:pushToken { type = \"DOT\", value = self:consumePattern \"^%.\" }\
1501 elseif stream:find \"^%w+\" then\
1502 self:pushToken { type = \"NAME\", value = self:consumePattern \"^%w+\" }\
1503 else\
1504 self:throw(\"Unexpected block '\".. ( stream:match( \"%S+\" ) or \"\" ) ..\"'\")\
1505 end\
1506 end\
1507end\
1508",
1509["XMLParser.ti"]="--[[\
1510 The XMLParser class \"is\" used to handle the lexing and parsing of XMLParser source into a parse tree.\
1511]]\
1512\
1513class \"XMLParser\" extends \"Parser\" {\
1514 tokens = false;\
1515 tree = false;\
1516}\
1517\
1518--[[\
1519 @constructor\
1520 @desc Creates a 'Lexer' instance with the source and stores the tokens provided. Invokes 'parse' once lexical analysis complete.\
1521]]\
1522function XMLParser:__init__( source )\
1523 local lex = XMLLexer( source )\
1524 self:super( lex.tokens )\
1525end\
1526\
1527--[[\
1528 @instance\
1529 @desc Iterates through every token and constructs a tree of XML layers\
1530]]\
1531function XMLParser:parse()\
1532 local stack, top, token = {{}}, false, self:stepForward()\
1533 local isTagOpen, settingAttribute\
1534\
1535 while token do\
1536 if settingAttribute then\
1537 if token.type == \"XML_ATTRIBUTE_VALUE\" or token.type == \"XML_STRING_ATTRIBUTE_VALUE\" then\
1538 top.arguments[ settingAttribute ] = token.value\
1539 settingAttribute = false\
1540 else\
1541 self:throw( \"Unexpected \"..token.type..\". Expected attribute value following XML_ASSIGNMENT token.\" )\
1542 end\
1543 else\
1544 if token.type == \"XML_OPEN\" then\
1545 if isTagOpen then\
1546 self:throw \"Unexpected XML_OPEN token. Expected XML attributes or end of tag.\"\
1547 end\
1548 isTagOpen = true\
1549\
1550 top = { type = token.value, arguments = {} }\
1551 table.insert( stack, top )\
1552 elseif token.type == \"XML_END\" then\
1553 local toClose = table.remove( stack )\
1554 top = stack[ #stack ]\
1555\
1556 if not top then\
1557 self:throw(\"Nothing to close with XML_END of type '\"..token.value..\"'\")\
1558 elseif toClose.type ~= token.value then\
1559 self:throw(\"Tried to close \"..toClose.type..\" with XML_END of type '\"..token.value..\"'\")\
1560 end\
1561\
1562 if not top.children then top.children = {} end\
1563 table.insert( top.children, toClose )\
1564 elseif token.type == \"XML_END_CLOSE\" then\
1565 top = stack[ #stack - 1 ]\
1566\
1567 if not top then\
1568 self:throw(\"Unexpected XML_END_CLOSE tag (/>)\")\
1569 end\
1570\
1571 if not top.children then top.children = {} end\
1572 table.insert( top.children, table.remove( stack ) )\
1573 elseif token.type == \"XML_CLOSE\" then\
1574 isTagOpen = false\
1575 elseif token.type == \"XML_ATTRIBUTE\" then\
1576 local next = self:stepForward()\
1577\
1578 if next.type == \"XML_ASSIGNMENT\" then\
1579 settingAttribute = token.value\
1580 else\
1581 top.arguments[ token.value ] = true\
1582 self.position = self.position - 1\
1583 end\
1584 elseif token.type == \"XML_CONTENT\" then\
1585 if not top.type then\
1586 self:throw(\"Unexpected XML_CONTENT. Invalid content: \"..token.value)\
1587 end\
1588\
1589 top.content = token.value\
1590 else\
1591 self:throw(\"Unexpected \"..token.type)\
1592 end\
1593 end\
1594\
1595 if token.type == \"XML_END\" or token.type == \"XML_END_CLOSE\" then\
1596 isTagOpen = false\
1597 end\
1598\
1599 if top.content and top.children then\
1600 self:throw \"XML layers cannot contain child nodes and XML_CONTENT at the same time\"\
1601 end\
1602\
1603 token = self:stepForward()\
1604 end\
1605 self.tree = stack[ 1 ].children\
1606end\
1607\
1608--[[\
1609 @static\
1610 @desc When lexing the XML arguments they are all stored as strings as a result of the string operations to find tokens.\
1611 This function converts a value to the type given (#2)\
1612 @param <var - argumentValue>, <string - desiredType>\
1613 @return <desiredType* - value>\
1614\
1615 *Note: desiredType is passed as type string, however the return is the value type defined inside the string. eg: desiredType: \"number\" will return a number, not a string.\
1616]]\
1617function XMLParser.static.convertArgType( argumentValue, desiredType )\
1618 local vType = type( argumentValue )\
1619 argumentValue = vType == \"number\" and math.ceil( argumentValue ) or argumentValue\
1620\
1621 if not desiredType or not argumentValue or vType == desiredType then\
1622 return argumentValue\
1623 end\
1624\
1625 if desiredType == \"string\" then\
1626 return tostring( argumentValue )\
1627 elseif desiredType == \"number\" then\
1628 return tonumber( argumentValue ) and math.ceil( tonumber( argumentValue ) ) or error( \"Failed to cast argument to number. Value: \"..tostring( argumentValue )..\" is not a valid number\" )\
1629 elseif desiredType == \"boolean\" then\
1630 if argumentValue == \"true\" then return true\
1631 elseif argumentValue == \"false\" then return false\
1632 else\
1633 return error( \"Failed to cast argument to boolean. Value: \"..tostring( argumentValue )..\" is not a valid boolean (true or false)\" )\
1634 end\
1635 elseif desiredType == \"colour\" or desiredType == \"color\" then\
1636 if argumentValue == \"transparent\" or argumentValue == \"trans\" then\
1637 return 0\
1638 end\
1639 return tonumber( argumentValue ) or colours[ argumentValue ] or colors[ argumentValue ] or error( \"Failed to cast argument to colour (number). Value: \"..tostring( argumentValue )..\" is not a valid colour\" )\
1640 else\
1641 return error( \"Failed to cast argument. Unknown target type '\"..tostring( desiredType )..\"'\" )\
1642 end\
1643end\
1644",
1645["Image.ti"]="local function getFileExtension( path )\
1646 return path:match \".+%.(.-)$\" or \"\"\
1647end\
1648\
1649class \"Image\" extends \"Node\" {\
1650 static = { imageParsers = {} };\
1651 imagePath = false;\
1652}\
1653\
1654function Image:__init__( ... )\
1655 self:super()\
1656 self:resolve( ... )\
1657end\
1658\
1659--[[\
1660 @instance\
1661 @desc Depending on the file extension (self.path), an image parser will be called.\
1662 To add support for more extensions, simply add the function to the classes static ( Image.static.addParser( extension, function ) )\
1663]]\
1664function Image:parseImage()\
1665 local path = self.path\
1666 if type( path ) ~= \"string\" then\
1667 return error(\"Failed to parse image, path '\"..tostring( path )..\"' is invalid\")\
1668 elseif not fs.exists( path ) or fs.isDir( path ) then\
1669 return error(\"Failed to parse image, path '\"..path..\"' is invalid and cannot be opened for parsing\")\
1670 end\
1671\
1672 local ext = getFileExtension( path )\
1673 if not Image.imageParsers[ ext ] then\
1674 return error(\"Failed to parse image, no image parser exists for \" .. ( ext == \"\" and \"'no ext'\" or \"'.\" .. ext .. \"'\" ) .. \" files for '\"..path..\"'\")\
1675 end\
1676\
1677 local f = fs.open( path, \"r\" )\
1678 local stream = f.readAll()\
1679 f.close()\
1680\
1681 local width, height, pixels = Image.imageParsers[ ext ]( stream )\
1682 for y = 1, height do\
1683 local pos = ( y - 1 ) * width\
1684 for x = 1, width do\
1685 local posX = pos + x\
1686 self.canvas.buffer[ posX ] = pixels[ posX ] or { \" \" }\
1687 end\
1688 end\
1689\
1690 self.width, self.height = width, height\
1691 self.changed = true\
1692end\
1693\
1694function Image:setPath( path )\
1695 self.path = path\
1696 self:parseImage()\
1697end\
1698\
1699function Image.static.setImageParser( extension, parserFunction )\
1700 if type( extension ) ~= \"string\" or type( parserFunction ) ~= \"function\" then\
1701 return error \"Failed to set image parser. Invalid arguments, expected string, function\"\
1702 end\
1703\
1704 Image.static.imageParsers[ extension ] = parserFunction\
1705\
1706 return Image\
1707end\
1708\
1709function Image:draw() end\
1710configureConstructor {\
1711 orderedArguments = { \"path\" },\
1712 requiredArguments = { \"path\" },\
1713 useProxy = { \"path\" },\
1714 argumentTypes = {\
1715 path = \"string\"\
1716 }\
1717}\
1718",
1719["Dropdown.ti"]="class \"Dropdown\" extends \"Container\" {\
1720 maxHeight = false;\
1721\
1722 prompt = \"Please select\";\
1723 horizontalAlign = \"left\";\
1724\
1725 openIndicator = \" \\31\";\
1726 closedIndicator = \" \\16\";\
1727\
1728 backgroundColour = colours.lightBlue;\
1729 colour = colours.white;\
1730\
1731 activeBackgroundColour = colours.cyan;\
1732\
1733 selectedColour = colours.white;\
1734 selectedBackgroundColour = colours.grey;\
1735 selectedOption = false;\
1736 options = {};\
1737}\
1738\
1739function Dropdown:__init__( ... )\
1740 self:super( ... )\
1741\
1742 self.optionDisplay = self:addNode( Button \"\":linkProperties( self, \"horizontalAlign\", \"disabledColour\", \"disabledBackgroundColour\", \"activeColour\", \"activeBackgroundColour\" ):on(\"trigger\", function() self:toggleOptionDisplay() end) )\
1743 self.optionContainer = self:addNode( ScrollContainer( 1, 2, self.width ):set{ xScrollAllowed = false } )\
1744\
1745 self:closeOptionDisplay()\
1746\
1747 self.transparent = true\
1748 self.consumeAll = false\
1749end\
1750\
1751function Dropdown:closeOptionDisplay()\
1752 local cont = self.optionContainer\
1753 cont.visible, cont.enabled = false, false\
1754\
1755 self:queueAreaReset()\
1756 self:updateDisplayButton()\
1757end\
1758\
1759function Dropdown:openOptionDisplay()\
1760 local cont = self.optionContainer\
1761 cont.visible, cont.enabled = true, true\
1762\
1763 self:queueAreaReset()\
1764 self:updateDisplayButton()\
1765end\
1766\
1767function Dropdown:toggleOptionDisplay()\
1768 if self.optionContainer.visible then\
1769 self:closeOptionDisplay()\
1770 else\
1771 self:openOptionDisplay()\
1772 end\
1773end\
1774\
1775function Dropdown:setEnabled( enabled )\
1776 self.super:setEnabled( enabled )\
1777 if not enabled then\
1778 self:closeOptionDisplay()\
1779 end\
1780end\
1781\
1782function Dropdown:updateDisplayButton()\
1783 self.optionDisplay.text = ( type( self.selectedOption ) == \"table\" and self.selectedOption[ 1 ] or self.prompt ) .. ( self.optionContainer.visible and self.openIndicator or self.closedIndicator )\
1784 self.optionDisplay.width = #self.optionDisplay.text\
1785\
1786 self.optionDisplay:set {\
1787 colour = self.selectedColour,\
1788 backgroundColour = self.selectedBackgroundColour\
1789 }\
1790end\
1791\
1792function Dropdown:updateOptions()\
1793 local cont = self.optionContainer\
1794 cont:clearNodes()\
1795\
1796 self:updateDisplayButton()\
1797\
1798 local options = self.options\
1799 for i = 1, #options do\
1800 local option = options[ i ][ 1 ]\
1801 if options[ i ] ~= self.selectedOption then\
1802 cont:addNode(Button( option, 1, #cont.nodes + 1, self.width ):on(\"trigger\", function( this )\
1803 self.selectedOption = options[ i ]\
1804 end):linkProperties( self, \"colour\", \"activeColour\", \"disabledColour\", \"backgroundColour\", \"activeBackgroundColour\", \"disabledBackgroundColour\", \"horizontalAlign\" ))\
1805 end\
1806 end\
1807\
1808 local count = #cont.nodes\
1809 if self.maxHeight then\
1810 cont.height, self.height = math.min( count, self.maxHeight - 1 ), math.min( count + 1, self.maxHeight )\
1811 else\
1812 cont.height, self.height = count, count + 1\
1813 end\
1814\
1815 if #options > 0 then cont.yScroll = math.min( cont.yScroll, count ) end\
1816end\
1817\
1818function Dropdown:getSelectedValue()\
1819 if type( self.selectedOption ) ~= \"table\" then return end\
1820\
1821 return self.selectedOption[ 2 ]\
1822end\
1823\
1824function Dropdown:addOption( option, value )\
1825 if type( option ) ~= \"string\" or value == nil then\
1826 return error \"Failed to add option to Dropdown node. Expected two arguments: string, val - where val is not nil\"\
1827 end\
1828\
1829 self:removeOption( option )\
1830 table.insert( self.options, { option, value } )\
1831\
1832 self:updateOptions()\
1833end\
1834\
1835function Dropdown:removeOption( option )\
1836 local options = self.options\
1837 for i = #options, 1, -1 do\
1838 if options[ i ] == option then\
1839 table.remove( options, i )\
1840 end\
1841 end\
1842\
1843 self:updateOptions()\
1844end\
1845\
1846function Dropdown:setPrompt( prompt )\
1847 self.prompt = prompt\
1848 self.optionDisplay.text = prompt\
1849end\
1850\
1851function Dropdown:setSelectedOption( selected )\
1852 self.selectedOption = selected\
1853 self:closeOptionDisplay()\
1854 self:updateOptions()\
1855\
1856 self:executeCallbacks( \"change\", selected )\
1857end\
1858\
1859function Dropdown:handle( eventObj )\
1860 if not self.super:handle( eventObj ) then return end\
1861\
1862 if eventObj:is \"mouse_click\" and not self:isMouseColliding( eventObj ) and self.optionContainer.visible then\
1863 self:closeOptionDisplay()\
1864 eventObj.handled = true\
1865 end\
1866\
1867 return true\
1868end\
1869\
1870function Dropdown:addTMLObject( TMLObj )\
1871 if TMLObj.type == \"Option\" then\
1872 if TMLObj.content and TMLObj.arguments.value then\
1873 self:addOption( TMLObj.content, TMLObj.arguments.value )\
1874 else\
1875 error \"Failed to add TML object to Dropdown object. 'Option' tag must include content (not children) and a 'value' argument\"\
1876 end\
1877 else\
1878 error( \"Failed to add TML object to Dropdown object. Only 'Option' tags are accepted, '\" .. tostring( TMLObj.type ) .. \"' is invalid\" )\
1879 end\
1880end\
1881\
1882configureConstructor({\
1883 orderedArguments = { \"X\", \"Y\", \"width\", \"maxHeight\", \"prompt\" },\
1884 argumentTypes = {\
1885 maxHeight = \"number\",\
1886 prompt = \"string\"\
1887 }\
1888}, true)\
1889",
1890["Terminal.ti"]="local function isThreadRunning( obj )\
1891 if not obj.thread then return false end\
1892\
1893 return obj.thread.running\
1894end\
1895\
1896class \"Terminal\" extends \"Node\" mixin \"MFocusable\" {\
1897 static = {\
1898 focusedEvents = {\
1899 MOUSE = true,\
1900 KEY = true,\
1901 CHAR = true\
1902 }\
1903 };\
1904\
1905 canvas = true;\
1906 displayThreadStatus = true;\
1907}\
1908\
1909function Terminal:__init__( ... )\
1910 self:resolve( ... )\
1911 self:super()\
1912\
1913 self.canvas = RedirectCanvas( self )\
1914 self.redirect = self.canvas:getTerminalRedirect()\
1915end\
1916\
1917function Terminal:wrapChunk()\
1918 if type( self.chunk ) ~= \"function\" then\
1919 return error \"Cannot wrap chunk. No chunk function set.\"\
1920 end\
1921\
1922 self.canvas:resetTerm()\
1923\
1924 self.thread = Thread( self.chunk )\
1925 self:resume( GenericEvent \"titanium_terminal_start\" )\
1926end\
1927\
1928function Terminal:resume( event )\
1929 if not isThreadRunning( self ) then return end\
1930\
1931 if not Titanium.typeOf( event, \"Event\", true ) then\
1932 return error \"Invalid event object passed to resume terminal thread\"\
1933 end\
1934\
1935 local thread, old = self.thread, term.redirect( self.redirect )\
1936 thread:filterHandle( event )\
1937 term.redirect( old )\
1938\
1939 if not thread.running then\
1940 if type( thread.exception ) == \"string\" then\
1941 if self.displayThreadStatus then\
1942 self:emulate(function() printError( \"Thread Crashed: \" .. tostring( thread.exception ) ) end)\
1943 end\
1944\
1945 self:executeCallbacks(\"exception\", thread)\
1946 else\
1947 if self.displayThreadStatus then\
1948 self:emulate(function() print \"Finished\" end)\
1949 end\
1950\
1951 self:executeCallbacks(\"graceful_finish\", thread)\
1952 end\
1953\
1954 self:executeCallbacks(\"finish\", thread)\
1955 end\
1956\
1957 self.changed = true\
1958end\
1959\
1960function Terminal:emulate( fn )\
1961 if type( fn ) ~= \"function\" then\
1962 return error(\"Failed to emulate function. '\"..tostring( fn )..\" is not valid'\")\
1963 end\
1964\
1965 local old = term.redirect( self.redirect )\
1966 local ok, err = pcall( fn )\
1967 term.redirect( old )\
1968\
1969 if not ok then\
1970 return error(\"Failed to emulate function. Error: \"..tostring( err ), 3)\
1971 end\
1972end\
1973\
1974function Terminal:setChunk( chunk )\
1975 self.chunk = chunk\
1976 self:wrapChunk()\
1977end\
1978\
1979function Terminal:getCaretInfo()\
1980 local c = self.canvas\
1981 return isThreadRunning( self ) and c.tCursor, c.tX + self.X - 1, c.tY + self.Y - 1, c.tColour\
1982end\
1983\
1984function Terminal:handle( eventObj )\
1985 if eventObj.handled or not isThreadRunning( self ) then return end\
1986\
1987 if eventObj.main == \"MOUSE\" then\
1988 if eventObj:withinParent( self ) then self:focus() else self:unfocus() end\
1989 eventObj = eventObj:clone( self )\
1990 end\
1991\
1992 if Terminal.focusedEvents[ eventObj.main ] and not self.focused then return end\
1993 self:resume( eventObj )\
1994end\
1995\
1996function Terminal:draw( force ) end\
1997\
1998configureConstructor({\
1999 orderedArguments = { \"X\", \"Y\", \"width\", \"height\", \"chunk\" },\
2000 argumentTypes = { chunk = \"function\" },\
2001 useProxy = { \"chunk\" }\
2002}, true)\
2003",
2004["Thread.ti"]="class \"Thread\" {\
2005 running = false;\
2006\
2007 func = false;\
2008 co = false;\
2009\
2010 filter = false;\
2011 exception = false;\
2012\
2013 titaniumEvents = false;\
2014}\
2015\
2016function Thread:__init__( ... )\
2017 self:resolve( ... )\
2018 self:start()\
2019end\
2020\
2021function Thread:start()\
2022 self.co = coroutine.create( self.func )\
2023 self.running = true\
2024 self.filter = false\
2025end\
2026\
2027function Thread:stop()\
2028 self.running = false\
2029end\
2030\
2031function Thread:filterHandle( eventObj )\
2032 if self.titaniumEvents then\
2033 self:handle( eventObj )\
2034 else\
2035 self:handle( unpack( eventObj.data ) )\
2036 end\
2037end\
2038\
2039function Thread:handle( ... )\
2040 if not self.running then return false end\
2041\
2042 local tEvents, cFilter, eMain, co, ok, filter = self.titaniumEvents, self.filter, select( 1, ... ), self.co\
2043 if tEvents then\
2044 if not cFilter or ( eMain:is( cFilter ) or eMain:is( \"terminate\" ) ) then\
2045 ok, filter = coroutine.resume( co, eMain )\
2046 else return end\
2047 else\
2048 if not cFilter or ( eMain == cFilter or eMain == \"terminate\" ) then\
2049 ok, filter = coroutine.resume( co, ... )\
2050 else return end\
2051 end\
2052\
2053 if ok then\
2054 if coroutine.status( co ) == \"dead\" then\
2055 self.running = false\
2056 end\
2057\
2058 self.filter = filter\
2059 else\
2060 self.exception = filter\
2061 self.running = false\
2062 end\
2063end\
2064\
2065configureConstructor {\
2066 orderedArguments = { \"func\", \"titaniumEvents\", \"id\" },\
2067 requiredArguments = { \"func\" }\
2068}\
2069",
2070["NodeQuery.ti"]="local function format( original, symbol, final )\
2071 local wrapper = type( original ) == \"string\" and '\"' or \"\"\
2072 local finalWrapper = type( final ) == \"string\" and '\"' or \"\"\
2073\
2074 return (\"return %s%s%s %s %s%s%s\"):format( wrapper, tostring( original ), wrapper, symbol, finalWrapper, tostring( final ), finalWrapper )\
2075end\
2076\
2077local function testCondition( node, condition )\
2078 local fn, err = loadstring( format( node[ condition.property ], condition.symbol, condition.value ) )\
2079 if fn then return fn() end\
2080\
2081 return fn()\
2082end\
2083\
2084local function queryScope( scope, section, results )\
2085 local last = {}\
2086\
2087 local node\
2088 for i = 1, #scope do\
2089 node = scope[ i ]\
2090\
2091 if ( not section.id or node.id == section.id ) and\
2092 ( not section.type or section.type == \"*\" or node.__type == section.type ) and\
2093 ( not section.classes or node:hasClass( section.classes ) ) then\
2094 local condition, failed = section.condition\
2095 if condition then\
2096 local conditionPart\
2097 for c = 1, #condition do\
2098 if not testCondition( node, condition[ c ] ) then\
2099 failed = true\
2100 break\
2101 end\
2102 end\
2103 end\
2104\
2105 if not failed then\
2106 last[ #last + 1 ] = node\
2107 end\
2108 end\
2109 end\
2110\
2111 return last\
2112end\
2113\
2114local function createScope( results, direct )\
2115 local scope = {}\
2116 for i = 1, #results do\
2117 local innerScope = direct and results[ i ].nodes or results[ i ].collatedNodes\
2118\
2119 for r = 1, #innerScope do\
2120 scope[ #scope + 1 ] = innerScope[ r ]\
2121 end\
2122 end\
2123\
2124 return scope\
2125end\
2126\
2127local function performQuery( query, base )\
2128 local lastResults, section = base\
2129\
2130 for i = 1, #query do\
2131 section = query[ i ]\
2132 lastResults = queryScope( createScope( lastResults, section.direct ), section )\
2133 end\
2134\
2135 return lastResults\
2136end\
2137\
2138class \"NodeQuery\" {\
2139 static = { supportedMethods = { \"addClass\", \"removeClass\", \"setClass\", \"set\", \"animate\", \"on\", \"off\" } };\
2140 result = false;\
2141\
2142 parent = false;\
2143}\
2144\
2145function NodeQuery:__init__( parent, queryString )\
2146 if not ( Titanium.isInstance( parent ) and type( queryString ) == \"string\" ) then\
2147 return error \"Node query requires Titanium instance and string query\"\
2148 end\
2149 self.parent = parent\
2150\
2151 self.parsedQuery = QueryParser( queryString ).query\
2152 self.result = self:query()\
2153\
2154 local sup = NodeQuery.supportedMethods\
2155 for i = 1, #sup do\
2156 self[ sup[ i ] ] = function( self, ... ) self:executeOnNodes( sup[ i ], ... ) end\
2157 end\
2158end\
2159\
2160--[[\
2161 @static\
2162 @desc Returns a table containing the nodes matching the conditions set in 'query'\
2163 @return <table - results>\
2164]]\
2165function NodeQuery:query()\
2166 local query, results = self.parsedQuery, {}\
2167 if type( query ) ~= \"table\" then return error( \"Cannot perform query. Invalid query object passed\" ) end\
2168\
2169 local parent = { self.parent }\
2170 for i = 1, #query do\
2171 local res = performQuery( query[ i ], parent )\
2172\
2173 for r = 1, #res do\
2174 results[ #results + 1 ] = res[ r ]\
2175 end\
2176 end\
2177\
2178 return results\
2179end\
2180\
2181--[[\
2182 @instance\
2183 @desc Returns true if the class 'class' exists on all nodes in the result set, false otherwise\
2184 @param <table|string - class>\
2185 @return <boolean - hasClass>\
2186]]\
2187function NodeQuery:hasClass( class )\
2188 local nodes = self.result\
2189 for i = 1, #nodes do\
2190 if not nodes[ i ]:hasClass( class ) then\
2191 return false\
2192 end\
2193 end\
2194\
2195 return true\
2196end\
2197\
2198function NodeQuery:each( fn )\
2199 local nodes = self.result\
2200 for i = 1, #nodes do\
2201 fn( nodes[ i ] )\
2202 end\
2203end\
2204\
2205--[[\
2206 @instance\
2207 @desc Iterates over each node in the result set, calling 'fnName' with arguments '...' on each\
2208 @param <string - fnName>, [vararg - ...]\
2209]]\
2210function NodeQuery:executeOnNodes( fnName, ... )\
2211 local nodes, node = self.result\
2212 for i = 1, #nodes do\
2213 node = nodes[ i ]\
2214\
2215 if node:can( fnName ) then\
2216 node[ fnName ]( node, ... )\
2217 end\
2218 end\
2219end\
2220",
2221["Page.ti"]="class \"Page\" extends \"ScrollContainer\"\
2222\
2223function Page:setParent( parent )\
2224 self.super:setParent( parent )\
2225\
2226 self.width = parent.width\
2227 self.height = parent.height\
2228end\
2229\
2230configureConstructor {\
2231 orderedArguments = { \"id\" },\
2232 requiredArguments = { \"id\" }\
2233}\
2234",
2235["Input.ti"]="--[[\
2236 The Input class \"provides\" the user with the ability to insert a single line of text.\
2237]]\
2238\
2239local stringRep, stringSub = string.rep, string.sub\
2240class \"Input\" extends \"Node\" mixin \"MActivatable\" mixin \"MFocusable\" {\
2241 position = 0;\
2242 scroll = 0;\
2243 value = \"\";\
2244\
2245 selection = false;\
2246 selectedColour = false;\
2247 selectedBackgroundColour = colours.lightBlue;\
2248\
2249 placeholder = false;\
2250 placeholderColour = 256;\
2251\
2252 allowMouse = true;\
2253 allowKey = true;\
2254 allowChar = true;\
2255\
2256 limit = 0;\
2257 mask = \"\";\
2258}\
2259\
2260--[[\
2261 @constructor\
2262 @desc Constructs the instance by resolving arguments and registering used properties\
2263]]\
2264function Input:__init__( ... )\
2265 self:resolve( ... )\
2266 self:register( \"width\", \"selectedColour\", \"selectedBackgroundColour\", \"limit\" )\
2267\
2268 self:super()\
2269end\
2270\
2271--[[\
2272 @instance\
2273 @desc Sets the input to active if clicked on, sets active and focused to false if the mouse click was not on the input.\
2274 @param <MouseEvent - event>, <boolean - handled>, <boolean - within>\
2275]]\
2276function Input:onMouseClick( event, handled, within )\
2277 if within and not handled then\
2278 if self.focused then\
2279 local application, pos, width, scroll = self.application, self.position, self.width, self.scroll\
2280 local clickedPos = math.min( #self.value, event.X - self.X + self.scroll )\
2281\
2282 if application:isPressed( keys.leftShift ) or application:isPressed( keys.rightShift ) then\
2283 if clickedPos ~= pos then\
2284 self.selection = clickedPos\
2285 else self.selection = false end\
2286 else self.position, self.selection = clickedPos, false end\
2287 end\
2288\
2289 self.active, event.handled = true, true\
2290 else\
2291 self.active = false\
2292 self:unfocus()\
2293 end\
2294end\
2295\
2296--[[\
2297 @instance\
2298 @desc If a mouse drag occurs while the input is focused, the selection will be moved to the mouse drag location, creating a selection between the cursor position and the drag position\
2299 @param <MouseEvent - event>, <boolean - handled>, <boolean - within>\
2300]]\
2301function Input:onMouseDrag( event, handled, within )\
2302 if not self.focused or handled then return end\
2303 self.selection = math.min( #self.value, event.X - self.X + self.scroll )\
2304 event.handled = true\
2305end\
2306\
2307--[[\
2308 @instance\
2309 @desc If the mouse up missed the input or the event was already handled, active and false are set to false.\
2310 If within and not handled and input is active focused is set to true. Active is set to false on all conditions.\
2311 @param <MouseEvent - event>, <boolean - handled>, <boolean - within>\
2312]]\
2313function Input:onMouseUp( event, handled, within )\
2314 if ( not within or handled ) and self.focused then\
2315 self:unfocus()\
2316 elseif within and not handled and self.active and not self.focused then\
2317 self:focus()\
2318 end\
2319\
2320 self.active = false\
2321end\
2322\
2323--[[\
2324 @instance\
2325 @desc Catches char events and inserts the character pressed into the input's value.\
2326 @param <CharEvent - event>, <boolean - handled>\
2327]]\
2328function Input:onChar( event, handled )\
2329 if not self.focused or handled then return end\
2330\
2331 local value, position, selection = self.value, self.position, self.selection\
2332 if selection then\
2333 local start, stop = math.min( selection, position ), math.max( selection, position )\
2334 start = start > stop and start - 1 or start\
2335\
2336 self.value, self.selection = stringSub( value, 1, start ) .. event.char .. stringSub( value, stop + ( start < stop and 1 or 2 ) ), false\
2337 self.position = start + 1\
2338 self.changed = true\
2339 else\
2340 if self.limit > 0 and #value >= self.limit then return end\
2341\
2342 self.value = stringSub( value, 1, position ) .. event.char .. stringSub( value, position + 1 )\
2343 self.position = self.position + 1\
2344 end\
2345\
2346 self:executeCallbacks \"change\"\
2347\
2348 event.handled = true\
2349end\
2350\
2351--[[\
2352 @instance\
2353 @desc Catches key down events and performs an action depending on the key pressed\
2354 @param <KeyEvent - event>, <boolean - handled>\
2355]]\
2356function Input:onKeyDown( event, handled )\
2357 if not self.focused or handled then return end\
2358\
2359 local value, position = self.value, self.position\
2360 local valueLen = #value\
2361 if event.sub == \"DOWN\" then\
2362 local key, selection, position, application = event.keyName, self.selection, self.position, self.application\
2363 local isPressed, start, stop = application:isPressed( keys.leftShift ) or application:isPressed( keys.rightShift )\
2364\
2365 if selection then\
2366 start, stop = selection < position and selection or position, selection > position and selection + 1 or position + 1\
2367 else start, stop = position - 1, position end\
2368\
2369 if key == \"enter\" then\
2370 self:executeCallbacks( \"trigger\", self.value, self.selection and self:getSelectedValue() )\
2371 elseif selection then\
2372 if key == \"delete\" or key == \"backspace\" then\
2373 self.value = stringSub( value, 1, start ) .. stringSub( value, stop )\
2374 self.position = start\
2375 self.selection = false\
2376 elseif not isPressed and ( key == \"left\" or key == \"right\" ) then\
2377 self.position = key == \"left\" and start + 1 or key == \"right\" and stop - 2\
2378 self.selection = false\
2379 end\
2380 end\
2381\
2382 local cSelection = self.selection or self.position\
2383 local function set( offset )\
2384 if isPressed then self.selection = cSelection + offset\
2385 else self.position = self.position + offset; self.selection = false end\
2386 end\
2387\
2388 if key == \"left\" then set( -1 )\
2389 elseif key == \"right\" then set( 1 ) else\
2390 if key == \"home\" then\
2391 set( isPressed and -cSelection or -position )\
2392 elseif key == \"end\" then\
2393 set( isPressed and valueLen - cSelection or valueLen - position )\
2394 elseif key == \"backspace\" and isPressed then\
2395 self.value, self.position = stringSub( self.value, stop + 1 ), 0\
2396 end\
2397 end\
2398\
2399 if not isPressed then\
2400 if key == \"backspace\" and start >= 0 and not selection then\
2401 self.value = stringSub( value, 1, start ) .. stringSub( value, stop + 1 )\
2402 self.position = start\
2403 elseif key == \"delete\" and not selection then\
2404 self.value, self.changed = stringSub( value, 1, stop ) .. stringSub( value, stop + 2 ), true\
2405 end\
2406 end\
2407 end\
2408end\
2409\
2410function Input:onLabelClicked( label, event, handled, within )\
2411 self:focus()\
2412 event.handled = true\
2413end\
2414\
2415--[[\
2416 @instance\
2417 @desc Draws the inputs background and text to the parent canvas\
2418 @param [boolean - force]\
2419]]\
2420function Input:draw( force )\
2421 local raw = self.raw\
2422 if raw.changed or force then\
2423 local canvas, tc, bg = raw.canvas, raw.colour, raw.backgroundColour\
2424 if raw.focused then tc, bg = raw.focusedColour, raw.focusedBackgroundColour\
2425 elseif raw.active then tc, bg = raw.activeColour, raw.activeBackgroundColour end\
2426\
2427 canvas:clear( bg )\
2428\
2429 local position, width, value, selection, placeholder = self.position, self.width, self.mask ~= \"\" and stringRep( self.mask, #self.value ) or self.value, self.selection, self.placeholder\
2430 if self.focused or not placeholder or #value > 0 then\
2431 if self.selection then\
2432 local start, stop = selection < position and selection or position, selection > position and selection + 1 or position + 1\
2433 if start < stop then stop = stop - 1 end\
2434\
2435 local startPos = -self.scroll + 1\
2436\
2437 canvas:drawTextLine( startPos, 1, stringSub( value, 1, start + 1 ), tc, bg )\
2438 canvas:drawTextLine( startPos + start, 1, stringSub( value, start + 1, stop ), self.focused and self.selectedColour or tc, self.focused and self.selectedBackgroundColour or bg )\
2439 canvas:drawTextLine( startPos + stop, 1, stringSub( value, stop + 1 ), tc, bg )\
2440 else\
2441 canvas:drawTextLine( -self.scroll + 1, 1, value, tc, bg )\
2442 end\
2443 else canvas:drawTextLine( 1, 1, stringSub( placeholder, 1, self.width ), self.placeholderColour, bg ) end\
2444\
2445 raw.changed = false\
2446 end\
2447end\
2448\
2449--[[\
2450 @instance\
2451 @desc Attempts to reposition the scroll of the input box depending on the position indicator\
2452 @param <number - indicator>\
2453]]\
2454function Input:repositionScroll( indicator )\
2455 local limit = self.limit\
2456 local isLimit = limit > 0\
2457\
2458 if indicator >= self.width and indicator > ( self.scroll + self.width - 1 ) then\
2459 self.scroll = math.min( indicator - self.width + 1, #self.value - self.width + 1 ) - ( isLimit and indicator >= limit and 1 or 0 )\
2460 elseif indicator <= self.scroll then\
2461 self.scroll = math.max( self.scroll - ( self.scroll - indicator ), 0 )\
2462 else self.scroll = math.max( math.min( self.scroll, #self.value - self.width + 1 ), 0 ) end\
2463end\
2464\
2465--[[\
2466 @instance\
2467 @desc If the given selection is a number, it will be adjusted to fit within the bounds of the input and set. If not, the value will be raw set.\
2468 @param <number|boolean - selection>\
2469]]\
2470function Input:setSelection( selection )\
2471 if type( selection ) == \"number\" then\
2472 local newSelection = math.max( math.min( selection, #self.value ), 0 )\
2473 self.selection = newSelection ~= self.position and newSelection or false\
2474 else self.selection = selection end\
2475\
2476 self:repositionScroll( self.selection or self.position )\
2477 self.changed = true\
2478end\
2479\
2480--[[\
2481 @instance\
2482 @desc Returns the value of the input that is selected\
2483 @return <string - selectedValue>\
2484]]\
2485function Input:getSelectedValue()\
2486 local selection, position = self.selection, self.position\
2487 return stringSub( self.value, ( selection < position and selection or position ) + 1, ( selection > position and selection or position ) )\
2488end\
2489\
2490--[[\
2491 @instance\
2492 @desc If the given position is equal to the (inputs) selection, the selection will be reset.\
2493 If not equal, the value will be adjusted to fit inside the bounds of the input and then set.\
2494 @param <number - pos>\
2495]]\
2496function Input:setPosition( pos )\
2497 if self.selection == pos then self.selection = false end\
2498 self.position, self.changed = math.max( math.min( pos, #self.value ), 0 ), true\
2499\
2500 self:repositionScroll( self.position )\
2501end\
2502\
2503--[[\
2504 @instance\
2505 @desc When called, returns the state of the caret, its position (absolute) and colour.\
2506 @return <boolean - caretEnabled>, <number - caretX>, <number - caretY>, <colour - caretColour>\
2507]]\
2508function Input:getCaretInfo()\
2509 local sX, sY = self:getAbsolutePosition()\
2510 local limit = self.limit\
2511\
2512 return not self.selection and ( limit <= 0 or self.position < limit ), sX + ( self.position - self.scroll ), sY, self.focusedColour\
2513end\
2514\
2515\
2516configureConstructor({\
2517 orderedArguments = { \"X\", \"Y\", \"width\" },\
2518 argumentTypes = { value = \"string\", position = \"number\", selection = \"number\", placeholder = \"string\", placeholderColour = \"colour\", selectedColour = \"colour\", selectedBackgroundColour = \"colour\", limit = \"number\", mask = \"string\" },\
2519 useProxy = { \"toggled\" }\
2520}, true)\
2521",
2522["MAnimationManager.ti"]="class \"MAnimationManager\" abstract() {\
2523 animations = {};\
2524 animationTimer = false;\
2525\
2526 time = false;\
2527}\
2528\
2529--[[\
2530 @desc When the animation timer ticks, update animations attached to this application and requeue the timer if more animations must occur.\
2531]]\
2532function MAnimationManager:updateAnimations()\
2533 local dt = os.clock() - self.time\
2534\
2535 local anims, anim = self.animations\
2536 for i = #anims, 1, -1 do\
2537 anim = anims[ i ]\
2538\
2539 if anim:update( dt ) then\
2540 if type( anim.promise ) == \"function\" then\
2541 anim:promise( self )\
2542 end\
2543\
2544 self:removeAnimation( anim )\
2545 end\
2546 end\
2547\
2548 self.timer = false\
2549 if #anims > 0 then self:restartAnimationTimer() end\
2550end\
2551\
2552--[[\
2553 @instance\
2554 @desc Adds an animation to this object, on update this animation will be updated\
2555 @param <Tween - animation>\
2556]]\
2557function MAnimationManager:addAnimation( animation )\
2558 if not Titanium.typeOf( animation, \"Tween\", true ) then\
2559 return error(\"Failed to add animation to manager. '\"..tostring( animation )..\"' is invalid, Tween instance expected\")\
2560 end\
2561\
2562 self:removeAnimation( animation.name )\
2563 table.insert( self.animations, animation )\
2564\
2565 if not self.timer then\
2566 self:restartAnimationTimer()\
2567 end\
2568\
2569 return animation\
2570end\
2571\
2572--[[\
2573 @instance\
2574 @desc Removes an animation from this object, it will stop receiving updates from this object\
2575]]\
2576function MAnimationManager:removeAnimation( animation )\
2577 local searchName\
2578 if type( animation ) == \"string\" then\
2579 searchName = true\
2580 elseif not Titanium.typeOf( animation, \"Tween\", true ) then\
2581 return error(\"Failed to remove animation from manager. '\"..tostring( animation )..\"' is invalid, Tween instance expected\")\
2582 end\
2583\
2584 local anims = self.animations\
2585 for i = 1, #anims do\
2586 if ( searchName and anims[ i ].name == animation ) or ( not searchName and anims[ i ] == animation ) then\
2587 return table.remove( anims, i )\
2588 end\
2589 end\
2590end\
2591\
2592--[[\
2593 @instance\
2594 @desc When an animation is queued the timer is created for 'time' (0.05). This replaces the currently running timer (if any).\
2595 The objects 'time' is then updated to the current time (os.clock)\
2596 @param [number - time]\
2597]]\
2598function MAnimationManager:restartAnimationTimer( time )\
2599 if self.timer then\
2600 os.cancelTimer( self.timer )\
2601 end\
2602\
2603 self.time = os.clock()\
2604 self.timer = os.startTimer( type( time ) == \"number\" and time or .05 )\
2605end\
2606",
2607["Component.ti"]="--[[\
2608 A Component is an object that can be respresented visually.\
2609--]]\
2610\
2611class \"Component\" abstract() mixin \"MPropertyManager\" {\
2612 width = 1;\
2613 height = 1;\
2614 X = 1;\
2615 Y = 1;\
2616\
2617 changed = true;\
2618\
2619 backgroundChar = \" \";\
2620}\
2621\
2622function Component:queueAreaReset()\
2623 local parent = self.parent\
2624 if parent then\
2625 parent:redrawArea( self.X, self.Y, self.width, self.height )\
2626 end\
2627\
2628 self.changed = true\
2629end\
2630\
2631function Component:set( tbl )\
2632 if type( tbl ) ~= \"table\" then\
2633 return error \"Table expected\"\
2634 end\
2635\
2636 for property, value in pairs( tbl ) do\
2637 self[ property ] = value\
2638 end\
2639\
2640 return self\
2641end\
2642\
2643function Component:setX( X )\
2644 self:queueAreaReset()\
2645 self.X = X\
2646end\
2647\
2648function Component:setY( Y )\
2649 self:queueAreaReset()\
2650 self.Y = Y\
2651end\
2652\
2653function Component:setWidth( width )\
2654 self:queueAreaReset()\
2655\
2656 self.width = width\
2657 self.canvas.width = width\
2658end\
2659\
2660function Component:setHeight( height )\
2661 self:queueAreaReset()\
2662\
2663 self.height = height\
2664 self.canvas.height = height\
2665end\
2666\
2667function Component:setColour( colour )\
2668 self.colour = colour\
2669 self.canvas.colour = colour\
2670\
2671 self.changed = true\
2672end\
2673\
2674function Component:setBackgroundColour( backgroundColour )\
2675 self.backgroundColour = backgroundColour\
2676 self.canvas.backgroundColour = backgroundColour\
2677\
2678 self.changed = true\
2679end\
2680\
2681function Component:setTransparent( transparent )\
2682 self.transparent = transparent\
2683 self.canvas.transparent = transparent\
2684\
2685 self.changed = true\
2686end\
2687\
2688function Component:setBackgroundChar( backgroundChar )\
2689 if backgroundChar == \"nil\" then\
2690 backgroundChar = nil\
2691 end\
2692\
2693 self.backgroundChar = backgroundChar\
2694 self.canvas.backgroundChar = backgroundChar\
2695\
2696 self.changed = true\
2697end\
2698\
2699function Component:setBackgroundTextColour( backgroundTextColour )\
2700 self.backgroundTextColour = backgroundTextColour\
2701 self.canvas.backgroundTextColour = backgroundTextColour\
2702\
2703 self.changed = true\
2704end\
2705\
2706configureConstructor {\
2707 orderedArguments = { \"X\", \"Y\", \"width\", \"height\" },\
2708 argumentTypes = { X = \"number\", Y = \"number\", width = \"number\", height = \"number\", colour = \"colour\", backgroundColour = \"colour\", backgroundTextColour = \"colour\", transparent = \"boolean\" }\
2709} alias {\
2710 color = \"colour\",\
2711 backgroundColor = \"backgroundColour\"\
2712}\
2713",
2714["Titanium.lua"]="--[[\
2715 Event declaration\
2716 =================\
2717\
2718 Titanium needs to know what class types to spawn when an event is spawned, for flexibility this can be edited whenever you see fit. The matrix\
2719 starts blank, so we define basic events here. (on event type 'key', spawn instance of type 'value')\
2720]]\
2721Event.static.matrix = {\
2722 mouse_click = MouseEvent,\
2723 mouse_drag = MouseEvent,\
2724 mouse_up = MouseEvent,\
2725 mouse_scroll = MouseEvent,\
2726\
2727 key = KeyEvent,\
2728 key_up = KeyEvent,\
2729\
2730 char = CharEvent\
2731}\
2732\
2733--[[\
2734 Image Parsing\
2735 =============\
2736\
2737 Titaniums Image class parses image files based on their extension, two popular formats (nfp and default) are supported by default, however this can be expanded like you see here.\
2738 These functions are expected to return the dimensions of the image and, a buffer (2D table) of pixels to be drawn directly to the images canvas. Pixels that do not exist in the image\
2739 need not be acounted for, Titanium will automatically fill those as 'blank' pixels by setting them as 'transparent'.\
2740\
2741 See the default functions below for good examples of image parsing.\
2742]]\
2743\
2744Image.setImageParser(\"\", function( stream ) -- Default CC images, no extension\
2745 -- Break image into lines, find the maxwidth of the image (the length of the longest line)\
2746 local hex = TermCanvas.static.hex\
2747 width, lines, pixels = 1, {}, {}\
2748 for line in stream:gmatch \"([^\\n]*)\\n?\" do\
2749 width = math.max( width, #line )\
2750 lines[ #lines + 1 ] = line\
2751 end\
2752\
2753 -- Iterate each line, forming a buffer of pixels with missing information (whitespace) being left nil\
2754 for l = 1, #lines do\
2755 local y, line = width * ( l - 1 ), lines[ l ]\
2756\
2757 for i = 1, width do\
2758 local colour = hex[ line:sub( i, i ) ]\
2759 pixels[ y + i ] = { \" \", colour, colour }\
2760 end\
2761 end\
2762\
2763 return width, #lines, pixels\
2764end).setImageParser(\"nfp\", function( stream ) -- NFP images, .nfp extension\
2765 --TODO: Look into nfp file format and write parser\
2766end)\
2767\
2768--[[\
2769 Tween setup\
2770 ===========\
2771\
2772 The following blocks of code define the functions that will be invoked when an animation that used that type of easing is updated. These functions\
2773 are adjusted versions (the algorithm has remained the same, however code formatting and variable names are largely changed to match Titanium) of\
2774 the easing functions published by kikito on GitHub. Refer to 'LICENSE' in this project root for more information (and Enrique's license).\
2775\
2776 The functions are passed 4 arguments, these arguments are listed below:\
2777 - clock: This argument contains the current clock time of the Tween being updated, this is used to tell how far through the animation we are (in seconds)\
2778 - initial: The value of the property being animated at the instantiation of the tween. This is usually added as a Y-Axis transformation.\
2779 - change: The difference of the initial and final property value. ie: How much the value will have to change to match the final from where it was as instantiation.\
2780 - duration: The total duration of the running Tween.\
2781\
2782 Certain functions are passed extra arguments. The Tween class doesn't pass these in, however custom animation engines could invoke these easing functions\
2783 through `Tween.static.easing.<easingType>`.\
2784]]\
2785\
2786local abs, pow, asin, sin, sqrt, pi = math.abs, math.pow, math.asin, math.sin, math.sqrt, math.pi\
2787local easing = Tween.static.easing\
2788-- Linear easing function\
2789Tween.addEasing(\"linear\", function( clock, initial, change, duration )\
2790 return change * clock / duration + initial\
2791end)\
2792\
2793-- Quad easing functions\
2794Tween.addEasing(\"inQuad\", function( clock, initial, change, duration )\
2795 return change * pow( clock / duration, 2 ) + initial\
2796end).addEasing(\"outQuad\", function( clock, initial, change, duration )\
2797 local clock = clock / duration\
2798 return -change * clock * ( clock - 2 ) + initial\
2799end).addEasing(\"inOutQuad\", function( clock, initial, change, duration )\
2800 local clock = clock / duration * 2\
2801 if clock < 1 then\
2802 return change / 2 * pow( clock, 2 ) + initial\
2803 end\
2804\
2805 return -change / 2 * ( ( clock - 1 ) * ( clock - 3 ) - 1 ) + initial\
2806end).addEasing(\"outInQuad\", function( clock, initial, change, duration )\
2807 if clock < duration / 2 then\
2808 return easing.outQuad( clock * 2, initial, change / 2, duration )\
2809 end\
2810\
2811 return easing.inQuad( ( clock * 2 ) - duration, initial + change / 2, change / 2, duration)\
2812end)\
2813\
2814-- Cubic easing functions\
2815Tween.addEasing(\"inCubic\", function( clock, initial, change, duration )\
2816 return change * pow( clock / duration, 3 ) + initial\
2817end).addEasing(\"outCubic\", function( clock, initial, change, duration )\
2818 return change * ( pow( clock / duration - 1, 3 ) + 1 ) + initial\
2819end).addEasing(\"inOutCubic\", function( clock, initial, change, duration )\
2820 local clock = clock / duration * 2\
2821 if clock < 1 then\
2822 return change / 2 * clock * clock * clock + initial\
2823 end\
2824\
2825 clock = clock - 2\
2826 return change / 2 * (clock * clock * clock + 2) + initial\
2827end).addEasing(\"outInCubic\", function( clock, initial, change, duration )\
2828 if clock < duration / 2 then\
2829 return easing.outCubic( clock * 2, initial, change / 2, duration )\
2830 end\
2831\
2832 return easing.inCubic( ( clock * 2 ) - duration, initial + change / 2, change / 2, duration )\
2833end)\
2834\
2835-- Quart easing functions\
2836Tween.addEasing(\"inQuart\", function( clock, initial, change, duration )\
2837 return change * pow( clock / duration, 4 ) + initial\
2838end).addEasing(\"outQuart\", function( clock, initial, change, duration )\
2839 return -change * ( pow( clock / duration - 1, 4 ) - 1 ) + initial\
2840end).addEasing(\"inOutQuart\", function( clock, initial, change, duration )\
2841 local clock = clock / duration * 2\13\
2842 if clock < 1 then\13\
2843 return change / 2 * pow(clock, 4) + initial\13\
2844 end\13\
2845\13\
2846 return -change / 2 * ( pow( clock - 2, 4 ) - 2 ) + initial\
2847end).addEasing(\"outInQuart\", function( clock, initial, change, duration )\
2848 if clock < duration / 2 then\13\
2849 return easing.outQuart( clock * 2, initial, change / 2, duration )\13\
2850 end\13\
2851\13\
2852 return easing.inQuart( ( clock * 2 ) - duration, initial + change / 2, change / 2, duration )\
2853end)\
2854\
2855-- Quint easing functions\
2856Tween.addEasing(\"inQuint\", function( clock, initial, change, duration )\
2857 return change * pow( clock / duration, 5 ) + initial\
2858end).addEasing(\"outQuint\", function( clock, initial, change, duration )\
2859 return change * ( pow( clock / duration - 1, 5 ) + 1 ) + initial\
2860end).addEasing(\"inOutQuint\", function( clock, initial, change, duration )\
2861 local clock = clock / duration * 2\
2862 if clock < 1 then\
2863 return change / 2 * pow( clock, 5 ) + initial\
2864 end\
2865\
2866 return change / 2 * (pow( clock - 2, 5 ) + 2 ) + initial\
2867end).addEasing(\"outInQuint\", function( clock, initial, change, duration )\
2868 if clock < duration / 2 then\
2869 return easing.outQuint( clock * 2, initial, change / 2, duration )\
2870 end\
2871\
2872 return easing.inQuint( ( clock * 2 ) - duration, initial + change / 2, change / 2, duration )\
2873end)\
2874\
2875-- Sine easing functions\
2876Tween.addEasing(\"inSine\", function( clock, initial, change, duration )\
2877 return -change * cos( clock / duration * ( pi / 2 ) ) + change + initial\
2878end).addEasing(\"outSine\", function( clock, initial, change, duration )\
2879 return change * sin( clock / duration * ( pi / 2 ) ) + initial\
2880end).addEasing(\"inOutSine\", function( clock, initial, change, duration )\
2881 return -change / 2 * ( cos( pi * clock / duration ) - 1 ) + initial\
2882end).addEasing(\"outInSine\", function( clock, initial, change, duration )\
2883 if clock < duration / 2 then\
2884 return easing.outSine( clock * 2, initial, change / 2, duration )\
2885 end\
2886\
2887 return easing.inSine( ( clock * 2 ) - duration, initial + change / 2, change / 2, duration )\
2888end)\
2889\
2890-- Expo easing functions\
2891Tween.addEasing(\"inExpo\", function( clock, initial, change, duration )\
2892 if clock == 0 then\
2893 return initial\
2894 end\
2895 return change * pow( 2, 10 * ( clock / duration - 1 ) ) + initial - change * 0.001\
2896end).addEasing(\"outExpo\", function( clock, initial, change, duration )\
2897 if clock == duration then\
2898 return initial + change\
2899 end\
2900\
2901 return change * 1.001 * ( -pow( 2, -10 * clock / duration ) + 1 ) + initial\
2902end).addEasing(\"inOutExpo\", function( clock, initial, change, duration )\
2903 if clock == 0 then\
2904 return initial\
2905 elseif clock == duration then\
2906 return initial + change\
2907 end\
2908\
2909 local clock = clock / duration * 2\
2910 if clock < 1 then\
2911 return change / 2 * pow( 2, 10 * ( clock - 1 ) ) + initial - change * 0.0005\
2912 end\
2913\
2914 return change / 2 * 1.0005 * ( -pow( 2, -10 * ( clock - 1 ) ) + 2 ) + initial\
2915end).addEasing(\"outInExpo\", function( clock, initial, change, duration )\
2916 if clock < duration / 2 then\
2917 return easing.outExpo( clock * 2, initial, change / 2, duration )\
2918 end\
2919\
2920 return easing.inExpo( ( clock * 2 ) - duration, initial + change / 2, change / 2, duration )\
2921end)\
2922\
2923-- Circ easing functions\
2924Tween.addEasing(\"inCirc\", function( clock, initial, change, duration )\
2925 return -change * ( sqrt( 1 - pow( clock / duration, 2 ) ) - 1 ) + initial\
2926end).addEasing(\"outCirc\", function( clock, initial, change, duration )\
2927 return change * sqrt( 1 - pow( clock / duration - 1, 2 ) ) + initial\
2928end).addEasing(\"inOutCirc\", function( clock, initial, change, duration )\
2929 local clock = clock / duration * 2\
2930 if clock < 1 then\
2931 return -change / 2 * ( sqrt( 1 - clock * clock ) - 1 ) + initial\
2932 end\
2933\
2934 clock = clock - 2\
2935 return change / 2 * ( sqrt( 1 - clock * clock ) + 1 ) + initial\
2936end).addEasing(\"outInCirc\", function( clock, initial, change, duration )\
2937 if clock < duration / 2 then\
2938 return easing.outCirc( clock * 2, initial, change / 2, duration )\
2939 end\
2940\
2941 return easing.inCirc( ( clock * 2 ) - duration, initial + change / 2, change / 2, duration )\
2942end)\
2943\
2944-- Elastic easing functions\
2945local function calculatePAS(p,a,change,duration)\
2946 local p, a = p or duration * 0.3, a or 0\
2947 if a < abs( change ) then\
2948 return p, change, p / 4 -- p, a, s\
2949 end\
2950\
2951 return p, a, p / ( 2 * pi ) * asin( change / a ) -- p,a,s\
2952end\
2953\
2954Tween.addEasing(\"inElastic\", function( clock, initial, change, duration, amplitude, period )\
2955 if clock == 0 then return initial end\
2956\
2957 local clock, s = clock / duration\
2958 if clock == 1 then\
2959 return initial + change\
2960 end\
2961\
2962 clock, p, a, s = clock - 1, calculatePAS( p, a, change, duration )\
2963 return -( a * pow( 2, 10 * clock ) * sin( ( clock * duration - s ) * ( 2 * pi ) / p ) ) + initial\
2964end).addEasing(\"outElastic\", function( clock, initial, change, duration, amplitude, period )\
2965 if clock == 0 then\
2966 return initial\
2967 end\
2968 local clock, s = clock / duration\
2969\
2970 if clock == 1 then\
2971 return initial + change\
2972 end\
2973\
2974 local p,a,s = calculatePAS( period, amplitude, change, duration )\
2975 return a * pow( 2, -10 * clock ) * sin( ( clock * duration - s ) * ( 2 * pi ) / p ) + change + initial\
2976end).addEasing(\"inOutElastic\", function( clock, initial, change, duration, amplitude, period )\
2977 if clock == 0 then return initial end\
2978\
2979 local clock = clock / duration * 2\
2980 if clock == 2 then return initial + change end\
2981\
2982 local clock, p, a, s = clock - 1, calculatePAS( period, amplitude, change, duration )\
2983 if clock < 0 then\
2984 return -0.5 * ( a * pow( 2, 10 * clock ) * sin( ( clock * duration - s ) * ( 2 * pi ) / p ) ) + initial\
2985 end\
2986\
2987 return a * pow( 2, -10 * clock ) * sin( ( clock * duration - s ) * ( 2 * pi ) / p ) * 0.5 + change + initial\
2988end).addEasing(\"outInElastic\", function( clock, initial, change, duration, amplitude, period )\
2989 if clock < duration / 2 then\
2990 return easing.outElastic( clock * 2, initial, change / 2, duration, amplitude, period )\
2991 end\
2992\
2993 return easing.inElastic( ( clock * 2 ) - duration, initial + change / 2, change / 2, duration, amplitude, period )\
2994end)\
2995\
2996-- Back easing functions\
2997Tween.addEasing(\"inBack\", function( clock, initial, change, duration, s )\
2998 local s, clock = s or 1.70158, clock / duration\
2999\
3000 return change * clock * clock * ( ( s + 1 ) * clock - s ) + initial\
3001end).addEasing(\"outBack\", function( clock, initial, change, duration, s )\
3002 local s, clock = s or 1.70158, clock / duration - 1\
3003\
3004 return change * ( clock * clock * ( ( s + 1 ) * clock + s ) + 1 ) + initial\
3005end).addEasing(\"inOutBack\", function( clock, initial, change, duration, s )\
3006 local s, clock = ( s or 1.70158 ) * 1.525, clock / duration * 2\
3007 if clock < 1 then\
3008 return change / 2 * ( clock * clock * ( ( s + 1 ) * clock - s ) ) + initial\
3009 end\
3010\
3011 clock = clock - 2\
3012 return change / 2 * ( clock * clock * ( ( s + 1 ) * clock + s ) + 2 ) + initial\
3013end).addEasing(\"outInBack\", function( clock, initial, change, duration, s )\
3014 if clock < duration / 2 then\
3015 return easing.outBack( clock * 2, initial, change / 2, duration, s )\
3016 end\
3017\
3018 return easing.inBack( ( clock * 2 ) - duration, initial + change / 2, change / 2, duration, s )\
3019end)\
3020\
3021-- Bounce easing functions\
3022Tween.addEasing(\"inBounce\", function( clock, initial, change, duration )\
3023 return change - easing.outBounce( duration - clock, 0, change, duration ) + initial\
3024end).addEasing(\"outBounce\", function( clock, initial, change, duration )\
3025 local clock = clock / duration\
3026 if clock < 1 / 2.75 then\
3027 return change * ( 7.5625 * clock * clock ) + initial\
3028 elseif clock < 2 / 2.75 then\
3029 clock = clock - ( 1.5 / 2.75 )\
3030 return change * ( 7.5625 * clock * clock + 0.75 ) + initial\
3031 elseif clock < 2.5 / 2.75 then\
3032 clock = clock - ( 2.25 / 2.75 )\
3033 return change * ( 7.5625 * clock * clock + 0.9375 ) + initial\
3034 end\
3035\
3036 clock = clock - (2.625 / 2.75)\
3037 return change * (7.5625 * clock * clock + 0.984375) + initial\
3038end).addEasing(\"inOutBounce\", function( clock, initial, change, duration )\
3039 if clock < duration / 2 then\
3040 return easing.inBounce( clock * 2, 0, change, duration ) * 0.5 + initial\
3041 end\
3042\
3043 return easing.outBounce( clock * 2 - duration, 0, change, duration ) * 0.5 + change * .5 + initial\
3044end).addEasing(\"outInBounce\", function( clock, initial, change, duration )\
3045 if clock < duration / 2 then\
3046 return easing.outBounce( clock * 2, initial, change / 2, duration )\
3047 end\
3048\
3049 return easing.inBounce( ( clock * 2 ) - duration, initial + change / 2, change / 2, duration )\
3050end)\
3051",
3052["CharEvent.ti"]="class \"CharEvent\" extends \"Event\" {\
3053 main = \"CHAR\";\
3054 char = false;\
3055}\
3056\
3057function CharEvent:__init__( name, char )\
3058 self.name = name\
3059 self.char = char\
3060\
3061 self.data = { name, char }\
3062end\
3063",
3064["Container.ti"]="class \"Container\" extends \"Node\" mixin \"MNodeContainer\" {\
3065 allowMouse = true;\
3066 allowKey = true;\
3067 allowChar = true;\
3068\
3069 consumeAll = true;\
3070}\
3071\
3072--[[\
3073 @instance\
3074 @desc Constructs the Container node with the value passed. If a nodes table is passed each entry inside of it will be added to the container as a node\
3075 @param [number - X], [number - Y], [number - width], [number - height], [table - nodes]\
3076]]\
3077function Container:__init__( ... )\
3078 self:resolve( ... )\
3079\
3080 local toset = self.nodes\
3081 self.nodes = {}\
3082\
3083 if type( toset ) == \"table\" then\
3084 for i = 1, #toset do\
3085 self:addNode( toset[ i ] )\
3086 end\
3087 end\
3088\
3089 self:super()\
3090end\
3091\
3092--[[\
3093 @instance\
3094 @desc Returns true if the node given is visible inside of the container\
3095 @param <Node - node>, [number - width], [number - height]\
3096 @return <boolean - visible>\
3097]]\
3098function Container:isNodeInBounds( node, width, height )\
3099 local left, top = node.X, node.Y\
3100\
3101 return not ( ( left + node.width ) < 1 or left > ( width or self.width ) or top > ( height or self.height ) or ( top + node.height ) < 1 )\
3102end\
3103\
3104--[[\
3105 @instance\
3106 @desc Draws contained nodes to container canvas. Nodes are only drawn if they are visible inside the container\
3107 @param [boolean - force], [number - offsetX], [number - offsetY]\
3108]]\
3109function Container:draw( force, offsetX, offsetY )\
3110 if self.changed or force then\
3111 local canvas = self.canvas\
3112\
3113 local width, height = self.width, self.height\
3114 local nodes, node = self.nodes\
3115 local offsetX, offsetY = offsetX or 0, offsetY or 0\
3116\
3117 for i = 1, #nodes do\
3118 node = nodes[ i ]\
3119\
3120 if node.needsRedraw and node.visible then\
3121 node:draw( force )\
3122\
3123 node.canvas:drawTo( canvas, node.X + offsetX, node.Y + offsetY )\
3124 node.needsRedraw = false\
3125 end\
3126 end\
3127\
3128 self.changed = false\
3129 end\
3130end\
3131\
3132--[[\
3133 @instance\
3134 @desc Redirects all events to child nodes. Mouse events are adjusted to become relative to this container. Event handlers on Container are still fired if present\
3135 @param <Event - event>\
3136 @return <boolean - propagate>\
3137]]\
3138function Container:handle( eventObj )\
3139 if not self.super:handle( eventObj ) then return end\
3140\
3141 local clone\
3142 if eventObj.main == \"MOUSE\" then\
3143 clone = eventObj:clone( self )\
3144 clone.isWithin = clone.isWithin and eventObj:withinParent( self ) or false\
3145 end\
3146\
3147 self:shipEvent( clone or eventObj )\
3148 if clone and clone.isWithin and ( self.consumeAll or clone.handled ) then\
3149 eventObj.handled = true\
3150 end\
3151 return true\
3152end\
3153\
3154function Container:shipEvent( event )\
3155 local nodes = self.nodes\
3156 for i = #nodes, 1, -1 do\
3157 nodes[ i ]:handle( event )\
3158 end\
3159end\
3160\
3161function Container:setWidth( width )\
3162\9self.super:setWidth( width )\
3163\9local nodes = self.nodes\
3164\9for i = 1, #nodes do\
3165\9\9nodes[i].needsRedraw = true\
3166\9end\
3167end\
3168\
3169function Container:setHeight( height )\
3170\9self.super:setHeight( height )\
3171\9local nodes = self.nodes\
3172\9for i = 1, #nodes do\
3173\9\9nodes[i].needsRedraw = true\
3174\9end\
3175end\
3176\
3177\
3178configureConstructor({\
3179 orderedArguments = { \"X\", \"Y\", \"width\", \"height\", \"nodes\", \"backgroundColour\" },\
3180 argumentTypes = {\
3181 nodes = \"table\"\
3182 }\
3183}, true)\
3184",
3185["PageContainer.ti"]="--[[\
3186 The PageContainer serves as a container that shows one 'page' at a time. Preset (or completely custom) animated transitions can be used when\
3187 a new page is selected.\
3188]]\
3189\
3190class \"PageContainer\" extends \"Container\" {\
3191 scroll = 0;\
3192\
3193 animationDuration = 0.25;\
3194 animationEasing = \"outQuad\";\
3195 customAnimation = false;\
3196 selectedPage = false;\
3197\
3198 pageIndexes = {};\
3199}\
3200\
3201--[[\
3202 @instance\
3203 @desc Intercepts the draw call, adding the x scroll to the x offset\
3204 @param [boolean - force], [number - offsetX], [number - offsetY]\
3205]]\
3206function PageContainer:draw( force, offsetX, offsetY )\
3207 return self.super:draw( force, ( offsetX or 0 ) - self.scroll, offsetY )\
3208end\
3209\
3210--[[\
3211 @instance\
3212 @desc If a MOUSE event is handled, it's X co-ordinate is adjusted using the scroll offset of the page container.\
3213 @param <Event Instance - eventObj>\
3214 @return <boolean - propagate>\
3215]]\
3216function PageContainer:handle( eventObj )\
3217 if not self.super.super:handle( eventObj ) then return end\
3218\
3219 local clone\
3220 if eventObj.main == \"MOUSE\" then\
3221 clone = eventObj:clone( self )\
3222 clone.X = clone.X + self.scroll\
3223 clone.isWithin = clone.isWithin and eventObj:withinParent( self ) or false\
3224 end\
3225\
3226 self:shipEvent( clone or eventObj )\
3227 if clone and clone.isWithin and ( self.consumeAll or clone.handled ) then\
3228 eventObj.handled = true\
3229 end\
3230 return true\
3231end\
3232\
3233--[[\
3234 @instance\
3235 @desc Selects the new page using the 'pageID'. If a function is given as argument #2 'animationOverride', it will be called instead of the customAnimation set (or the default animation method used).\
3236 Therefore the animationOverride is given full control of the transition, allowing for easy one-off transition effects.\
3237\
3238 If 'customAnimation' is set on the instance, it will be called if no 'animationOverride' is provided, providing a more long term override method.\
3239\
3240 If neither are provided, a normal animation will take place, using 'animationDuration' and 'animationEasing' set on the instance as parameters for the animation.\
3241 @param <string - pageID>, [function - animationOverride]\
3242]]\
3243function PageContainer:selectPage( pageID, animationOverride )\
3244 local page = self:getPage( pageID )\
3245\
3246 self.selectedPage = page\
3247 if type( animationOverride ) == \"function\" then\
3248 return animationOverride( self.currentPage, page )\
3249 elseif self.customAnimation then\
3250 return self.customAnimation( self.currentPage, page )\
3251 end\
3252\
3253 self:animate( self.__ID .. \"_PAGE_CONTAINER_SELECTION\", \"scroll\", ( self:getPagePosition( pageID ) - 1 ) * self.width, self.animationDuration, self.animationEasing )\
3254end\
3255\
3256--[[\
3257 @instance\
3258 @desc Returns an integer representing the position of the page. These may change as pages are added and removed from the PageContainer - don't rely on them remaining constant\
3259 @param <string - pageID>\
3260 @return <number - position>\
3261]]\
3262function PageContainer:getPagePosition( pageID )\
3263 local indexes = self.pageIndexes\
3264 for i = 1, #indexes do\
3265 if indexes[ i ] == pageID then\
3266 return i\
3267 end\
3268 end\
3269end\
3270\
3271--[[\
3272 @instance\
3273 @desc Ensures the node being added to the PageContainer is a 'Page' node because no other nodes should be added directly to this node\
3274 @param <Page Instance - node>\
3275 @return 'param1 (node)'\
3276]]\
3277function PageContainer:addNode( node )\
3278 if Titanium.typeOf( node, \"Page\", true ) then\
3279 local pgInd = self.pageIndexes\
3280 if self:getPagePosition( node.id ) then\
3281 return error(\"Cannot add page '\"..tostring( node )..\"'. Another page with the same ID already exists inside this PageContainer\")\
3282 end\
3283\
3284 pgInd[ #pgInd + 1 ] = node.id\
3285 node.X = ( #pgInd - 1 ) * self.width + 1\
3286\
3287 return self.super:addNode( node )\
3288 end\
3289\
3290 return error(\"Only 'Page' nodes can be added as direct children of 'PageContainer' nodes, '\"..tostring( node )..\"' is invalid\")\
3291end\
3292\
3293--[[\
3294 @instance\
3295 @desc A alias \"for\" 'addNode', contextualized for the PageContainer\
3296 @param <Page Instance - page>\
3297 @return 'param1 (page)'\
3298]]\
3299function PageContainer:addPage( page )\
3300 return self:addNode( page )\
3301end\
3302\
3303--[[\
3304 @instance\
3305 @desc A alias \"for\" 'getNode', contextualized for the PageContainer\
3306 @param <string - id>, [boolean - recursive]\
3307 @return [Node Instance - node]\
3308]]\
3309function PageContainer:getPage( ... )\
3310 return self:getNode( ... )\
3311end\
3312\
3313--[[\
3314 @instance\
3315 @desc A alias \"for\" 'removeNode', contextualized for the PageContainer\
3316 @param <Node Instance | string - id>\
3317 @return <boolean - success>, [node - removedNode]\
3318]]\
3319function PageContainer:removePage( ... )\
3320 return self:removeNode( ... )\
3321end\
3322\
3323--[[\
3324 @instance\
3325 @desc Shifts requests to clear the PageContainer area to the left, depending on the scroll position of the container\
3326 @param <number - x>, <number - y>, <number - width>, <number - height>\
3327]]\
3328function PageContainer:redrawArea( x, y, width, height )\
3329 self.super:redrawArea( x, y, width, height, -self.scroll )\
3330end\
3331\
3332--[[\
3333 @instance\
3334 @desc Due to the contents of the PageContainer not actually moving (just the scroll), the content of the PageContainer must be manually cleared.\
3335 To fit this demand, the area of the PageContainer is cleared when the scroll parameter is changed.\
3336]]\
3337function PageContainer:setScroll( scroll )\
3338 self.scroll = scroll\
3339 self:redrawArea( 1, 1, self.width, self.height )\
3340end\
3341",
3342["Canvas.ti"]="--[[\
3343 The Canvas object is used by all components. It facilitates the drawing of pixels which are stored in its buffer.\
3344\
3345 The Canvas object is abstract. If you need a canvas for your object 'NodeCanvas' and 'TermCanvas' are provided with Titanium and may suite your needs.\
3346--]]\
3347\
3348local function range( xBoundary, xDesired, width, canvasWidth )\
3349 local x1 = xBoundary > xDesired and 1 - xDesired or 1\
3350 local x2 = xDesired + width > canvasWidth and canvasWidth - xDesired or width\
3351\
3352 return x1, x2\
3353end\
3354\
3355class \"Canvas\" abstract() {\
3356 buffer = {};\
3357 last = {};\
3358\
3359 width = 51;\
3360 height = 19;\
3361\
3362 backgroundColour = 32768;\
3363 colour = 1;\
3364\
3365 transparent = false;\
3366}\
3367\
3368--[[\
3369 @constructor\
3370 @desc Constructs the canvas instance and binds it with the owner supplied.\
3371 @param <ClassInstance - owner>\
3372]]\
3373function Canvas:__init__( owner )\
3374 self.raw.owner = Titanium.isInstance( owner ) and owner or error(\"Invalid argument for Canvas. Expected instance owner, got '\"..tostring( owner )..\"'\")\
3375 self.raw.width = owner.raw.width\
3376 self.raw.height = owner.raw.height\
3377\
3378 self.raw.colour = owner.raw.colour\
3379 self.raw.backgroundChar = owner.raw.backgroundChar\
3380 if self.raw.backgroundChar == \"nil\" then\
3381 self.raw.backgroundChar = nil\
3382 end\
3383 self.raw.backgroundTextColour = owner.raw.backgroundTextColour\
3384 self.raw.backgroundColour = owner.raw.backgroundColour\
3385\
3386 self:clear()\
3387end\
3388\
3389--[[\
3390 @instance\
3391 @desc Replaces the canvas with a blank one\
3392 @param [number - colour]\
3393]]\
3394function Canvas:clear( colour )\
3395 local pixel, buffer = { not self.transparent and self.backgroundChar, self.colour, self.transparent and 0 or colour or self.backgroundColour }, self.buffer\
3396\
3397 for index = 1, self.width * self.height do\
3398 buffer[ index ] = pixel\
3399 end\
3400end\
3401\
3402--[[\
3403 @instance\
3404 @desc Clears an area of the canvas defined by the arguments provided.\
3405 @param <number - areaX>, <number - areaY>, <number - areaWidth>, <number - areaHeight>, [number - colour]\
3406]]\
3407function Canvas:clearArea( aX, aY, aWidth, aHeight, colour )\
3408 local aY, aX, cWidth = aY > 0 and aY - 1 or 0, aX > 0 and aX - 1 or 0, self.width\
3409 local pixel, buffer = { not self.transparent and self.backgroundChar, self.colour, self.transparent and 0 or colour or self.backgroundColour }, self.buffer\
3410\
3411 local xBoundary, yBoundary = cWidth - aX, self.height\
3412 local effectiveWidth = xBoundary < aWidth and xBoundary or aWidth\
3413 for y = 0, -1 + ( aHeight < yBoundary and aHeight or yBoundary ) do\
3414 local pos = aX + ( y + aY ) * cWidth\
3415 for x = 1, effectiveWidth do\
3416 buffer[ pos + x ] = pixel\
3417 end\
3418 end\
3419end\
3420\
3421--[[\
3422 @instance\
3423 @desc Updates the transparency setting of the canvas and then clears the canvas to apply this setting\
3424 @param <number - colour>\
3425]]\
3426function Canvas:setTransparent( transparent )\
3427 self.transparent = transparent\
3428 self:clear()\
3429end\
3430\
3431--[[\
3432 @instance\
3433 @desc Updates the colour of the canvas and then clears the canvas\
3434 @param <number - colour>\
3435]]\
3436function Canvas:setColour( colour )\
3437 self.colour = colour\
3438 self:clear()\
3439end\
3440\
3441--[[\
3442 @instance\
3443 @desc Updates the background colour of the canvas and then clears the canvas\
3444 @param <number - backgroundColour>\
3445]]\
3446function Canvas:setBackgroundColour( backgroundColour )\
3447 self.backgroundColour = backgroundColour\
3448 self:clear()\
3449end\
3450\
3451--[[\
3452 @instance\
3453 @desc Updates the background character to be used when clearing the canvas. Clears the canvas to apply the change\
3454 @param <string/false/nil - char>\
3455]]\
3456function Canvas:setBackgroundChar( char )\
3457 self.backgroundChar = char\
3458 self:clear()\
3459end\
3460\
3461function Canvas:setWidth( width )\
3462\9self.width = width\
3463\9self:clear()\
3464end\
3465\
3466function Canvas:setHeight( height )\
3467\9self.height = height\
3468\9self:clear()\
3469end\
3470\
3471--[[\
3472 @instance\
3473 @desc Draws the canvas to the target 'canvas' using the X and Y offsets. Pixels that are marked transparent are not drawn.\
3474 @param <Canvas - canvas>, [number - offsetX], [number - offsetY]\
3475]]\
3476function Canvas:drawTo( canvas, offsetX, offsetY )\
3477 local offsetX = offsetX - 1 or 0\
3478 local offsetY = offsetY - 1 or 0\
3479\
3480 local sRaw, tRaw = self.raw, canvas.raw\
3481 local width, height, buffer = sRaw.width, sRaw.height, sRaw.buffer\
3482 local tWidth, tHeight, tBuffer = tRaw.width, tRaw.height, tRaw.buffer\
3483\
3484 local colour, backgroundColour, backgroundTextColour = sRaw.colour, sRaw.backgroundColour, sRaw.backgroundTextColour\
3485 local xStart, xEnd = range( 1, offsetX, width, tWidth )\
3486\
3487 local cache, tCache, top, tc, tf, tb, bot, bc, bf, bb, tPos = 0, offsetX + ( offsetY * tWidth )\
3488 for y = 1, height do\
3489 local cY = y + offsetY\
3490 if cY >= 1 and cY <= tHeight then\
3491 for x = xStart, xEnd do\
3492 top = buffer[ cache + x ]\
3493 tc, tf, tb, tPos = top[ 1 ], top[ 2 ], top[ 3 ], tCache + x\
3494 bot = tBuffer[ tPos ]\
3495 bc, bf, bb = bot[ 1 ], bot[ 2 ], bot[ 3 ]\
3496\
3497 if tc and ( tf and tf ~= 0 ) and ( tb and tb ~= 0 ) then\
3498 tBuffer[ tPos ] = top\
3499 elseif not tc and tf == 0 and tb == 0 and bc and bf ~= 0 and bb ~= 0 then\
3500 tBuffer[ tPos ] = bot\
3501 else\
3502 local nc, nf, nb = tc or bc, tf or colour, tb or backgroundColour\
3503\
3504 if not tc then\
3505 nf = backgroundTextColour or bf\
3506 end\
3507\
3508 tBuffer[ tPos ] = { nc, nf == 0 and bf or nf, nb == 0 and bb or nb }\
3509 end\
3510 end\
3511 elseif cY > tHeight then\
3512 break\
3513 end\
3514\
3515 cache = cache + width\
3516 tCache = tCache + tWidth\
3517 end\
3518end\
3519",
3520["ContextMenu.ti"]="--[[\
3521 The ContextMenu class \"allows\" developers to dynamically spawn context menus with content they can customize. This node takes the application bounds into account and ensures\
3522 the content doesn't spill out of view.\
3523--]]\
3524class \"ContextMenu\" extends \"Container\" {\
3525 static = {\
3526 allowedTypes = { \"Button\", \"Label\" }\
3527 };\
3528}\
3529\
3530--[[\
3531 @constructor\
3532 @desc Resolves constructor arguments and invokes super. The canvas of this node is also marked transparent, as the canvas of this node is a rectangular shape surrounding all subframes.\
3533 @param <table - structure>*\
3534\
3535 Note: Ordered arguments inherited from other classes not included\
3536]]\
3537function ContextMenu:__init__( ... )\
3538 self:resolve( ... )\
3539 self:super()\
3540\
3541 self.transparent = true\
3542end\
3543\
3544--[[\
3545 @instance\
3546 @desc Population of the context menu requires a parent to be present. Therefore, when the parent is set on a node we will populate the\
3547 context menu, instead of at instantiation\
3548 @param <Node - parent>\
3549]]\
3550function ContextMenu:setParent( parent )\
3551 self.parent = parent\
3552\
3553 if parent then\
3554 local frame = self:addNode( ScrollContainer() )\
3555 frame.frameID = 1\
3556\
3557 self:populate( frame, self.structure )\
3558 frame.visible = true\
3559 end\
3560end\
3561\
3562--[[\
3563 @instance\
3564 @desc Populates the context menu with the options specified in the 'structure' table.\
3565 Accounts for application edge by positioning the menu as to avoid the menu contents spilling out of view.\
3566 @param <MNodeContainer* - parent>, <table - structure>\
3567\
3568 Note: The 'parent' param must be a node that can contain other nodes.\
3569]]\
3570function ContextMenu:populate( frame, structure )\
3571 local queue, q, totalWidth, totalHeight, negativeX = { { frame, structure } }, 1, 0, 0, 1\
3572\
3573 while q <= #queue do\
3574 local menu, structure, width = queue[ q ][ 1 ], queue[ q ][ 2 ], 0\
3575 local rules, Y = {}, 0\
3576\
3577 for i = 1, #structure do\
3578 Y = Y + 1\
3579 local part = structure[ i ]\
3580 local partType = part[ 1 ]:lower()\
3581\
3582 if partType == \"custom\" then\
3583 --TODO: Custom menu entries\
3584 else\
3585 if partType == \"menu\" then\
3586 local subframe = self:addNode( ScrollContainer( nil, menu.Y + Y - 1 ) )\
3587 if not menu.subframes then\
3588 menu.subframes = { subframe }\
3589 else\
3590 table.insert( menu.subframes, subframe )\
3591 end\
3592\
3593 subframe.visible = false\
3594\
3595 local id = #self.nodes\
3596 subframe.frameID = id\
3597 menu:addNode( Button( part[ 2 ], 1, Y ):on( \"trigger\", function()\
3598 local subframes = menu.subframes\
3599 for i = 1, #subframes do\
3600 if subframes[ i ] ~= subframe and subframes[ i ].visible then\
3601 self:closeFrame( subframes[ i ].frameID )\
3602 end\
3603 end\
3604\
3605 if subframe.visible then\
3606 self:closeFrame( id )\
3607 else\
3608 subframe.visible = true\
3609 end\
3610 end ) )\
3611\
3612 table.insert( queue, { subframe, part[ 3 ], menu } )\
3613 elseif partType == \"rule\" then\
3614 rules[ #rules + 1 ] = Y\
3615 elseif partType == \"button\" then\
3616 menu:addNode( Button( part[ 2 ], 1, Y ):on( \"trigger\", part[ 3 ] ) )\
3617 elseif partType == \"label\" then\
3618 menu:addNode( Label( part[ 2 ], 1, Y ) )\
3619 end\
3620\
3621 if partType ~= \"rule\" then\
3622 width = math.max( width, #part[ 2 ] )\
3623 end\
3624 end\
3625 end\
3626\
3627 if width == 0 then error \"Failed to populate context menu. Content given has no detectable width (or zero). Cannot proceed without width greater than 0\" end\
3628\
3629 for n = 1, #menu.nodes do menu.nodes[ n ].width = width end\
3630 for r = 1, #rules do menu:addNode( Label( (\"-\"):rep( width ), 1, rules[ r ] ) ) end\
3631\
3632 local parentMenu, widthOffset, relX = queue[ q ][ 3 ], 0, 0\
3633 if parentMenu then\
3634 widthOffset, relX = parentMenu.width, parentMenu.X\
3635 end\
3636\
3637 local spill = ( relX + widthOffset + width + self.X - 1 ) - self.parent.width\
3638 if spill > 0 then\
3639 menu.X = relX - ( parentMenu and width or spill )\
3640 else\
3641 menu.X = relX + widthOffset\
3642 end\
3643 negativeX = math.min( negativeX, menu.X )\
3644\
3645 menu.width, menu.height = width, Y - math.max( menu.Y + Y - self.parent.height, 0 )\
3646 menu:cacheContent()\
3647\
3648 totalWidth, totalHeight = totalWidth + menu.width, totalHeight + math.max( menu.height - ( parentMenu and parentMenu.Y or 0 ), 1 )\
3649 q = q + 1\
3650 end\
3651\
3652 if negativeX < 1 then\
3653 local nodes = self.nodes\
3654 for i = 1, #nodes do\
3655 nodes[ i ].X = nodes[ i ].X - negativeX + 1\
3656 end\
3657\
3658 self.X = self.X + negativeX\
3659 end\
3660\
3661 self.width = totalWidth\
3662 self.height = totalHeight\
3663end\
3664\
3665--[[\
3666 @instance\
3667 @desc A modified Container.shipEvent to avoid shipping events to hidden submenus.\
3668 @param <Event - event>\
3669]]\
3670function ContextMenu:shipEvent( event )\
3671 local nodes = self.nodes\
3672 for i = #nodes, 1, -1 do\
3673 if nodes[ i ].visible then\
3674 nodes[ i ]:handle( event )\
3675 end\
3676 end\
3677end\
3678\
3679--[[\
3680 @instance\
3681 @desc Invokes super (container) handle function. If event is a mouse event and it missed an open subframe the frames will be closed (if it was a CLICK) and the event will be unhandled\
3682 allowing further propagation and usage throughout the application.\
3683 @param <Event - eventObj>\
3684 @return <boolean - propagate>\
3685]]\
3686function ContextMenu:handle( eventObj )\
3687 if not self.super:handle( eventObj ) then return end\
3688\
3689 if eventObj.main == \"MOUSE\" and not self:isMouseColliding( eventObj ) then\
3690 if eventObj.sub == \"CLICK\" then self:closeFrame( 1 ) end\
3691 eventObj.handled = false\
3692 end\
3693\
3694 return true\
3695end\
3696\
3697--[[\
3698 @instance\
3699 @desc Closes the frame using 'frameID', which represents the position of the frame in the 'nodes' table\
3700 @param <number - frameID>\
3701]]\
3702function ContextMenu:closeFrame( frameID )\
3703 local framesToClose, i = { self.nodes[ frameID ] }, 1\
3704 while i <= #framesToClose do\
3705 local subframes = framesToClose[ i ].subframes or {}\
3706 for f = 1, #subframes do\
3707 if subframes[ f ].visible then\
3708 framesToClose[ #framesToClose + 1 ] = subframes[ f ]\
3709 end\
3710 end\
3711\
3712 framesToClose[ i ].visible = false\
3713 i = i + 1\
3714 end\
3715\
3716 self.changed = true\
3717end\
3718\
3719configureConstructor {\
3720 orderedArguments = { \"structure\" },\
3721 requiredArguments = { \"structure\" },\
3722 argumentTypes = {\
3723 structure = \"table\"\
3724 }\
3725}\
3726",
3727["Class.lua"]="--[[\
3728 Titanium Class System - Version 1.1\
3729\
3730 Copyright (c) Harry Felton 2016\
3731]]\
3732\
3733local classes, classRegistry, currentClass, currentRegistry = {}, {}\
3734local reserved = {\
3735 static = true,\
3736 super = true,\
3737 __type = true,\
3738 isCompiled = true,\
3739 compile = true\
3740}\
3741\
3742local missingClassLoader\
3743\
3744local getters = setmetatable( {}, { __index = function( self, name )\
3745 self[ name ] = \"get\" .. name:sub( 1, 1 ):upper() .. name:sub( 2 )\
3746\
3747 return self[ name ]\
3748end })\
3749\
3750local setters = setmetatable( {}, { __index = function( self, name )\
3751 self[ name ] = \"set\"..name:sub(1, 1):upper()..name:sub(2)\
3752\
3753 return self[ name ]\
3754end })\
3755\
3756local isNumber = {}\
3757for i = 0, 15 do isNumber[2 ^ i] = true end\
3758\
3759--[[ Constants ]]--\
3760local ERROR_BUG = \"\\nPlease report this via GitHub @ hbomb79/Titanium\"\
3761local ERROR_GLOBAL = \"Failed to %s to %s\\n\"\
3762local ERROR_NOT_BUILDING = \"No class is currently being built. Declare a class before invoking '%s'\"\
3763\
3764--[[ Helper functions ]]--\
3765local function throw( ... )\
3766 return error( table.concat( { ... }, \"\\n\" ) , 2 )\
3767end\
3768\
3769local function verifyClassEntry( target )\
3770 return type( target ) == \"string\" and type( classes[ target ] ) == \"table\" and type( classRegistry[ target ] ) == \"table\"\
3771end\
3772\
3773local function verifyClassObject( target, autoCompile )\
3774 if not Titanium.isClass( target ) then\
3775 return false\
3776 end\
3777\
3778 if autoCompile and not target:isCompiled() then\
3779 target:compile()\
3780 end\
3781\
3782 return true\
3783end\
3784\
3785local function isBuilding( ... )\
3786 if type( currentRegistry ) == \"table\" or type( currentClass ) == \"table\" then\
3787 if not ( currentRegistry and currentClass ) then\
3788 throw(\"Failed to validate currently building class objects\", \"The 'currentClass' and 'currentRegistry' variables are not both set\\n\", \"currentClass: \"..tostring( currentClass ), \"currentRegistry: \"..tostring( currentRegistry ), ERROR_BUG)\
3789 end\
3790 return true\
3791 end\
3792\
3793 if #({ ... }) > 0 then\
3794 return throw( ... )\
3795 else\
3796 return false\
3797 end\
3798end\
3799\
3800local function getClass( target )\
3801 if verifyClassEntry( target ) then\
3802 return classes[ target ]\
3803 elseif missingClassLoader then\
3804 local oC, oCReg = currentClass, currentRegistry\
3805 currentClass, currentRegistry = nil, nil\
3806\
3807 missingClassLoader( target )\
3808 local c = classes[ target ]\
3809 if not verifyClassObject( c, true ) then\
3810 throw(\"Failed to load missing class '\"..target..\"'.\\n\", \"The missing class loader failed to load class '\"..target..\"'.\\n\")\
3811 end\
3812\
3813 currentClass, currentRegistry = oC, oCReg\
3814\
3815 return c\
3816 else throw(\"Class '\"..target..\"' not found\") end\
3817end\
3818\
3819local function deepCopy( source )\
3820 if type( source ) == \"table\" then\
3821 local copy = {}\
3822 for key, value in next, source, nil do\
3823 copy[ deepCopy( key ) ] = deepCopy( value )\
3824 end\
3825 return copy\
3826 else\
3827 return source\
3828 end\
3829end\
3830\
3831local function propertyCatch( tbl )\
3832 if type( tbl ) == \"table\" then\
3833 if tbl.static then\
3834 if type( tbl.static ) ~= \"table\" then\
3835 throw(\"Invalid entity found in trailing property table\", \"Expected type 'table' for entity 'static'. Found: \"..tostring( tbl.static ), \"\\nThe 'static' entity is for storing static variables, refactor your class declaration.\")\
3836 end\
3837\
3838\
3839 local cStatic, cOwnedStatics = currentRegistry.static, currentRegistry.ownedStatics\
3840 for key, value in pairs( tbl.static ) do\
3841 if reserved[ key ] then\
3842 throw(\
3843 \"Failed to set static key '\"..key..\"' on building class '\"..currentRegistry.type..\"'\",\
3844 \"'\"..key..\"' is reserved by Titanium for internal processes.\"\
3845 )\
3846 end\
3847\
3848 cStatic[ key ] = value\
3849 cOwnedStatics[ key ] = type( value ) == \"nil\" and nil or true\
3850 end\
3851\
3852 tbl.static = nil\
3853 end\
3854\
3855 local cKeys, cOwned = currentRegistry.keys, currentRegistry.ownedKeys\
3856 for key, value in pairs( tbl ) do\
3857 cKeys[ key ] = value\
3858 cOwned[ key ] = type( value ) == \"nil\" and nil or true\
3859 end\
3860 elseif type( tbl ) ~= \"nil\" then\
3861 throw(\"Invalid trailing entity caught\\n\", \"An invalid object was caught trailing the class declaration for '\"..currentRegistry.type..\"'.\\n\", \"Object: '\"..tostring( tbl )..\"' (\"..type( tbl )..\")\"..\"\\n\", \"Expected [tbl | nil]\")\
3862 end\
3863end\
3864\
3865local function createFunctionWrapper( fn, superLevel )\
3866 return function( instance, ... )\
3867 local oldSuper = instance:setSuper( superLevel )\
3868\
3869 local v = { fn( ... ) }\
3870\
3871 instance.super = oldSuper\
3872\
3873 return unpack( v )\
3874 end\
3875end\
3876\
3877\
3878--[[ Local Functions ]]--\
3879local function compileSupers( targets )\
3880 local inheritedKeys, superMatrix = {}, {}, {}\
3881 local function compileSuper( target, id )\
3882 local factories = {}\
3883 local targetType = target.__type\
3884 local targetReg = classRegistry[ targetType ]\
3885\
3886 for key, value in pairs( targetReg.keys ) do\
3887 if not reserved[ key ] then\
3888 local toInsert = value\
3889 if type( value ) == \"function\" then\
3890 factories[ key ] = function( instance, ... )\
3891 --print(\"Super factory for \"..key..\"\\nArgs: \"..( function( args ) local s = \"\"; for i = 1, #args do s = s .. \" - \" .. tostring( args[ i ] ) .. \"\\n\" end return s end )( { ... } ))\
3892 local oldSuper = instance:setSuper( id + 1 )\
3893 local v = { value( instance, ... ) }\
3894\
3895 instance.super = oldSuper\
3896 return unpack( v )\
3897 end\
3898\
3899 toInsert = factories[ key ]\
3900 end\
3901\
3902 inheritedKeys[ key ] = toInsert\
3903 end\
3904 end\
3905\
3906 -- Handle inheritance\
3907 for key, value in pairs( inheritedKeys ) do\
3908 if type( value ) == \"function\" and not factories[ key ] then\
3909 factories[ key ] = value\
3910 end\
3911 end\
3912\
3913 superMatrix[ id ] = { factories, targetReg }\
3914 end\
3915\
3916 for id = #targets, 1, -1 do compileSuper( targets[ id ], id ) end\
3917\
3918 return inheritedKeys, function( instance )\
3919 local matrix, matrixReady = {}\
3920 local function generateMatrix( target, id )\
3921 local superTarget, matrixTbl, matrixMt = superMatrix[ id ], {}, {}\
3922 local factories, reg = superTarget[ 1 ], superTarget[ 2 ]\
3923\
3924 matrixTbl.__type = reg.type\
3925\
3926 local raw, owned, wrapCache, factory, upSuper = reg.raw, reg.ownedKeys, {}\
3927\
3928 function matrixMt:__tostring()\
3929 return \"[\"..reg.type..\"] Super #\"..id..\" of '\"..instance.__type..\"' instance\"\
3930 end\
3931 function matrixMt:__newindex( k, v )\
3932 if not matrixReady and k == \"super\" then\
3933 upSuper = v\
3934 return\
3935 end\
3936\
3937 throw(\"Cannot set keys on super. Illegal action.\")\
3938 end\
3939 function matrixMt:__index( k )\
3940 factory = factories[ k ]\
3941 if factory then\
3942 if not wrapCache[ k ] then\
3943 wrapCache[ k ] = (function( _, ... )\
3944 return factory( instance, ... )\
3945 end)\
3946 end\
3947\
3948 return wrapCache[ k ]\
3949 else\
3950 if k == \"super\" then\
3951 return upSuper\
3952 else\
3953 return throw(\"Cannot lookup value for key '\"..k..\"' on super\", \"Only functions can be accessed from supers.\")\
3954 end\
3955 end\
3956 end\
3957 function matrixMt:__call( instance, ... )\
3958 local init = self.__init__\
3959 if type( init ) == \"function\" then\
3960 return init( self, ... )\
3961 else\
3962 throw(\"Failed to execute super constructor. __init__ method not found\")\
3963 end\
3964 end\
3965\
3966 setmetatable( matrixTbl, matrixMt )\
3967 return matrixTbl\
3968 end\
3969\
3970 local last = matrix\
3971 for id = 1, #targets do\
3972 last.super = generateMatrix( targets[ id ], id )\
3973 last = last.super\
3974 end\
3975\
3976 martixReady = true\
3977 return matrix\
3978 end\
3979end\
3980local function mergeValues( a, b )\
3981 if type( a ) == \"table\" and type( b ) == \"table\" then\
3982 local merged = deepCopy( a ) or throw( \"Invalid base table for merging.\" )\
3983\
3984 if #b == 0 and next( b ) then\
3985 for key, value in pairs( b ) do merged[ key ] = value end\
3986 elseif #b > 0 then\
3987 for i = 1, #b do table.insert( merged, i, b[ i ] ) end\
3988 end\
3989\
3990 return merged\
3991 end\
3992\
3993 return b == nil and a or b\
3994end\
3995local constructorTargets = { \"orderedArguments\", \"requiredArguments\", \"argumentTypes\", \"useProxy\" }\
3996local function compileConstructor( superReg )\
3997 local constructorConfiguration = {}\
3998\
3999 local superConfig, currentConfig = superReg.constructor, currentRegistry.constructor\
4000 if not currentConfig and superConfig then\
4001 currentRegistry.constructor = superConfig\
4002 return\
4003 elseif currentConfig and not superConfig then\
4004 superConfig = {}\
4005 elseif not currentConfig and not superConfig then\
4006 return\
4007 end\
4008\
4009 local constructorKey\
4010 for i = 1, #constructorTargets do\
4011 constructorKey = constructorTargets[ i ]\
4012 if not ( ( constructorKey == \"orderedArguments\" and currentConfig.clearOrdered ) or ( constructorKey == \"requiredArguments\" and currentConfig.clearRequired ) ) then\
4013 currentConfig[ constructorKey ] = mergeValues( superConfig[ constructorKey ], currentConfig[ constructorKey ] )\
4014 end\
4015 end\
4016end\
4017local function compileCurrent()\
4018 isBuilding(\
4019 \"Cannot compile current class.\",\
4020 \"No class is being built at time of call. Declare a class be invoking 'compileCurrent'\"\
4021 )\
4022 local ownedKeys, ownedStatics, allMixins = currentRegistry.ownedKeys, currentRegistry.ownedStatics, currentRegistry.allMixins\
4023\
4024 -- Mixins\
4025 local cConstructor = currentRegistry.constructor\
4026 for target in pairs( currentRegistry.mixins ) do\
4027 allMixins[ target ] = true\
4028 local reg = classRegistry[ target ]\
4029\
4030 local t = { { reg.keys, currentRegistry.keys, ownedKeys }, { reg.static, currentRegistry.static, ownedStatics }, { reg.alias, currentRegistry.alias, currentRegistry.alias } }\
4031 for i = 1, #t do\
4032 local source, target, owned = t[ i ][ 1 ], t[ i ][ 2 ], t[ i ][ 3 ]\
4033 for key, value in pairs( source ) do\
4034 if not owned[ key ] then\
4035 target[ key ] = value\
4036 end\
4037 end\
4038 end\
4039\
4040 local constructor = reg.constructor\
4041 if constructor then\
4042 if constructor.clearOrdered then cConstructor.orderedArguments = nil end\
4043 if constructor.clearRequired then cConstructor.requiredArguments = nil end\
4044\
4045 local target\
4046 for i = 1, #constructorTargets do\
4047 target = constructorTargets[ i ]\
4048 cConstructor[ target ] = mergeValues( cConstructor[ target ], constructor[ target ] )\
4049 end\
4050 end\
4051 end\
4052\
4053 -- Supers\
4054 local superKeys\
4055 if currentRegistry.super then\
4056 local supers = {}\
4057\
4058 local last, c, newC = currentRegistry.super.target\
4059 while last do\
4060 c = getClass( last, true )\
4061\
4062 supers[ #supers + 1 ] = c\
4063 newC = classRegistry[ last ].super\
4064 last = newC and newC.target or false\
4065 end\
4066\
4067 superKeys, currentRegistry.super.matrix = compileSupers( supers )\
4068\
4069 -- Inherit alias from previous super\
4070 local currentAlias = currentRegistry.alias\
4071 for alias, redirect in pairs( classRegistry[ supers[ 1 ].__type ].alias ) do\
4072 if currentAlias[ alias ] == nil then\
4073 currentAlias[ alias ] = redirect\
4074 end\
4075 end\
4076\
4077 for mName in pairs( classRegistry[ supers[ 1 ].__type ].allMixins ) do\
4078 allMixins[ mName ] = true\
4079 end\
4080\
4081 compileConstructor( classRegistry[ supers[ 1 ].__type ] )\
4082 end\
4083\
4084 -- Generate instance function wrappers\
4085 local instanceWrappers, instanceVariables = {}, {}\
4086 for key, value in pairs( currentRegistry.keys ) do\
4087 if type( value ) == \"function\" then\
4088 instanceWrappers[ key ] = true\
4089 instanceVariables[ key ] = createFunctionWrapper( value, 1 )\
4090 else\
4091 instanceVariables[ key ] = value\
4092 end\
4093 end\
4094 if superKeys then\
4095 for key, value in pairs( superKeys ) do\
4096 if not instanceVariables[ key ] then\
4097 if type( value ) == \"function\" then\
4098 instanceWrappers[ key ] = true\
4099 instanceVariables[ key ] = function( _, ... ) return value( ... ) end\
4100 else\
4101 instanceVariables[ key ] = value\
4102 end\
4103 end\
4104 end\
4105 end\
4106\
4107 -- Finish compilation\
4108 currentRegistry.initialWrappers = instanceWrappers\
4109 currentRegistry.initialKeys = instanceVariables\
4110 currentRegistry.compiled = true\
4111\
4112 currentRegistry = nil\
4113 currentClass = nil\
4114\
4115end\
4116local function spawn( target, ... )\
4117 if not verifyClassEntry( target ) then\
4118 throw(\
4119 \"Failed to spawn class instance of '\"..tostring( target )..\"'\",\
4120 \"A class entity named '\"..tostring( target )..\"' doesn't exist.\"\
4121 )\
4122 end\
4123\
4124 local classEntry, classReg = classes[ target ], classRegistry[ target ]\
4125 if classReg.abstract or not classReg.compiled then\
4126 throw(\
4127 \"Failed to instantiate class '\"..classReg.type..\"'\",\
4128 \"Class '\"..classReg.type..\"' \"..(classReg.abstract and \"is abstract. Cannot instantiate abstract class.\" or \"has not been compiled. Cannot instantiate.\")\
4129 )\
4130 end\
4131\
4132 local wrappers, wrapperCache = deepCopy( classReg.initialWrappers ), {}\
4133 local raw = deepCopy( classReg.initialKeys )\
4134 local alias = classReg.alias\
4135\
4136 local instanceID = string.sub( tostring( raw ), 8 )\
4137\
4138 local supers = {}\
4139 local function indexSupers( last, ID )\
4140 while last.super do\
4141 supers[ ID ] = last.super\
4142 last = last.super\
4143 ID = ID + 1\
4144 end\
4145 end\
4146\
4147 local instanceObj, instanceMt = { raw = raw, __type = target, __instance = true, __ID = instanceID }, { __metatable = {} }\
4148 local getting, useGetters, setting, useSetters = {}, true, {}, true\
4149 function instanceMt:__index( k )\
4150 local k = alias[ k ] or k\
4151\
4152 local getFn = getters[ k ]\
4153 if useGetters and not getting[ k ] and wrappers[ getFn ] then\
4154 getting[ k ] = true\
4155 local v = self[ getFn ]( self )\
4156 getting[ k ] = nil\
4157\
4158 return v\
4159 elseif wrappers[ k ] then\
4160 if not wrapperCache[ k ] then\
4161 wrapperCache[ k ] = function( ... )\
4162 --print(\"Wrapper for \"..k..\". Arguments: \"..( function( args ) local s = \"\"; for i = 1, #args do s = s .. \" - \" .. tostring( args[ i ] ) .. \"\\n\" end return s end )( { ... } ) )\
4163 return raw[ k ]( self, ... )\
4164 end\
4165 end\
4166\
4167 return wrapperCache[ k ]\
4168 else return raw[ k ] end\
4169 end\
4170\
4171 function instanceMt:__newindex( k, v )\
4172 local k = alias[ k ] or k\
4173\
4174 local setFn = setters[ k ]\
4175 if useSetters and not setting[ k ] and wrappers[ setFn ] then\
4176 setting[ k ] = true\
4177 self[ setFn ]( self, v )\
4178 setting[ k ] = nil\
4179 elseif type( v ) == \"function\" and useSetters then\
4180 wrappers[ k ] = true\
4181 raw[ k ] = createFunctionWrapper( v, 1 )\
4182 else\
4183 wrappers[ k ] = nil\
4184 raw[ k ] = v\
4185 end\
4186 end\
4187\
4188 function instanceMt:__tostring()\
4189 return \"[Instance] \"..target..\" (\"..instanceID..\")\"\
4190 end\
4191\
4192 if classReg.super then\
4193 instanceObj.super = classReg.super.matrix( instanceObj ).super\
4194 indexSupers( instanceObj, 1 )\
4195 end\
4196\
4197 local old\
4198 function instanceObj:setSuper( target )\
4199 old, instanceObj.super = instanceObj.super, supers[ target ]\
4200 return old\
4201 end\
4202\
4203 local function setSymKey( key, value )\
4204 useSetters = false\
4205 instanceObj[ key ] = value\
4206 useSetters = true\
4207 end\
4208\
4209 local resolved\
4210 local resolvedArguments = {}\
4211 function instanceObj:resolve( ... )\
4212 if resolved then return false end\
4213\
4214 local args, config = { ... }, classReg.constructor\
4215 if not config then\
4216 throw(\"Failed to resolve \"..tostring( instance )..\" constructor arguments. No configuration has been set via 'configureConstructor'.\")\
4217 end\
4218\
4219 local configRequired, configOrdered, configTypes, configProxy = config.requiredArguments, config.orderedArguments, config.argumentTypes or {}, config.useProxy or {}\
4220\
4221 local argumentsRequired = {}\
4222 if configRequired then\
4223 local target = type( configRequired ) == \"table\" and configRequired or configOrdered\
4224\
4225 for i = 1, #target do argumentsRequired[ target[ i ] ] = true end\
4226 end\
4227\
4228 local orderedMatrix = {}\
4229 for i = 1, #configOrdered do orderedMatrix[ configOrdered[ i ] ] = i end\
4230\
4231 local proxyAll, proxyMatrix = type( configProxy ) == \"boolean\" and configProxy, {}\
4232 if not proxyAll then\
4233 for i = 1, #configProxy do proxyMatrix[ configProxy[ i ] ] = true end\
4234 end\
4235\
4236 local function handleArgument( position, name, value )\
4237 local desiredType = configTypes[ name ]\
4238 if desiredType == \"colour\" or desiredType == \"color\" then\
4239 --TODO: Check if number is valid (maybe?)\
4240 desiredType = \"number\"\
4241 end\
4242\
4243 if desiredType and type( value ) ~= desiredType then\
4244 return throw(\"Failed to resolve '\"..tostring( target )..\"' constructor arguments. Invalid type for argument '\"..name..\"'. Type \"..configTypes[ name ]..\" expected, \"..type( value )..\" was received.\")\
4245 end\
4246\
4247 resolvedArguments[ name ], argumentsRequired[ name ] = true, nil\
4248 if proxyAll or proxyMatrix[ name ] then\
4249 self[ name ] = value\
4250 else\
4251 setSymKey( name, value )\
4252 end\
4253 end\
4254\
4255 for iter, value in pairs( args ) do\
4256 if configOrdered[ iter ] then\
4257 handleArgument( iter, configOrdered[ iter ], value )\
4258 elseif type( value ) == \"table\" then\
4259 for key, v in pairs( value ) do\
4260 handleArgument( orderedMatrix[ key ], key, v )\
4261 end\
4262 else\
4263 return throw(\"Failed to resolve '\"..tostring( target )..\"' constructor arguments. Invalid argument found at ordered position \"..iter..\".\")\
4264 end\
4265 end\
4266\
4267 if next( argumentsRequired ) then\
4268 local str, name = \"\"\
4269 local function append( cnt )\
4270 str = str ..\"- \"..cnt..\"\\n\"\
4271 end\
4272\
4273 return throw(\"Failed to resolve '\"..tostring( target )..\"' constructor arguments. The following required arguments were not provided:\\n\\n\"..(function()\
4274 str = \"Ordered:\\n\"\
4275 for i = 1, #configOrdered do\
4276 name = configOrdered[ i ]\
4277 if argumentsRequired[ name ] then\
4278 append( name .. \" [#\"..i..\"]\" )\
4279 argumentsRequired[ name ] = nil\
4280 end\
4281 end\
4282\
4283 if next( argumentsRequired ) then\
4284 str = str .. \"\\nTrailing:\\n\"\
4285 for name, _ in pairs( argumentsRequired ) do append( name ) end\
4286 end\
4287\
4288 return str\
4289 end)())\
4290 end\
4291\
4292 resolved = true\
4293 return true\
4294 end\
4295 instanceObj.__resolved = resolvedArguments\
4296\
4297 function instanceObj:can( method )\
4298 return wrappers[ method ] or false\
4299 end\
4300\
4301 local locked = { __index = true, __newindex = true }\
4302 function instanceObj:setMetaMethod( method, fn )\
4303 if type( method ) ~= \"string\" then\
4304 throw( \"Failed to set metamethod '\"..tostring( method )..\"'\", \"Expected string for argument #1, got '\"..tostring( method )..\"' of type \"..type( method ) )\
4305 elseif type( fn ) ~= \"function\" then\
4306 throw( \"Failed to set metamethod '\"..tostring( method )..\"'\", \"Expected function for argument #2, got '\"..tostring( fn )..\"' of type \"..type( fn ) )\
4307 end\
4308\
4309 method = \"__\"..method\
4310 if locked[ method ] then\
4311 throw( \"Failed to set metamethod '\"..tostring( method )..\"'\", \"Metamethod locked\" )\
4312 end\
4313\
4314 instanceMt[ method ] = fn\
4315 end\
4316\
4317 function instanceObj:lockMetaMethod( method )\
4318 if type( method ) ~= \"string\" then\
4319 throw( \"Failed to lock metamethod '\"..tostring( method )..\"'\", \"Expected string, got '\"..tostring( method )..\"' of type \"..type( method ) )\
4320 end\
4321\
4322 locked[ \"__\"..method ] = true\
4323 end\
4324\
4325 setmetatable( instanceObj, instanceMt )\
4326 if type( instanceObj.__init__ ) == \"function\" then instanceObj:__init__( ... ) end\
4327\
4328 for mName in pairs( classReg.allMixins ) do\
4329 if type( instanceObj[ mName ] ) == \"function\" then instanceObj[ mName ]( instanceObj ) end\
4330 end\
4331\
4332 if type( instanceObj.__postInit__ ) == \"function\" then instanceObj:__postInit__( ... ) end\
4333\
4334 return instanceObj\
4335end\
4336\
4337\
4338--[[ Global functions ]]--\
4339\
4340function class( name )\
4341 if isBuilding() then\
4342 throw(\
4343 \"Failed to declare class '\"..tostring( name )..\"'\",\
4344 \"A new class cannot be declared until the currently building class has been compiled.\",\
4345 \"\\nCompile '\"..tostring( currentRegistry.type )..\"' before declaring '\"..tostring( name )..\"'\"\
4346 )\
4347 end\
4348\
4349 local function nameErr( reason )\
4350 throw( \"Failed to declare class '\"..tostring( name )..\"'\\n\", string.format( \"Class name %s is not valid. %s\", tostring( name ), reason ) )\
4351 end\
4352\
4353 if type( name ) ~= \"string\" then\
4354 nameErr \"Class names must be a string\"\
4355 elseif not name:find \"%a\" then\
4356 nameErr \"No alphabetic characters could be found\"\
4357 elseif name:find \"%d\" then\
4358 nameErr \"Class names cannot contain digits\"\
4359 elseif classes[ name ] then\
4360 nameErr \"A class with that name already exists\"\
4361 elseif reserved[ name ] then\
4362 nameErr (\"'\"..name..\"' is reserved for Titanium processes\")\
4363 else\
4364 local char = name:sub( 1, 1 )\
4365 if char ~= char:upper() then\
4366 nameErr \"Class names must begin with an uppercase character\"\
4367 end\
4368 end\
4369\
4370 local classReg = {\
4371 type = name,\
4372\
4373 static = {},\
4374 keys = {},\
4375 ownedStatics = {},\
4376 ownedKeys = {},\
4377\
4378 initialWrappers = {},\
4379 initialKeys = {},\
4380\
4381 mixins = {},\
4382 allMixins = {},\
4383 alias = {},\
4384\
4385 constructor = false,\
4386 super = false,\
4387\
4388 compiled = false,\
4389 abstract = false\
4390 }\
4391\
4392 -- Class metatable\
4393 local classMt = { __metatable = {} }\
4394 function classMt:__tostring()\
4395 return (classReg.compiled and \"[Compiled] \" or \"\") .. \"Class '\"..name..\"'\"\
4396 end\
4397\
4398 local keys, owned = classReg.keys, classReg.ownedKeys\
4399 local staticKeys, staticOwned = classReg.static, classReg.ownedStatics\
4400 function classMt:__newindex( k, v )\
4401 if classReg.compiled then\
4402 throw(\
4403 \"Failed to set key on class base.\", \"\",\
4404 \"This class base is compiled, once a class base is compiled new keys cannot be added to it\",\
4405 \"\\nPerhaps you meant to set the static key '\"..name..\".static.\"..k..\"' instead.\"\
4406 )\
4407 end\
4408\
4409 keys[ k ] = v\
4410 owned[ k ] = type( v ) == \"nil\" and nil or true\
4411 end\
4412 function classMt:__index( k )\
4413 if owned[ k ] then\
4414 throw (\
4415 \"Access to key '\"..k..\"' denied.\",\
4416 \"Instance keys cannot be accessed from a class base, regardless of compiled state\",\
4417 classReg.ownedStatics[ k ] and \"\\nPerhaps you meant to access the static variable '\" .. name .. \".static.\".. k .. \"' instead\" or nil\
4418 )\
4419 elseif staticOwned[ k ] then\
4420 return staticKeys[ k ]\
4421 end\
4422 end\
4423 function classMt:__call( ... )\
4424 return spawn( name, ... )\
4425 end\
4426\
4427 -- Static metatable\
4428 local staticMt = { __index = staticKeys }\
4429 function staticMt:__newindex( k, v )\
4430 staticKeys[ k ] = v\
4431 staticOwned[ k ] = type( v ) == \"nil\" and nil or true\
4432 end\
4433\
4434 -- Class object\
4435 local classObj = { __type = name }\
4436 classObj.static = setmetatable( {}, staticMt )\
4437 classObj.compile = compileCurrent\
4438\
4439 function classObj:isCompiled() return classReg.compiled end\
4440\
4441 function classObj:getRegistry() return classReg end\
4442\
4443 setmetatable( classObj, classMt )\
4444\
4445 -- Export\
4446 currentRegistry = classReg\
4447 classRegistry[ name ] = classReg\
4448\
4449 currentClass = classObj\
4450 classes[ name ] = classObj\
4451\
4452 _G[ name ] = classObj\
4453\
4454 return propertyCatch\
4455end\
4456\
4457function extends( name )\
4458 isBuilding(\
4459 string.format( ERROR_GLOBAL, \"extend\", \"target class '\"..tostring( name )..\"'\" ), \"\",\
4460 string.format( ERROR_NOT_BUILDING, \"extends\" )\
4461 )\
4462\
4463 currentRegistry.super = {\
4464 target = name\
4465 }\
4466 return propertyCatch\
4467end\
4468\
4469function mixin( name )\
4470 if type( name ) ~= \"string\" then\
4471 throw(\"Invalid mixin target '\"..tostring( name )..\"'\")\
4472 end\
4473\
4474 isBuilding(\
4475 string.format( ERROR_GLOBAL, \"mixin\", \"target class '\".. name ..\"'\" ),\
4476 string.format( ERROR_NOT_BUILDING, \"mixin\" )\
4477 )\
4478\
4479 local mixins = currentRegistry.mixins\
4480 if mixins[ name ] then\
4481 throw(\
4482 string.format( ERROR_GLOBAL, \"mixin class '\".. name ..\"'\", \"class '\"..currentRegistry.type)\
4483 \"'\".. name ..\"' has already been mixed in to this target class.\"\
4484 )\
4485 end\
4486\
4487 if not getClass( name, true ) then\
4488 throw(\
4489 string.format( ERROR_GLOBAL, \"mixin class '\".. name ..\"'\", \"class '\"..currentRegistry.type ),\
4490 \"The mixin class '\".. name ..\"' failed to load\"\
4491 )\
4492 end\
4493\
4494 mixins[ name ] = true\
4495 return propertyCatch\
4496end\
4497\
4498function abstract()\
4499 isBuilding(\
4500 \"Failed to enforce abstract class policy\\n\",\
4501 string.format( ERROR_NOT_BUILDING, \"abstract\" )\
4502 )\
4503\
4504 currentRegistry.abstract = true\
4505 return propertyCatch\
4506end\
4507\
4508function alias( target )\
4509 local FAIL_MSG = \"Failed to implement alias targets\\n\"\
4510 isBuilding( FAIL_MSG, string.format( ERROR_NOT_BUILDING, \"alias\" ) )\
4511\
4512 local tbl = type( target ) == \"table\" and target or (\
4513 type( target ) == \"string\" and (\
4514 type( _G[ target ] ) == \"table\" and _G[ target ] or throw( FAIL_MSG, \"Failed to find '\"..tostring( target )..\"' table in global environment.\" )\
4515 ) or throw( FAIL_MSG, \"Expected type table as target, got '\"..tostring( target )..\"' of type \"..type( target ) )\
4516 )\
4517\
4518 local cAlias = currentRegistry.alias\
4519 for alias, redirect in pairs( tbl ) do\
4520 cAlias[ alias ] = redirect\
4521 end\
4522\
4523 return propertyCatch\
4524end\
4525\
4526function configureConstructor( config, clearOrdered, clearRequired )\
4527 isBuilding(\
4528 \"Failed to configure class constructor\\n\",\
4529 string.format( ERROR_NOT_BUILDING, \"configureConstructor\" )\
4530 )\
4531\
4532 if type( config ) ~= \"table\" then\
4533 throw (\
4534 \"Failed to configure class constructor\\n\",\
4535 \"Expected type 'table' as first argument\"\
4536 )\
4537 end\
4538\
4539 local constructor = {\
4540 clearOrdered = clearOrdered or nil,\
4541 clearRequired = clearRequired or nil\
4542 }\
4543 for key, value in pairs( config ) do constructor[ key ] = value end\
4544\
4545 currentRegistry.constructor = constructor\
4546 return propertyCatch\
4547end\
4548\
4549--[[ Class Library ]]--\
4550Titanium = {}\
4551\
4552function Titanium.getGetterName( property ) return getters[ property ] end\
4553\
4554function Titanium.getSetterName( property ) return setters[ property ] end\
4555\
4556function Titanium.getClass( name )\
4557 return classes[ name ]\
4558end\
4559\
4560function Titanium.getClasses()\
4561 return classes\
4562end\
4563\
4564function Titanium.isClass( target )\
4565 return type( target ) == \"table\" and type( target.__type ) == \"string\" and verifyClassEntry( target.__type )\
4566end\
4567\
4568function Titanium.isInstance( target )\
4569 return Titanium.isClass( target ) and target.__instance\
4570end\
4571\
4572function Titanium.typeOf( target, classType, instance )\
4573 if not Titanium.isClass( target ) or ( instance and not Titanium.isInstance( target ) ) then\
4574 return false\
4575 end\
4576\
4577 local targetReg = classRegistry[ target.__type ]\
4578\
4579 return targetReg.type == classType or ( targetReg.super and Titanium.typeOf( classes[ targetReg.super.target ], classType ) ) or false\
4580end\
4581\
4582function Titanium.mixesIn( target, mixinName )\
4583 if not Titanium.isClass( target ) then return false end\
4584\
4585 return classRegistry[ target.__type ].allMixins[ mixinName ]\
4586end\
4587\
4588function Titanium.setClassLoader( fn )\
4589 if type( fn ) ~= \"function\" then\
4590 throw( \"Failed to set class loader\", \"Value '\"..tostring( fn )..\"' is invalid, expected function\" )\
4591 end\
4592\
4593 missingClassLoader = fn\
4594end\
4595\
4596local preprocessTargets = {\"class\", \"extends\", \"alias\", \"mixin\"}\
4597function Titanium.preprocess( text )\
4598 local keyword\
4599 for i = 1, #preprocessTargets do\
4600 keyword = preprocessTargets[ i ]\
4601\
4602 for value in text:gmatch( keyword .. \" ([_%a][_%w]*)%s\" ) do\
4603 text = text:gsub( keyword .. \" \" .. value, keyword..\" \\\"\"..value..\"\\\"\" )\
4604 end\
4605 end\
4606\
4607 for name in text:gmatch( \"abstract class (\\\".[^%s]+\\\")\" ) do\
4608 text = text:gsub( \"abstract class \"..name, \"class \"..name..\" abstract()\" )\
4609 end\
4610\
4611 return text\
4612end\
4613",
4614["Checkbox.ti"]="--[[\
4615 The checkbox is a node that can be toggled on and off\
4616]]\
4617\
4618class \"Checkbox\" extends \"Node\" mixin \"MActivatable\" mixin \"MTogglable\" {\
4619 checkedMark = \"x\";\
4620 uncheckedMark = \" \";\
4621\
4622 allowMouse = true;\
4623}\
4624\
4625--[[\
4626 @constructor\
4627 @desc Resolves arguments and calls super constructor\
4628 @param <number - X>, <number - Y>\
4629]]\
4630function Checkbox:__init__( ... )\
4631 self:resolve( ... )\
4632 self:super()\
4633\
4634 self:register(\"checkedMark\", \"uncheckedMark\")\
4635end\
4636\
4637--[[\
4638 @instance\
4639 @desc Sets the checkbox to 'active' when clicked\
4640 @param <MouseEvent - event>, <boolean - handled>, <boolean - within>\
4641]]\
4642function Checkbox:onMouseClick( event, handled, within )\
4643 if not handled then\
4644 self.active = within\
4645\
4646 if within then\
4647 event.handled = true\
4648 end\
4649 end\
4650end\
4651\
4652--[[\
4653 @instance\
4654 @desc Sets the checkbox to inactive when the mouse button is released. If released on checkbox while active 'onToggle' callback is fired and the checkbox is toggled.\
4655 @param <MouseEvent - event>, <boolean - handled>, <boolean - within>\
4656]]\
4657function Checkbox:onMouseUp( event, handled, within )\
4658 if not handled and within and self.active then\
4659 self:toggle( event, handled, within )\
4660\
4661 event.handled = true\
4662 end\
4663\
4664 self.active = false\
4665end\
4666\
4667--[[\
4668 @instance\
4669 @desc If a label which specifies this node as its 'labelFor' paramater is clicked this function will be called, causing the checkbox to toggle\
4670 @param <LabelInstance - label>, <MouseEvent - event>, <boolean - handled>, <boolean - within>\
4671]]\
4672function Checkbox:onLabelClicked( label, event, handled, within )\
4673 self:toggle( event, handled, within, label )\
4674 event.handled = true\
4675end\
4676\
4677--[[\
4678 @instance\
4679 @desc Draws the checkbox to the canvas\
4680 @param [boolean - force]\
4681]]\
4682function Checkbox:draw( force )\
4683 local raw = self.raw\
4684 if raw.changed or force then\
4685 local toggled, tc, bg = self.toggled\
4686 if not self.enabled then\
4687 tc, bg = raw.disabledColour, raw.disabledBackgroundColour\
4688 elseif toggled then\
4689 tc, bg = raw.toggledColour, raw.toggledBackgroundColour\
4690 elseif self.active then\
4691 tc, bg = raw.activeColour, raw.activeBackgroundColour\
4692 end\
4693\
4694 raw.canvas:drawPoint( 1, 1, toggled and raw.checkedMark or raw.uncheckedMark, tc, bg )\
4695 raw.changed = false\
4696 end\
4697end\
4698\
4699configureConstructor( {\
4700 orderedArguments = { \"X\", \"Y\" },\
4701 argumentTypes = {\
4702 checkedMark = \"string\",\
4703 uncheckedMark = \"string\"\
4704 }\
4705}, true, true )\
4706",
4707["Parser.ti"]="--[[\
4708 The parser class \"should\" be extended by classes that are used to parser lexer token output.\
4709]]\
4710\
4711class \"Parser\" abstract() {\
4712 position = 0;\
4713 tokens = {};\
4714}\
4715\
4716--[[\
4717 @constructor\
4718 @desc Sets the tokens of the parser to those passed and begins parsing\
4719 @param <table - tokens>\
4720]]\
4721function Parser:__init__( tokens )\
4722 if type( tokens ) ~= \"table\" then\
4723 return error \"Failed to parse. Invalid tokens\"\
4724 end\
4725\
4726 self.tokens = tokens\
4727 self:parse()\
4728end\
4729\
4730--[[\
4731 @instance\
4732 @desc Returns the token at 'position'\
4733]]\
4734function Parser:getCurrentToken()\
4735 return self.tokens[ self.position ]\
4736end\
4737\
4738--[[\
4739 @instance\
4740 @desc Returns the token 'amount' ahead of the current position. Defaults to one position ahead\
4741]]\
4742function Parser:peek( amount )\
4743 return self.tokens[ self.position + ( amount or 1 ) ]\
4744end\
4745\
4746--[[\
4747 @instance\
4748 @desc Tests the adjacent tokens to see if they are the correct type. Offsets can be provided and missing tokens can be configured to cause test failure\
4749 @param [string - before], [string - after], [boolean - optional], [number - beforeOffset], [number - afterOffset], [boolean - disallowMissing]\
4750\
4751 Note: If a token doesn't exist, it will NOT cause the test to fail unless 'disallowMissing' is set to true.\
4752]]\
4753function Parser:testAdjacent( before, after, optional, beforeOffset, afterOffset, disallowMissing )\
4754 local leading, leadingPass, trailing, trailingPass = false, not before, false, not after\
4755 local function test( token, filter )\
4756 if not token then return not disallowMissing end\
4757\
4758 if type( filter ) == \"table\" then\
4759 for i = 1, #filter do\
4760 if token.type == filter[ i ] then return true end\
4761 end\
4762 else return token.type == filter end\
4763 end\
4764\
4765 if before then\
4766 leading = self:peek( -1 - ( beforeOffset or 0 ) )\
4767 leadingPass = test( leading, before )\
4768 end\
4769\
4770\
4771 if after then\
4772 trailing = self:peek( 1 + ( afterOffset or 0 ) )\
4773 trailingPass = test( trailing, after )\
4774 end\
4775\
4776 return ( optional and ( trailingPass or leadingPass ) or ( not optional and trailingPass and leadingPass ) ), leading, trailing\
4777end\
4778\
4779--[[\
4780 @instance\
4781 @desc Advances 'position' by one and returns the token at the new position\
4782]]\
4783function Parser:stepForward( amount )\
4784 self.position = self.position + ( amount or 1 )\
4785 return self:getCurrentToken()\
4786end\
4787\
4788--[[\
4789 @instance\
4790 @desc Throws a error prefixed with information about the token being parsed at the time of error.\
4791]]\
4792function Parser:throw( e, token )\
4793 local token = token or self:getCurrentToken()\
4794 if not token then\
4795 return error( \"Parser (\"..tostring( self.__type )..\") Error: \"..e, 2 )\
4796 end\
4797\
4798 return error( \"Parser (\"..tostring( self.__type )..\") Error. Line \"..token.line..\", char \"..token.char .. \": \"..e, 2 )\
4799end\
4800",
4801["Button.ti"]="--[[\
4802 A Button is a node that can be clicked to trigger a callback.\
4803 The button can containtext which can span multiple lines, however if too much text is entered it will be truncated to fit the button dimensions.\
4804\
4805 If buttonLock is 1 or 2, the mouse button used to click the button must match the value. Eg: If self.buttonLock == 1, the mouse button must be 1 when clicked to effect the button.\
4806 If the buttonLock value is 0, any button value will be accepted\
4807]]\
4808\
4809class \"Button\" extends \"Node\" mixin \"MTextDisplay\" mixin \"MActivatable\" {\
4810 allowMouse = true;\
4811 buttonLock = 1;\
4812}\
4813\
4814--[[\
4815 @constructor\
4816 @desc Accepts button arguments and resolves them.\
4817 @param <string - text>, [number - X], [number - Y], [number - width], [number - height]\
4818]]\
4819function Button:__init__( ... )\
4820 self:resolve( ... )\
4821 self:super()\
4822\
4823 self:register(\"width\", \"height\", \"buttonLock\")\
4824end\
4825\
4826--[[\
4827 @instance\
4828 @desc Sets the button to 'active' when the button is clicked with the valid mouse button (self.buttonLock)\
4829 @param <MouseEvent - event>, <boolean - handled>, <boolean - within>\
4830]]\
4831function Button:onMouseClick( event, handled, within )\
4832 if not handled and within and ( self.buttonLock == 0 or event.button == self.buttonLock ) then\
4833 self.active, event.handled = true, true\
4834 end\
4835end\
4836\
4837--[[\
4838 @instance\
4839 @desc Sets the button to inactive when the mouse button is released. If released on button while active 'onTrigger' callback is fired.\
4840 @param <MouseEvent - event>, <boolean - handled>, <boolean - within>\
4841]]\
4842function Button:onMouseUp( event, handled, within )\
4843 if within and not handled and self.active then\
4844 event.handled = true\
4845 self:executeCallbacks \"trigger\"\
4846 end\
4847\
4848 self.active = false\
4849end\
4850\
4851--[[\
4852 @instance\
4853 @desc Draws the text to the node canvas\
4854 @param [boolean - force]\
4855]]\
4856function Button:draw( force )\
4857 local raw = self.raw\
4858 if raw.changed or force then\
4859 local tc, bg\
4860 if not self.enabled then\
4861 bg, tc = raw.disabledBackgroundColour, raw.disabledColour\
4862 elseif self.active then\
4863 bg, tc = raw.activeBackgroundColour, raw.activeColour\
4864 end\
4865\
4866 raw.canvas:clear( bg )\
4867 self:drawText( bg, tc )\
4868\
4869 raw.changed = false\
4870 end\
4871end\
4872\
4873--[[\
4874 @instance\
4875 @desc Sets the text of the button and then wraps the new text for display.\
4876 @param <string - text>\
4877]]\
4878function Button:setText( text )\
4879 if self.text == text then return end\
4880\
4881 self.text = text\
4882 self.changed = true\
4883 self:wrapText()\
4884end\
4885\
4886--[[\
4887 @instance\
4888 @desc Sets the width of the button and then re-wraps the text to fit in the dimensions.\
4889 @param <number - width>\
4890]]\
4891function Button:setWidth( width )\
4892 self.super:setWidth( width )\
4893 self:wrapText()\
4894end\
4895\
4896configureConstructor {\
4897 orderedArguments = { \"text\" },\
4898 requiredArguments = { \"text\" },\
4899 argumentTypes = {\
4900 buttonLock = \"number\"\
4901 }\
4902}\
4903",
4904["RedirectCanvas.ti"]="--[[\
4905 The RedirectCanvas is a class \"to\" be used by nodes that wish to redirect the term object. This canvas provides a terminal redirect and keeps track\
4906 of the terminals properties set inside the wrapped program (via the term methods).\
4907\
4908 This allows emulation of a shell program inside of Titanium without causing visual issues due to the shell program drawing directly to the terminal and not\
4909 through Titaniums canvas system.\
4910--]]\
4911\
4912local stringLen, stringSub = string.len, string.sub\
4913local isColour = term.isColour()\
4914\
4915local function testColour( col )\
4916 if not isColour and ( col ~= 1 or col ~= 32768 or col ~= 256 or col ~= 128 ) then\
4917 error \"Colour not supported\"\
4918 end\
4919\
4920 return true\
4921end\
4922\
4923class \"RedirectCanvas\" extends \"NodeCanvas\"\
4924\
4925function RedirectCanvas:__init__( ... )\
4926 self:resetTerm()\
4927 self:super( ... )\
4928end\
4929\
4930function RedirectCanvas:resetTerm()\
4931 self.tX, self.tY, self.tColour, self.tBackgroundColour, self.tCursor = 1, 1, 1, 32768, false;\
4932 self:clear( 32768, true )\
4933end\
4934\
4935--[[\
4936 @instance\
4937 @desc Returns a table compatible with `term.redirect`\
4938 @return <table - redirect>\
4939]]\
4940function RedirectCanvas:getTerminalRedirect()\
4941 local redirect = {}\
4942\
4943 function redirect.write( text )\
4944 text = tostring( text )\
4945 local tc, bg, tX, tY = self.tColour, self.tBackgroundColour, self.tX, self.tY\
4946 local buffer, position = self.buffer, self.width * ( tY - 1 ) + tX\
4947\
4948 for i = 1, math.min( stringLen( text ), self.width - tX + 1 ) do\
4949 buffer[ position ] = { stringSub( text, i, i ), tc, bg }\
4950 position = position + 1\
4951 end\
4952\
4953 self.tX = tX + stringLen( text )\
4954 end\
4955\
4956 function redirect.blit( text, colour, background )\
4957 if stringLen( text ) ~= stringLen( colour ) or stringLen( text ) ~= stringLen( background ) then\
4958 return error \"blit arguments must be the same length\"\
4959 end\
4960\
4961 local tX, hex = self.tX, TermCanvas.static.hex\
4962 local buffer, position = self.buffer, self.width * ( self.tY - 1 ) + tX\
4963\
4964 for i = 1, math.min( stringLen( text ), self.width - tX + 1 ) do\
4965 buffer[ position ] = { stringSub( text, i, i ), hex[ stringSub( colour, i, i ) ], hex[ stringSub( background, i, i ) ] }\
4966 position = position + 1\
4967 end\
4968\
4969 self.tX = tX + stringLen( text )\
4970 end\
4971\
4972 function redirect.clear()\
4973 self:clear( self.tBackgroundColour, true )\
4974 end\
4975\
4976 function redirect.clearLine()\
4977 local px = { \" \", self.tColour, self.tBackgroundColour }\
4978 local buffer, position = self.buffer, self.width * ( self.tY - 1 )\
4979\
4980 for i = 1, self.width do\
4981 buffer[ position ] = px\
4982 position = position + 1\
4983 end\
4984 end\
4985\
4986 function redirect.getCursorPos()\
4987 return self.tX, self.tY\
4988 end\
4989\
4990 function redirect.setCursorPos( x, y )\
4991 self.tX, self.tY = math.floor( x ), math.floor( y )\
4992 end\
4993\
4994 function redirect.getSize()\
4995 return self.width, self.height\
4996 end\
4997\
4998 function redirect.setCursorBlink( blink )\
4999 self.tCursor = blink\
5000 end\
5001\
5002 function redirect.setTextColour( tc )\
5003 if testColour( tc ) then\
5004 self.tColour = tc\
5005 end\
5006 end\
5007\
5008 function redirect.getTextColour()\
5009 return self.tColour\
5010 end\
5011\
5012 function redirect.setBackgroundColour( bg )\
5013 if testColour( bg ) then\
5014 self.tBackgroundColour = bg\
5015 end\
5016 end\
5017\
5018 function redirect.getBackgroundColour()\
5019 return self.tBackgroundColour\
5020 end\
5021\
5022 function redirect.scroll( n )\
5023 local offset, buffer, nL = self.width * n, self.buffer, n < 0\
5024 local pixelCount, blank = self.width * self.height, { \" \", self.tColour, self.tBackgroundColour }\
5025\
5026 for i = nL and pixelCount or 1, nL and 1 or pixelCount, nL and -1 or 1 do\
5027 buffer[ i ] = buffer[ i + offset ] or blank\
5028 end\
5029 end\
5030\
5031 function redirect.isColour()\
5032 return isColour\
5033 end\
5034\
5035 -- American spelling compatibility layer\
5036 redirect.isColor = redirect.isColour\
5037\9redirect.setBackgroundColor = redirect.setBackgroundColour\
5038\9redirect.setTextColor = redirect.setTextColour\
5039\9redirect.getBackgroundColor = redirect.getBackgroundColour\
5040\9redirect.getTextColor = redirect.getTextColour\
5041\
5042 return redirect\
5043end\
5044\
5045--[[\
5046 @instance\
5047 @desc Modified Canvas.clear. Only sets pixels that do not exist (doesn't really clear the canvas, just ensures it is the correct size).\
5048 This is to prevent the program running via the term redirect isn't cleared away. Call this function with 'force' and all pixels will be\
5049 replaced (the terminal redirect uses this method).\
5050\
5051 Alternatively, self.getTerminalRedirect.clear() will also clear the canvas entirely\
5052 @param [number - col], [boolean - force]\
5053]]\
5054function RedirectCanvas:clear( col, force )\
5055 local col = col or self.tBackgroundColour\
5056 local pixel, buffer = { \" \", col, col }, self.buffer\
5057\
5058 for index = 1, self.width * self.height do\
5059 if not buffer[ index ] or force then\
5060 buffer[ index ] = pixel\
5061 end\
5062 end\
5063end\
5064",
5065["MouseEvent.ti"]="local string_sub, string_upper = string.sub, string.upper\
5066\
5067class \"MouseEvent\" extends \"Event\" {\
5068 inBounds = false;\
5069 main = \"MOUSE\";\
5070\
5071 isWithin = true;\
5072}\
5073\
5074--[[\
5075 @constructor\
5076 @desc Sets the values given in their respective instance keys. Stores all values into a table 'data'.\
5077 @param <string - name>, <number - button>, <number - X>, <number - Y>, [string - sub]\
5078]]\
5079function MouseEvent:__init__( name, button, X, Y, sub )\
5080 self.name = name\
5081 self.button = button\
5082 self.X = X\
5083 self.Y = Y\
5084\
5085 self.sub = sub or string_upper( string_sub( name, 7 ) )\
5086\
5087 self.data = { name, button, X, Y }\
5088end\
5089\
5090--[[\
5091 @instance\
5092 @desc Returns true if the mouse event was inside of the bounds provided.\
5093 @param <number - x>, <number - y>, <number - w>, <number - h>\
5094 @return <boolean - inBounds>\
5095]]\
5096function MouseEvent:within( x, y, w, h )\
5097 local X, Y = self.X, self.Y\
5098\
5099 return X >= x and Y >= y and X <= -1 + x + w and Y <= -1 + y + h\
5100end\
5101\
5102--[[\
5103 @instance\
5104 @desc Returns true if the mouse event was inside the bounds of the parent provided (x, y, width and height)\
5105 @param <NodeContainer - parent>\
5106]]\
5107function MouseEvent:withinParent( parent )\
5108 return self:within( parent.X, parent.Y, parent.width, parent.height )\
5109end\
5110\
5111--[[\
5112 @instance\
5113 @desc Clones 'self' and adjusts it's X & Y positions so that they're relative to 'parent'.\
5114 @param <NodeContainer - parent>\
5115 @return <MouseEvent Instance - clone>\
5116\
5117 Note: The clone's 'handle' method has been adjusted to also call 'handle' on the master event obj (self).\
5118]]\
5119function MouseEvent:clone( parent )\
5120 local clone = MouseEvent( self.name, self.button, self.X - parent.X + 1, self.Y - parent.Y + 1, self.sub )\
5121\
5122 clone.handled = self.handled\
5123 clone.isWithin = self.isWithin\
5124 clone.setHandled = function( clone, handled )\
5125 clone.handled = handled\
5126 self.handled = handled --TODO: Make sure 'self' refers to the object which 'clone' was invoked on, not the clone.\
5127 end\
5128\
5129 return clone\
5130end\
5131",
5132["Tween.ti"]="class \"Tween\" {\
5133 static = {\
5134 easing = {}\
5135 };\
5136\
5137 object = false;\
5138\
5139 property = false;\
5140 initial = false;\
5141 final = false;\
5142\
5143 duration = 0;\
5144 clock = 0;\
5145}\
5146\
5147--[[\
5148 @constructor\
5149 @desc Constructs the tween instance, converting the 'easing' property into a function (if it's a string) and also stores the initial value of the property for later use.\
5150 @param <Object - object>, <string - name>, <string - property>, <number - final>, <number - duration>, [string/function - easing]\
5151]]\
5152function Tween:__init__( ... )\
5153 self:resolve( ... )\
5154 if not Titanium.isInstance( self.object ) then\
5155 return error(\"Argument 'object' for tween must be a Titanium instance. '\"..tostring( self.object )..\"' is not a Titanium instance.\")\
5156 end\
5157\
5158 local easing = self.easing or \"linear\"\
5159 if type( easing ) == \"string\" then\
5160 self.easing = Tween.static.easing[ easing ] or error(\"Easing type '\"..tostring( easing )..\"' could not be found in 'Tween.static.easing'.\")\
5161 elseif type( easing ) == \"function\" then\
5162 self.easing = easing\
5163 else\
5164 return error \"Tween easing invalid. Must be a function to be invoked or name of easing type\"\
5165 end\
5166\
5167 self.initial = self.object[ self.property ]\
5168 self.clock = 0\
5169end\
5170\
5171--[[\
5172 @instance\
5173 @desc Sets the 'property' of 'object' to the rounded (down) result of the easing function selected. Passes the current clock time, the initial value, the difference between the initial and final values and the total Tween duration.\
5174]]\
5175function Tween:performEasing()\
5176 self.object[ self.property ] = math.floor( self.easing( self.clock, self.initial, self.final - self.initial, self.duration ) + .5 )\
5177end\
5178\
5179--[[\
5180 @instance\
5181 @desc Updates the tween by increasing 'clock' by 'dt' via the setter 'setClock'\
5182 @param <number - dt>\
5183 @return <boolean - finished>\
5184]]\
5185function Tween:update( dt )\
5186 return self:setClock( self.clock + dt )\
5187end\
5188\
5189--[[\
5190 @instance\
5191 @desc Sets the clock time to zero\
5192 @param <boolean - finished>\
5193]]\
5194function Tween:reset()\
5195 return self:setClock( 0 )\
5196end\
5197\
5198--[[\
5199 @instance\
5200 @desc Sets the current 'clock'. If the clock is a boundary number, it is adjusted to match the boundary - otherwise it is set as is. Once set, 'performEasing' is called and the state of the Tween (finished or not) is returned\
5201 @param <number - clock>\
5202 @return <boolean - finished>\
5203]]\
5204function Tween:setClock( clock )\
5205 if clock <= 0 then\
5206 self.clock = 0\
5207 elseif clock >= self.duration then\
5208 self.clock = self.duration\
5209 else\
5210 self.clock = clock\
5211 end\
5212\
5213 self:performEasing()\
5214 return self.clock >= self.duration\
5215end\
5216\
5217--[[\
5218 @static\
5219 @desc Binds the function 'easingFunction' to 'easingName'. Whenever an animation that uses easing of type 'easingName' is updated, this function will be called to calculate the value\
5220 @param <string - easingName>, <function - easingFunction>\
5221 @return <Class Base - Tween>\
5222]]\
5223function Tween.static.addEasing( easingName, easingFunction )\
5224 if type( easingFunction ) ~= \"function\" then\
5225 return error \"Easing function must be of type 'function'\"\
5226 end\
5227\
5228 Tween.static.easing[ easingName ] = easingFunction\
5229 return Tween\
5230end\
5231\
5232configureConstructor {\
5233 orderedArguments = { \"object\", \"name\", \"property\", \"final\", \"duration\", \"easing\", \"promise\" },\
5234 requiredArguments = { \"object\", \"name\", \"property\", \"final\", \"duration\" },\
5235 argumentTypes = {\
5236 name = \"string\",\
5237 property = \"string\",\
5238 final = \"number\",\
5239 duration = \"number\",\
5240 promise = \"function\"\
5241 }\
5242}\
5243",
5244["XMLLexer.ti"]="class \"XMLLexer\" extends \"Lexer\" {\
5245 openTag = false;\
5246 definingAttribute = false;\
5247 currentAttribute = false;\
5248}\
5249\
5250--[[\
5251 @instance\
5252 @desc Converts the stream into tokens by way of pattern matching\
5253]]\
5254function XMLLexer:tokenize()\
5255 self:trimStream()\
5256 local stream, openTag, currentAttribute, definingAttribute = self:trimStream(), self.openTag, self.currentAttribute, self.definingAttribute\
5257 local first = stream:sub( 1, 1 )\
5258\
5259 if stream:find \"^<(%w+)\" then\
5260 self:pushToken({type = \"XML_OPEN\", value = self:consumePattern \"^<(%w+)\"})\
5261 self.openTag = true\
5262 elseif stream:find \"^</(%w+)>\" then\
5263 self:pushToken({type = \"XML_END\", value = self:consumePattern \"^</(%w+)>\"})\
5264 self.openTag = false\
5265 elseif stream:find \"^/>\" then\
5266 self:pushToken({type = \"XML_END_CLOSE\"})\
5267 self:consume( 2 )\
5268 self.openTag = false\
5269 elseif openTag and stream:find \"^%w+\" then\
5270 self:pushToken({type = definingAttribute and \"XML_ATTRIBUTE_VALUE\" or \"XML_ATTRIBUTE\", value = self:consumePattern \"^%w+\"})\
5271\
5272 if not definingAttribute then\
5273 self.currentAttribute = true\
5274 return\
5275 end\
5276 elseif not openTag and stream:find \"^([^<]+)\" then\
5277 local content = self:consumePattern \"^([^<]+)\"\
5278\
5279 local newlines = select( 2, content:gsub(\"\\n\", \"\") )\
5280 if newlines then self:newline( newlines ) end\
5281\
5282 self:pushToken({type = \"XML_CONTENT\", value = content })\
5283 elseif first == \"=\" then\
5284 self:pushToken({type = \"XML_ASSIGNMENT\", value = \"=\"})\
5285 self:consume( 1 )\
5286\
5287 if currentAttribute then\
5288 self.definingAttribute = true\
5289 end\
5290\
5291 return\
5292 elseif first == \"'\" or first == \"\\\"\" then\
5293 self:pushToken({type = definingAttribute and \"XML_STRING_ATTRIBUTE_VALUE\" or \"XML_STRING\", value = self:consumeString( first )})\
5294 elseif first == \">\" then\
5295 self:pushToken({type = \"XML_CLOSE\"})\
5296 self.openTag = false\
5297 self:consume( 1 )\
5298 else\
5299 self:throw(\"Unexpected block '\"..stream:match(\"(.-)%s\")..\"'\")\
5300 end\
5301\
5302 if self.currentAttribute then self.currentAttribute = false end\
5303 if self.definingAttribute then self.definingAttribute = false end\
5304end\
5305",
5306["MTextDisplay.ti"]="--[[\
5307 This mixin \"is\" designed to be used by nodes that wish to display formatted text (e.g: Button, TextContainer).\
5308 The 'drawText' function should be called from the node during draw time.\
5309]]\
5310\
5311local string_len, string_find, string_sub, string_gsub, string_match = string.len, string.find, string.sub, string.gsub, string.match\
5312\
5313class \"MTextDisplay\" abstract() {\
5314 lineConfig = {\
5315 lines = false;\
5316 alignedLines = false;\
5317 offsetY = 0;\
5318 };\
5319\
5320 verticalPadding = 0;\
5321 horizontalPadding = 0;\
5322\
5323 verticalAlign = \"top\";\
5324 horizontalAlign = \"left\";\
5325}\
5326\
5327--[[\
5328 @constructor\
5329 @desc Registers properties used by this class \"with\" the theme handler if the object mixes in 'MThemeable'\
5330]]\
5331function MTextDisplay:MTextDisplay()\
5332 if Titanium.mixesIn( self, \"MThemeable\" ) then\
5333 self:register( \"text\", \"verticalAlign\", \"horizontalAlign\", \"verticalPadding\", \"horizontalPadding\" )\
5334 end\
5335end\
5336\
5337--[[\
5338 @instance\
5339 @desc Generates a table of text lines by wrapping on newlines or when the line gets too long.\
5340]]\
5341function MTextDisplay:wrapText()\
5342 local text, width, lines = self.text, self.width, {}\
5343\
5344 while text and string_len( text ) > 0 do\
5345 local section, pre, post = string_sub( text, 1, width )\
5346\
5347 if string_find( section, \"\\n\" ) then\
5348 pre, post = string_match( text, \"(.-)\\n%s*(.*)$\" )\
5349 elseif string_len( text ) <= width then\
5350 pre = text\
5351 else\
5352 local lastSpace = string_find( section, \"%s[%S]*$\" )\
5353\
5354 pre = lastSpace and string_gsub( string_sub( text, 1, lastSpace - 1 ), \"%s+$\", \"\" ) or section\
5355 post = lastSpace and string_sub( text, lastSpace + 1 ) or string_sub( text, width + 1 )\
5356 end\
5357\
5358 lines[ #lines + 1 ], text = pre, post\
5359 end\
5360\
5361 self.lineConfig.lines = lines\
5362end\
5363\
5364--[[\
5365 @instance\
5366 @desc Uses 'wrapText' to generate the information required to draw the text to the canvas correctly.\
5367]]\
5368function MTextDisplay:drawText( bg, tc )\
5369 local lines = self.lineConfig.lines\
5370 if not lines then\
5371 self:wrapText()\
5372 lines = self.lineConfig.lines\
5373 end\
5374\
5375 local vPadding, hPadding = self.verticalPadding, self.horizontalPadding\
5376\
5377 local yOffset, xOffset = vPadding, hPadding\
5378 local vAlign, hAlign = self.verticalAlign, self.horizontalAlign\
5379 local width, height = self.width, self.height\
5380\
5381 if vAlign == \"centre\" then\
5382 yOffset = math.floor( ( height / 2 ) - ( #lines / 2 ) + .5 ) + vPadding\
5383 elseif vAlign == \"bottom\" then\
5384 yOffset = height - #lines - vPadding\
5385 end\
5386\
5387 local canvas, line = self.canvas\
5388 for i = 1, #lines do\
5389 local line, xOffset = lines[ i ], hPadding\
5390 if hAlign == \"centre\" then\
5391 xOffset = math.floor( width / 2 - ( #line / 2 ) + .5 )\
5392 elseif hAlign == \"right\" then\
5393 xOffset = width - #line - hPadding + 1\
5394 end\
5395\
5396 canvas:drawTextLine( xOffset + 1, i + yOffset, line, tc, bg )\
5397 end\
5398end\
5399\
5400configureConstructor {\
5401 argumentTypes = {\
5402 verticalPadding = \"number\",\
5403 horizontalPadding = \"number\",\
5404\
5405 verticalAlign = \"string\",\
5406 horizontalAlign = \"string\",\
5407\
5408 text = \"string\"\
5409 }\
5410}\
5411",
5412["GenericEvent.ti"]="--[[\
5413 The GenericEvent class \"is\" spawned when an event that Titanium doesn't understand is caught in the Application event loop.\
5414\
5415 If you wish to spawn another sort of class \"when\" a certain event is caught, consider using `Event.static.bindEvent`.\
5416]]\
5417\
5418class \"GenericEvent\" extends \"Event\"\
5419\
5420--[[\
5421 @constructor\
5422 @desc Constructs the GenericEvent instance by storing all passed arguments in 'data'. The first index (1) of data is stored inside 'name'\
5423 @param <string - name>, [var - arg1], ...\
5424]]\
5425function GenericEvent:__init__( ... )\
5426 local args = { ... }\
5427\
5428 self.name = args[ 1 ]\
5429 self.main = self.name:upper()\
5430\
5431 self.data = args\
5432end\
5433",
5434["MPropertyManager.ti"]="--[[\
5435 Tracks property changes and invokes custom callbacks when they change.\
5436\
5437 Note: Only supports watching of arguments that have had their types set via `configure`.\
5438]]\
5439\
5440class \"MPropertyManager\" abstract() {\
5441 watching = {};\
5442 foreignWatchers = {};\
5443 links = {};\
5444 binds = {};\
5445}\
5446\
5447--[[\
5448 @constructor\
5449 @desc Hooks into all properties whose types have been defined. Un-hooked arguments cannot be watched.\
5450]]\
5451function MPropertyManager:MPropertyManager()\
5452 local properties = Titanium.getClass( self.__type ):getRegistry().constructor\
5453 if not ( properties or properties.argumentTypes ) then return end\
5454\
5455 for property in pairs( properties.argumentTypes ) do\
5456 local setterName = Titanium.getSetterName( property )\
5457 local oldSetter = self.raw[ setterName ]\
5458\
5459 self[ setterName ] = function( instance, value )\
5460 value = self:updateWatchers( property, value )\
5461\
5462 if oldSetter then\
5463 oldSetter( self, instance, value )\
5464 else\
5465 self[ property ] = value\
5466 end\
5467 end\
5468 end\
5469\
5470 -- Destroys local and foreign watcher instructions\
5471 self:on(\"remove\", function( instance )\
5472 self:unwatchForeignProperty \"*\"\
5473 self:unwatchProperty( \"*\", false, true )\
5474 end)\
5475end\
5476\
5477--[[\
5478 @instance\
5479 @desc Invokes the callback function of any watching links, passing the instance and value.\
5480 @param <string - property>, [var - value]\
5481 @return [var - value]\
5482]]\
5483function MPropertyManager:updateWatchers( property, value )\
5484 local function updateWatchers( prop )\
5485 local watchers = self.watching[ prop ]\
5486 if watchers then\
5487 for i = 1, #watchers do\
5488 local newVal = watchers[ i ][ 1 ]( self, prop, value )\
5489\
5490 if newVal ~= nil then\
5491 value = newVal\
5492 end\
5493 end\
5494 end\
5495 end\
5496\
5497 if property == \"*\" then\
5498 for prop in pairs( self.watching ) do updateWatchers( prop ) end\
5499 else\
5500 updateWatchers( property )\
5501 end\
5502\
5503 return value\
5504end\
5505\
5506--[[\
5507 @instance\
5508 @desc Adds a watch instruction on 'object' for 'property'. The instruction is logged in 'foreignWatchers' for future modification (ie: destruction)\
5509 @param <string - property>, <Instance - object>, <function - callback>, [string - name]\
5510]]\
5511function MPropertyManager:watchForeignProperty( property, object, callback, name )\
5512 if object == self then\
5513 return error \"Target object is not foreign. Select a foreign object or use :watchProperty\"\
5514 end\
5515\
5516 if not self.foreignWatchers[ property ] then self.foreignWatchers[ property ] = {} end\
5517 table.insert( self.foreignWatchers[ property ], object )\
5518\
5519 object:watchProperty( property, callback, name, self )\
5520end\
5521\
5522--[[\
5523 @instance\
5524 @desc Destroys the watch instruction for 'property'. If 'property' is '*', all property watchers are removed. If 'object' is given, only foreign links towards 'object' will be removed.\
5525 @param <string - property>, [Instance - object]\
5526]]\
5527function MPropertyManager:unwatchForeignProperty( property, object, name )\
5528 local function unwatchProp( prop )\
5529 local foreignWatchers = self.foreignWatchers[ prop ]\
5530\
5531 if foreignWatchers then\
5532 for i = #foreignWatchers, 1, -1 do\
5533 if not object or foreignWatchers[ i ] == object then\
5534 foreignWatchers[ i ]:unwatchProperty( prop, name, true )\
5535 table.remove( foreignWatchers, i )\
5536 end\
5537 end\
5538 end\
5539 end\
5540\
5541 if property == \"*\" then\
5542 for prop in pairs( self.foreignWatchers ) do unwatchProp( prop ) end\
5543 else\
5544 unwatchProp( property )\
5545 end\
5546end\
5547\
5548--[[\
5549 @instance\
5550 @desc Removes headless references of 'property' to foreign links for 'object'. Used when the foreign target (object) has severed connection and traces must be removed from the creator (self).\
5551 @param <string - property>, <string - object>\
5552]]\
5553function MPropertyManager:destroyForeignLink( property, object )\
5554 local watching = self.foreignWatchers[ property ]\
5555 if not watching then return end\
5556\
5557 for i = #watching, 1, -1 do\
5558 if watching[ i ] == object then\
5559 table.remove( watching, i )\
5560 end\
5561 end\
5562end\
5563\
5564--[[\
5565 @instance\
5566 @desc Instructs this object to call 'callback' when 'property' changes\
5567 @param <string - property>, <function - callback>, [string - name], [boolean - foreignOrigin]\
5568]]\
5569function MPropertyManager:watchProperty( property, callback, name, foreignOrigin )\
5570 if name then\
5571 self:unwatchProperty( property, name )\
5572 end\
5573\
5574 if not self.watching[ property ] then self.watching[ property ] = {} end\
5575 table.insert( self.watching[ property ], { callback, name, foreignOrigin } )\
5576end\
5577\
5578--[[\
5579 @instance\
5580 @desc Removes watch instructions for 'property'. If 'name' is given, only watch instructions with that name will be removed.\
5581 If 'foreign' is true, watch instructions marked as originating from a foreign source will also be removed - else, only local instructions will be removed.\
5582 If 'preserveForeign' and 'foreign' are true, foreign links will be removed, however they will NOT be disconnected from their origin\
5583 @param <string - property>, [string - name], [boolean - foreign], [boolean - preserveForeign]\
5584]]\
5585function MPropertyManager:unwatchProperty( property, name, foreign, preserveForeign )\
5586 local function unwatchProp( prop )\
5587 local watching = self.watching[ prop ]\
5588\
5589 if watching then\
5590 for i = #watching, 1, -1 do\
5591 if ( not name or watching[ i ][ 2 ] == name ) and ( foreign and watching[ i ][ 3 ] or ( not foreign and not watching[ i ][ 3 ] ) ) then\
5592 if foreign and not preserveForeign then\
5593 watching[ i ][ 3 ]:destroyForeignLink( prop, self )\
5594 end\
5595\
5596 table.remove( watching, i )\
5597 end\
5598 end\
5599 end\
5600 end\
5601\
5602 if property == \"*\" then\
5603 for prop in pairs( self.watching ) do unwatchProp( prop ) end\
5604 else\
5605 unwatchProp( property )\
5606 end\
5607end\
5608\
5609--[[\
5610 @instance\
5611 @desc Links properties given to 'target'. Properties can consist of tables or string values. If table, the first index represents the name of the foreign property to link to (belonging to 'target') and the second the local property to bind (belongs to 'self')\
5612 If the property is a string, the foreign property and local property match and a simple bind is produced\
5613 @param <Instance - target>, <var - properties>\
5614 @return <Instance - self>\
5615]]\
5616function MPropertyManager:linkProperties( target, ... )\
5617 local links = self.links\
5618 local function createLink( foreignProperty, localProperty )\
5619 localProperty = localProperty or foreignProperty\
5620\
5621 if self.links[ localProperty ] then\
5622 return error(\"Failed to link foreign property '\"..tostring(foreignProperty)..\"' from '\"..tostring(target)..\"' to local property '\"..tostring(localProperty)..\"'. A link already exists for this local property, remove that link before linking\")\
5623 end\
5624\
5625 self:watchForeignProperty( foreignProperty, target, function( _, __, value )\
5626 self[ localProperty ] = value\
5627 end, \"PROPERTY_LINK_\" .. self.__ID )\
5628\
5629 links[ localProperty ], self[ localProperty ] = target, target[ foreignProperty ]\
5630 end\
5631\
5632 local properties = { ... }\
5633 for i = 1, #properties do\
5634 local prop = properties[ i ]\
5635 if type( prop ) == \"table\" then createLink( prop[ 1 ], prop[ 2 ] ) else createLink( prop ) end\
5636 end\
5637\
5638 return self\
5639end\
5640\
5641--[[\
5642 @instance\
5643 @desc Creates a dynamic property link to self and all provided arguments. Can be removed using 'unlinkProperties'\
5644 @param <string - property>, <table - arguments>, <string - equation>\
5645]]\
5646function MPropertyManager:dynamicallyLinkProperty( property, arguments, equation )\
5647 if self.links[ property ] then\
5648 return error(\"Failed to create dynamic link for '\"..property..\"'. A link already exists for this property\")\
5649 elseif self.binds[ property ] then\
5650 return error(\"Lingering dynamic bind found for property '\"..property..\"'. Failed to bind property\")\
5651 end\
5652\
5653 self.links[ property ], self.binds[ property ] = true, DynamicValue( self, property, arguments, equation )\
5654end\
5655\
5656--[[\
5657 @instance\
5658 @desc Removes the property link for foreign properties ..., bound to 'target'. The properties provided represent the foreign property that is bound to, not the local property.\
5659 @param <Instance - target>, <... - foreignProperties>\
5660 @return <Instance - self>\
5661]]\
5662function MPropertyManager:unlinkProperties( target, ... )\
5663 local properties, links, binds = { ... }, self.links, self.binds\
5664 for i = 1, #properties do\
5665 local prop = properties[ i ]\
5666 if binds[ prop ] then\
5667 binds[ prop ]:detach()\
5668 binds[ prop ], links[ prop ] = nil, nil\
5669 else\
5670 self:unwatchForeignProperty( prop, target, \"PROPERTY_LINK_\" .. self.__ID )\
5671\
5672 if links[ prop ] == target then\
5673 links[ prop ] = nil\
5674 end\
5675 end\
5676 end\
5677\
5678 return self\
5679end\
5680",
5681["Event.ti"]="class \"Event\" abstract() {\
5682 static = {\
5683 matrix = {}\
5684 }\
5685}\
5686\
5687--[[\
5688 @instance\
5689 @desc Returns true if the event name (index '1' of data) matches the parameter 'event' provided\
5690 @param <string - event>\
5691 @return <boolean - eq>\
5692]]\
5693function Event:is( event )\
5694 return self.name == event\
5695end\
5696\
5697--[[\
5698 @instance\
5699 @desc Sets the 'handled' paramater to true. This indicates the event has been used and should not be used.\
5700]]\
5701function Event:setHandled( handled )\
5702 self.raw.handled = handled\
5703end\
5704\
5705--[[\
5706 @static\
5707 @desc Instantiates an event object if an entry for that event type is present inside the event matrix.\
5708 @param <string - eventName>, [vararg - eventData]\
5709 @return <Instance*>\
5710\
5711 *Note: The type of instance is variable. If an entry is present inside the matrix that class \"will\" be\
5712 instantiated, otherwise a 'GenericEvent' instance will be returned.\
5713]]\
5714function Event.static.spawn( name, ... )\
5715 return ( Event.matrix[ name ] or GenericEvent )( name, ... )\
5716end\
5717\
5718--[[\
5719 @static\
5720 @desc Adds an entry to the event matrix. When an event named 'name' is caught, the class 'clasType' will be instantiated\
5721 @param <string - name>, <string - classType>\
5722]]\
5723function Event.static.bindEvent( name, classType )\
5724 Event.matrix[ name ] = Titanium.getClass( classType ) or error( \"Class '\"..tostring( classType )..\"' cannot be found\" )\
5725end\
5726\
5727--[[\
5728 @static\
5729 @desc Removes an entry from the event matrix.\
5730 @param <string - name>\
5731]]\
5732function Event.static.unbindEvent( name )\
5733 Event.matrix[ name ] = nil\
5734end\
5735",
5736["DynamicValue.ti"]="class \"DynamicValue\" {\
5737 propertyValues = {};\
5738 properties = {};\
5739}\
5740\
5741--[[\
5742 @constructor\
5743 @desc Creates watcher instructions towards 'target' for each property linked\
5744 @param <Instance - target>, <string - property>, <table - properties>, <string - equation>\
5745]]\
5746function DynamicValue:__init__( ... )\
5747 self:resolve( ... )\
5748 self:attach()\
5749\
5750 local reg = Titanium.getClass( self.target.__type ):getRegistry().constructor\
5751 if reg and reg.argumentTypes then\
5752 self.type = reg.argumentTypes[ self.property ]\
5753 end\
5754\
5755 self:solve()\
5756end\
5757\
5758--[[\
5759 @instance\
5760 @desc Attach watch instructions to each required argument\
5761]]\
5762function DynamicValue:attach()\
5763 local properties = self.properties\
5764 for i = 1, #properties do\
5765 local obj, prop = properties[ i ][ 2 ], properties[ i ][ 1 ]\
5766\
5767 obj:watchProperty( prop, function( _, __, val )\
5768 self.propertyValues[ i ] = val\
5769 self:solve()\
5770 end, \"DYNAMIC_LINK_\" .. self.__ID )\
5771\
5772 self.propertyValues[ i ] = obj[ prop ]\
5773 end\
5774end\
5775\
5776--[[\
5777 @instance\
5778 @desc Detaches the watcher instructions towards the targets of the dynamic value\
5779]]\
5780function DynamicValue:detach()\
5781 local properties = self.properties\
5782 for i = 1, #properties do\
5783 local prop = properties[ i ]\
5784 prop[ 2 ]:unwatchProperty( prop[ 1 ], \"DYNAMIC_LINK_\" .. self.__ID )\
5785 end\
5786end\
5787\
5788--[[\
5789 @instance\
5790 @desc Solves the 'equation' by inserting the values fetched off of linked targets\
5791]]\
5792function DynamicValue:solve()\
5793 local fn, err = loadstring( self.equation )\
5794 if not fn then return error(\"Failed to solve dynamic value equation (\"..tostring( eq )..\"). Parse exception: \" .. tostring( err )) end\
5795\
5796 local ok, val = pcall( fn, self.propertyValues )\
5797 if not ok then return error(\"Failed to solve dyamic value equation (\"..tostring( eq )..\"). Runtime exception: \" .. tostring( val )) end\
5798\
5799 self.target[ self.property ] = XMLParser.convertArgType( val, self.type )\
5800end\
5801\
5802configureConstructor {\
5803 orderedArguments = { \"target\", \"property\", \"properties\", \"equation\" },\
5804 argumentTypes = {\
5805 property = \"string\",\
5806 properties = \"table\",\
5807 equation = \"string\"\
5808 },\
5809 requiredArguments = true\
5810}\
5811",
5812["MFocusable.ti"]="--[[\
5813 A focusable object is an object that after a mouse_click and a mouse_up event occur on the object is 'focused'.\
5814\
5815 An 'input' is a good example of a focusable node, it is activatable (while being clicked) but it also focusable (allows you to type after being focused).\
5816]]\
5817\
5818class \"MFocusable\" abstract() {\
5819 focused = false;\
5820}\
5821\
5822function MFocusable:MFocusable()\
5823 if Titanium.mixesIn( self, \"MThemeable\" ) then\
5824 self:register(\"focused\", \"focusedColour\", \"focusedBackgroundColour\")\
5825 end\
5826end\
5827\
5828function MFocusable:setEnabled( enabled )\
5829 self.super:setEnabled( enabled )\
5830\
5831 if not enabled and self.focused then\
5832 self:unfocus()\
5833 end\
5834end\
5835\
5836function MFocusable:setFocused( focused )\
5837 local raw = self.raw\
5838 if raw.focused == focused then return end\
5839\
5840 self.changed = true\
5841 self.focused = focused\
5842end\
5843\
5844function MFocusable:focus()\
5845 if not self.enabled then return end\
5846\
5847 if self.application then self.application:focusNode( self ) end\
5848 self.focused = true\
5849end\
5850\
5851function MFocusable:unfocus()\
5852 if self.application then self.application:unfocusNode( self ) end\
5853 self.focused = false\
5854end\
5855\
5856configureConstructor {\
5857 argumentTypes = {\
5858 focusedBackgroundColour = \"colour\",\
5859 focusedColour = \"colour\",\
5860 focused = \"boolean\"\
5861 }\
5862} alias {\
5863 focusedColor = \"focusedColour\",\
5864 focusedBackgroundColor = \"focusedBackgroundColour\"\
5865}\
5866",
5867["QueryLexer.ti"]="class \"QueryLexer\" extends \"Lexer\"\
5868\
5869function QueryLexer:tokenize()\
5870 if self.stream:find \"^%s\" and not self.inCondition then\
5871 self:pushToken { type = \"QUERY_SEPERATOR\" }\
5872 end\
5873\
5874 local stream = self:trimStream()\
5875\
5876 if self.inCondition then\
5877 self:tokenizeCondition( stream )\
5878 elseif stream:find \"^%b[]\" then\
5879 self:pushToken { type = \"QUERY_COND_OPEN\" }\
5880 self:consume( 1 )\
5881\
5882 self.inCondition = true\
5883 elseif stream:find \"^%,\" then\
5884 self:pushToken { type = \"QUERY_END\", value = self:consumePattern \"^%,\" }\
5885 elseif stream:find \"^>\" then\
5886 self:pushToken { type = \"QUERY_DIRECT_PREFIX\", value = self:consumePattern \"^>\" }\
5887 elseif stream:find \"^#[^%s%.#%[%,]*\" then\
5888 self:pushToken { type = \"QUERY_ID\", value = self:consumePattern \"^#([^%s%.#%[]*)\" }\
5889 elseif stream:find \"^%.[^%s#%[%,]*\" then\
5890 self:pushToken { type = \"QUERY_CLASS\", value = self:consumePattern \"^%.([^%s%.#%[]*)\" }\
5891 elseif stream:find \"^[^,%s#%.%[]*\" then\
5892 self:pushToken { type = \"QUERY_TYPE\", value = self:consumePattern \"^[^,%s#%.%[]*\" }\
5893 else\
5894 self:throw(\"Unexpected block '\"..stream:match(\"(.-)%s\")..\"'\")\
5895 end\
5896end\
5897\
5898function QueryLexer:tokenizeCondition( stream )\
5899 local first = stream:sub( 1, 1 )\
5900 if stream:find \"%b[]\" then\
5901 self:throw( \"Nested condition found '\"..tostring( stream:match \"%b[]\" )..\"'\" )\
5902 elseif stream:find \"^%b''\" or stream:find '^%b\"\"' then\
5903 local cnt = self:consumePattern( first == \"'\" and \"^%b''\" or '^%b\"\"' ):sub( 2, -2 )\
5904 if cnt:find \"%b''\" or cnt:find '%b\"\"' then\
5905 self:throw( \"Nested string found inside '\"..tostring( cnt )..\"'\" )\
5906 end\
5907\
5908 self:pushToken { type = \"QUERY_COND_STRING_ENTITY\", value = cnt }\
5909 elseif stream:find \"^%w+\" then\
5910 self:pushToken { type = \"QUERY_COND_ENTITY\", value = self:consumePattern \"^%w+\" }\
5911 elseif stream:find \"^%,\" then\
5912 self:pushToken { type = \"QUERY_COND_SEPERATOR\" }\
5913 self:consume( 1 )\
5914 elseif stream:find \"^[%p~]+\" then\
5915 self:pushToken { type = \"QUERY_COND_SYMBOL\", value = self:consumePattern \"^[%p~]+\" }\
5916 elseif stream:find \"^%]\" then\
5917 self:pushToken { type = \"QUERY_COND_CLOSE\" }\
5918 self:consume( 1 )\
5919 self.inCondition = false\
5920 else\
5921 self:throw(\"Invalid condition syntax. Expected property near '\"..tostring( stream:match \"%S*\" )..\"'\")\
5922 end\
5923end\
5924",
5925["MTogglable.ti"]="--[[\
5926 A small mixin \"to\" avoid rewriting code used by nodes that can be toggled on or off.\
5927]]\
5928\
5929class \"MTogglable\" abstract() {\
5930 toggled = false;\
5931\
5932 toggledColour = colours.red;\
5933 toggledBackgroundColour = colours.white;\
5934}\
5935\
5936--[[\
5937 @constructor\
5938 @desc Registers properties used by this class \"with\" the theme handler if the object mixes in 'MThemeable'\
5939]]\
5940function MTogglable:MTogglable()\
5941 if Titanium.mixesIn( self, \"MThemeable\" ) then\
5942 self:register(\"toggled\", \"toggledColour\", \"toggledBackgroundColour\")\
5943 end\
5944end\
5945\
5946--[[\
5947 @instance\
5948 @desc 'toggled' to the opposite of what it currently is (toggles)\
5949]]\
5950function MTogglable:toggle( ... )\
5951 self:setToggled( not self.toggled, ... )\
5952end\
5953\
5954--[[\
5955 @instance\
5956 @desc Sets toggled to 'toggled' and changed to 'true' when the 'toggled' param doesn't match the current value of toggled.\
5957 @param <boolean - toggled>, [vararg - onToggleArguments]\
5958]]\
5959function MTogglable:setToggled( toggled, ... )\
5960 if self.toggled ~= toggled then\
5961 self.raw.toggled = toggled\
5962 self.changed = true\
5963\
5964 self:executeCallbacks( \"toggle\", ... )\
5965 end\
5966end\
5967\
5968configureConstructor {\
5969 argumentTypes = {\
5970 toggled = \"boolean\",\
5971 toggledColour = \"colour\",\
5972 toggledBackgroundColour = \"colour\"\
5973 }\
5974} alias {\
5975 toggledColor = \"toggledColour\",\
5976 toggledBackgroundColor = \"toggledBackgroundColour\"\
5977}\
5978",
5979["TermCanvas.ti"]="--[[\
5980 The TermCanvas is an object that draws it's buffer directly to the ComputerCraft term object, unlike the NodeCanvas.\
5981\
5982 The TermCanvas should be used by high level objects, like 'Application'. Nodes should not be drawing directly to the term object.\
5983 If your object needs to draw to the canvas this class \"should\" be used.\
5984\
5985 Unlike NodeCanvas, TermCanvas has no drawing functions as it's purpose is not to generate the buffer, just draw it to the term object.\
5986 Nodes generate their content and store it in your buffer (and theirs aswell).\
5987--]]\
5988\
5989local hex = {}\
5990for i = 0, 15 do\
5991 hex[2 ^ i] = (\"%x\"):format( i ) -- %x = lowercase hexadecimal\
5992 hex[(\"%x\"):format( i )] = 2 ^ i\
5993end\
5994\
5995local tableConcat = table.concat\
5996\
5997class \"TermCanvas\" extends \"Canvas\" {\
5998 static = { hex = hex };\
5999}\
6000\
6001function TermCanvas:__init__( owner )\
6002 self:super( owner )\
6003\
6004 self.raw.X = owner.raw.X\
6005 self.raw.Y = owner.raw.Y\
6006end\
6007\
6008function TermCanvas:draw( force )\
6009 local owner = self.owner\
6010 local buffer, last = self.buffer, self.last\
6011 local X, Y, width, height = owner.X, owner.Y - 1, self.width, self.height\
6012 local colour, backgroundChar, backgroundTextColour, backgroundColour = self.colour, self.backgroundChar, self.backgroundTextColour, self.backgroundColour\
6013\
6014 local position, px, lpx = 1\
6015 for y = 1, height do\
6016 local changed\
6017\
6018 for x = 1, width do\
6019 px, lpx = buffer[ position ], last[ position ]\
6020\
6021 if force or not lpx or ( px[ 1 ] ~= lpx[ 1 ] or px[ 2 ] ~= lpx[ 2 ] or px[ 3 ] ~= lpx[ 3 ] ) then\
6022 changed = true\
6023\
6024 position = position - ( x - 1 )\
6025 break\
6026 end\
6027\
6028 position = position + 1\
6029 end\
6030\
6031 if changed then\
6032 local rowText, rowColour, rowBackground, pixel = {}, {}, {}\
6033\
6034 for x = 1, width do\
6035 pixel = buffer[ position ]\
6036 last[ position ] = pixel\
6037\
6038 local c, fg, bg = pixel[1], pixel[2], pixel[3]\
6039\
6040 rowColour[ x ] = hex[ type(fg) == \"number\" and fg ~= 0 and fg or colour or 1 ]\
6041 rowBackground[ x ] = hex[ type(bg) == \"number\" and bg ~= 0 and bg or backgroundColour or 32768 ]\
6042 if c then\
6043 rowText[ x ] = c or backgroundChar or \" \"\
6044 else\
6045 rowText[ x ] = backgroundChar or \" \"\
6046 rowColour[ x ] = hex[ backgroundTextColour or 1 ]\
6047 end\
6048\
6049 position = position + 1\
6050 end\
6051\
6052 term.setCursorPos( X, y + Y )\
6053 term.blit( tableConcat( rowText ), tableConcat( rowColour ), tableConcat( rowBackground ) )\
6054 end\
6055 end\
6056end\
6057",
6058["MCallbackManager.ti"]="--[[\
6059 The callback manager is a mixin \"that\" can be used by classes that want to provide an easy way for a developer to assign actions on certain conditions.\
6060\
6061 These conditions may include node specific callbacks, like a button click or input submission.\
6062]]\
6063\
6064class \"MCallbackManager\" abstract() {\
6065 callbacks = {}\
6066}\
6067\
6068--[[\
6069 @instance\
6070 @desc Assigns a function 'fn' to 'callbackName'.\
6071 @param <string - name>, <function - fn>, [string - id]\
6072]]\
6073function MCallbackManager:on( callbackName, fn, id )\
6074 if not ( type( callbackName ) == \"string\" and type( fn ) == \"function\" ) or ( id and type( id ) ~= \"string\" ) then\
6075 return error \"Expected string, function, [string]\"\
6076 end\
6077\
6078 local callbacks = self.callbacks\
6079 if not callbacks[ callbackName ] then callbacks[ callbackName ] = {} end\
6080\
6081 table.insert( callbacks[ callbackName ], { fn, id } )\
6082\
6083 return self\
6084end\
6085\
6086--[[\
6087 @instance\
6088 @desc Removes all callbacks for a certain condition. If an id is provided only callbacks matching that id will be executed.\
6089 @param <string - callbackName>, [string - id]\
6090]]\
6091function MCallbackManager:off( callbackName, id )\
6092 if id then\
6093 local callbacks = self.callbacks[ callbackName ]\
6094\
6095 if callbacks then\
6096 for i = #callbacks, 1, -1 do\
6097 if callbacks[ i ][ 2 ] == id then\
6098 table.remove( callbacks, i )\
6099 end\
6100 end\
6101 end\
6102 else self.callbacks[ callbackName ] = nil end\
6103\
6104 return self\
6105end\
6106\
6107--[[\
6108 @instance\
6109 @desc Executes all assigned functions for 'callbackName' with 'self' and the arguments passed to this function.\
6110 @param <string - callbackName>, [vararg - ...]\
6111]]\
6112function MCallbackManager:executeCallbacks( callbackName, ... )\
6113 local callbacks = self.callbacks[ callbackName ]\
6114\
6115 if callbacks then\
6116 for i = 1, #callbacks do callbacks[ i ][ 1 ]( self, ... ) end\
6117 end\
6118end\
6119\
6120function MCallbackManager:canCallback( name )\
6121 return #self.callbacks[ name ] > 0\
6122end\
6123",
6124["MActivatable.ti"]="--[[\
6125 A mixin \"to\" reuse code commonly written when developing nodes that can be (de)activated.\
6126]]\
6127\
6128class \"MActivatable\" abstract() {\
6129 active = false;\
6130\
6131 activeColour = colours.white;\
6132 activeBackgroundColour = colours.cyan;\
6133}\
6134\
6135--[[\
6136 @constructor\
6137 @desc Registers properties used by this class \"with\" the theme handler if the object mixes in 'MThemeable'\
6138]]\
6139function MActivatable:MActivatable()\
6140 if Titanium.mixesIn( self, \"MThemeable\" ) then\
6141 self:register( \"active\", \"activeColour\", \"activeBackgroundColour\" )\
6142 end\
6143end\
6144\
6145--[[\
6146 @instance\
6147 @desc Sets the 'active' property to the 'active' argument passed. When the 'active' property changes the node will become 'changed'.\
6148 @param <boolean - active>\
6149]]\
6150function MActivatable:setActive( active )\
6151 local raw = self.raw\
6152 if raw.active == active then return end\
6153\
6154 raw.active = active\
6155 self:queueAreaReset()\
6156end\
6157\
6158configureConstructor {\
6159 argumentTypes = { active = \"boolean\", activeColour = \"colour\", activeBackgroundColour = \"colour\" }\
6160} alias {\
6161 activeColor = \"activeColour\",\
6162 activeBackgroundColor = \"activeBackgroundColour\"\
6163}\
6164",
6165["Lexer.ti"]="class \"Lexer\" abstract() {\
6166 static = {\
6167 escapeChars = {\
6168 a = \"\\a\",\
6169 b = \"\\b\",\
6170 f = \"\\f\",\
6171 n = \"\\n\",\
6172 r = \"\\r\",\
6173 t = \"\\t\",\
6174 v = \"\\v\"\
6175 }\
6176 };\
6177\
6178 stream = false;\
6179\
6180 tokens = {};\
6181\
6182 line = 1;\
6183 char = 1;\
6184}\
6185\
6186--[[\
6187 @constructor\
6188 @desc Constructs the Lexer instance by providing the instance with a 'stream'.\
6189 @param <string - stream>, [boolean - manual]\
6190]]\
6191function Lexer:__init__( stream, manual )\
6192 if type( stream ) ~= \"string\" then\
6193 return error \"Failed to initialise Lexer instance. Invalid stream paramater passed (expected string)\"\
6194 end\
6195 self.stream = stream\
6196\
6197 if not manual then\
6198 self:formTokens()\
6199 end\
6200end\
6201\
6202--[[\
6203 @instance\
6204 @desc This function is used to repeatedly call 'tokenize' until the stream has been completely consumed.\
6205]]\
6206function Lexer:formTokens()\
6207 while self.stream and self.stream:find \"%S\" do\
6208 self:tokenize()\
6209 end\
6210end\
6211\
6212--[[\
6213 @instance\
6214 @desc A simple function that is used to add a token to the instances 'tokens' table.\
6215 @param <table - token>\
6216]]\
6217function Lexer:pushToken( token )\
6218 local tokens = self.tokens\
6219\
6220 token.char = self.char\
6221 token.line = self.line\
6222 tokens[ #tokens + 1 ] = token\
6223end\
6224\
6225--[[\
6226 @instance\
6227 @desc Consumes the stream by 'amount'.\
6228]]\
6229function Lexer:consume( amount )\
6230 local stream = self.stream\
6231 self.stream = stream:sub( amount + 1 )\
6232\
6233 self.char = self.char + amount\
6234 return content\
6235end\
6236\
6237--[[\
6238 @instance\
6239 @desc Uses the Lua pattern provided to select text from the stream that matches the pattern. The text is then consumed from the stream (entire pattern, not just selected text)\
6240 @param <string - pattern>, [number - offset]\
6241]]\
6242function Lexer:consumePattern( pattern, offset )\
6243 local cnt = self.stream:match( pattern )\
6244\
6245 self:consume( select( 2, self.stream:find( pattern ) ) + ( offset or 0 ) )\
6246 return cnt\
6247end\
6248\
6249--[[\
6250 @instance\
6251 @desc Searches for the next occurence of 'opener'. Once found all text between the first two occurences is selected and consumed resulting in a XML_STRING token.\
6252 @param <char - opener>\
6253 @return <string - consumedString>\
6254]]\
6255function Lexer:consumeString( opener )\
6256 local stream, closingIndex = self.stream\
6257\
6258 if stream:find( opener, 2 ) then\
6259 local str, c, escaped = {}\
6260 for i = 2, #stream do\
6261 c = stream:sub( i, i )\
6262\
6263 if escaped then\
6264 str[ #str + 1 ] = Lexer.escapeChars[ c ] or c\
6265 escaped = false\
6266 elseif c == \"\\\\\" then\
6267 escaped = true\
6268 elseif c == opener then\
6269 self:consume( i )\
6270 return table.concat( str )\
6271 else\
6272 str[ #str + 1 ] = c\
6273 end\
6274 end\
6275 end\
6276\
6277 self:throw( \"Failed to lex stream. Expected string end (\"..opener..\")\" )\
6278end\
6279\
6280--[[\
6281 @instance\
6282 @desc Removes all trailing spaces from\
6283]]\
6284function Lexer:trimStream()\
6285 local stream = self.stream\
6286\
6287 local newLn = stream:match(\"^(\\n+)\")\
6288 if newLn then self:newline( #newLn ) end\
6289\
6290 local spaces = select( 2, stream:find \"^%s*%S\" )\
6291\
6292 self.stream = stream:sub( spaces )\
6293 self.char = self.char + spaces - 1\
6294\
6295 return self.stream\
6296end\
6297\
6298--[[\
6299 @instance\
6300 @desc Advanced 'line' by 'amount' (or 1) and sets 'char' back to zero\
6301]]\
6302function Lexer:newline( amount )\
6303 self.line = self.line + ( amount or 1 )\
6304 self.char = 0\
6305end\
6306\
6307--[[\
6308 @instance\
6309 @desc Throws error 'e' prefixed with information regarding current position and stores the error in 'exception' for later reference\
6310]]\
6311function Lexer:throw( e )\
6312 self.exception = \"Lexer (\" .. tostring( self.__type ) .. \") Exception at line '\"..self.line..\"', char '\"..self.char..\"': \"..e\
6313 return error( self.exception )\
6314end\
6315",
6316["ScrollContainer.ti"]="class \"ScrollContainer\" extends \"Container\" {\
6317 cache = {};\
6318\
6319 xScroll = 0;\
6320 yScroll = 0;\
6321\
6322 xScrollAllowed = true;\
6323 yScrollAllowed = true;\
6324 propagateMouse = true;\
6325\
6326 trayColour = 256;\
6327 scrollbarColour = 128;\
6328 activeScrollbarColour = colours.cyan;\
6329\
6330 mouse = {\
6331 selected = false;\
6332 origin = false;\
6333 };\
6334}\
6335\
6336function ScrollContainer:__init__( ... )\
6337 self:register( \"scrollbarColour\", \"activeScrollbarColour\", \"trayColour\" )\
6338 self:super( ... )\
6339end\
6340\
6341--[[ Event Listeners ]]--\
6342function ScrollContainer:onMouseClick( event, handled, within )\
6343 if handled or not within then return end\
6344\
6345 local cache, mouse, key = self.cache, self.mouse\
6346 local X, Y = event.X - self.X + 1, event.Y - self.Y + 1\
6347\
6348 if cache.yScrollActive and X == self.width and Y <= cache.displayHeight then\
6349 key = \"y\"\
6350 elseif cache.xScrollActive and Y == self.height and X <= cache.displayWidth then\
6351 key = \"x\"\
6352 else return end\
6353\
6354 local scrollFn = self[ \"set\"..key:upper()..\"Scroll\" ]\
6355 local edge, size = cache[ key .. \"ScrollPosition\" ], cache[ key .. \"ScrollSize\" ]\
6356 local cScale, dScale = cache[ \"content\" .. ( key == \"x\" and \"Width\" or \"Height\" ) ], cache[ \"display\" .. ( key == \"x\" and \"Width\" or \"Height\" ) ]\
6357\
6358 local rel = key == \"x\" and X or Y\
6359 if rel < edge then\
6360 event.handled = scrollFn( self, math.floor( cScale * ( rel / dScale ) - .5 ) )\
6361 elseif rel >= edge and rel <= edge + size - 1 then\
6362 mouse.selected, mouse.origin = key == \"x\" and \"h\" or \"v\", rel - edge + 1\
6363 elseif rel > edge + size - 1 then\
6364 event.handled = scrollFn( self, math.floor( cScale * ( ( rel - size + 1 ) / dScale ) - .5 ) )\
6365 end\
6366\
6367 self:cacheScrollbarPosition()\
6368 self.changed = true\
6369end\
6370\
6371function ScrollContainer:onMouseScroll( event, handled, within )\
6372 local cache, app = self.cache, self.application\
6373 if handled or not within or not ( cache.xScrollActive or cache.yScrollActive ) then return end\
6374\
6375 local isXScroll = ( cache.xScrollActive and ( not cache.yScrollActive or ( app:isPressed( keys.leftShift ) or app:isPressed( keys.rightShift ) ) ) )\
6376\
6377 event.handled = self[\"set\".. ( isXScroll and \"X\" or \"Y\" ) ..\"Scroll\"]( self, self[ ( isXScroll and \"x\" or \"y\" ) .. \"Scroll\" ] + event.button )\
6378 self:cacheScrollbarPosition()\
6379end\
6380\
6381function ScrollContainer:onMouseUp( event, handled, within )\
6382 if self.mouse.selected then\
6383 self.mouse.selected = false\
6384 self.changed = true\
6385 end\
6386end\
6387\
6388function ScrollContainer:onMouseDrag( event, handled, within )\
6389 local mouse, cache = self.mouse, self.cache\
6390 if handled or not mouse.selected then return end\
6391\
6392 local isV = mouse.selected == \"v\"\
6393 local key = isV and \"Y\" or \"X\"\
6394 local scaleKey = isV and \"Height\" or \"Width\"\
6395\
6396 event.handled = self[ \"set\" .. key .. \"Scroll\" ]( self, math.floor( cache[\"content\" .. scaleKey ] * ( ( ( event[ key ] - self[ key ] + 1 ) - mouse.origin ) / cache[\"display\" .. scaleKey ] ) - .5 ) )\
6397end\
6398\
6399function ScrollContainer:addNode( node, ... )\
6400 self.super:addNode( node, ... )\
6401\
6402 self:cacheContent()\
6403\
6404 return node\
6405end\
6406\
6407--[[ Core Functions ]]--\
6408function ScrollContainer:handle( eventObj )\
6409 local cache, isWithin = self.cache, eventObj.isWithin\
6410 local cloneEv\
6411\
6412 if eventObj.main == \"MOUSE\" then\
6413 eventObj.isWithin = eventObj:withinParent( self )\
6414 if ( not cache.yScrollActive or ( eventObj.X - self.X + 1 ) ~= self.width ) and ( not cache.xScrollActive or ( eventObj.Y - self.Y + 1 ) ~= self.height ) then\
6415 cloneEv = eventObj:clone( self )\
6416 cloneEv.Y = cloneEv.Y + self.yScroll\
6417 cloneEv.X = cloneEv.X + self.xScroll\
6418 end\
6419 else cloneEv = eventObj end\
6420\
6421 if cloneEv then self:shipEvent( cloneEv ) end\
6422 local r = self.super.super:handle( eventObj )\
6423\
6424 eventObj.isWithin = isWithin\
6425 return r == nil and true or r\
6426end\
6427\
6428function ScrollContainer:isNodeInBounds( node, width, height )\
6429 local left, top = node.X - self.xScroll, node.Y - self.yScroll\
6430\
6431 return not ( ( left + node.width ) < 1 or left > ( width or self.width ) or top > ( height or self.height ) or ( top + node.height ) < 1 )\
6432end\
6433\
6434function ScrollContainer:draw( force )\
6435 if self.changed or force then\
6436 self.super:draw( force, -self.xScroll, -self.yScroll )\
6437 self:drawScrollbars()\
6438 end\
6439end\
6440\
6441function ScrollContainer:drawScrollbars()\
6442 local cache, canvas = self.cache, self.canvas\
6443 local xEnabled, yEnabled = cache.xScrollActive, cache.yScrollActive\
6444\
6445 if xEnabled then\
6446 canvas:drawBox( 1, self.height, cache.displayWidth, 1, self.trayColour )\
6447 canvas:drawBox( cache.xScrollPosition, self.height, cache.xScrollSize, 1, self.mouse.selected == \"h\" and self.activeScrollbarColour or self.scrollbarColour )\
6448 end\
6449\
6450 if yEnabled then\
6451 canvas:drawBox( self.width, 1, 1, cache.displayHeight, self.trayColour )\
6452 canvas:drawBox( self.width, cache.yScrollPosition, 1, cache.yScrollSize, self.mouse.selected == \"v\" and self.activeScrollbarColour or self.scrollbarColour )\
6453 end\
6454\
6455 if yEnabled and xEnabled then\
6456 canvas:drawPoint( self.width, self.height, \" \", 1, self.trayColour )\
6457 end\
6458end\
6459\
6460function ScrollContainer:redrawArea( x, y, width, height )\
6461 self.super:redrawArea( x, y, width, height, -self.xScroll, -self.yScroll )\
6462end\
6463\
6464function ScrollContainer:setYScroll( yScroll )\
6465 local oY, cache = self.yScroll, self.cache\
6466 self.yScroll = math.max( 0, math.min( cache.contentHeight - cache.displayHeight, yScroll ) )\
6467\
6468 self:cacheScrollbarPosition()\
6469 if ( not self.propagateMouse ) or oY ~= self.yScroll then\
6470 return true\
6471 end\
6472end\
6473\
6474function ScrollContainer:setXScroll( xScroll )\
6475 local oX, cache = self.xScroll, self.cache\
6476 self.xScroll = math.max( 0, math.min( cache.contentWidth - cache.displayWidth, xScroll ) )\
6477\
6478 self:cacheScrollbarPosition()\
6479 if ( not self.propagateMouse ) or oX ~= self.xScroll then\
6480 return true\
6481 end\
6482end\
6483\
6484function ScrollContainer:setHeight( height )\
6485 self.super:setHeight( height )\
6486 self:cacheContent()\
6487end\
6488\
6489function ScrollContainer:setWidth( width )\
6490 self.super:setWidth( width )\
6491 self:cacheContent()\
6492end\
6493\
6494--[[ Caching Functions ]]--\
6495function ScrollContainer:cacheContent()\
6496 self:cacheContentSize()\
6497 self:cacheActiveScrollbars()\
6498end\
6499\
6500function ScrollContainer:cacheContentSize()\
6501 local w, h = 0, 0\
6502\
6503 local nodes, node = self.nodes\
6504 for i = 1, #nodes do\
6505 node = nodes[ i ]\
6506\
6507 w = math.max( node.X + node.width - 1, w )\
6508 h = math.max( node.Y + node.height - 1, h )\
6509 end\
6510\
6511 self.cache.contentWidth, self.cache.contentHeight = w, h\
6512end\
6513\
6514function ScrollContainer:cacheDisplaySize()\
6515 local cache = self.cache\
6516 cache.displayWidth, cache.displayHeight = self.width - ( cache.yScrollActive and 1 or 0 ), self.height - ( cache.xScrollActive and 1 or 0 )\
6517\
6518 self:cacheScrollbarSize()\
6519end\
6520\
6521function ScrollContainer:cacheActiveScrollbars()\
6522 local cache = self.cache\
6523 local cWidth, cHeight, sWidth, sHeight = cache.contentWidth, cache.contentHeight, self.width, self.height\
6524 local xAllowed, yAllowed = self.xScrollAllowed, self.yScrollAllowed\
6525\
6526 local horizontal, vertical\
6527 if ( cWidth > sWidth and xAllowed ) or ( cHeight > sHeight and yAllowed ) then\
6528 cache.xScrollActive, cache.yScrollActive = cWidth > sWidth - 1 and xAllowed, cHeight > sHeight - 1 and yAllowed\
6529 else\
6530 cache.xScrollActive, cache.yScrollActive = false, false\
6531 end\
6532\
6533 self:cacheDisplaySize()\
6534end\
6535\
6536function ScrollContainer:cacheScrollbarSize()\
6537 local cache = self.cache\
6538 cache.xScrollSize, cache.yScrollSize = math.floor( cache.displayWidth * ( cache.displayWidth / cache.contentWidth ) + .5 ), math.floor( cache.displayHeight * ( cache.displayHeight / cache.contentHeight ) + .5 )\
6539\
6540 self:cacheScrollbarPosition()\
6541end\
6542\
6543function ScrollContainer:cacheScrollbarPosition()\
6544 local cache = self.cache\
6545 cache.xScrollPosition, cache.yScrollPosition = math.ceil( self.xScroll / cache.contentWidth * cache.displayWidth + .5 ), math.ceil( self.yScroll / cache.contentHeight * cache.displayHeight + .5 )\
6546\
6547 self.changed = true\
6548 self:redrawArea( 1, 1, self.width, self.height )\
6549end\
6550\
6551configureConstructor {\
6552 argumentTypes = {\
6553 scrollbarColour = \"colour\",\
6554 activeScrollbarColour = \"colour\",\
6555 xScrollAllowed = \"boolean\",\
6556 yScrollAllowed = \"boolean\"\
6557 }\
6558}\
6559",
6560["Node.ti"]="--[[\
6561 A Node is an object which makes up the applications graphical user interface (GUI).\
6562\
6563 Objects such as labels, buttons and text inputs are nodes.\
6564--]]\
6565\
6566class \"Node\" abstract() extends \"Component\" mixin \"MThemeable\" mixin \"MCallbackManager\" {\
6567 static = {\
6568 eventMatrix = {\
6569 mouse_click = \"onMouseClick\",\
6570 mouse_drag = \"onMouseDrag\",\
6571 mouse_up = \"onMouseUp\",\
6572 mouse_scroll = \"onMouseScroll\",\
6573\
6574 key = \"onKeyDown\",\
6575 key_up = \"onKeyUp\",\
6576 char = \"onChar\"\
6577 },\
6578 anyMatrix = {\
6579 MOUSE = \"onMouse\",\
6580 KEY = \"onKey\"\
6581 }\
6582 };\
6583\
6584 disabledColour = 128;\
6585 disabledBackgroundColour = 256;\
6586\
6587 allowMouse = false;\
6588 allowKey = false;\
6589 allowChar = false;\
6590 useAnyCallbacks = false;\
6591\
6592 enabled = true;\
6593 parentEnabled = true;\
6594\
6595 visible = true;\
6596\
6597 needsRedraw = true;\
6598 parent = false;\
6599}\
6600\
6601--[[\
6602 @constructor\
6603 @desc Creates a NodeCanvas (bound to self) and stores it inside of `self.canvas`. This canvas is drawn to the parents canvas at draw time.\
6604]]\
6605function Node:__init__()\
6606 self:register( \"X\", \"Y\", \"colour\", \"backgroundColour\", \"enabled\", \"visible\", \"disabledColour\", \"disabledBackgroundColour\" )\
6607\
6608 if not self.canvas then self.raw.canvas = NodeCanvas( self ) end\
6609end\
6610\
6611--[[\
6612 @constructor\
6613 @desc Finishes construction by hooking the theme manager into the node.\
6614]]\
6615function Node:__postInit__()\
6616 self:hook()\
6617end\
6618\
6619function Node:setParentEnabled( enabled )\
6620 self.parentEnabled = enabled\
6621 self.changed = true\
6622end\
6623\
6624function Node:setNeedsRedraw( needsRedraw )\
6625 self.needsRedraw = needsRedraw\
6626\
6627 if needsRedraw and self.parent then self.parent.needsRedraw = needsRedraw end\
6628end\
6629\
6630--[[\
6631 @instance\
6632 @desc Sets the enabled property of the node to 'enabled'. Sets node's 'changed' to true.\
6633 @param <boolean - enabled>\
6634]]\
6635function Node:setEnabled( enabled )\
6636 self.enabled = enabled\
6637 self.changed = true\
6638end\
6639\
6640--[[\
6641 TODO\
6642]]\
6643function Node:getEnabled()\
6644 if not self.parentEnabled then\
6645 return false\
6646 end\
6647\
6648 return self.enabled\
6649end\
6650\
6651--[[\
6652 TODO\
6653]]\
6654function Node:setParent( parent )\
6655 self.parent = parent\
6656 self.changed = true\
6657\
6658 if parent then\
6659 self.parentEnabled = Titanium.typeOf( parent, \"Application\" ) or parent.enabled\
6660 end\
6661end\
6662\
6663--[[\
6664 @instance\
6665 @desc Sets the node to visible/invisible depending on 'visible' paramater\
6666 @param <boolean - visible>\
6667]]\
6668function Node:setVisible( visible )\
6669 self.visible = visible\
6670 self.changed = true\
6671 if not visible then\
6672 self:queueAreaReset()\
6673 end\
6674end\
6675\
6676--[[\
6677 @instance\
6678 @desc Sets the changed state of this node to 'changed'. If 'changed' then the parents of this node will also have changed set to true.\
6679 @param <boolean - changed>\
6680]]\
6681function Node:setChanged( changed )\
6682 self.changed = changed\
6683\
6684 if changed then\
6685 local parent = self.parent\
6686 if parent and not parent.changed then\
6687 parent.changed = true\
6688 end\
6689\
6690 self.needsRedraw = true\
6691 end\
6692end\
6693\
6694--[[\
6695 @instance\
6696 @desc Handles events by triggering methods on the node depending on the event object passed\
6697 @param <Event Instance* - eventObj>\
6698 @return <boolean - propagate>\
6699\
6700 *Note: The event instance passed can be of variable type, ideally it extends 'Event' so that required methods are implemented on the eventObj.\
6701]]\
6702function Node:handle( eventObj )\
6703 if not self.enabled then return false end\
6704\
6705 local main, sub, within = eventObj.main, eventObj.sub, false\
6706 local handled = eventObj.handled\
6707\
6708 if main == \"MOUSE\" then\
6709 if self.allowMouse then\
6710 within = eventObj.isWithin and eventObj:withinParent( self ) or false\
6711 else return end\
6712 elseif ( main == \"KEY\" and not self.allowKey ) or ( main == \"CHAR\" and not self.allowChar ) then\
6713 return\
6714 end\
6715\
6716 local fn = Node.eventMatrix[ eventObj.name ] or \"onEvent\"\
6717 if self:can( fn ) then\
6718 self[ fn ]( self, eventObj, handled, within )\
6719 end\
6720\
6721 if self.useAnyCallbacks then\
6722 local anyFn = Node.anyMatrix[ main ]\
6723 if self:can( anyFn ) then\
6724 self[ anyFn ]( self, eventObj, handled, within )\
6725 end\
6726 end\
6727\
6728 return true\
6729end\
6730\
6731--[[\
6732 @instance\
6733 @desc Returns the absolute X, Y position of a node rather than its position relative to it's parent.\
6734 @return <number - X>, <number - Y>\
6735]]\
6736function Node:getAbsolutePosition()\
6737 local parent, application = self.parent, self.application\
6738 if parent then\
6739 if parent == application then\
6740 return -1 + application.X + self.X, -1 + application.Y + self.Y\
6741 end\
6742\
6743 local pX, pY = self.parent:getAbsolutePosition()\
6744 return -1 + pX + self.X, -1 + pY + self.Y\
6745 else return self.X, self.Y end\
6746end\
6747\
6748function Node:animate( ... )\
6749 if not self.application then return end\
6750\
6751 return self.application:addAnimation( Tween( self, ... ) )\
6752end\
6753\
6754configureConstructor {\
6755 argumentTypes = { enabled = \"boolean\", visible = \"boolean\", disabledColour = \"colour\", disabledBackgroundColour = \"colour\" }\
6756}\
6757",
6758["NodeCanvas.ti"]="--[[\
6759 The NodeCanvas is an object that allows classes to draw to their canvas using functions that are useful when drawing 'nodes', hence the name.\
6760\
6761 The NodeCanvas is most suited to these use cases:\
6762 - You would like to utilise helpful functions that making drawing nodes easier.\
6763 - Your object is not high level. High level objects use to TermCanvas which draws to the terminal, the NodeCanvas draws to a parent canvas\
6764--]]\
6765\
6766local string_sub = string.sub\
6767\
6768class \"NodeCanvas\" extends \"Canvas\"\
6769\
6770function NodeCanvas:drawPoint( x, y, char, tc, bg )\
6771 if #char > 1 then return error \"drawPoint can only draw one character\" end\
6772\
6773 self.buffer[ ( self.width * ( y - 1 ) ) + x ] = { char, tc or self.colour, bg or self.backgroundColour }\
6774end\
6775\
6776function NodeCanvas:drawTextLine( x, y, text, tc, bg )\
6777 local tc, bg = tc or self.colour, bg or self.backgroundColour\
6778\
6779 local buffer, start = self.buffer, ( self.width * ( y - 1 ) ) + x\
6780 for i = 1, #text do\
6781 buffer[ -1 + start + i ] = { string_sub( text, i, i ), tc, bg }\
6782 end\
6783end\
6784\
6785function NodeCanvas:drawBox( x, y, width, height, col )\
6786 local tc, bg = self.colour, col or self.backgroundColour\
6787 local buffer = self.buffer\
6788\
6789 local px = { \" \", tc, bg }\
6790 for y = math.max( 0, y ), y + height - 1 do\
6791 for x = math.max( 1, x ), x + width - 1 do\
6792 buffer[ ( self.width * ( y - 1 ) ) + x ] = px\
6793 end\
6794 end\
6795end\
6796",
6797["Label.ti"]="--[[\
6798 A Label is a node which displays a single line of text. The text cannot be changed by the user directly, however the text can be changed by the program.\
6799--]]\
6800\
6801class \"Label\" extends \"Node\" {\
6802 labelFor = false;\
6803\
6804 allowMouse = true;\
6805 active = false;\
6806}\
6807\
6808--[[\
6809 @constructor\
6810 @param <string - text>, [number - X], [number - Y]\
6811]]\
6812function Label:__init__( ... )\
6813 self:resolve( ... )\
6814 self.raw.width = #self.text\
6815\
6816 self:super()\
6817 self:register \"text\"\
6818end\
6819\
6820--[[\
6821 @instance\
6822 @desc Mouse click event handler. On click the label will wait for a mouse up, if found labelFor is notified\
6823 @param <MouseEvent Instance - event>, <boolean - handled>, <boolean - within>\
6824]]\
6825function Label:onMouseClick( event, handled, within )\
6826 self.active = self.labelFor and within and not handled\
6827end\
6828\
6829--[[\
6830 @instance\
6831 @desc If the mouse click handler has set the label to active, trigger the onLabelClicked callback\
6832 @param <MouseEvent Instance - event>, <boolean - handled>, <boolean - within>\
6833]]\
6834function Label:onMouseUp( event, handled, within )\
6835 if not self.labelFor then return end\
6836\
6837 local labelFor = self.application:getNode( self.labelFor, true )\
6838 if self.active and not handled and within and labelFor:can \"onLabelClicked\" then\
6839 labelFor:onLabelClicked( self, event, handled, within )\
6840 end\
6841\
6842 self.active = false\
6843end\
6844\
6845--[[\
6846 @instance\
6847 @desc Clears the Label's canvas and draws a line of text if the label has changed.\
6848 @param [boolean - force]\
6849]]\
6850function Label:draw( force )\
6851 local raw = self.raw\
6852 if raw.changed or force then\
6853 raw.canvas:drawTextLine( 1, 1, raw.text )\
6854\
6855 raw.changed = false\
6856 end\
6857end\
6858\
6859--[[\
6860 @instance\
6861 @desc Sets the text of a node. Once set, the nodes 'changed' status is set to true along with its parent(s)\
6862 @param <string - text>\
6863]]\
6864function Label:setText( text )\
6865 if self.text == text then return end\
6866\
6867 self.text = text\
6868 self.width = #text\
6869end\
6870\
6871configureConstructor({\
6872 orderedArguments = { \"text\", \"X\", \"Y\" },\
6873 requiredArguments = { \"text\" },\
6874 argumentTypes = { text = \"string\" }\
6875}, true)\
6876",
6877["Application.ti"]="--[[\
6878 An Application object is the entry point to a Titanium Application.\
6879 The program loop, nodes, threads and animations are handled by the Application object.\
6880--]]\
6881\
6882class \"Application\" extends \"Component\" mixin \"MThemeManager\" mixin \"MKeyHandler\" mixin \"MCallbackManager\" mixin \"MAnimationManager\" mixin \"MNodeContainer\" {\
6883 width = 51;\
6884 height = 19;\
6885\
6886 threads = {};\
6887 nodes = {};\
6888\
6889 running = false;\
6890 terminatable = false;\
6891}\
6892\
6893--[[\
6894 @constructor\
6895 @desc Constructs an instance of the Application by setting all necessary unique properties on it\
6896 @param [number - width], [number - height]\
6897 @return <nil>\
6898]]\
6899function Application:__init__( ... )\
6900 self:resolve( ... )\
6901 self.canvas = TermCanvas( self )\
6902\
6903 self:setMetaMethod(\"add\", function( a, b )\
6904 local t = a ~= self and a or b\
6905\
6906 if Titanium.typeOf( t, \"Node\", true ) then\
6907 return self:addNode( t )\
6908 elseif Titanium.typeOf( t, \"Thread\", true ) then\
6909 return self:addThread( t )\
6910 end\
6911\
6912 error \"Invalid targets for application '__add'. Expected node or thread.\"\
6913 end)\
6914end\
6915\
6916function Application:focusNode( node )\
6917 if not Titanium.typeOf( node, \"Node\", true ) then\
6918 return error \"Failed to update application focused node. Invalid node object passed.\"\
6919 end\
6920\
6921 self:unfocusNode()\
6922 self.focusedNode = node\
6923 node.changed = true\
6924\
6925\
6926 node:executeCallbacks( \"focus\", self )\
6927end\
6928\
6929function Application:unfocusNode( targetNode )\
6930 local node = self.focusedNode\
6931 if not node or ( targetNode ~= node ) then return end\
6932\
6933 self.focusedNode = nil\
6934\
6935 node.raw.focused = false\
6936 node.changed = true\
6937\
6938 node:executeCallbacks( \"unfocus\", self )\
6939end\
6940\
6941--[[\
6942 @instance\
6943 @desc Adds a new thread named 'name' running 'func'. This thread will receive events caught by the Application engine\
6944 @param <threadObj - Thread Instance>\
6945 @return [threadObj | error]\
6946]]\
6947function Application:addThread( threadObj )\
6948 if not Titanium.typeOf( threadObj, \"Thread\", true ) then\
6949 error( \"Failed to add thread, object '\"..tostring( threadObj )..\"' is invalid. Thread Instance required\")\
6950 end\
6951\
6952 table.insert( self.threads, threadObj )\
6953\
6954 return threadObj\
6955end\
6956\
6957--[[\
6958 @instance\
6959 @desc Removes the thread named 'name'*\
6960 @param <Instance 'Thread'/string name - target>\
6961 @return <boolean - success>, [node - removedThread**]\
6962\
6963 *Note: In order for the thread to be removed its 'id' field must match the 'id' parameter.\
6964 **Note: Removed thread will only be returned if a thread was removed (and thus success 'true')\
6965]]\
6966function Application:removeThread( target )\
6967 if not Titanium.typeOf( target, \"Thread\", true ) then\
6968 return error( \"Cannot perform search for thread using target '\"..tostring( target )..\"'.\" )\
6969 end\
6970\
6971 local searchID = type( target ) == \"string\"\
6972 local threads, thread, threadID = self.threads\
6973 for i = 1, #threads do\
6974 thread = threads[ i ]\
6975\
6976 if ( searchID and thread.id == target ) or ( not searchID and thread == target ) then\
6977 thread:stop()\
6978\
6979 table.remove( threads, i )\
6980 return true, thread\
6981 end\
6982 end\
6983\
6984 return false\
6985end\
6986\
6987--[[\
6988 @instance\
6989 @desc Ships events to threads, if the thread requires a Titanium event, that will be passed instead.\
6990 @param <AnyEvent - eventObj>, <vararg - eData>\
6991]]\
6992function Application:handleThreads( eventObj, ... )\
6993 local threads = self.threads\
6994\
6995 local thread\
6996 for i = 1, #threads do\
6997 thread = threads[ i ]\
6998\
6999 if thread.titaniumEvents then\
7000 thread:handle( eventObj )\
7001 else\
7002 thread:handle( ... )\
7003 end\
7004 end\
7005end\
7006\
7007--[[\
7008 @instance\
7009 @desc Begins the program loop\
7010]]\
7011function Application:start( preserve )\
7012 if not preserve then\
7013 Titanium.VFS = nil\
7014 end\
7015\
7016 self.running = true\
7017 while self.running do\
7018 self:draw()\
7019 local event = { coroutine.yield() }\
7020 local eName = event[ 1 ]\
7021\
7022 if eName == \"timer\" and event[ 2 ] == self.timer then\
7023 self:updateAnimations()\
7024 elseif eName == \"terminate\" and self.terminatable then\
7025 printError \"Application Terminated\"\
7026 self:stop()\
7027 end\
7028\
7029 self:handle( unpack( event ) )\
7030 end\
7031end\
7032\
7033--[[\
7034 @instance\
7035 @desc Draws changed nodes (or all nodes if 'force' is true)\
7036 @param [boolean - force]\
7037]]\
7038function Application:draw( force )\
7039 if not self.changed and not force then return end\
7040\
7041 if self.needsThemeUpdate then\
7042 self:update()\
7043 self.needsThemeUpdate = false\
7044 end\
7045\
7046 local canvas = self.canvas\
7047 local nodes, node = self.nodes\
7048\
7049 for i = 1, #nodes do\
7050 node = nodes[ i ]\
7051 if node.needsRedraw and node.visible then\
7052 node:draw( force )\
7053\
7054 node.canvas:drawTo( canvas, node.X, node.Y )\
7055 node.needsRedraw = false\
7056 end\
7057 end\
7058 self.changed = false\
7059\
7060 local focusedNode, caretEnabled, caretX, caretY, caretColour = self.focusedNode\
7061 if focusedNode and focusedNode:can \"getCaretInfo\" then\
7062 caretEnabled, caretX, caretY, caretColour = focusedNode:getCaretInfo()\
7063 end\
7064\
7065 term.setCursorBlink( caretEnabled or false )\
7066 canvas:draw( force )\
7067\
7068 if caretEnabled then\
7069 term.setTextColour( caretColour or self.colour or 32768 )\
7070 term.setCursorPos( caretX or 1, caretY or 1 )\
7071 end\
7072end\
7073\
7074--[[\
7075 @instance\
7076 @desc Spawns a Titanium event instance and ships it to nodes and threads.\
7077 @param <table - event>\
7078]]\
7079function Application:handle( eName, ... )\
7080 local eventObject = Event.spawn( eName, ... )\
7081 if eventObject.main == \"KEY\" then self:handleKey( eventObject ) end\
7082\
7083 local nodes, node = self.nodes\
7084 for i = #nodes, 1, -1 do\
7085 node = nodes[ i ]\
7086 -- The node will update itself depending on the event. Once all are updated they are drawn if changed.\
7087 if node then node:handle( eventObject ) end\
7088 end\
7089\
7090 self:executeCallbacks( eName, eventObject )\
7091 self:handleThreads( eventObject, eName, ... )\
7092end\
7093\
7094--[[\
7095 @instance\
7096 @desc Stops the program loop\
7097]]\
7098function Application:stop()\
7099 if self.running then\
7100 self.running = false\
7101 os.queueEvent( \"ti_app_close\" )\
7102 else\
7103 return error \"Application already stopped\"\
7104 end\
7105end\
7106",
7107}
7108local scriptFiles = {
7109["Titanium.lua"]=true,
7110["Class.lua"]=true,
7111}
7112local preLoad = {
7113}
7114local loaded = {}
7115local function loadFile( name, verify )
7116 if loaded[ name ] then return end
7117
7118 local content = files[ name ]
7119 if content then
7120 local output, err = loadstring( content, name )
7121 if not output or err then return error( "Failed to load Lua chunk. File '"..name.."' has a syntax error: "..tostring( err ), 0 ) end
7122
7123 local ok, err = pcall( output )
7124 if not ok or err then return error( "Failed to execute Lua chunk. File '"..name.."' crashed: "..tostring( err ), 0 ) end
7125
7126 if verify then
7127 local className = name:gsub( "%..*", "" )
7128 local class = Titanium.getClass( className )
7129
7130 if class then
7131 if not class:isCompiled() then class:compile() end
7132 else return error( "File '"..name.."' failed to create class '"..className.."'" ) end
7133 end
7134
7135 loaded[ name ] = true
7136 else return error("Failed to load Titanium. File '"..tostring( name ).."' cannot be found.") end
7137end
7138
7139-- Load our class file
7140loadFile( "Class.lua" )
7141
7142Titanium.setClassLoader(function( name )
7143 local fName = name..".ti"
7144
7145 if not files[ fName ] then
7146 return error("Failed to find file '"..fName..", to load missing class '"..name.."'.", 3)
7147 else
7148 loadFile( fName, true )
7149 end
7150end)
7151
7152-- Load any files specified by our config file
7153for i = 1, #preLoad do loadFile( preLoad[ i ], not scriptFiles[ preLoad[ i ] ] ) end
7154
7155-- Load all class files
7156for name in pairs( files ) do if not scriptFiles[ name ] then
7157 loadFile( name, true )
7158end end
7159
7160-- Load all script files
7161for name in pairs( scriptFiles ) do loadFile( name, false ) end