...
 
Commits (43)
......@@ -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>
......
<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 ]
......
--[[
@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
......
--[[
@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
end
page.isPositionTemporary = true
page.raw.position = currentIndex
end
page:updatePosition()
page:updatePosition()
end
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"
}
}
\ No newline at end of file
}
......@@ -298,6 +298,8 @@ end
--[[ Caching Functions ]]--
function ScrollContainer:cacheContent()
if self.positionChanged and self.fluidPositions then self:resolveFluidPositions() end
self:cacheContentSize()
self:cacheActiveScrollbars()
......
--[[
@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"
}
......
--[[
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
......
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 {
......
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
\ No newline at end of file
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
\ No newline at end of file