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