mirror of
https://github.com/hbomb79/Titanium.git
synced 2025-01-22 09:22:05 -05:00
Merge branch 'develop' into 'master'
v0.1.0-beta.1 See merge request !42
This commit is contained in:
commit
2249e1615a
28 changed files with 729 additions and 110 deletions
11
README.md
11
README.md
|
@ -11,13 +11,10 @@ Titanium
|
|||
#### About
|
||||
Titanium is a feature-rich, reliable framework for creating GUIs within ComputerCraft. Titanium offers a quick and easy experience for those that just want to jump in, however more powerful tools are on standby if you need them.
|
||||
|
||||
![](http://puu.sh/rd2ty/c8cc92aa93.gif)
|
||||
![](http://puu.sh/wXIqN/2691563f54.gif)
|
||||
|
||||
#### Alpha
|
||||
Titanium is currently in alpha and your feedback, bug reports, suggestions (via [issues](https://gitlab.com/hbomb79/Titanium/issues)) and [merge requests](https://gitlab.com/hbomb79/Titanium/merge_requests) are welcomed.
|
||||
Titanium is currently in beta and your feedback, bug reports, suggestions (via [issues](https://gitlab.com/hbomb79/Titanium/issues)) and [merge requests](https://gitlab.com/hbomb79/Titanium/merge_requests) are welcomed.
|
||||
|
||||
#### Download and Installation
|
||||
Information regarding the multiple methods to download Titanium can be found at the [titanium website](http://harryfelton.web44.net/titanium/).
|
||||
|
||||
#### Documentation
|
||||
The [documentation website](http://harryfelton.web44.net/titanium/doc) automatically updates to provide information regarding the latest release of Titanium (ability to view older documentation is provided).
|
||||
#### Getting Started
|
||||
The [Titanium website](http://harryfelton.web44.net/titanium/) is loaded with useful guides, and class reference documentation (automatically kept up to date with latest Titanium installation). Head on over there now to get started with Titanium.
|
||||
|
|
|
@ -26,7 +26,7 @@ end)
|
|||
|
||||
runTask("COLLATED_NODES_COUNT", function()
|
||||
local collated = TestApplication.collatedNodes
|
||||
return #collated == 9 or #collated
|
||||
return #collated == 8 or #collated
|
||||
end)
|
||||
|
||||
runTask("TML_STRING_ASSIGN", function()
|
||||
|
|
|
@ -86,7 +86,7 @@ local app = {
|
|||
masterTheme = Theme.fromFile( "masterTheme", "example/ui/master.theme" ),
|
||||
|
||||
-- Grab our page container which was created in our TML file. This page container has two pages, which we will get into later
|
||||
pages = Manager:query "PageContainer".result[1],
|
||||
pages = Manager:query "#mainContainer".result[1],
|
||||
|
||||
-- Store some commonly used assets for our animated sidebar
|
||||
sidePane = {
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<colour>white</colour>
|
||||
<verticalAlign>centre</verticalAlign>
|
||||
<horizontalAlign>centre</horizontalAlign>
|
||||
<width dynamic>#self.text + 2</width>
|
||||
</Button>
|
||||
|
||||
<RadioButton>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<PageContainer Z=1 X=1 Y=1 width="$application.width" height="$application.height">
|
||||
<Page id="main" xScrollAllowed="false">
|
||||
<TabbedPageContainer Z=1 X=1 Y=1 width="$application.width" height="$application.height" id="mainContainer">
|
||||
<Page id="main" name="Landing" xScrollAllowed="false">
|
||||
<Label X=2 Y=2 class="header">Titanium</Label>
|
||||
<Label X=2 Y=3 class="sub">Test Application</Label>
|
||||
|
||||
|
@ -16,17 +16,17 @@
|
|||
<Label X=2 Y=10 id="selected_name_display" class="sub">No selected text</Label>
|
||||
|
||||
<Button X=41 Y=18 width=10 id="exit_button" enabled="false">Exit</Button>
|
||||
<Button X=2 Y=18 height=1 width=14 id="toggle">Toggle Theme</Button>
|
||||
<Button X=2 Y=18 id="toggle">Toggle Theme</Button>
|
||||
|
||||
<Button X=2 Y=15 height=1 width=13 id="pane_toggle">Toggle Pane</Button>
|
||||
<Button X=2 Y=15 id="pane_toggle">Toggle Pane</Button>
|
||||
|
||||
<Label id="left" X=5 Y=16 class="sub hotkey_part">ctrl</Label>
|
||||
<Label X=9 Y=16 class="sub hotkey_part" id="hyphen_seperator">-</Label>
|
||||
<Label id="right" X=10 Y=16 class="sub hotkey_part">p</Label>
|
||||
|
||||
<Button width=7 height=1 X="${Container#pane}.X - 2 - self.width" Y=2 class="page_change" targetPage="console" id="shell_link">Shell</Button>
|
||||
<Button width=6 height=1 X="${#shell_link}.X - 2 - self.width" Y=2 class="page_change" targetPage="text" id="button_link">Text</Button>
|
||||
<Button width="$#self.text + 2" height=1 X="${#button_link}.X - 2 - self.width" Y=2 class="page_change" targetPage="windows">Windows</Button>
|
||||
<Button X="${Container#pane}.X - 2 - self.width" Y=2 class="page_change" targetPage="console" id="shell_link">Shell</Button>
|
||||
<Button X="${#shell_link}.X - 2 - self.width" Y=2 class="page_change" targetPage="text" id="button_link">Text</Button>
|
||||
<Button X="${#button_link}.X - 2 - self.width" Y=2 class="page_change" targetPage="windows">Windows</Button>
|
||||
|
||||
<Dropdown X=23 Y=6 width=25 maxHeight=7 Z=2>
|
||||
<Option value="1">Example Option 1</Option>
|
||||
|
@ -45,7 +45,7 @@
|
|||
<Slider X=23 width=15 Y=11 id="animationSlider" value=2/>
|
||||
<Label X="${#animationSlider}.X + ( {#animationSlider}.width / 2 ) - self.width / 2" Y=12 id="animationD" class="sub">${#animationSlider}.value * 0.15 .. 's'</Label>
|
||||
|
||||
<Container id="pane" width=21 height=19 X=52 backgroundColour="grey" Z=3>
|
||||
<Container id="pane" width=21 height="$parent.height" X=52 backgroundColour="grey" Z=3>
|
||||
<Label colour=1 X=2 Y=2>Settings</Label>
|
||||
|
||||
<ScrollContainer X=2 Y=4 width=20 height=10>
|
||||
|
@ -78,25 +78,25 @@
|
|||
</Container>
|
||||
</Page>
|
||||
|
||||
<Page id="console" xScrollAllowed="false">
|
||||
<Page id="console" name="Terminal" xScrollAllowed="false">
|
||||
<Terminal X=2 Y=2 width=49 height=15 id="shell"/>
|
||||
<Button class="page_change" X=18 Y=18 width=6 targetPage="main">Back</Button>
|
||||
<Button class="page_change" X=26 Y=18 width=6 targetPage="text">Next</Button>
|
||||
<Button class="page_change" X=18 Y=18 targetPage="main">Back</Button>
|
||||
<Button class="page_change" X=26 Y=18 targetPage="text">Next</Button>
|
||||
</Page>
|
||||
|
||||
<Page id="text" xScrollAllowed="false">
|
||||
<Page id="text" xScrollAllowed="false" name="Editor">
|
||||
<EditableTextContainer X=2 Y=2 width="$parent.width - 2" height="$parent.height - 4" horizontalAlign="left" colour="256" focusedColour="white" backgroundColour="grey"/>
|
||||
|
||||
<Button class="page_change" X=18 Y=18 width=6 targetPage="console">Back</Button>
|
||||
<Button class="page_change" X=26 Y=18 width=6 targetPage="main">Home</Button>
|
||||
<Button class="page_change" X=18 Y=18 targetPage="console">Back</Button>
|
||||
<Button class="page_change" X=26 Y=18 targetPage="main">Home</Button>
|
||||
</Page>
|
||||
|
||||
<Page id="windows" xScrollAllowed="false" position=1>
|
||||
<Page id="windows" xScrollAllowed="false" position=1 name="Windows">
|
||||
<Window Z=5 X=6 Y=3 width=25 height=6 backgroundColour="256" title="Example Window" focusedBackgroundColour="lightBlue" minHeight="7">
|
||||
<Label class="centre" Y=2 colour=128>Drag me around!</Label>
|
||||
<Input width="$parent.width - 2" backgroundColour="red" X=2 Y=4/>
|
||||
</Window>
|
||||
|
||||
<Button class="page_change" X=23 Y=18 width=6 targetPage="main">Home</Button>
|
||||
<Button class="page_change" X=23 Y=18 targetPage="main">Home</Button>
|
||||
</Page>
|
||||
</PageContainer>
|
||||
</TabbedPageContainer>
|
||||
|
|
|
@ -171,7 +171,7 @@ function Application:schedule( fn, time, repeating, name )
|
|||
self:unschedule( name )
|
||||
end
|
||||
|
||||
local ID = os.startTimer( time ) --TODO: Use timer util to re-use timer IDs
|
||||
local ID = os.startTimer( time )
|
||||
self.timers[ ID ] = { fn, time, repeating, name }
|
||||
|
||||
return ID
|
||||
|
@ -270,8 +270,11 @@ function Application:draw( force )
|
|||
node.needsRedraw = false
|
||||
end
|
||||
end
|
||||
self.changed = false
|
||||
|
||||
-- Shade the application content, and draw the dialog container over the top of all other nodes
|
||||
if self.isDialogOpen then self:drawDialogs() end
|
||||
|
||||
self.changed = false
|
||||
local focusedNode, caretEnabled, caretX, caretY, caretColour = self.focusedNode
|
||||
if focusedNode then
|
||||
focusedNode:resolveProjectorFocus()
|
||||
|
@ -301,6 +304,8 @@ function Application:handle( eName, ... )
|
|||
local eventObject = Event.spawn( eName, ... )
|
||||
if eventObject.main == "KEY" then self:handleKey( eventObject ) end
|
||||
|
||||
if self.isDialogOpen then self.dialogContainer:handle( eventObject ) end
|
||||
|
||||
local nodes, node = self.nodes
|
||||
for i = #nodes, 1, -1 do
|
||||
node = nodes[ i ]
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
--[[
|
||||
@instance width - number (def. 1) - The objects width, defines the width of the canvas.
|
||||
@instance height - number (def. 1) - The objects width, defines the height of the canvas.
|
||||
@instance X - number (def. 1) - The objects X position.
|
||||
@instance Y - number (def. 1) - The objects Y position.
|
||||
@instance X - number (def. 1) - The objects X position. Replaced when fluid positioning is used.
|
||||
@instance Y - number (def. 1) - The objects Y position. Replaced when fluid positioning is used.
|
||||
@instance changed - boolean (def. true) - If true, the node will be redrawn by it's parent. This propagates up to the application, before being drawn to the CraftOS term object. Set to false after draw.
|
||||
@instance backgroundChar - string (def. " ") - Defines the character used when redrawing the canvas. Can be set to "nil" to use no character at all.
|
||||
@instance positioning - string (def. "fluid") - The positioning type of the node -- used when the parent resolves fluid positions (when the parent has 'resolveFluid' set to true).
|
||||
@instance marginLeft - number (def. 0) - When the parent is resolving fluid positions, this value is used to calculate the amount of space to the left of the node (from the edge, or an adjacent node). marginLeft takes priority over marginRight.
|
||||
@instance marginRight - number (def. 0) - When the parent is resolving fluid positions, this value is used to calculate the amount of space to the right of node (from an adjacent node).
|
||||
@instance marginTop - number (def. 0) - When the parent is resolving fluid positions, this value is used to calculate the amount of space above the node (from an adjacent node, or the edge of the parent). marginTop takes priority over marginBottom.
|
||||
@instance marginBottom - number (def. 0) - When the parent is resolving fluid positions, this value is used to calculate the amount of space under the node (from an adjacent node).
|
||||
|
||||
A Component is an object that can be represented visually.
|
||||
]]
|
||||
|
@ -12,6 +17,7 @@
|
|||
abstract class Component mixin MPropertyManager {
|
||||
width = 1;
|
||||
height = 1;
|
||||
|
||||
X = 1;
|
||||
Y = 1;
|
||||
|
||||
|
@ -22,6 +28,12 @@ abstract class Component mixin MPropertyManager {
|
|||
shader = false;
|
||||
shadeText = true;
|
||||
shadeBackground = true;
|
||||
|
||||
marginLeft = 0;
|
||||
marginRight = 0;
|
||||
marginTop = 0;
|
||||
marginBottom = 0;
|
||||
positioning = false;
|
||||
}
|
||||
|
||||
--[[
|
||||
|
@ -98,6 +110,7 @@ end
|
|||
@param <number - width>
|
||||
]]
|
||||
function Component:setWidth( width )
|
||||
if self.parent then self.parent.positionChanged = true end
|
||||
self:queueAreaReset()
|
||||
|
||||
width = math.ceil( width )
|
||||
|
@ -111,6 +124,7 @@ end
|
|||
@param <number - height>
|
||||
]]
|
||||
function Component:setHeight( height )
|
||||
if self.parent then self.parent.positionChanged = true end
|
||||
self:queueAreaReset()
|
||||
|
||||
height = math.ceil( height )
|
||||
|
@ -184,7 +198,7 @@ end
|
|||
|
||||
configureConstructor {
|
||||
orderedArguments = { "X", "Y", "width", "height" },
|
||||
argumentTypes = { X = "number", Y = "number", width = "number", height = "number", colour = "colour", backgroundColour = "colour", backgroundTextColour = "colour", transparent = "boolean", shader = "ANY", shadeText="ANY", shadeBackground="ANY" }
|
||||
argumentTypes = { X = "number", Y = "number", width = "number", height = "number", colour = "colour", backgroundColour = "colour", backgroundTextColour = "colour", transparent = "boolean", shader = "ANY", shadeText="ANY", shadeBackground="ANY", marginLeft = "number", marginRight = "number", marginTop = "number", marginBottom = "number", positioning = "ANY" }
|
||||
} alias {
|
||||
color = "colour",
|
||||
backgroundColor = "backgroundColour"
|
||||
|
|
|
@ -92,7 +92,7 @@ function DynamicValue:attach()
|
|||
stack[ 2 ]:watchProperty( stack[ 1 ], function( _, __, value )
|
||||
self.cachedValues[ i ] = value
|
||||
self:solve()
|
||||
end, "DYNAMIC_VALUE_" .. self.__ID )
|
||||
end, "DYNAMIC_VALUE_" .. self.__ID .. "_" .. i )
|
||||
end
|
||||
|
||||
self.attached = true
|
||||
|
@ -110,7 +110,7 @@ function DynamicValue:detach()
|
|||
for i = 1, #resolvedStacks do
|
||||
stack = resolvedStacks[ i ]
|
||||
|
||||
stack[ 2 ]:unwatchProperty( stack[ 1 ], "DYNAMIC_VALUE_" .. self.__ID )
|
||||
stack[ 2 ]:unwatchProperty( stack[ 1 ], "DYNAMIC_VALUE_" .. self.__ID .. "_" .. i )
|
||||
end
|
||||
|
||||
self.attached = false
|
||||
|
|
|
@ -174,6 +174,8 @@ function Node:setApplication( application )
|
|||
self.application = application
|
||||
self:resolveProjector()
|
||||
|
||||
self:refreshDynamicValues()
|
||||
|
||||
self.changed = true
|
||||
end
|
||||
|
||||
|
|
|
@ -51,6 +51,11 @@ end
|
|||
@param [boolean - force], [number - offsetX], [number - offsetY]
|
||||
]]
|
||||
function Container:draw( force, offsetX, offsetY )
|
||||
if self.positionChanged and self.fluidPositions then
|
||||
self:resolveFluidPositions()
|
||||
self.positionChanged = false
|
||||
end
|
||||
|
||||
if self.changed or force then
|
||||
local canvas = self.canvas
|
||||
|
||||
|
|
45
src/classes/nodes/DialogWindow.ti
Normal file
45
src/classes/nodes/DialogWindow.ti
Normal file
|
@ -0,0 +1,45 @@
|
|||
--[[
|
||||
@instance body - string (def. "This is a dialog window")
|
||||
|
||||
An enhanced version of the Window node that provides basic dialog prompt structure (title, body and action container).
|
||||
|
||||
Using :addNode will insert the given node as an action node into the action container. This, combined with fluid layouts makes creating
|
||||
simple dialog prompts super easy and flexible.
|
||||
]]
|
||||
|
||||
class DialogWindow extends Window {
|
||||
body = "This is a dialog window"
|
||||
}
|
||||
|
||||
--[[
|
||||
@constructor
|
||||
@desc Constructs the dialog by;
|
||||
- Creating the body and action container
|
||||
- Constructing the node via the super
|
||||
- Inserting the body and action container
|
||||
@param [number - X], [number - Y], [number - width], [number - height], [string - title], [string - body]
|
||||
]]
|
||||
function DialogWindow:__init__( ... )
|
||||
self.bodyText = TextContainer( "" ):set( "id", "TEST" ):set { text = "$parent.parent.body", X = 1, Y = 1, width = "$parent.width", height = "$parent.height - 3 < 2 and 2 or parent.height - 3", colour = 256, horizontalAlign = "centre", Z = 1 }
|
||||
self.actionContainer = ScrollContainer( 2 ):set{ id = "TESTING", Y = "$parent.height - 1", width = "$parent.width - 2", height = 2, fluidPositions = true, Z = 2, positioning = "fluid" }
|
||||
|
||||
self:super( ... )
|
||||
|
||||
self.content:addNode( self.bodyText )
|
||||
self.content:addNode( self.actionContainer )
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Overrides Window:createProxies -- Redirects attempts to create proxy methods to the action container (methods in Window.static.proxyMethods act on the action container)
|
||||
]]
|
||||
function DialogWindow:createProxies()
|
||||
self.super:createProxies( self.actionContainer )
|
||||
end
|
||||
|
||||
configureConstructor( {
|
||||
orderedArguments = { "X", "Y", "width", "height", "title", "body" },
|
||||
argumentTypes = {
|
||||
body = "string"
|
||||
}
|
||||
}, true )
|
|
@ -19,7 +19,6 @@ function OverlayContainer:__init__( ... )
|
|||
self:super()
|
||||
|
||||
self.transparent = true
|
||||
self.consumeAll = false
|
||||
|
||||
self.canvas.onlyShadeBottom = true
|
||||
end
|
||||
|
@ -51,7 +50,7 @@ function OverlayContainer:handle( eventObj )
|
|||
end
|
||||
|
||||
self:shipEvent( clone or eventObj )
|
||||
if clone and clone.isWithin then
|
||||
if clone and clone.isWithin and ( self.consumeAll or clone.handled ) then
|
||||
if not clone.handled then
|
||||
self:executeCallbacks "miss"
|
||||
end
|
||||
|
|
|
@ -15,9 +15,11 @@ class Page extends ScrollContainer
|
|||
]]
|
||||
function Page:setParent( parent )
|
||||
if Titanium.typeOf( parent, "PageContainer", true ) then
|
||||
self:linkProperties( parent, "width", "height" )
|
||||
else
|
||||
self:unlinkProperties( parent, "width", "height" )
|
||||
parent:linkPage( self )
|
||||
elseif parent then
|
||||
return error("Page nodes can ONLY be children of PageContainers, or there subclasses.")
|
||||
elseif self.parent then
|
||||
self.parent:unlinkPage( self )
|
||||
end
|
||||
|
||||
self.super:setParent( parent )
|
||||
|
@ -57,7 +59,7 @@ function Page:getAbsolutePosition( limit )
|
|||
end
|
||||
|
||||
local pX, pY = self.parent:getAbsolutePosition()
|
||||
return -1 + pX + self.X - parent.scroll, -1 + pY + self.Y
|
||||
return -1 + pX + self.X - parent.scroll - self.xScroll, -1 + pY + self.Y - self.yScroll
|
||||
else return self.X, self.Y end
|
||||
end
|
||||
|
||||
|
|
|
@ -48,6 +48,27 @@ function PageContainer:handle( eventObj )
|
|||
return true
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Links the target page to this page container (ties width/height of Page to parents)
|
||||
@param <Page Instance - page>
|
||||
]]
|
||||
function PageContainer:linkPage( page )
|
||||
if not Titanium.typeOf( page, "Page", true ) then return error("Failed to link page '"..tostring( page ).."'. Expected Page instance.") end
|
||||
page:linkProperties( self, "width", "height" )
|
||||
page.Y = 1
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Removes links to the target page
|
||||
@param <Page Instance - page>
|
||||
]]
|
||||
function PageContainer:unlinkPage( page )
|
||||
if not Titanium.typeOf( page, "Page", true ) then return error("Failed to unlink page '"..tostring( page ).."'. Expected Page instance.") end
|
||||
page:unlinkProperties( self, "width", "height" )
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@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
|
||||
|
@ -81,26 +102,29 @@ function PageContainer:updatePagePositions()
|
|||
local pages, usedIndexes = self.nodes, {}
|
||||
for i = 1, #pages do
|
||||
local page = pages[ i ]
|
||||
|
||||
local pagePosition = page.position
|
||||
if pagePosition and not page.isPositionTemporary then
|
||||
usedIndexes[ pagePosition ] = true
|
||||
if Titanium.typeOf( page, "Page", true ) then
|
||||
local pagePosition = page.position
|
||||
if pagePosition and not page.isPositionTemporary then
|
||||
usedIndexes[ pagePosition ] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local currentIndex = 0
|
||||
for i = 1, #pages do
|
||||
local page = pages[ i ]
|
||||
if not page.position or page.isPositionTemporary then
|
||||
repeat
|
||||
currentIndex = currentIndex + 1
|
||||
until not usedIndexes[ currentIndex ]
|
||||
if Titanium.typeOf( page, "Page", true ) then
|
||||
if not page.position or page.isPositionTemporary then
|
||||
repeat
|
||||
currentIndex = currentIndex + 1
|
||||
until not usedIndexes[ currentIndex ]
|
||||
|
||||
page.isPositionTemporary = true
|
||||
page.raw.position = currentIndex
|
||||
page.isPositionTemporary = true
|
||||
page.raw.position = currentIndex
|
||||
end
|
||||
|
||||
page:updatePosition()
|
||||
end
|
||||
|
||||
page:updatePosition()
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -125,6 +149,25 @@ function PageContainer:addNode( node )
|
|||
return error("Only 'Page' nodes can be added as direct children of 'PageContainer' nodes, '"..tostring( node ).."' is invalid")
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Overrides the super :removeNode so that page checking can be performed.
|
||||
If removed page was the currently selected page, the page selection is reset
|
||||
|
||||
Tabs are reset after the node has been removed.
|
||||
@param <Instance 'Node'/string name - target>
|
||||
@return <boolean - success>, [node - removedNode]
|
||||
]]
|
||||
function PageContainer:removeNode( ... )
|
||||
local rem, node = self.super:removeNode( ... )
|
||||
if rem and node == self.selectedPage then
|
||||
self.selectedPage = nil
|
||||
end
|
||||
|
||||
self:formTabs()
|
||||
return rem, node
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc An alias for 'addNode', contextualized for the PageContainer
|
||||
|
@ -204,6 +247,7 @@ configureConstructor {
|
|||
animationDuration = "number",
|
||||
animationEasing = "string",
|
||||
customAnimation = "function",
|
||||
selectedPage = "string"
|
||||
selectedPage = "string",
|
||||
scroll = "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -298,6 +298,8 @@ end
|
|||
|
||||
--[[ Caching Functions ]]--
|
||||
function ScrollContainer:cacheContent()
|
||||
if self.positionChanged and self.fluidPositions then self:resolveFluidPositions() end
|
||||
|
||||
self:cacheContentSize()
|
||||
self:cacheActiveScrollbars()
|
||||
|
||||
|
|
247
src/classes/nodes/TabbedPageContainer.ti
Normal file
247
src/classes/nodes/TabbedPageContainer.ti
Normal file
|
@ -0,0 +1,247 @@
|
|||
--[[
|
||||
@local
|
||||
@desc Spawns a scroll button by adding the button to the parent (via super.super to avoid page verification)
|
||||
@param <string - text>, <boolean - forward>, <TabbedPageContainer Instance - parent>
|
||||
@return <Button Instance - b>
|
||||
]]
|
||||
local function spawnScrollButton( text, forward, parent )
|
||||
local b = Button( text ):set { width = 1, enabled = "$self.visible", height = "$parent.tabHeight" }
|
||||
|
||||
b:on("trigger", function( self )
|
||||
parent:moveTabs( ( forward and -1 or 1 ) * ( parent.width / 2 ) )
|
||||
end):addClass "scroller"
|
||||
|
||||
return parent.super.super:addNode( b )
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance tabHeight - number (def. 1) - The height of the tabs, and therefore the amount of space consumed at the top of the node. All pages are shifted DOWN by this amount to make space for tabs
|
||||
@instance scrollButtons - boolean (def. true) - If the tabs overflow the container, two buttons (one for backward, one for forward) will appear to the left and right (respectively) of the tab container, allowing simple scrolling. Set to false to hide this buttons
|
||||
@instance smartTabWidth - boolean(def. true) - When true, the tabs will try and consume as much of the total node width as possible -- This setting does not apply if the minimum width of the tabs (the text + the padding) exceeds the width of the node (ie: scrolling becomes possible)
|
||||
@instance tabPadding - number (def. 1) - The space to the left AND the right of the text inside each tab
|
||||
@instance tabScroll - number (def. 0) - The current scroll of the tabs. Changing this setting should be avoided -- Instead use :moveTabs
|
||||
@instance tabAlignment - string (def. "centre") - The horizontal AND vertical alignment of the text inside each tab
|
||||
@instance tabColour - colour (def. 1) - The text colour of unselected tabs
|
||||
@instance tabBackgroundColour - colour (def. 1) - The background colour of unselected tabs
|
||||
@instance selectedTabColour - colour (def. 1) - The text colour of selected tabs
|
||||
@instance selectedTabBackgroundColour - colour (def. 1) - The background colour of selected tabs
|
||||
|
||||
A variant on the PageContainer which provides a bar at the top of the node containing draggable, scrollable tabs that represent all pages inside the container.
|
||||
]]
|
||||
|
||||
class TabbedPageContainer extends PageContainer {
|
||||
tabHeight = 1;
|
||||
smartTabWidth = true;
|
||||
scrollButtons = true;
|
||||
|
||||
tabPadding = 1;
|
||||
tabScroll = 0;
|
||||
tabAlignment = "centre";
|
||||
|
||||
tabColour = 1;
|
||||
tabBackgroundColour = colours.lightBlue;
|
||||
|
||||
selectedTabColour = 1;
|
||||
selectedTabBackgroundColour = colours.cyan;
|
||||
}
|
||||
|
||||
--[[
|
||||
@constructor
|
||||
@desc Constructs the container by creating the tab container, and the left/right scroll buttons (only visible if scrollButtons is true and content exceeds width of container)
|
||||
@param [number - X], [number - Y], [number - width], [number - height], [table - nodes]
|
||||
]]
|
||||
function TabbedPageContainer:__init__( ... )
|
||||
self:resolve( ... )
|
||||
self:super()
|
||||
|
||||
self.tabContainer = self.super.super:addNode( TabContainer() )
|
||||
|
||||
self.leftScrollButton = spawnScrollButton( "<", true, self ):set( "X", "$parent.scroll + 1" )
|
||||
self.rightScrollButton = spawnScrollButton( ">", false, self ):set( "X", "$parent.scroll + parent.width" )
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Centres the tab representing the selected page inside the tab bar.
|
||||
@param [boolean - noAnimation]
|
||||
]]
|
||||
function TabbedPageContainer:centreActivePageButton( noAnimation )
|
||||
local button = self.tabContainer:query "Button.active".result[ 1 ]
|
||||
|
||||
self:moveTabs( 0, false, button.X + ( button.width / 2 ) - ( self.width / 2 ) )
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Calls PageContainer:updatePagePositions. Once pages are updated, the tabs are reformed to represent the new page(s)
|
||||
]]
|
||||
function TabbedPageContainer:updatePagePositions()
|
||||
self.super:updatePagePositions()
|
||||
self:formTabs()
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Selects the new page by calling PageContainer:selectPage and then updating the active tab (:updateActiveTab)
|
||||
@param <string - pageID>, [function - animationOverride]
|
||||
]]
|
||||
function TabbedPageContainer:selectPage( ... )
|
||||
self.super:selectPage( ... )
|
||||
self:updateActiveTab()
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Updates the active tab by colouring the tab representing the selected page using the 'selected' variants of the tab colours and centering the tab
|
||||
]]
|
||||
function TabbedPageContainer:updateActiveTab()
|
||||
self.tabContainer:query "Button.active":each( function( tab )
|
||||
tab:set {
|
||||
backgroundColour = "$parent.parent.parent.tabBackgroundColour",
|
||||
colour = "$parent.parent.parent.tabColour"
|
||||
}
|
||||
|
||||
tab:removeClass "active"
|
||||
end )
|
||||
|
||||
if self.selectedPage then
|
||||
local selectedTab = self.tabContainer:query( ("Button#%s"):format( self.selectedPage.id ) ).result[ 1 ]
|
||||
if selectedTab then
|
||||
selectedTab:addClass "active"
|
||||
selectedTab.backgroundColour = "$parent.parent.parent.selectedTabBackgroundColour"
|
||||
selectedTab.colour = "$parent.parent.parent.selectedTabColour"
|
||||
|
||||
self:centreActivePageButton()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Forms the tabs for each page. The 'name' property of the page is used as the tab name (or, if no name is set, the 'id' is used instead)
|
||||
]]
|
||||
function TabbedPageContainer:formTabs()
|
||||
local tabs, width = {}, 1
|
||||
local nodes = self.nodes
|
||||
for i = 4, #nodes do
|
||||
local page = nodes[ i ]
|
||||
local content = page.name or page.id
|
||||
local w = ( self.tabPadding * 2 ) + #content
|
||||
|
||||
tabs[ page.position ], width = { content, page.id, w }, width + w
|
||||
end
|
||||
|
||||
self.tabContainer:removeNode "innerTabs"
|
||||
local container = self.tabContainer:addNode( Container() ):set {
|
||||
id = "innerTabs",
|
||||
height = "$parent.height"
|
||||
}
|
||||
|
||||
local function spawnTab( text, width, X, page )
|
||||
local tab = container:addNode( Button( text ) ):set {
|
||||
width = width,
|
||||
X = X,
|
||||
height = "$parent.parent.parent.tabHeight",
|
||||
backgroundColour = "$parent.parent.parent.tabBackgroundColour",
|
||||
colour = "$parent.parent.parent.tabColour",
|
||||
horizontalAlign = "$parent.parent.parent.tabAlignment",
|
||||
verticalAlign = "$parent.parent.parent.tabAlignment",
|
||||
id = page
|
||||
}
|
||||
|
||||
tab:on( "trigger", function() self:selectPage( page ) end )
|
||||
end
|
||||
|
||||
local extraSpacePerNode = math.floor( ( self.width - width ) / #tabs )
|
||||
if self.smartTabWidth and extraSpacePerNode >= 2 then
|
||||
if extraSpacePerNode % 2 ~= 0 then
|
||||
extraSpacePerNode = extraSpacePerNode - 1
|
||||
end
|
||||
else
|
||||
extraSpacePerNode = 0
|
||||
end
|
||||
|
||||
local widthOverlap = width > self.width
|
||||
local canScroll = widthOverlap and self.scrollButtons
|
||||
|
||||
container.X = widthOverlap and "$-parent.parent.tabScroll + 1" or "$parent.width / 2 - ( self.width / 2 ) + 1"
|
||||
self:query "Button.scroller":set( "visible", canScroll )
|
||||
self.tabContainer:set {
|
||||
X = "$parent.scroll + " .. ( canScroll and "2" or "1" ),
|
||||
width = "$parent.width - " .. ( canScroll and "2" or "0" )
|
||||
}
|
||||
|
||||
local keys = {}
|
||||
for i in pairs( tabs ) do keys[ #keys + 1 ] = i end
|
||||
table.sort( keys )
|
||||
|
||||
local w = 1
|
||||
for i = 1, #keys do
|
||||
local tab = tabs[ keys[ i ] ]
|
||||
spawnTab( tab[ 1 ], tab[ 3 ] + extraSpacePerNode, w, tab[ 2 ] )
|
||||
w = w + extraSpacePerNode + tab[ 3 ]
|
||||
end
|
||||
|
||||
container.width = width + ( extraSpacePerNode * #tabs )
|
||||
self:updateActiveTab()
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Moves the tabs by 'amount' (OR, if 'absolute' is provided, the scroll offset is set directly to it). If 'noAnimation', the scrolling occurs instantly
|
||||
@param <number - amount>, [boolean - noAnimation] - Scrolls tabs by this amount
|
||||
@param [number - amount], [boolean - noAnimation], <number - absolute> - SETS the tab scroll to 'absolute'
|
||||
]]
|
||||
function TabbedPageContainer:moveTabs( amount, noAnimation, absolute )
|
||||
local MAX = self:query "#innerTabs".result[ 1 ].width - self.width + ( self.scrollButtons and 1 or -1 )
|
||||
local val = math.min( math.max( absolute or ( self.tabScroll + amount ), 0 ), MAX < 1 and 1 or MAX )
|
||||
|
||||
if noAnimation then
|
||||
self.tabScroll = val
|
||||
else
|
||||
self:animate( "TAB_SCROLL", "tabScroll", val, 0.2, "inOutQuad")
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc A modified version of PageContainer:linkPage that links the page provided to this container (sets the height and Y to dynamically adjust depending on 'tabHeight' and links the width)
|
||||
]]
|
||||
function TabbedPageContainer:linkPage( page )
|
||||
page.height, page.Y = "$parent.height - parent.tabHeight", "$parent.tabHeight + 1"
|
||||
page:linkProperties( self, "width" )
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc A modified version of PageContainer:unlinkPage that unlinks the page provided from this container
|
||||
]]
|
||||
function TabbedPageContainer:unlinkPage( page )
|
||||
page:removeDynamicValue "height"
|
||||
page:removeDynamicValue "Y"
|
||||
page:unlinkProperties( self, "width" )
|
||||
end
|
||||
|
||||
--[[
|
||||
@setter
|
||||
@desc When 'scrollButtons' is updated, the tabs are reformed to reflect the new configuration
|
||||
@param <boolean - scrollButtons>
|
||||
]]
|
||||
function TabbedPageContainer:setScrollButtons( scrollButtons )
|
||||
self.scrollButtons = scrollButtons
|
||||
self:formTabs()
|
||||
end
|
||||
|
||||
configureConstructor {
|
||||
argumentTypes = {
|
||||
tabHeight = "number",
|
||||
autoTabWidth = "boolean",
|
||||
tabBackgroundColour = "colour",
|
||||
tabColour = "colour",
|
||||
selectedTabBackgroundColour = "colour",
|
||||
selectedTabColour = "colour",
|
||||
tabScroll = "number",
|
||||
scrollButtons = "boolean",
|
||||
tabAlignment = "string"
|
||||
}
|
||||
}
|
|
@ -135,9 +135,11 @@ end
|
|||
@desc Provides the information required by the nodes application to draw the application caret.
|
||||
@return <boolean - caretEnabled>, <number - caretX>, <number - caretY>, <colour - caretColour>
|
||||
]]
|
||||
function Terminal:getCaretInfo()
|
||||
function Terminal:getCaretInfo( parentLimit )
|
||||
local c = self.canvas
|
||||
return isThreadRunning( self ) and c.tCursor, c.tX + self.X - 1, c.tY + self.Y - 1, c.tColour
|
||||
local sX, sY = self:getAbsolutePosition( parentLimit )
|
||||
|
||||
return isThreadRunning( self ) and c.tCursor, sX + c.tX - 1, sY + c.tY - 1, c.tColour
|
||||
end
|
||||
|
||||
--[[
|
||||
|
|
|
@ -43,7 +43,8 @@
|
|||
|
||||
class Window extends Container mixin MFocusable mixin MInteractable {
|
||||
static = {
|
||||
proxyMethods = { "addNode", "removeNode", "getNode", "query", "clearNodes" }
|
||||
proxyMethods = { "addNode", "removeNode", "getNode", "query", "clearNodes" },
|
||||
proxyProperties = { "marginLeft", "marginRight", "marginTop", "marginBottom", "fluidDimensions", "positioning", "minWidth", "minHeight", "maxWidth", "maxHeight", "positionChanged" }
|
||||
};
|
||||
|
||||
titleBar = true;
|
||||
|
@ -85,10 +86,11 @@ function Window:__init__( ... )
|
|||
colour = "$not parent.enabled and parent.disabledColour or parent.titleBarColour",
|
||||
|
||||
visible = "$parent.titleBar",
|
||||
enabled = "$self.visible"
|
||||
enabled = "$self.visible",
|
||||
positioning = "normal"
|
||||
})
|
||||
|
||||
self.titleBarTitle = self.titleBarContent:addNode( Label( "" ) ):set( "id", "title" )
|
||||
self.titleBarTitle = self.titleBarContent:addNode( Label( "" ) ):set { id = "title" }
|
||||
|
||||
local b = self.titleBarContent:addNode( Button( "" ):set( "X", "$parent.width" ) )
|
||||
b:set {
|
||||
|
@ -115,16 +117,12 @@ function Window:__init__( ... )
|
|||
id = "content"
|
||||
} )
|
||||
|
||||
for _, name in pairs( Window.static.proxyMethods ) do
|
||||
self[ name ] = function( self, ... )
|
||||
return self.content[ name ]( self.content, ... )
|
||||
end
|
||||
|
||||
self[ name .. "Raw" ] = function( self, ... )
|
||||
return self.super[ name ]( self, ... )
|
||||
end
|
||||
for _, name in pairs( Window.static.proxyProperties ) do
|
||||
self.content[ name ] = ("$parent.%s"):format( name )
|
||||
end
|
||||
|
||||
self:createProxies()
|
||||
|
||||
self:on("remove", function() self:executeCallbacks "close" end)
|
||||
self:watchProperty( "width", function( _, __, value )
|
||||
return self:updateWidth( value )
|
||||
|
@ -149,18 +147,37 @@ function Window:__postInit__()
|
|||
self.super:__postInit__()
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc For each method in Window.static.proxyMethods, a 'raw' method (ie: addNodeRaw) is created (which performs the action on the window), and a normal method (ie: addNode) is
|
||||
created that performs the action on the 'content' provided (for example, the 'actionContainer' of a DialogWindow)
|
||||
@param <Instance - content>
|
||||
]]
|
||||
function Window:createProxies( content )
|
||||
content = content or self.content
|
||||
for _, name in pairs( Window.static.proxyMethods ) do
|
||||
self[ name ] = function( self, ... )
|
||||
return content[ name ]( content, ... )
|
||||
end
|
||||
|
||||
self[ name .. "Raw" ] = function( self, ... )
|
||||
return self.super[ name ]( self, ... )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Handles a mouse click by checking the location of the click. Depending on the location will either move, resize, focus or unfocus the window.
|
||||
@param <MouseEvent Instance - event>, <boolean - handled>, <boolean - within>
|
||||
]]
|
||||
function Window:onMouseClick( event, handled, within )
|
||||
if within then
|
||||
if not handled and event.button == 1 then
|
||||
local X, Y = event.X - self.X + 1, event.Y - self.Y + 1
|
||||
if within and not ( self.shadow and ( X == self.width or Y == self.height ) ) and not handled then
|
||||
if event.button == 1 then
|
||||
self:focus()
|
||||
self:executeCallbacks "windowFocus"
|
||||
|
||||
local X, Y = event.X - self.X + 1, event.Y - self.Y + 1
|
||||
if self.moveable and Y == 1 and ( X >= 1 and X <= self.titleBarContent.width - ( self.closeable and 1 or 0 ) ) then
|
||||
self:updateMouse( "move", X, Y )
|
||||
event.handled = true
|
||||
|
@ -193,6 +210,14 @@ function Window:onMouseDrag( event, handled, within )
|
|||
self:handleMouseDrag( event, handled, within )
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Redirects attempts to resolve the fluid positions of this Window to the content instead
|
||||
]]
|
||||
function Window:resolveFluidPositions()
|
||||
self.content:resolveFluidPositions()
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Updates the titleBar label to display a correctly truncated version of the windows title.
|
||||
|
@ -357,12 +382,6 @@ configureConstructor {
|
|||
|
||||
moveable = "boolean",
|
||||
|
||||
minWidth = "number",
|
||||
minHeight = "number",
|
||||
|
||||
maxWidth = "number",
|
||||
maxHeight = "number",
|
||||
|
||||
shadow = "boolean",
|
||||
shadowColour = "colour"
|
||||
}
|
||||
|
|
78
src/classes/nodes/sub_classes/TabContainer.ti
Normal file
78
src/classes/nodes/sub_classes/TabContainer.ti
Normal file
|
@ -0,0 +1,78 @@
|
|||
--[[
|
||||
A class specifically designed for the TabbedPageContainer to contain the tabs (and facilitate mouse scrolling, dragging and tab selection)
|
||||
]]
|
||||
|
||||
class TabContainer extends Container mixin MInteractable
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Overrides the Node post constructor to insert height and background colour dynamic values (not possible in main constructor as the MThemeable mixin has not hooked into the instance yet)
|
||||
@param <... - args> - Passed to super __postInit__
|
||||
]]
|
||||
function TabContainer:__postInit__( ... )
|
||||
self.super:__postInit__( ... )
|
||||
|
||||
|
||||
self.height = "$parent.tabHeight or 1"
|
||||
self.backgroundColour = "$parent.tabBackgroundColour"
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc If the mouse event has not been handled and is within this node, the TabContainers interactive mode is set to 'tabScroller'.
|
||||
@param <MouseEvent Instance - event>, <boolean - handled>, <boolean - within>
|
||||
]]
|
||||
function TabContainer:onMouseClick( event, handled, within )
|
||||
if handled or not within then return end
|
||||
self:updateMouse( "tabScroller", event.X + self.parent.tabScroll, event.Y )
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc If the mouse is released, the interactive mode of the node is reset. If the tab scroller was dragged before the release, the active node is deactivated to
|
||||
prevent triggering after dragging tabs.
|
||||
@param <MouseEvent Instance - event>, <boolean - handled>, <boolean - within>
|
||||
]]
|
||||
function TabContainer:onMouseUp( event, handled, within )
|
||||
self:updateMouse( false )
|
||||
if self.dragged then
|
||||
self.dragged = false
|
||||
|
||||
local nodes = self.nodes[ 1 ].nodes
|
||||
for i = 1, #nodes do
|
||||
if nodes[ i ].active then nodes[ i ].active = false; return end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc If the event is not handled and is within this node, the tabs are moved by 5 places in the direction of the scroll.
|
||||
@param <MouseEvent Instance - event>, <boolean - handled>, <boolean - within>
|
||||
]]
|
||||
function TabContainer:onMouseScroll( event, handled, within )
|
||||
if handled or not within then return end
|
||||
self.parent:moveTabs( event.button * 5, true )
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc If the event has not been handled, and the tab containers interactive mode is set the event is passed to the interactive handler (MInteractable:handleMouseDrag)
|
||||
@param <MouseEvent Instance - event>, <boolean - handled>, <boolean - within>
|
||||
]]
|
||||
function TabContainer:onMouseDrag( event, handled, within )
|
||||
if handled and self.mouse then return end
|
||||
|
||||
self:handleMouseDrag( event, handled, within )
|
||||
self.dragged = true
|
||||
end
|
||||
|
||||
--[[
|
||||
@setter
|
||||
@desc When 'tabScroll' is changed, the tabs on the parent are shifted to that position (via absolute)
|
||||
@param <number - tabScroll>
|
||||
]]
|
||||
function TabContainer:setTabScroll( tabScroll )
|
||||
self.tabScroll = tabScroll
|
||||
self.parent:moveTabs( false, true, -tabScroll )
|
||||
end
|
|
@ -62,7 +62,7 @@ end
|
|||
function Lexer:pushToken( token )
|
||||
local tokens = self.tokens
|
||||
|
||||
token.char = self.char
|
||||
token.char = self.char - ( token.value and #token.value or 1 )
|
||||
token.line = self.line
|
||||
tokens[ #tokens + 1 ] = token
|
||||
end
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
A lexer that processes node queries into tokens used by QueryParser
|
||||
]]
|
||||
|
||||
class QueryLexer extends Lexer
|
||||
class QueryLexer extends Lexer {
|
||||
static = {
|
||||
validSymbols = { "==", "<", ">", ">=", "<=", "~=" }
|
||||
}
|
||||
}
|
||||
|
||||
--[[
|
||||
@instance
|
||||
|
@ -26,14 +30,16 @@ function QueryLexer:tokenize()
|
|||
self:consume( 1 )
|
||||
|
||||
self.inCondition = true
|
||||
elseif stream:find "^[%[%]]" then
|
||||
self:throw("Unmatched conditon opening/closing ([ or ])")
|
||||
elseif stream:find "^%," then
|
||||
self:pushToken { type = "QUERY_END", value = self:consumePattern "^%," }
|
||||
elseif stream:find "^>" then
|
||||
self:pushToken { type = "QUERY_DIRECT_PREFIX", value = self:consumePattern "^>" }
|
||||
elseif stream:find "^#[^%s%.#%[%,]*" then
|
||||
self:pushToken { type = "QUERY_ID", value = self:consumePattern "^#([^%s%.#%[]*)" }
|
||||
self:pushToken { type = "QUERY_ID", value = self:consumePattern "^#([^%s%.#%[%,]*)" }
|
||||
elseif stream:find "^%.[^%s#%[%,]*" then
|
||||
self:pushToken { type = "QUERY_CLASS", value = self:consumePattern "^%.([^%s%.#%[]*)" }
|
||||
self:pushToken { type = "QUERY_CLASS", value = self:consumePattern "^%.([^%s#%[%,]*)" }
|
||||
elseif stream:find "^[^,%s#%.%[]*" then
|
||||
self:pushToken { type = "QUERY_TYPE", value = self:consumePattern "^[^,%s#%.%[]*" }
|
||||
else
|
||||
|
@ -60,15 +66,23 @@ function QueryLexer:tokenizeCondition( stream )
|
|||
elseif stream:find "^%w+" then
|
||||
self:pushToken { type = "QUERY_COND_ENTITY", value = self:consumePattern "^%w+" }
|
||||
elseif stream:find "^%," then
|
||||
self:pushToken { type = "QUERY_COND_SEPERATOR" }
|
||||
self:pushToken { type = "QUERY_COND_SEPERATOR", value = "," }
|
||||
self:consume( 1 )
|
||||
elseif stream:find "^#" then
|
||||
self:pushToken { type = "QUERY_COND_MODIFIER", value = "#" }
|
||||
self:consume( 1 )
|
||||
elseif stream:find "^[%p~]+" then
|
||||
self:pushToken { type = "QUERY_COND_SYMBOL", value = self:consumePattern "^[%p~]+" }
|
||||
elseif stream:find "^%]" then
|
||||
self:pushToken { type = "QUERY_COND_CLOSE" }
|
||||
self:pushToken { type = "QUERY_COND_CLOSE", value = "]" }
|
||||
self:consume( 1 )
|
||||
self.inCondition = false
|
||||
else
|
||||
for _, val in pairs( QueryLexer.static.validSymbols ) do
|
||||
if stream:find( ("^%s"):format( val ) ) then
|
||||
self:pushToken { type = "QUERY_COND_SYMBOL", value = self:consumePattern( ( "^%s" ):format( val ) ) }
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
self:throw("Invalid condition syntax. Expected property near '"..tostring( stream:match "%S*" ).."'")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -110,7 +110,7 @@ function DynamicEqParser:resolveStacks( target, allowFailure )
|
|||
else self:throw("Invalid stack start '"..stackStart.."'. Only self, parent and application allowed") end
|
||||
|
||||
for p = 2, #stack - 1 do
|
||||
if not instancePoint then self:throw("Failed to resolve stacks. Index '"..stack[ p ].."' could not be accessed on '"..tostring( instancePoint ).."'") end
|
||||
if not instancePoint then if allowFailure then return end self:throw("Failed to resolve stacks. Index '"..stack[ p ].."' could not be accessed on '"..tostring( instancePoint ).."'") end
|
||||
instancePoint = instancePoint[ stack[ p ] ]
|
||||
end
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
local function parseValue( val )
|
||||
if val == "true" then return true
|
||||
elseif val == "false" then return false end
|
||||
|
||||
return tonumber( val ) or error("Invalid value passed for parsing '"..tostring( val ).."'")
|
||||
elseif val == "false" then return false
|
||||
else return tonumber( val ) or tostring( val ) end
|
||||
end
|
||||
|
||||
--[[
|
||||
|
@ -98,21 +97,41 @@ function QueryParser:parseCondition()
|
|||
|
||||
local token = self:stepForward()
|
||||
while true do
|
||||
if token.type == "QUERY_COND_ENTITY" and ( condition.symbol or not condition.property ) then
|
||||
condition[ condition.symbol and "value" or "property" ] = condition.symbol and parseValue( token.value ) or token.value
|
||||
if token.type == "QUERY_COND_ENTITY" then
|
||||
if condition.symbol then
|
||||
if condition.value then
|
||||
self:throw( "Unexpected entity '"..tostring( token.value ).."'. Expected end of condition block, or condition seperator preceding another condition" )
|
||||
else
|
||||
condition.value = parseValue( token.value )
|
||||
end
|
||||
else
|
||||
if condition.property then
|
||||
self:throw( "Unexpected entity '"..tostring( token.value ).."'. Expected operator symbol" )
|
||||
else
|
||||
condition.property = token.value
|
||||
end
|
||||
end
|
||||
elseif token.type == "QUERY_COND_STRING_ENTITY" and condition.symbol then
|
||||
condition.value = token.value
|
||||
elseif token.type == "QUERY_COND_SYMBOL" and not condition.property and token.value == "#" then
|
||||
elseif token.type == "QUERY_COND_MODIFIER" and not condition.property then
|
||||
condition.modifier = token.value
|
||||
elseif token.type == "QUERY_COND_SYMBOL" and ( condition.property ) then
|
||||
if condition.symbol then
|
||||
self:throw( "Unexpected symbol '"..tostring( token.value ).."." )
|
||||
end
|
||||
|
||||
condition.symbol = token.value
|
||||
elseif token.type == "QUERY_COND_SEPERATOR" and next( condition ) then
|
||||
if not ( condition.property and condition.value and condition.symbol ) then
|
||||
self:throw "No valid condition was found before ',' (Unexpected query condition seperator)"
|
||||
end
|
||||
|
||||
conditions[ #conditions + 1 ] = condition
|
||||
condition = {}
|
||||
elseif token.type == "QUERY_COND_CLOSE" and ( not condition.property or ( condition.property and condition.value ) ) then
|
||||
elseif token.type == "QUERY_COND_CLOSE" and ( not condition.property or ( condition.property and condition.value and condition.symbol ) ) then
|
||||
break
|
||||
else
|
||||
self:throw( "Unexpected '"..token.value.."' inside of condition block" )
|
||||
self:throw( "Unexpected '" .. tostring( token.value ) .. "' (" .. token.type .. ") inside of condition block" )
|
||||
end
|
||||
|
||||
token = self:stepForward()
|
||||
|
@ -120,6 +139,8 @@ function QueryParser:parseCondition()
|
|||
|
||||
if next( condition ) then
|
||||
conditions[ #conditions + 1 ] = condition
|
||||
elseif #conditions == 0 then
|
||||
self:throw "Unexpected ']'. No conditions were defined (empty condition block)"
|
||||
end
|
||||
|
||||
return #conditions > 0 and conditions or nil
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
]]
|
||||
|
||||
abstract class MDialogManager {
|
||||
dialogs = {};
|
||||
dialogContainer = false;
|
||||
}
|
||||
|
||||
|
@ -14,17 +13,28 @@ abstract class MDialogManager {
|
|||
@desc Creates the dialogContainer (OverlayContainer) and sets important dynamic values on it
|
||||
]]
|
||||
function MDialogManager:MDialogManager()
|
||||
self.dialogContainer = self:addNode( OverlayContainer() ):set {
|
||||
id = "application_dialog_overlay",
|
||||
visible = "$parent.isDialogOpen",
|
||||
enabled = "$self.visible",
|
||||
-- Create a node that is *loosely* linked to the application (not inside app.nodes, or added via :addNode but does reference it via `cont.application`).
|
||||
self.dialogContainer = OverlayContainer():set {
|
||||
id = "application_dialog_container",
|
||||
application = self,
|
||||
parent = self,
|
||||
|
||||
width = "$application.width",
|
||||
height = "$application.height",
|
||||
consumeWhenDisabled = false,
|
||||
Z = 10000
|
||||
height = "$application.height"
|
||||
}
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Draw the dialog window container to the application canvas, typically used after drawing nodes to the TermCanvas
|
||||
]]
|
||||
function MDialogManager:drawDialogs()
|
||||
local container = self.dialogContainer
|
||||
|
||||
container:draw()
|
||||
container.canvas:drawTo( self.canvas, 1, 1, container.shader, container.shadeText, container.shadeBackground )
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc Adds a dialog window (Window instance) to the application and assigns important binds to it (when closed or focused this mixin handles it)
|
||||
|
@ -43,6 +53,7 @@ function MDialogManager:addDialog( dialog )
|
|||
end)
|
||||
|
||||
self.isDialogOpen = true
|
||||
return dialog
|
||||
end
|
||||
|
||||
--[[
|
||||
|
@ -66,6 +77,7 @@ function MDialogManager:removeDialog( dialog )
|
|||
self.dialogContainer:removeNode( dialog )
|
||||
|
||||
self.isDialogOpen = #self.dialogContainer.nodes > 0
|
||||
return dialog
|
||||
end
|
||||
|
||||
configureConstructor {
|
||||
|
|
101
src/mixins/MFluidLayout.ti
Normal file
101
src/mixins/MFluidLayout.ti
Normal file
|
@ -0,0 +1,101 @@
|
|||
local function max( a, b ) return a > b and a or b end
|
||||
local function min( a, b ) return a < b and a or b end
|
||||
|
||||
--[[
|
||||
@instance fluidPositioning - boolean (def. false) - When true this node will resolve fluid positioning (X/Y), using the child nodes position, margin, and alignment properties.
|
||||
@instance fluidDimensions - boolean (def. false) - When true this node will change the width and height of the node based on the content inside. The dimensions are limited to (min/max)Width/Height *if* set (if false, the limit is not used).
|
||||
@instance minWidth - number (def. 1) - Defines the minimum width that can be set when using fluidDimensions (see fluidDimensions property).
|
||||
@instance maxWidth - number (def. false) - Defines the maximum width that can be set when using fluidDimensions (see fluidDimensions property).
|
||||
@instance minHeight - number (def. 1) - Defines the minimum height that can be set when using fluidDimensions (see fluidDimensions property).
|
||||
@instance maxHeight - number (def. false) - Defines the maximum height that can be set when using fluidDimensions (see fluidDimensions property).
|
||||
|
||||
****************************************************************************
|
||||
** **
|
||||
** Features provided by this class should be considered unstable and **
|
||||
** avoided to ensure reliable execution of Titanium based applications **
|
||||
** **
|
||||
****************************************************************************
|
||||
* Optimisation is lacking inside this class -- Performance may be impacted *
|
||||
****************************************************************************
|
||||
]]
|
||||
|
||||
abstract class MFluidLayout {
|
||||
fluidPositions = false;
|
||||
fluidDimensions = false;
|
||||
|
||||
minWidth = 1;
|
||||
maxWidth = false;
|
||||
|
||||
minHeight = 1;
|
||||
maxHeight = false;
|
||||
|
||||
positionChanged = false;
|
||||
}
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc WIP
|
||||
]]
|
||||
function MFluidLayout:resolveFluidPositions()
|
||||
local nodes = self.nodes
|
||||
local X, Y, rowHeight, prevNodeMarginRight, rowCount, maxRowWidth = 1, 1, 0, 0, 0, 0
|
||||
local maxWidth = not self.fluidDimensions and self.width or self.maxWidth
|
||||
|
||||
for i = 1, #nodes do
|
||||
local currentNode = nodes[ i ]
|
||||
if currentNode.id == "TESTING" then error(tostring( self ) .. " (ID: "..tostring( self.id )..") is resolving", 3) end
|
||||
|
||||
if ( not currentNode.positioning and self.positioning == "fluid" ) or currentNode.positioning == "fluid" then
|
||||
if maxWidth and X + currentNode.marginLeft + currentNode.width + 1 > maxWidth then
|
||||
maxRowWidth = max( maxRowWidth, X )
|
||||
|
||||
X, Y = 1, Y + rowHeight
|
||||
rowHeight, prevNodeMarginRight = 0, 0
|
||||
rowCount = rowCount + 1
|
||||
end
|
||||
|
||||
currentNode.X = X + currentNode.marginLeft + prevNodeMarginRight
|
||||
currentNode.Y = Y + currentNode.marginTop
|
||||
|
||||
prevNodeMarginRight = currentNode.marginRight
|
||||
X = currentNode.X + currentNode.width
|
||||
|
||||
rowHeight = max( rowHeight, currentNode.marginTop + currentNode.height + currentNode.marginBottom )
|
||||
end
|
||||
end
|
||||
|
||||
if self.fluidDimensions then
|
||||
local row = Y + rowHeight - 1
|
||||
|
||||
self.width = max( self.minWidth, self.maxWidth and rowCount ~= 0 and min( self.maxWidth, maxRowWidth ) or X - 1 )
|
||||
self.height = max( self.minHeight, self.maxHeight and min( self.maxHeight, row ) or row )
|
||||
end
|
||||
|
||||
self.positionChanged = false
|
||||
end
|
||||
|
||||
--[[
|
||||
@instance
|
||||
@desc
|
||||
]]
|
||||
function MFluidLayout:setPositionChanged( changed )
|
||||
-- self.changed = true
|
||||
self.positionChanged = changed
|
||||
|
||||
local nodes = self.collatedNodes
|
||||
for i = 1, #nodes do
|
||||
nodes[ i ].positionChanged = changed
|
||||
end
|
||||
end
|
||||
|
||||
configureConstructor {
|
||||
argumentTypes = {
|
||||
minWidth = "number",
|
||||
minHeight = "number",
|
||||
maxWidth = "number",
|
||||
maxHeight = "number",
|
||||
fluidDimensions = "boolean",
|
||||
fluidPositions = "boolean",
|
||||
positionChanged = "boolean"
|
||||
}
|
||||
}
|
|
@ -8,7 +8,8 @@ abstract class MInteractable {
|
|||
static = {
|
||||
properties = {
|
||||
move = { "X", "Y" },
|
||||
resize = { "width", "height" }
|
||||
resize = { "width", "height" },
|
||||
tabScroller = { "tabScroll" }
|
||||
},
|
||||
|
||||
callbacks = {
|
||||
|
@ -57,7 +58,11 @@ function MInteractable:handleMouseDrag( eventObj, handled, within )
|
|||
local props = MInteractable.static.properties[ mouse[ 1 ] ]
|
||||
if not props then return end
|
||||
|
||||
self[ props[ 1 ] ], self[ props[ 2 ] ] = eventObj.X - mouse[ 2 ] + 1, eventObj.Y - mouse[ 3 ] + 1
|
||||
self[ props[ 1 ] ] = eventObj.X - mouse[ 2 ] + 1
|
||||
if props[ 2 ] then
|
||||
self[ props[ 2 ] ] = eventObj.Y - mouse[ 3 ] + 1
|
||||
end
|
||||
|
||||
|
||||
eventObj.handled = true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ local function resetNode( self, node )
|
|||
self:clearCollatedNodes()
|
||||
end
|
||||
|
||||
abstract class MNodeContainer {
|
||||
abstract class MNodeContainer mixin MFluidLayout {
|
||||
nodes = {}
|
||||
}
|
||||
|
||||
|
|
|
@ -88,12 +88,16 @@ end
|
|||
@desc Calls :retrieveThemes on the child nodes, meaning they will re-fetch their rules from the manager after clearing any current ones.
|
||||
]]
|
||||
function MThemeManager:dispatchThemeRules()
|
||||
local nodes = self.collatedNodes
|
||||
local function dispatchAndYield( targets )
|
||||
themeYield()
|
||||
for i = 1, #targets do
|
||||
if os.clock() - t > 8 then themeYield() end
|
||||
|
||||
themeYield()
|
||||
for i = 1, #nodes do
|
||||
if os.clock() - t > 8 then themeYield() end
|
||||
|
||||
nodes[ i ]:retrieveThemes()
|
||||
targets[ i ]:retrieveThemes()
|
||||
end
|
||||
end
|
||||
|
||||
dispatchAndYield( self.collatedNodes )
|
||||
self.dialogContainer:retrieveThemes() -- Pretty sure I need this... Not certain, I'll leave it here for now.
|
||||
dispatchAndYield( self.dialogContainer.collatedNodes )
|
||||
end
|
Loading…
Reference in a new issue