mirror of
				https://github.com/kepler155c/opus
				synced 2025-10-31 15:43:00 +00:00 
			
		
		
		
	Ui enhancements 2.0 (#31)
* canvas overhaul * minor tweaks * list mode for overview * bugfixes + tweaks for editor 2.0 * minor tweaks * more editor work * refactor + new transitions * use layout() where appropriate and cleanup * mouse triple click + textEntry scroll ind * cleanup * cleanup + theme editor * color rework + cleanup * changes for deprecated ui methods * can now use named colors
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,2 @@ | ||||
| /ignore | ||||
| .project | ||||
|   | ||||
| @@ -82,16 +82,60 @@ local Browser = UI.Page { | ||||
| 		}, | ||||
| 		sortColumn = 'name', | ||||
| 		y = 2, ey = -2, | ||||
| 		sortCompare = function(self, a, b) | ||||
| 			if self.sortColumn == 'fsize' then | ||||
| 				return a.size < b.size | ||||
| 			elseif self.sortColumn == 'flags' then | ||||
| 				return a.flags < b.flags | ||||
| 			end | ||||
| 			if a.isDir == b.isDir then | ||||
| 				return a.name:lower() < b.name:lower() | ||||
| 			end | ||||
| 			return a.isDir | ||||
| 		end, | ||||
| 		getRowTextColor = function(_, file) | ||||
| 			if file.marked then | ||||
| 				return colors.green | ||||
| 			end | ||||
| 			if file.isDir then | ||||
| 				return colors.cyan | ||||
| 			end | ||||
| 			if file.isReadOnly then | ||||
| 				return colors.pink | ||||
| 			end | ||||
| 			return colors.white | ||||
| 		end, | ||||
| 		eventHandler = function(self, event) | ||||
| 			if event.type == 'copy' then -- let copy be handled by parent | ||||
| 				return false | ||||
| 			end | ||||
| 			return UI.ScrollingGrid.eventHandler(self, event) | ||||
| 		end | ||||
| 	}, | ||||
| 	statusBar = UI.StatusBar { | ||||
| 		columns = { | ||||
| 			{ key = 'status'               }, | ||||
| 			{ key = 'totalSize', width = 6 }, | ||||
| 		}, | ||||
| 		draw = function(self) | ||||
| 			if self.parent.dir then | ||||
| 				local info = '#:' .. Util.size(self.parent.dir.files) | ||||
| 				local numMarked = Util.size(marked) | ||||
| 				if numMarked > 0 then | ||||
| 					info = info .. ' M:' .. numMarked | ||||
| 				end | ||||
| 				self:setValue('info', info) | ||||
| 				self:setValue('totalSize', formatSize(self.parent.dir.totalSize)) | ||||
| 				UI.StatusBar.draw(self) | ||||
| 			end | ||||
| 		end, | ||||
| 	}, | ||||
| 	question = UI.Question { | ||||
| 		y = -2, x = -19, | ||||
| 		label = 'Delete', | ||||
| 	}, | ||||
| 	notification = UI.Notification { }, | ||||
| 	associations = UI.SlideOut { | ||||
| 		backgroundColor = colors.cyan, | ||||
| 		menuBar = UI.MenuBar { | ||||
| 			buttons = { | ||||
| 				{ text = 'Save',    event = 'save'    }, | ||||
| @@ -99,7 +143,7 @@ local Browser = UI.Page { | ||||
| 			}, | ||||
| 		}, | ||||
| 		grid = UI.ScrollingGrid { | ||||
| 			x = 2, ex = -6, y = 3, ey = -5, | ||||
| 			x = 2, ex = -6, y = 3, ey = -8, | ||||
| 			columns = { | ||||
| 				{ heading = 'Extension', key = 'name'  }, | ||||
| 				{ heading = 'Program',   key = 'value' }, | ||||
| @@ -114,8 +158,11 @@ local Browser = UI.Page { | ||||
| 			x = -4, y = 6, | ||||
| 			text = '-', event = 'remove_entry', help = 'Remove', | ||||
| 		}, | ||||
| 		[1] = UI.Window { | ||||
| 			x = 2, y = -6, ex = -6, ey = -3, | ||||
| 		}, | ||||
| 		form = UI.Form { | ||||
| 			x = 3, y = -3, ey = -2, | ||||
| 			x = 3, y = -5, ex = -7, ey = -3, | ||||
| 			margin = 1, | ||||
| 			manualControls = true, | ||||
| 			[1] = UI.TextEntry { | ||||
| @@ -137,9 +184,7 @@ local Browser = UI.Page { | ||||
| 				text = 'Add', event = 'add_association', | ||||
| 			}, | ||||
| 		}, | ||||
| 		statusBar = UI.StatusBar { | ||||
| 			backgroundColor = colors.cyan, | ||||
| 		}, | ||||
| 		statusBar = UI.StatusBar { }, | ||||
| 	}, | ||||
| 	accelerators = { | ||||
| 		[ 'control-q' ] = 'quit', | ||||
| @@ -175,51 +220,6 @@ function Browser.menuBar:getActive(menuItem) | ||||
| 	return true | ||||
| end | ||||
|  | ||||
| function Browser.grid:sortCompare(a, b) | ||||
| 	if self.sortColumn == 'fsize' then | ||||
| 		return a.size < b.size | ||||
| 	elseif self.sortColumn == 'flags' then | ||||
| 		return a.flags < b.flags | ||||
| 	end | ||||
| 	if a.isDir == b.isDir then | ||||
| 		return a.name:lower() < b.name:lower() | ||||
| 	end | ||||
| 	return a.isDir | ||||
| end | ||||
|  | ||||
| function Browser.grid:getRowTextColor(file) | ||||
| 	if file.marked then | ||||
| 		return colors.green | ||||
| 	end | ||||
| 	if file.isDir then | ||||
| 		return colors.cyan | ||||
| 	end | ||||
| 	if file.isReadOnly then | ||||
| 		return colors.pink | ||||
| 	end | ||||
| 	return colors.white | ||||
| end | ||||
|  | ||||
| function Browser.grid:eventHandler(event) | ||||
| 	if event.type == 'copy' then -- let copy be handled by parent | ||||
| 		return false | ||||
| 	end | ||||
| 	return UI.ScrollingGrid.eventHandler(self, event) | ||||
| end | ||||
|  | ||||
| function Browser.statusBar:draw() | ||||
| 	if self.parent.dir then | ||||
| 		local info = '#:' .. Util.size(self.parent.dir.files) | ||||
| 		local numMarked = Util.size(marked) | ||||
| 		if numMarked > 0 then | ||||
| 			info = info .. ' M:' .. numMarked | ||||
| 		end | ||||
| 		self:setValue('info', info) | ||||
| 		self:setValue('totalSize', formatSize(self.parent.dir.totalSize)) | ||||
| 		UI.StatusBar.draw(self) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Browser:setStatus(status, ...) | ||||
| 	self.notification:info(string.format(status, ...)) | ||||
| end | ||||
| @@ -255,7 +255,6 @@ function Browser:getDirectory(directory) | ||||
| end | ||||
|  | ||||
| function Browser:updateDirectory(dir) | ||||
|  | ||||
| 	dir.size = 0 | ||||
| 	dir.totalSize = 0 | ||||
| 	Util.clear(dir.files) | ||||
| @@ -344,7 +343,7 @@ function Browser:eventHandler(event) | ||||
| 	local file = self.grid:getSelected() | ||||
|  | ||||
| 	if event.type == 'quit' then | ||||
| 		Event.exitPullEvents() | ||||
| 		UI:quit() | ||||
|  | ||||
| 	elseif event.type == 'edit' and file then | ||||
| 		self:run('edit', file.name) | ||||
| @@ -432,28 +431,25 @@ function Browser:eventHandler(event) | ||||
|  | ||||
| 	elseif event.type == 'delete' then | ||||
| 		if self:hasMarked() then | ||||
| 			local width = self.statusBar:getColumnWidth('status') | ||||
| 			self.statusBar:setColumnWidth('status', UI.term.width) | ||||
| 			self.statusBar:setValue('status', 'Delete marked? (y/n)') | ||||
| 			self.statusBar:draw() | ||||
| 			self.statusBar:sync() | ||||
| 			local _, ch = os.pullEvent('char') | ||||
| 			if ch == 'y' or ch == 'Y' then | ||||
| 				for _,m in pairs(marked) do | ||||
| 					pcall(function() | ||||
| 						fs.delete(m.fullName) | ||||
| 					end) | ||||
| 				end | ||||
| 			end | ||||
| 			marked = { } | ||||
| 			self.statusBar:setColumnWidth('status', width) | ||||
| 			self.statusBar:setValue('status', '/' .. self.dir.name) | ||||
| 			self:updateDirectory(self.dir) | ||||
|  | ||||
| 			self.statusBar:draw() | ||||
| 			self.grid:draw() | ||||
| 			self:setFocus(self.grid) | ||||
| 			self.question:show() | ||||
| 		end | ||||
| 		return true | ||||
|  | ||||
| 	elseif event.type == 'question_yes' then | ||||
| 		for _,m in pairs(marked) do | ||||
| 			pcall(fs.delete, m.fullName) | ||||
| 		end | ||||
| 		marked = { } | ||||
| 		self:updateDirectory(self.dir) | ||||
|  | ||||
| 		self.question:hide() | ||||
| 		self.statusBar:draw() | ||||
| 		self.grid:draw() | ||||
| 		self:setFocus(self.grid) | ||||
|  | ||||
| 	elseif event.type == 'question_no' then | ||||
| 		self.question:hide() | ||||
| 		self:setFocus(self.grid) | ||||
|  | ||||
| 	elseif event.type == 'copy' or event.type == 'cut' then | ||||
| 		if self:hasMarked() then | ||||
| @@ -549,6 +545,4 @@ local args = Util.parse(...) | ||||
| Browser:setDir(args[1] or shell.dir()) | ||||
|  | ||||
| UI:setPage(Browser) | ||||
|  | ||||
| Event.pullEvents() | ||||
| UI.term:reset() | ||||
| UI:start() | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
| local help   = _G.help | ||||
|  | ||||
| UI:configure('Help', ...) | ||||
| @@ -12,11 +11,11 @@ for _,topic in pairs(help.topics()) do | ||||
| end | ||||
|  | ||||
| UI:addPage('main', UI.Page { | ||||
| 	labelText = UI.Text { | ||||
| 	UI.Text { | ||||
| 		x = 3, y = 2, | ||||
| 		value = 'Search', | ||||
| 	}, | ||||
| 	filter = UI.TextEntry { | ||||
| 	UI.TextEntry { | ||||
| 		x = 10, y = 2, ex = -3, | ||||
| 		limit = 32, | ||||
| 	}, | ||||
| @@ -38,9 +37,7 @@ UI:addPage('main', UI.Page { | ||||
|  | ||||
| 		elseif event.type == 'grid_select' then | ||||
| 			if self.grid:getSelected() then | ||||
| 				local name = self.grid:getSelected().name | ||||
|  | ||||
| 				UI:setPage('topic', name) | ||||
| 				UI:setPage('topic', self.grid:getSelected().name) | ||||
| 			end | ||||
|  | ||||
| 		elseif event.type == 'text_change' then | ||||
| @@ -57,6 +54,7 @@ UI:addPage('main', UI.Page { | ||||
| 			self.grid:update() | ||||
| 			self.grid:setIndex(1) | ||||
| 			self.grid:draw() | ||||
|  | ||||
| 		else | ||||
| 			return UI.Page.eventHandler(self, event) | ||||
| 		end | ||||
| @@ -64,13 +62,12 @@ UI:addPage('main', UI.Page { | ||||
| }) | ||||
|  | ||||
| UI:addPage('topic', UI.Page { | ||||
| 	backgroundColor = colors.black, | ||||
| 	backgroundColor = 'black', | ||||
| 	titleBar = UI.TitleBar { | ||||
| 		title = 'text', | ||||
| 		event = 'back', | ||||
| 	}, | ||||
| 	helpText = UI.TextArea { | ||||
| 		backgroundColor = colors.black, | ||||
| 		x = 2, ex = -1, y = 3, ey = -2, | ||||
| 	}, | ||||
| 	accelerators = { | ||||
|   | ||||
| @@ -58,11 +58,16 @@ local page = UI.Page { | ||||
| 		}, | ||||
| 		[2] = UI.Tab { | ||||
| 			tabTitle = 'Output', | ||||
| 			backgroundColor = 'black', | ||||
| 			output = UI.Embedded { | ||||
| 				visible = true, | ||||
| 				y = 2, | ||||
| 				maxScroll = 1000, | ||||
| 				backgroundColor = colors.black, | ||||
| 				backgroundColor = 'black', | ||||
| 			}, | ||||
| 			draw = function(self) | ||||
| 				self:write(1, 1, string.rep('\131', self.width), 'black', 'primary') | ||||
| 				self:drawChildren() | ||||
| 			end, | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
| @@ -157,7 +162,7 @@ function page:eventHandler(event) | ||||
| 		local sz = #value | ||||
| 		local pos = self.prompt.entry.pos | ||||
| 		self:setPrompt(autocomplete(sandboxEnv, value, self.prompt.entry.pos)) | ||||
| 		self.prompt:setPosition(pos + #value - sz) | ||||
| 		self.prompt:setPosition(pos + #(self.prompt.value or '') - sz) | ||||
| 		self.prompt:updateCursor() | ||||
|  | ||||
| 	elseif event.type == 'device' then | ||||
| @@ -196,7 +201,6 @@ function page:eventHandler(event) | ||||
| 			command = nil | ||||
| 			self.grid:setValues(t) | ||||
| 			self.grid:setIndex(1) | ||||
| 			self.grid:adjustWidth() | ||||
| 			self:draw() | ||||
| 		end | ||||
| 		return true | ||||
| @@ -243,7 +247,6 @@ function page:setResult(result) | ||||
| 	end | ||||
| 	self.grid:setValues(t) | ||||
| 	self.grid:setIndex(1) | ||||
| 	self.grid:adjustWidth() | ||||
| 	self:draw() | ||||
| end | ||||
|  | ||||
| @@ -373,7 +376,7 @@ function page:executeStatement(statement) | ||||
| 	end | ||||
|  | ||||
| 	if _exit then | ||||
| 		UI:exitPullEvents() | ||||
| 		UI:quit() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| @@ -382,7 +385,8 @@ if args[1] then | ||||
| 	command = 'args[1]' | ||||
| 	sandboxEnv.args = args | ||||
| 	page:setResult(args[1]) | ||||
| 	page:setPrompt(command) | ||||
| end | ||||
|  | ||||
| UI:setPage(page) | ||||
| UI:pullEvents() | ||||
| UI:start() | ||||
|   | ||||
| @@ -4,7 +4,6 @@ local Socket = require('opus.socket') | ||||
| local UI     = require('opus.ui') | ||||
| local Util   = require('opus.util') | ||||
|  | ||||
| local colors     = _G.colors | ||||
| local device     = _G.device | ||||
| local network    = _G.network | ||||
| local os         = _G.os | ||||
| @@ -56,6 +55,31 @@ local page = UI.Page { | ||||
| 		columns = gridColumns, | ||||
| 		sortColumn = 'label', | ||||
| 		autospace = true, | ||||
| 		getRowTextColor = function(self, row, selected) | ||||
| 			if not row.active then | ||||
| 				return 'lightGray' | ||||
| 			end | ||||
| 			return UI.Grid.getRowTextColor(self, row, selected) | ||||
| 		end, | ||||
| 		getDisplayValues = function(_, row) | ||||
| 			row = Util.shallowCopy(row) | ||||
| 			if row.uptime then | ||||
| 				if row.uptime < 60 then | ||||
| 					row.uptime = string.format("%ds", math.floor(row.uptime)) | ||||
| 				elseif row.uptime < 3600 then | ||||
| 					row.uptime = string.format("%sm", math.floor(row.uptime / 60)) | ||||
| 				else | ||||
| 					row.uptime = string.format("%sh", math.floor(row.uptime / 3600)) | ||||
| 				end | ||||
| 			end | ||||
| 			if row.fuel then | ||||
| 				row.fuel = row.fuel > 0 and Util.toBytes(row.fuel) or '' | ||||
| 			end | ||||
| 			if row.distance then | ||||
| 				row.distance = Util.toBytes(Util.round(row.distance, 1)) | ||||
| 			end | ||||
| 			return row | ||||
| 		end, | ||||
| 	}, | ||||
| 	ports = UI.SlideOut { | ||||
| 		titleBar = UI.TitleBar { | ||||
| @@ -72,17 +96,22 @@ local page = UI.Page { | ||||
| 			sortColumn = 'port', | ||||
| 			autospace = true, | ||||
| 		}, | ||||
| 		eventHandler = function(self, event) | ||||
| 			if event.type == 'grid_select' then | ||||
| 				shell.openForegroundTab('Sniff ' .. event.selected.port) | ||||
| 			end | ||||
| 			return UI.SlideOut.eventHandler(self, event) | ||||
| 		end, | ||||
| 	}, | ||||
| 	help = UI.SlideOut { | ||||
| 		backgroundColor = colors.cyan, | ||||
| 		x = 5, ex = -5, height = 8, y = -8, | ||||
| 		titleBar = UI.TitleBar { | ||||
| 			title = 'Network Help', | ||||
| 			event = 'slide_hide', | ||||
| 		}, | ||||
| 		text = UI.TextArea { | ||||
| 			x = 2, y = 2, | ||||
| 			backgroundColor = colors.cyan, | ||||
| 			x = 1, y = 2, | ||||
| 			marginLeft = 1, | ||||
| 			value = [[ | ||||
|  | ||||
| In order to connect to another computer: | ||||
| @@ -127,13 +156,6 @@ local function sendCommand(host, command) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function page.ports:eventHandler(event) | ||||
| 	if event.type == 'grid_select' then | ||||
| 		shell.openForegroundTab('Sniff ' .. event.selected.port) | ||||
| 	end | ||||
| 	return UI.SlideOut.eventHandler(self, event) | ||||
| end | ||||
|  | ||||
| function page.ports.grid:update() | ||||
| 	local transport = network:getTransport() | ||||
|  | ||||
| @@ -230,7 +252,7 @@ function page:eventHandler(event) | ||||
| 		Config.update('network', config) | ||||
|  | ||||
| 	elseif event.type == 'quit' then | ||||
| 		Event.exitPullEvents() | ||||
| 		UI:quit() | ||||
| 	end | ||||
| 	UI.Page.eventHandler(self, event) | ||||
| end | ||||
| @@ -243,33 +265,6 @@ function page.menuBar:getActive(menuItem) | ||||
| 	return menuItem.noCheck or not not t | ||||
| end | ||||
|  | ||||
| function page.grid:getRowTextColor(row, selected) | ||||
| 	if not row.active then | ||||
| 		return colors.lightGray | ||||
| 	end | ||||
| 	return UI.Grid.getRowTextColor(self, row, selected) | ||||
| end | ||||
|  | ||||
| function page.grid:getDisplayValues(row) | ||||
| 	row = Util.shallowCopy(row) | ||||
| 	if row.uptime then | ||||
| 		if row.uptime < 60 then | ||||
| 			row.uptime = string.format("%ds", math.floor(row.uptime)) | ||||
| 		elseif row.uptime < 3600 then | ||||
| 			row.uptime = string.format("%sm", math.floor(row.uptime / 60)) | ||||
| 		else | ||||
| 			row.uptime = string.format("%sh", math.floor(row.uptime / 3600)) | ||||
| 		end | ||||
| 	end | ||||
| 	if row.fuel then | ||||
| 		row.fuel = row.fuel > 0 and Util.toBytes(row.fuel) or '' | ||||
| 	end | ||||
| 	if row.distance then | ||||
| 		row.distance = Util.toBytes(Util.round(row.distance, 1)) | ||||
| 	end | ||||
| 	return row | ||||
| end | ||||
|  | ||||
| Event.onInterval(1, function() | ||||
| 	page.grid:update() | ||||
| 	page.grid:draw() | ||||
| @@ -295,4 +290,4 @@ if not device.wireless_modem then | ||||
| end | ||||
|  | ||||
| UI:setPage(page) | ||||
| UI:pullEvents() | ||||
| UI:start() | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| local Alt      = require('opus.alternate') | ||||
| local Array    = require('opus.array') | ||||
| local class    = require('opus.class') | ||||
| local Config   = require('opus.config') | ||||
| local Event    = require('opus.event') | ||||
| @@ -9,7 +10,6 @@ local Tween    = require('opus.ui.tween') | ||||
| local UI       = require('opus.ui') | ||||
| local Util     = require('opus.util') | ||||
|  | ||||
| local colors     = _G.colors | ||||
| local device     = _G.device | ||||
| local fs         = _G.fs | ||||
| local os         = _G.os | ||||
| @@ -18,6 +18,12 @@ local shell      = _ENV.shell | ||||
| local term       = _G.term | ||||
| local turtle     = _G.turtle | ||||
|  | ||||
| --[[ | ||||
| 	turtle: 39x13 | ||||
| 	computer: 51x19 | ||||
| 	pocket: 26x20 | ||||
| ]] | ||||
|  | ||||
| if not _ENV.multishell then | ||||
| 	error('multishell is required') | ||||
| end | ||||
| @@ -26,6 +32,9 @@ local REGISTRY_DIR = 'usr/.registry' | ||||
| local DEFAULT_ICON = NFT.parse("\0308\0317\153\153\153\153\153\ | ||||
| \0307\0318\153\153\153\153\153\ | ||||
| \0308\0317\153\153\153\153\153") | ||||
| local TRANS_ICON = NFT.parse("\0302\0312\32\32\32\32\32\ | ||||
| \0302\0312\32\32\32\32\32\ | ||||
| \0302\0312\32\32\32\32\32") | ||||
|  | ||||
| -- overview | ||||
| local uid = _ENV.multishell.getCurrent() | ||||
| @@ -65,6 +74,7 @@ local function parseIcon(iconText) | ||||
| 			if icon.height > 3 or icon.width > 8 then | ||||
| 				error('Must be an NFT image - 3 rows, 8 cols max') | ||||
| 			end | ||||
| 			NFT.transparency(icon) | ||||
| 		end | ||||
| 		return icon | ||||
| 	end) | ||||
| @@ -76,45 +86,38 @@ local function parseIcon(iconText) | ||||
| 	return s, m | ||||
| end | ||||
|  | ||||
| UI.VerticalTabBar = class(UI.TabBar) | ||||
| function UI.VerticalTabBar:setParent() | ||||
| 	self.x = 1 | ||||
| 	self.width = 8 | ||||
| 	self.height = nil | ||||
| 	self.ey = -2 | ||||
| 	UI.TabBar.setParent(self) | ||||
| 	for k,c in pairs(self.children) do | ||||
| 		c.x = 1 | ||||
| 		c.y = k + 1 | ||||
| 		c.ox, c.oy = c.x, c.y | ||||
| 		c.ow = 8 | ||||
| 		c.width = 8 | ||||
| 	end | ||||
| end | ||||
|  | ||||
| local cx = 9 | ||||
| local cy = 1 | ||||
|  | ||||
| local page = UI.Page { | ||||
| 	container = UI.Viewport { | ||||
| 		x = cx, | ||||
| 		y = cy, | ||||
| 		x = 9, y = 1, | ||||
| 	}, | ||||
| 	tabBar = UI.TabBar { | ||||
| 		ey = -2, | ||||
| 		width = 8, | ||||
| 		selectedBackgroundColor = 'primary', | ||||
| 		backgroundColor = 'tertiary', | ||||
| 		layout = function(self) | ||||
| 			self.height = nil | ||||
| 			UI.TabBar.layout(self) | ||||
| 		end, | ||||
| 	}, | ||||
| 	tray = UI.Window { | ||||
| 		y = -1, width = 8, | ||||
| 		backgroundColor = colors.lightGray, | ||||
| 		newApp = UI.Button { | ||||
| 		backgroundColor = 'tertiary', | ||||
| 		newApp = UI.FlatButton { | ||||
| 			x = 2, | ||||
| 			text = '+', event = 'new', | ||||
| 		}, | ||||
| 		--[[ | ||||
| 		volume = UI.Button { | ||||
| 			x = 3, | ||||
| 			text = '\15', event = 'volume', | ||||
| 		},]] | ||||
| 		mode = UI.FlatButton { | ||||
| 			x = 4, | ||||
| 			text = '=', event = 'display_mode', | ||||
| 		}, | ||||
| 		help = UI.FlatButton { | ||||
| 			x = 6, | ||||
| 			text = '?', event = 'help', | ||||
| 		}, | ||||
| 	}, | ||||
| 	editor = UI.SlideOut { | ||||
| 		y = -12, height = 12, | ||||
| 		backgroundColor = colors.cyan, | ||||
| 		titleBar = UI.TitleBar { | ||||
| 			title = 'Edit Application', | ||||
| 			event = 'slide_hide', | ||||
| @@ -122,7 +125,7 @@ local page = UI.Page { | ||||
| 		form = UI.Form { | ||||
| 			y = 2, ey = -2, | ||||
| 			[1] = UI.TextEntry { | ||||
| 				formLabel = 'Title', formKey = 'title', limit = 11, help = 'Application title', | ||||
| 				formLabel = 'Title', formKey = 'title', limit = 11, width = 13, help = 'Application title', | ||||
| 				required = true, | ||||
| 			}, | ||||
| 			[2] = UI.TextEntry { | ||||
| @@ -130,23 +133,50 @@ local page = UI.Page { | ||||
| 				required = true, | ||||
| 			}, | ||||
| 			[3] = UI.TextEntry { | ||||
| 				formLabel = 'Category', formKey = 'category', limit = 11, help = 'Category of application', | ||||
| 				formLabel = 'Category', formKey = 'category', limit = 6, width = 8, help = 'Category of application', | ||||
| 				required = true, | ||||
| 			}, | ||||
| 			iconFile = UI.TextEntry { | ||||
| 				x = 11, ex = -12, y = 7, | ||||
| 				limit = 128, help = 'Path to icon file', | ||||
| 				shadowText = 'Path to icon file', | ||||
| 			editIcon = UI.Button { | ||||
| 				x = 11, y = 6, | ||||
| 				text = 'Edit', event = 'editIcon', help = 'Edit icon file', | ||||
| 			}, | ||||
| 			loadIcon = UI.Button { | ||||
| 				x = 11, y = 9, | ||||
| 				x = 11, y = 8, | ||||
| 				text = 'Load', event = 'loadIcon', help = 'Load icon file', | ||||
| 			}, | ||||
| 			helpIcon = UI.Button { | ||||
| 				x = 11, y = 8, | ||||
| 				text = 'Load', event = 'loadIcon', help = 'Load icon file', | ||||
| 			}, | ||||
| 			image = UI.NftImage { | ||||
| 				backgroundColor = colors.black, | ||||
| 				y = 7, x = 2, height = 3, width = 8, | ||||
| 				backgroundColor = 'black', | ||||
| 				y = 6, x = 2, height = 3, width = 8, | ||||
| 			}, | ||||
| 		}, | ||||
| 		file_open = UI.FileSelect { | ||||
| 			modal = true, | ||||
| 			enable = function() end, | ||||
| 			transitionHint = 'expandUp', | ||||
| 			show = function(self) | ||||
| 				UI.FileSelect.enable(self) | ||||
| 				self:focusFirst() | ||||
| 				self:draw() | ||||
| 			end, | ||||
| 			disable = function(self) | ||||
| 				UI.FileSelect.disable(self) | ||||
| 				self.parent:focusFirst() | ||||
| 				-- need to recapture as we are opening a modal within another modal | ||||
| 				self.parent:capture(self.parent) | ||||
| 			end, | ||||
| 			eventHandler = function(self, event) | ||||
| 				if event.type == 'select_cancel' then | ||||
| 					self:disable() | ||||
| 				elseif event.type == 'select_file' then | ||||
| 					self:disable() | ||||
| 				end | ||||
| 				return UI.FileSelect.eventHandler(self, event) | ||||
| 			end, | ||||
| 		}, | ||||
| 		notification = UI.Notification(), | ||||
| 		statusBar = UI.StatusBar(), | ||||
| 	}, | ||||
| @@ -205,7 +235,7 @@ local function loadApplications() | ||||
| 			return requirements[a.requires] | ||||
| 		end | ||||
|  | ||||
| 		return true -- Util.startsWith(a.run, 'http') or shell.resolveProgram(a.run) | ||||
| 		return true | ||||
| 	end) | ||||
|  | ||||
| 	local categories = { } | ||||
| @@ -215,6 +245,7 @@ local function loadApplications() | ||||
| 			categories[f.category] = true | ||||
| 			table.insert(buttons, { | ||||
| 				text = f.category, | ||||
| 				width = 8, | ||||
| 				selected = config.currentCategory == f.category | ||||
| 			}) | ||||
| 		end | ||||
| @@ -222,13 +253,13 @@ local function loadApplications() | ||||
| 	table.sort(buttons, function(a, b) return a.text < b.text end) | ||||
| 	table.insert(buttons, 1, { text = 'Recent' }) | ||||
|  | ||||
| 	Util.removeByValue(page.children, page.tabBar) | ||||
| 	for k,v in pairs(buttons) do | ||||
| 		v.x = 1 | ||||
| 		v.y = k + 1 | ||||
| 	end | ||||
|  | ||||
| 	page:add { | ||||
| 		tabBar = UI.VerticalTabBar { | ||||
| 			buttons = buttons, | ||||
| 		}, | ||||
| 	} | ||||
| 	page.tabBar.children = { } | ||||
| 	page.tabBar:addButtons(buttons) | ||||
|  | ||||
| 	--page.tabBar:selectTab(config.currentCategory or 'Apps') | ||||
| 	page.container:setCategory(config.currentCategory or 'Apps') | ||||
| @@ -243,7 +274,6 @@ UI.Icon.defaults = { | ||||
| function UI.Icon:eventHandler(event) | ||||
| 	if event.type == 'mouse_click' then | ||||
| 		self:setFocus(self.button) | ||||
| 		--self:emit({ type = self.button.event, button = self.button }) | ||||
| 		return true | ||||
| 	elseif event.type == 'mouse_doubleclick' then | ||||
| 		self:emit({ type = self.button.event, button = self.button }) | ||||
| @@ -259,37 +289,23 @@ function page.container:setCategory(categoryName, animate) | ||||
| 	self.children = { } | ||||
| 	self:reset() | ||||
|  | ||||
| 	local function filter(it, f) | ||||
| 		local ot = { } | ||||
| 		for _,v in pairs(it) do | ||||
| 			if f(v) then | ||||
| 				table.insert(ot, v) | ||||
| 			end | ||||
| 		end | ||||
| 		return ot | ||||
| 	end | ||||
|  | ||||
| 	local filtered | ||||
| 	local filtered = { } | ||||
|  | ||||
| 	if categoryName == 'Recent' then | ||||
| 		filtered = { } | ||||
|  | ||||
| 		for _,v in ipairs(config.Recent) do | ||||
| 			local app = Util.find(applications, 'key', v) | ||||
| 			if app then -- and fs.exists(app.run) then | ||||
| 			if app then | ||||
| 				table.insert(filtered, app) | ||||
| 			end | ||||
| 		end | ||||
|  | ||||
| 	else | ||||
| 		filtered = filter(applications, function(a) | ||||
| 			return a.category == categoryName -- and fs.exists(a.run) | ||||
| 		filtered = Array.filter(applications, function(a) | ||||
| 			return a.category == categoryName | ||||
| 		end) | ||||
| 		table.sort(filtered, function(a, b) return a.title < b.title end) | ||||
| 	end | ||||
|  | ||||
| 	for _,program in ipairs(filtered) do | ||||
|  | ||||
| 		local icon | ||||
| 		if extSupport and program.iconExt then | ||||
| 			icon = parseIcon(program.iconExt) | ||||
| @@ -304,27 +320,43 @@ function page.container:setCategory(categoryName, animate) | ||||
| 		local title = ellipsis(program.title, 8) | ||||
|  | ||||
| 		local width = math.max(icon.width + 2, #title + 2) | ||||
| 		table.insert(self.children, UI.Icon({ | ||||
| 			width = width, | ||||
| 			image = UI.NftImage({ | ||||
| 				x = math.floor((width - icon.width) / 2) + 1, | ||||
| 				image = icon, | ||||
| 				width = 5, | ||||
| 				height = 3, | ||||
| 			}), | ||||
| 			button = UI.Button({ | ||||
| 				x = math.floor((width - #title - 2) / 2) + 1, | ||||
| 				y = 4, | ||||
| 				text = title, | ||||
| 				backgroundColor = self.backgroundColor, | ||||
| 				backgroundFocusColor = colors.gray, | ||||
| 				textColor = colors.white, | ||||
| 				textFocusColor = colors.white, | ||||
| 				width = #title + 2, | ||||
| 				event = 'button', | ||||
| 				app = program, | ||||
| 			}), | ||||
| 		})) | ||||
| 		if config.listMode then | ||||
| 			table.insert(self.children, UI.Icon { | ||||
| 				width = self.width - 2, | ||||
| 				height = 1, | ||||
| 				UI.Button { | ||||
| 					x = 1, ex = -1, | ||||
| 					text = program.title, | ||||
| 					centered = false, | ||||
| 					backgroundColor = self:getProperty('backgroundColor'), | ||||
| 					backgroundFocusColor = 'gray', | ||||
| 					textColor = 'white', | ||||
| 					textFocusColor = 'white', | ||||
| 					event = 'button', | ||||
| 					app = program, | ||||
| 				} | ||||
| 			}) | ||||
| 		else | ||||
| 			table.insert(self.children, UI.Icon({ | ||||
| 				width = width, | ||||
| 				image = UI.NftImage({ | ||||
| 					x = math.floor((width - icon.width) / 2) + 1, | ||||
| 					image = icon, | ||||
| 				}), | ||||
| 				button = UI.Button({ | ||||
| 					x = math.floor((width - #title - 2) / 2) + 1, | ||||
| 					y = 4, | ||||
| 					text = title, | ||||
| 					backgroundColor = self:getProperty('backgroundColor'), | ||||
| 					backgroundFocusColor = 'gray', | ||||
| 					textColor = 'white', | ||||
| 					textFocusColor = 'white', | ||||
| 					width = #title + 2, | ||||
| 					event = 'button', | ||||
| 					app = program, | ||||
| 				}), | ||||
| 			})) | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	local gutter = 2 | ||||
| @@ -334,7 +366,8 @@ function page.container:setCategory(categoryName, animate) | ||||
| 	local col, row = gutter, 2 | ||||
| 	local count = #self.children | ||||
|  | ||||
| 	local r = math.random(1, 5) | ||||
| 	local r = math.random(1, 7) | ||||
| 	local frames = 5 | ||||
| 	-- reposition all children | ||||
| 	for k,child in ipairs(self.children) do | ||||
| 		if r == 1 then | ||||
| @@ -356,19 +389,27 @@ function page.container:setCategory(categoryName, animate) | ||||
| 				child.x = self.width | ||||
| 				child.y = self.height - 3 | ||||
| 			end | ||||
| 		elseif r == 6 then | ||||
| 			child.x = col | ||||
| 			child.y = 1 | ||||
| 		elseif r == 7 then | ||||
| 			child.x = 1 | ||||
| 			child.y = self.height - 3 | ||||
| 		end | ||||
| 		child.tween = Tween.new(6, child, { x = col, y = row }, 'linear') | ||||
| 		child.tween = Tween.new(frames, child, { x = col, y = row }, 'inQuad') | ||||
|  | ||||
| 		if not animate then | ||||
| 			child.x = col | ||||
| 			child.y = row | ||||
| 		end | ||||
|  | ||||
| 		self:setViewHeight(row + (config.listMode and 1 or 4)) | ||||
|  | ||||
| 		if k < count then | ||||
| 			col = col + child.width | ||||
| 			if col + self.children[k + 1].width + gutter - 2 > self.width then | ||||
| 				col = gutter | ||||
| 				row = row + 5 | ||||
| 				row = row + (config.listMode and 1 or 5) | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| @@ -378,15 +419,12 @@ function page.container:setCategory(categoryName, animate) | ||||
| 		local function transition() | ||||
| 			local i = 1 | ||||
| 			return function() | ||||
| 				self:clear() | ||||
| 				for _,child in pairs(self.children) do | ||||
| 					child.tween:update(1) | ||||
| 					child.x = math.floor(child.x) | ||||
| 					child.y = math.floor(child.y) | ||||
| 					child:draw() | ||||
| 					child:move(math.floor(child.x), math.floor(child.y)) | ||||
| 				end | ||||
| 				i = i + 1 | ||||
| 				return i < 7 | ||||
| 				return i <= frames | ||||
| 			end | ||||
| 		end | ||||
| 		self:addTransition(transition) | ||||
| @@ -439,6 +477,9 @@ function page:eventHandler(event) | ||||
| 	elseif event.type == 'network' then | ||||
| 		shell.switchTab(shell.openTab('network')) | ||||
|  | ||||
| 	elseif event.type == 'help' then | ||||
| 		shell.switchTab(shell.openTab('Help Overview')) | ||||
|  | ||||
| 	elseif event.type == 'focus_change' then | ||||
| 		if event.focused.parent.UIElement == 'Icon' then | ||||
| 			event.focused.parent:scrollIntoView() | ||||
| @@ -473,6 +514,13 @@ function page:eventHandler(event) | ||||
| 		end | ||||
| 		self.editor:show({ category = category }) | ||||
|  | ||||
| 	elseif event.type == 'display_mode' then | ||||
| 		config.listMode = not config.listMode | ||||
| 		Config.update('Overview', config) | ||||
| 		loadApplications() | ||||
| 		self:refresh() | ||||
| 		self:draw() | ||||
|  | ||||
| 	elseif event.type == 'edit' then | ||||
| 		local focused = page:getFocused() | ||||
| 		if focused.app then | ||||
| @@ -480,7 +528,7 @@ function page:eventHandler(event) | ||||
| 		end | ||||
|  | ||||
| 	else | ||||
| 		UI.Page.eventHandler(self, event) | ||||
| 		return UI.Page.eventHandler(self, event) | ||||
| 	end | ||||
| 	return true | ||||
| end | ||||
| @@ -502,11 +550,6 @@ function page.editor:show(app) | ||||
| 	self:focusFirst() | ||||
| end | ||||
|  | ||||
| function page.editor.form.image:draw() | ||||
| 	self:clear() | ||||
| 	UI.NftImage.draw(self) | ||||
| end | ||||
|  | ||||
| function page.editor:updateApplications(app) | ||||
| 	if not app.key then | ||||
| 		app.key = SHA.compute(app.title) | ||||
| @@ -516,36 +559,51 @@ function page.editor:updateApplications(app) | ||||
| 	loadApplications() | ||||
| end | ||||
|  | ||||
| function page.editor:loadImage(filename) | ||||
| 	local s, m = pcall(function() | ||||
| 		local iconLines = Util.readFile(filename) | ||||
| 		if not iconLines then | ||||
| 			error('Must be an NFT image - 3 rows, 8 cols max') | ||||
| 		end | ||||
| 		local icon, m = parseIcon(iconLines) | ||||
| 		if not icon then | ||||
| 			error(m) | ||||
| 		end | ||||
| 		if extSupport then | ||||
| 			self.form.values.iconExt = iconLines | ||||
| 		else | ||||
| 			self.form.values.icon = iconLines | ||||
| 		end | ||||
| 		self.form.image:setImage(icon) | ||||
| 		self.form.image:draw() | ||||
| 	end) | ||||
| 	if not s and m then | ||||
| 		local msg = m:gsub('.*: (.*)', '%1') | ||||
| 		self.notification:error(msg) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function page.editor:eventHandler(event) | ||||
| 	if event.type == 'form_cancel' or event.type == 'cancel' then | ||||
| 		self:hide() | ||||
|  | ||||
| 	elseif event.type == 'focus_change' then | ||||
| 		self.statusBar:setStatus(event.focused.help or '') | ||||
| 		self.statusBar:draw() | ||||
|  | ||||
| 	elseif event.type == 'editIcon' then | ||||
| 		local filename = '/tmp/editing.nft' | ||||
| 		NFT.save(self.form.image.image or TRANS_ICON, filename) | ||||
| 		local success = shell.run('pain.lua ' .. filename) | ||||
| 		self.parent:dirty(true) | ||||
| 		if success then | ||||
| 			self:loadImage(filename) | ||||
| 		end | ||||
|  | ||||
| 	elseif event.type == 'select_file' then | ||||
| 		self:loadImage(event.file) | ||||
|  | ||||
| 	elseif event.type == 'loadIcon' then | ||||
| 		local s, m = pcall(function() | ||||
| 			local iconLines = Util.readFile(self.form.iconFile.value) | ||||
| 			if not iconLines then | ||||
| 				error('Must be an NFT image - 3 rows, 8 cols max') | ||||
| 			end | ||||
| 			local icon, m = parseIcon(iconLines) | ||||
| 			if not icon then | ||||
| 				error(m) | ||||
| 			end | ||||
| 			if extSupport then | ||||
| 				self.form.values.iconExt = iconLines | ||||
| 			else | ||||
| 				self.form.values.icon = iconLines | ||||
| 			end | ||||
| 			self.form.image:setImage(icon) | ||||
| 			self.form.image:draw() | ||||
| 		end) | ||||
| 		if not s and m then | ||||
| 			local msg = m:gsub('.*: (.*)', '%1') | ||||
| 			self.notification:error(msg) | ||||
| 		end | ||||
| 		self.file_open:show() | ||||
|  | ||||
| 	elseif event.type == 'form_invalid' then | ||||
| 		self.notification:error(event.message) | ||||
| @@ -554,8 +612,6 @@ function page.editor:eventHandler(event) | ||||
| 		local values = self.form.values | ||||
| 		self:hide() | ||||
| 		self:updateApplications(values) | ||||
| 		--page:refresh() | ||||
| 		--page:draw() | ||||
| 		config.currentCategory = values.category | ||||
| 		Config.update('Overview', config) | ||||
| 		os.queueEvent('overview_refresh') | ||||
| @@ -565,10 +621,6 @@ function page.editor:eventHandler(event) | ||||
| 	return true | ||||
| end | ||||
|  | ||||
| UI:setPages({ | ||||
| 	main = page, | ||||
| }) | ||||
|  | ||||
| local function reload() | ||||
| 	loadApplications() | ||||
| 	page:refresh() | ||||
| @@ -594,5 +646,4 @@ end) | ||||
| loadApplications() | ||||
|  | ||||
| UI:setPage(page) | ||||
|  | ||||
| UI:pullEvents() | ||||
| UI:start() | ||||
|   | ||||
| @@ -44,7 +44,6 @@ local page = UI.Page { | ||||
| 		marginRight = 0, marginLeft = 0, | ||||
| 	}, | ||||
| 	action = UI.SlideOut { | ||||
| 		backgroundColor = colors.cyan, | ||||
| 		titleBar = UI.TitleBar { | ||||
| 			event = 'hide-action', | ||||
| 		}, | ||||
| @@ -184,7 +183,7 @@ function page:eventHandler(event) | ||||
| 		self.action.button:draw() | ||||
|  | ||||
| 	elseif event.type == 'quit' then | ||||
| 		UI:exitPullEvents() | ||||
| 		UI:quit() | ||||
| 	end | ||||
| 	UI.Page.eventHandler(self, event) | ||||
| end | ||||
| @@ -196,4 +195,4 @@ Packages:downloadList() | ||||
| page:loadPackages() | ||||
| page:sync() | ||||
|  | ||||
| UI:pullEvents() | ||||
| UI:start() | ||||
|   | ||||
| @@ -5,14 +5,13 @@ local Util  = require('opus.util') | ||||
| local colors     = _G.colors | ||||
| local device     = _G.device | ||||
| local textutils  = _G.textutils | ||||
| local peripheral = _G.peripheral | ||||
| local multishell = _ENV.multishell | ||||
|  | ||||
| local gridColumns = {} | ||||
| table.insert(gridColumns, { heading = '#',  key = 'id', width = 5, align = 'right' }) | ||||
| table.insert(gridColumns, { heading = 'Port', key = 'portid', width = 5, align = 'right' }) | ||||
| table.insert(gridColumns, { heading = 'Reply', key = 'replyid', width = 5, align = 'right' }) | ||||
| if UI.defaultDevice.width > 50 then | ||||
| if UI.term.width > 50 then | ||||
| 	table.insert(gridColumns, { heading = 'Dist', key = 'distance', width = 6, align = 'right' }) | ||||
| end | ||||
| table.insert(gridColumns, { heading = 'Msg', key = 'packetStr' }) | ||||
| @@ -42,12 +41,13 @@ local page = UI.Page { | ||||
|  | ||||
| 	configSlide = UI.SlideOut { | ||||
| 		y = -11, | ||||
| 		titleBar = UI.TitleBar { title = 'Sniffer Config', event = 'config_close' }, | ||||
| 		titleBar = UI.TitleBar { title = 'Sniffer Config', event = 'config_close', backgroundColor = colors.black }, | ||||
| 		accelerators = { ['backspace'] = 'config_close' }, | ||||
| 		configTabs = UI.Tabs { | ||||
| 			y = 2, | ||||
| 			filterTab = UI.Tab { | ||||
| 				tabTitle = 'Filter', | ||||
| 				noFill = true, | ||||
| 				filterGridText = UI.Text { | ||||
| 					x = 2, y = 2, | ||||
| 					value = 'ID filter', | ||||
| @@ -130,7 +130,6 @@ local page = UI.Page { | ||||
| 			title = 'Packet Information', | ||||
| 			event = 'packet_close', | ||||
| 		}, | ||||
| 		backgroundColor = colors.cyan, | ||||
| 		accelerators = { | ||||
| 			['backspace'] = 'packet_close', | ||||
| 			['left'] = 'prev_packet', | ||||
| @@ -280,7 +279,7 @@ function page.packetSlide:eventHandler(event) | ||||
| end | ||||
|  | ||||
| function page.packetGrid:getDisplayValues(row) | ||||
| 	local row = Util.shallowCopy(row) | ||||
| 	row = Util.shallowCopy(row) | ||||
| 	row.distance = Util.toBytes(Util.round(row.distance), 2) | ||||
| 	return row | ||||
| end | ||||
| @@ -356,7 +355,7 @@ function page:eventHandler(event) | ||||
| 		self.packetSlide:show(event.selected) | ||||
|  | ||||
| 	elseif event.type == 'quit' then | ||||
| 		Event.exitPullEvents() | ||||
| 		UI:quit() | ||||
|  | ||||
| 	else return UI.Page.eventHandler(self, event) | ||||
| 	end | ||||
| @@ -386,4 +385,4 @@ if args[1] then | ||||
| end | ||||
|  | ||||
| UI:setPage(page) | ||||
| UI:pullEvents() | ||||
| UI:start() | ||||
|   | ||||
| @@ -11,7 +11,7 @@ local systemPage = UI.Page { | ||||
| 		settings = UI.Tab { | ||||
| 			tabTitle = 'Category', | ||||
| 			grid = UI.ScrollingGrid { | ||||
| 				y = 2, | ||||
| 				x = 2, y = 2, ex = -2, ey = -2, | ||||
| 				columns = { | ||||
| 					{ heading = 'Name',        key = 'name'        }, | ||||
| 					{ heading = 'Description', key = 'description' }, | ||||
| @@ -35,14 +35,14 @@ function systemPage.tabs.settings:eventHandler(event) | ||||
| 			tab:disable() | ||||
| 		end | ||||
| 		systemPage.tabs:selectTab(tab) | ||||
| 		self.parent:draw() | ||||
| 		--self.parent:draw() | ||||
| 		return true | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function systemPage:eventHandler(event) | ||||
| 	if event.type == 'quit' then | ||||
| 		UI:exitPullEvents() | ||||
| 		UI:quit() | ||||
|  | ||||
| 	elseif event.type == 'success_message' then | ||||
| 		self.notification:success(event.message) | ||||
| @@ -82,4 +82,4 @@ local plugins = loadDirectory(fs.combine(programDir, 'system'), { }) | ||||
| systemPage.tabs.settings.grid:setValues(plugins) | ||||
|  | ||||
| UI:setPage(systemPage) | ||||
| UI:pullEvents() | ||||
| UI:start() | ||||
|   | ||||
| @@ -3,6 +3,7 @@ local UI    = require('opus.ui') | ||||
|  | ||||
| local kernel     = _G.kernel | ||||
| local multishell = _ENV.multishell | ||||
| local tasks      = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines | ||||
|  | ||||
| UI:configure('Tasks', ...) | ||||
|  | ||||
| @@ -21,7 +22,7 @@ local page = UI.Page { | ||||
| 			{ heading = 'Status', key = 'status'    }, | ||||
| 			{ heading = 'Time',   key = 'timestamp' }, | ||||
| 		}, | ||||
| 		values = kernel.routines, | ||||
| 		values = tasks, | ||||
| 		sortColumn = 'uid', | ||||
| 		autospace = true, | ||||
| 		getDisplayValues = function (_, row) | ||||
| @@ -51,7 +52,7 @@ local page = UI.Page { | ||||
| 			end | ||||
| 		end | ||||
| 		if event.type == 'quit' then | ||||
| 			Event.exitPullEvents() | ||||
| 			UI:quit() | ||||
| 		end | ||||
| 		UI.Page.eventHandler(self, event) | ||||
| 	end | ||||
| @@ -64,4 +65,4 @@ Event.onInterval(1, function() | ||||
| end) | ||||
|  | ||||
| UI:setPage(page) | ||||
| UI:pullEvents() | ||||
| UI:start() | ||||
|   | ||||
| @@ -131,7 +131,7 @@ function page:eventHandler(event) | ||||
| 		shell.openForegroundTab('PackageManager') | ||||
|  | ||||
| 	elseif event.type == 'wizard_complete' or event.type == 'cancel' then | ||||
| 		UI.exitPullEvents() | ||||
| 		UI:quit() | ||||
|  | ||||
| 	else | ||||
| 		return UI.Page.eventHandler(self, event) | ||||
| @@ -140,4 +140,4 @@ function page:eventHandler(event) | ||||
| end | ||||
|  | ||||
| UI:setPage(page) | ||||
| UI:pullEvents() | ||||
| UI:start() | ||||
|   | ||||
							
								
								
									
										39
									
								
								sys/apps/fileui.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								sys/apps/fileui.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| local UI   = require('opus.ui') | ||||
| local Util = require('opus.util') | ||||
|  | ||||
| local shell  = _ENV.shell | ||||
| local multishell = _ENV.multishell | ||||
|  | ||||
| -- fileui [--path=path] [--exec=filename] [--title=title] | ||||
|  | ||||
| local page = UI.Page { | ||||
| 	fileselect = UI.FileSelect { }, | ||||
| 	eventHandler = function(self, event) | ||||
| 		if event.type == 'select_file' then | ||||
| 			self.selected = event.file | ||||
| 			UI:quit() | ||||
|  | ||||
| 		elseif event.type == 'select_cancel' then | ||||
| 			UI:quit() | ||||
| 		end | ||||
|  | ||||
| 		return UI.FileSelect.eventHandler(self, event) | ||||
| 	end, | ||||
| } | ||||
|  | ||||
| local _, args = Util.parse(...) | ||||
|  | ||||
| if args.title and multishell then | ||||
| 	multishell.setTitle(multishell.getCurrent(), args.title) | ||||
| end | ||||
|  | ||||
| UI:setPage(page, args.path) | ||||
| UI:start() | ||||
| UI.term:setCursorBlink(false) | ||||
|  | ||||
| if args.exec and page.selected then | ||||
| 	shell.openForegroundTab(string.format('%s %s', args.exec, page.selected)) | ||||
| 	return | ||||
| end | ||||
|  | ||||
| return page.selected | ||||
| @@ -133,6 +133,12 @@ page = UI.Page { | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	accelerators = { | ||||
| 		['shift-right'] = 'size', | ||||
| 		['shift-left' ] = 'size', | ||||
| 		['shift-up'   ] = 'size', | ||||
| 		['shift-down' ] = 'size', | ||||
| 	}, | ||||
| 	eventHandler = function (self, event) | ||||
| 		if event.type == 'focus_change' and isRelevant(event.focused) then | ||||
| 			focused = event.focused | ||||
| @@ -144,7 +150,6 @@ page = UI.Page { | ||||
| 				}) | ||||
| 			end | ||||
| 			self.tabs.properties.grid:setValues(t) | ||||
| 			self.tabs.properties.grid:update() | ||||
| 			self.tabs.properties.grid:draw() | ||||
|  | ||||
| 			t = { } | ||||
| @@ -156,7 +161,6 @@ page = UI.Page { | ||||
| 				end | ||||
| 			end | ||||
| 			self.tabs.methodsTab.grid:setValues(t) | ||||
| 			self.tabs.methodsTab.grid:update() | ||||
| 			self.tabs.methodsTab.grid:draw() | ||||
|  | ||||
| 		elseif event.type == 'edit_property' then | ||||
| @@ -168,6 +172,19 @@ page = UI.Page { | ||||
|  | ||||
| 		elseif event.type == 'editor_apply' then | ||||
| 			self.editor:hide() | ||||
|  | ||||
| 		elseif event.type == 'size' then | ||||
| 			local sizing = { | ||||
| 				['shift-right'] = {  1,  0 }, | ||||
| 				['shift-left' ] = { -1,  0 }, | ||||
| 				['shift-up'   ] = {  0, -1 }, | ||||
| 				['shift-down' ] = {  0,  1 }, | ||||
| 			} | ||||
| 			self.ox = math.max(self.ox + sizing[event.ie.code][1], 1) | ||||
| 			self.oy = math.max(self.oy + sizing[event.ie.code][2], 1) | ||||
| 			UI.term:clear() | ||||
| 			self:resize() | ||||
| 			self:draw() | ||||
| 		end | ||||
|  | ||||
| 		return UI.Page.eventHandler(self, event) | ||||
| @@ -175,4 +192,4 @@ page = UI.Page { | ||||
| } | ||||
|  | ||||
| UI:setPage(page) | ||||
| UI:pullEvents() | ||||
| UI:start() | ||||
|   | ||||
| @@ -33,7 +33,7 @@ Event.on('generate_keypair', function() | ||||
| 		table.insert(keyPairs, { generateKeyPair() }) | ||||
| 		_G._syslog('Generated keypair in ' .. timer()) | ||||
| 		if #keyPairs >= 3 then | ||||
| 	  		break | ||||
| 			break | ||||
| 		end | ||||
| 	end | ||||
| end) | ||||
|   | ||||
| @@ -66,11 +66,11 @@ local function run(env, ...) | ||||
| 		_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), fs.getName(path):match('([^%.]+)')) | ||||
| 	end | ||||
|  | ||||
| 	if isUrl then | ||||
| 		tProgramStack[#tProgramStack + 1] = path -- path:match("^https?://([^/:]+:?[0-9]*/?.*)$") | ||||
| 	else | ||||
| 		tProgramStack[#tProgramStack + 1] = path | ||||
| 	end | ||||
| 	tProgramStack[#tProgramStack + 1] = { | ||||
| 		path = path, -- path:match("^https?://([^/:]+:?[0-9]*/?.*)$") | ||||
| 		env = env, | ||||
| 		args = args, | ||||
| 	} | ||||
|  | ||||
| 	env[ "arg" ] = { [0] = path, table.unpack(args) } | ||||
| 	local r = { fn(table.unpack(args)) } | ||||
| @@ -278,6 +278,10 @@ function shell.getCompletionInfo() | ||||
| end | ||||
|  | ||||
| function shell.getRunningProgram() | ||||
| 	return tProgramStack[#tProgramStack] and tProgramStack[#tProgramStack].path | ||||
| end | ||||
|  | ||||
| function shell.getRunningInfo() | ||||
| 	return tProgramStack[#tProgramStack] | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -20,7 +20,7 @@ local aliasTab = UI.Tab { | ||||
| 		}, | ||||
| 	}, | ||||
| 	grid = UI.Grid { | ||||
| 		y = 5, | ||||
| 		x = 2, y = 5, ex = -2, ey = -2, | ||||
| 		sortColumn = 'alias', | ||||
| 		columns = { | ||||
| 			{ heading = 'Alias',   key = 'alias' }, | ||||
|   | ||||
| @@ -2,8 +2,6 @@ local Array  = require('opus.array') | ||||
| local Config = require('opus.config') | ||||
| local UI     = require('opus.ui') | ||||
|  | ||||
| local colors     = _G.colors | ||||
|  | ||||
| local tab = UI.Tab { | ||||
| 	tabTitle = 'Preferred', | ||||
| 	description = 'Select preferred applications', | ||||
| @@ -22,20 +20,19 @@ local tab = UI.Tab { | ||||
| 		disableHeader = true, | ||||
| 		columns = { | ||||
| 			{ key = 'file' }, | ||||
| 		} | ||||
| 		}, | ||||
| 		getRowTextColor = function(self, row) | ||||
| 			if row == self.values[1] then | ||||
| 				return 'yellow' | ||||
| 			end | ||||
| 			return UI.Grid.getRowTextColor(self, row) | ||||
| 		end, | ||||
| 	}, | ||||
| 	statusBar = UI.StatusBar { | ||||
| 		values = 'Double-click to set as preferred' | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| function tab.choices:getRowTextColor(row) | ||||
| 	if row == self.values[1] then | ||||
| 		return colors.yellow | ||||
| 	end | ||||
| 	return UI.Grid.getRowTextColor(self, row) | ||||
| end | ||||
|  | ||||
| function tab:updateChoices() | ||||
| 	local app = self.apps:getSelected().name | ||||
| 	local choices = { } | ||||
|   | ||||
| @@ -2,18 +2,17 @@ local Ansi   = require('opus.ansi') | ||||
| local Config = require('opus.config') | ||||
| local UI     = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| -- -t80x30 | ||||
|  | ||||
| if _G.http.websocket then | ||||
| 	local config = Config.load('cloud') | ||||
|  | ||||
| 	local tab = UI.Tab { | ||||
| 		tabTitle = 'Cloud', | ||||
| 		description = 'Cloud Catcher options', | ||||
| 		[1] = UI.Window { | ||||
| 			x = 2, y = 2, ex = -2, ey = 4, | ||||
| 		}, | ||||
| 		key = UI.TextEntry { | ||||
| 			x = 3, ex = -3, y = 2, | ||||
| 			x = 3, ex = -3, y = 3, | ||||
| 			limit = 32, | ||||
| 			value = config.key, | ||||
| 			shadowText = 'Cloud key', | ||||
| @@ -22,14 +21,15 @@ if _G.http.websocket then | ||||
| 			}, | ||||
| 		}, | ||||
| 		button = UI.Button { | ||||
| 			x = 3, y = 4, | ||||
| 			text = 'Update', | ||||
| 			x = -8, ex = -2, y = -2, | ||||
| 			text = 'Apply', | ||||
| 			event = 'update_key', | ||||
| 		}, | ||||
| 		labelText = UI.TextArea { | ||||
| 			x = 3, ex = -3, y = 6, | ||||
| 			textColor = colors.yellow, | ||||
| 			marginLeft = 0, marginRight = 0, | ||||
| 			x = 2, ex = -2, y = 5, ey = -4, | ||||
| 			textColor = 'yellow', | ||||
| 			backgroundColor = 'black', | ||||
| 			marginLeft = 1, marginRight = 1, marginTop = 1, | ||||
| 			value = string.format( | ||||
| [[Use a non-changing cloud key. Note that only a single computer can use this session at one time. | ||||
| To obtain a key, visit: | ||||
|   | ||||
| @@ -20,8 +20,8 @@ local tab = UI.Tab { | ||||
| 	description = 'Visualise HDD and disks usage', | ||||
|  | ||||
| 	drives = UI.ScrollingGrid { | ||||
| 		x = 2, y = 1, | ||||
| 		ex = '47%', ey = -7, | ||||
| 		x = 2, y = 2, | ||||
| 		ex = '47%', ey = -8, | ||||
| 		columns = { | ||||
| 			{ heading = 'Drive', key = 'name' }, | ||||
| 			{ heading = 'Side' ,key = 'side', textColor = colors.yellow } | ||||
| @@ -30,7 +30,7 @@ local tab = UI.Tab { | ||||
| 	}, | ||||
| 	infos = UI.Grid { | ||||
| 		x = '52%', y = 2, | ||||
| 		ex = -2, ey = -4, | ||||
| 		ex = -2, ey = -8, | ||||
| 		disableHeader = true, | ||||
| 		unfocusedBackgroundSelectedColor = colors.black, | ||||
| 		inactive = true, | ||||
| @@ -40,18 +40,23 @@ local tab = UI.Tab { | ||||
| 			{ key = 'value', align = 'right', textColor = colors.yellow }, | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	[1] = UI.Window { | ||||
| 		x = 2, y = -6, ex = -2, ey = -2, | ||||
| 		backgroundColor = colors.black, | ||||
| 	}, | ||||
| 	progress = UI.ProgressBar { | ||||
| 		x = 11, y = -2, | ||||
| 		ex = -2, | ||||
| 		x = 11, y = -3, | ||||
| 		ex = -3, | ||||
| 	}, | ||||
| 	percentage = UI.Text { | ||||
| 		x = 11, y = -3, | ||||
| 		ex = '47%', | ||||
| 		align = 'center', | ||||
| 		y = -4, width = 5, | ||||
| 		x = 12, | ||||
| 		--align = 'center', | ||||
| 		backgroundColor = colors.black, | ||||
| 	}, | ||||
| 	icon = UI.NftImage { | ||||
| 		x = 2, y = -5, | ||||
| 		x = 2, y = -6, ey = -2, | ||||
| 		backgroundColor = colors.black, | ||||
| 		image = NFT.parse(NftImages.blank) | ||||
| 	}, | ||||
| } | ||||
|   | ||||
| @@ -4,11 +4,11 @@ local colors     = _G.colors | ||||
| local peripheral = _G.peripheral | ||||
| local settings   = _G.settings | ||||
|  | ||||
| local tab = UI.Tab { | ||||
| return peripheral.find('monitor') and UI.Tab { | ||||
| 	tabTitle = 'Kiosk', | ||||
| 	description = 'Kiosk options', | ||||
| 	form = UI.Form { | ||||
| 		x = 2, ex = -2, | ||||
| 		x = 2, y = 2, ex = -2, ey = 5, | ||||
| 		manualControls = true, | ||||
| 		monitor = UI.Chooser { | ||||
| 			formLabel = 'Monitor', formKey = 'monitor', | ||||
| @@ -22,41 +22,36 @@ local tab = UI.Tab { | ||||
| 			}, | ||||
| 			help = 'Adjust text scaling', | ||||
| 		}, | ||||
| 		labelText = UI.TextArea { | ||||
| 			x = 2, ex = -2, y = 5, | ||||
| 			textColor = colors.yellow, | ||||
| 			value = 'Settings apply to kiosk mode selected during startup' | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
| 	labelText = UI.TextArea { | ||||
| 		x = 2, ex = -2, y = 7, ey = -2, | ||||
| 		textColor = colors.yellow, | ||||
| 		backgroundColor = colors.black, | ||||
| 		value = 'Settings apply to kiosk mode selected during startup' | ||||
| 	}, | ||||
| 	enable = function(self) | ||||
| 		local choices = { } | ||||
|  | ||||
| function tab:enable() | ||||
| 	local choices = {	} | ||||
| 		peripheral.find('monitor', function(side) | ||||
| 			table.insert(choices, { name = side, value = side }) | ||||
| 		end) | ||||
|  | ||||
| 	peripheral.find('monitor', function(side) | ||||
| 		table.insert(choices, { name = side, value = side }) | ||||
| 	end) | ||||
| 		self.form.monitor.choices = choices | ||||
| 		self.form.monitor.value = settings.get('kiosk.monitor') | ||||
|  | ||||
| 	self.form.monitor.choices = choices | ||||
| 	self.form.monitor.value = settings.get('kiosk.monitor') | ||||
| 		self.form.textScale.value = settings.get('kiosk.textscale') | ||||
|  | ||||
| 	self.form.textScale.value = settings.get('kiosk.textscale') | ||||
|  | ||||
| 	UI.Tab.enable(self) | ||||
| end | ||||
|  | ||||
| function tab:eventHandler(event) | ||||
| 	if event.type == 'choice_change' then | ||||
| 		if self.form.monitor.value then | ||||
| 			settings.set('kiosk.monitor', self.form.monitor.value) | ||||
| 		UI.Tab.enable(self) | ||||
| 	end, | ||||
| 	eventHandler = function(self, event) | ||||
| 		if event.type == 'choice_change' then | ||||
| 			if self.form.monitor.value then | ||||
| 				settings.set('kiosk.monitor', self.form.monitor.value) | ||||
| 			end | ||||
| 			if self.form.textScale.value then | ||||
| 				settings.set('kiosk.textscale', self.form.textScale.value) | ||||
| 			end | ||||
| 			settings.save('.settings') | ||||
| 		end | ||||
| 		if self.form.textScale.value then | ||||
| 			settings.set('kiosk.textscale', self.form.textScale.value) | ||||
| 		end | ||||
| 		settings.save('.settings') | ||||
| 	end | ||||
| end | ||||
|  | ||||
| if peripheral.find('monitor') then | ||||
| 	return tab | ||||
| end | ||||
| } | ||||
|   | ||||
| @@ -4,23 +4,26 @@ local Util = require('opus.util') | ||||
| local fs   = _G.fs | ||||
| local os   = _G.os | ||||
|  | ||||
| local labelTab = UI.Tab { | ||||
| return UI.Tab { | ||||
| 	tabTitle = 'Label', | ||||
| 	description = 'Set the computer label', | ||||
| 	labelText = UI.Text { | ||||
| 		x = 3, y = 2, | ||||
| 		x = 3, y = 3, | ||||
| 		value = 'Label' | ||||
| 	}, | ||||
| 	label = UI.TextEntry { | ||||
| 		x = 9, y = 2, ex = -4, | ||||
| 		x = 9, y = 3, ex = -4, | ||||
| 		limit = 32, | ||||
| 		value = os.getComputerLabel(), | ||||
| 		accelerators = { | ||||
| 			enter = 'update_label', | ||||
| 		}, | ||||
| 	}, | ||||
| 	[1] = UI.Window { | ||||
| 		x = 2, y = 2, ex = -2, ey = 4, | ||||
| 	}, | ||||
| 	grid = UI.ScrollingGrid { | ||||
| 		y = 3, | ||||
| 		x = 2, y = 5, ex = -2, ey = -2, | ||||
| 		values = { | ||||
| 			{ name = '',  value = ''                  }, | ||||
| 			{ name = 'CC version',  value = Util.getVersion()                  }, | ||||
| @@ -30,20 +33,18 @@ local labelTab = UI.Tab { | ||||
| 			{ name = 'Computer ID', value = tostring(os.getComputerID())       }, | ||||
| 			{ name = 'Day',         value = tostring(os.day())                 }, | ||||
| 		}, | ||||
| 		disableHeader = true, | ||||
| 		inactive = true, | ||||
| 		columns = { | ||||
| 			{ key = 'name',  width = 12 }, | ||||
| 			{ key = 'value' }, | ||||
| 			{ key = 'value', textColor = colors.yellow }, | ||||
| 		}, | ||||
| 	}, | ||||
| 	eventHandler = function(self, event) | ||||
| 		if event.type == 'update_label' and self.label.value then | ||||
| 			os.setComputerLabel(self.label.value) | ||||
| 			self:emit({ type = 'success_message', message = 'Label updated' }) | ||||
| 			return true | ||||
| 		end | ||||
| 	end, | ||||
| } | ||||
|  | ||||
| function labelTab:eventHandler(event) | ||||
| 	if event.type == 'update_label' and self.label.value then | ||||
| 		os.setComputerLabel(self.label.value) | ||||
| 		self:emit({ type = 'success_message', message = 'Label updated' }) | ||||
| 		return true | ||||
| 	end | ||||
| end | ||||
|  | ||||
| return labelTab | ||||
|   | ||||
| @@ -9,12 +9,15 @@ local config = Config.load('multishell') | ||||
| local tab = UI.Tab { | ||||
| 	tabTitle = 'Launcher', | ||||
| 	description = 'Set the application launcher', | ||||
| 	[1] = UI.Window { | ||||
| 		x = 2, y = 2, ex = -2, ey = 5, | ||||
| 	}, | ||||
| 	launcherLabel = UI.Text { | ||||
| 		x = 3, y = 2, | ||||
| 		x = 3, y = 3, | ||||
| 		value = 'Launcher', | ||||
| 	}, | ||||
| 	launcher = UI.Chooser { | ||||
| 		x = 13, y = 2, width = 12, | ||||
| 		x = 13, y = 3, width = 12, | ||||
| 		choices = { | ||||
| 			{ name = 'Overview', value = 'sys/apps/Overview.lua' }, | ||||
| 			{ name = 'Shell',    value = 'sys/apps/ShellLauncher.lua'    }, | ||||
| @@ -22,18 +25,20 @@ local tab = UI.Tab { | ||||
| 		}, | ||||
| 	}, | ||||
| 	custom = UI.TextEntry { | ||||
| 		x = 13, ex = -3, y = 3, | ||||
| 		x = 13, ex = -3, y = 4, | ||||
| 		limit = 128, | ||||
| 		shadowText = 'File name', | ||||
| 	}, | ||||
| 	button = UI.Button { | ||||
| 		x = 3, y = 5, | ||||
| 		text = 'Update', | ||||
| 		x = -8, ex = -2, y = -2, | ||||
| 		text = 'Apply', | ||||
| 		event = 'update', | ||||
| 	}, | ||||
| 	labelText = UI.TextArea { | ||||
| 		x = 3, ex = -3, y = 7, | ||||
| 		x = 2, ex = -2, y = 6, ey = -4, | ||||
| 		backgroundColor = colors.black, | ||||
| 		textColor = colors.yellow, | ||||
| 		marginLeft = 1, marginRight = 1, marginTop = 1, | ||||
| 		value = 'Choose an application launcher', | ||||
| 	}, | ||||
| } | ||||
|   | ||||
| @@ -2,59 +2,61 @@ local Ansi   = require('opus.ansi') | ||||
| local Config = require('opus.config') | ||||
| local UI     = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
| local device = _G.device | ||||
|  | ||||
| local tab = UI.Tab { | ||||
| return UI.Tab { | ||||
| 	tabTitle = 'Network', | ||||
| 	description = 'Networking options', | ||||
| 	info = UI.TextArea { | ||||
| 		x = 3, y = 4, | ||||
| 		x = 2, y = 5, ex = -2, ey = -2, | ||||
| 		backgroundColor = colors.black, | ||||
| 		marginLeft = 1, marginRight = 1, marginTop = 1, | ||||
| 		value = string.format( | ||||
| [[%sSet the primary modem used for wireless communications.%s | ||||
|  | ||||
| Reboot to take effect.]], Ansi.yellow, Ansi.reset) | ||||
| 	}, | ||||
| 	[1] = UI.Window { | ||||
| 		x = 2, y = 2, ex = -2, ey = 4, | ||||
| 	}, | ||||
| 	label = UI.Text { | ||||
| 		x = 3, y = 2, | ||||
| 		x = 3, y = 3, | ||||
| 		value = 'Modem', | ||||
| 	}, | ||||
| 	modem = UI.Chooser { | ||||
| 		x = 10, ex = -3, y = 2, | ||||
| 		x = 10, ex = -3, y = 3, | ||||
| 		nochoice = 'auto', | ||||
| 	}, | ||||
| } | ||||
| 	enable = function(self) | ||||
| 		local width = 7 | ||||
| 		local choices = { | ||||
| 			{ name = 'auto',    value = 'auto' }, | ||||
| 			{ name = 'disable', value = 'none' }, | ||||
| 		} | ||||
|  | ||||
| function tab:enable() | ||||
| 	local width = 7 | ||||
| 	local choices = { | ||||
| 		{ name = 'auto',    value = 'auto' }, | ||||
| 		{ name = 'disable', value = 'none' }, | ||||
| 	} | ||||
| 		for k,v in pairs(device) do | ||||
| 			if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then | ||||
| 				table.insert(choices, { name = k, value = v.name }) | ||||
| 				width = math.max(width, #k) | ||||
| 			end | ||||
| 		end | ||||
|  | ||||
| 	for k,v in pairs(device) do | ||||
| 		if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then | ||||
| 			table.insert(choices, { name = k, value = v.name }) | ||||
| 			width = math.max(width, #k) | ||||
| 		self.modem.choices = choices | ||||
| 		--self.modem.width = width + 4 | ||||
|  | ||||
| 		local config = Config.load('os') | ||||
| 		self.modem.value = config.wirelessModem or 'auto' | ||||
|  | ||||
| 		UI.Tab.enable(self) | ||||
| 	end, | ||||
| 	eventHandler = function(self, event) | ||||
| 		if event.type == 'choice_change' then | ||||
| 			local config = Config.load('os') | ||||
| 			config.wirelessModem = self.modem.value | ||||
| 			Config.update('os', config) | ||||
| 			self:emit({ type = 'success_message', message = 'reboot to take effect' }) | ||||
| 			return true | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	self.modem.choices = choices | ||||
| 	--self.modem.width = width + 4 | ||||
|  | ||||
| 	local config = Config.load('os') | ||||
| 	self.modem.value = config.wirelessModem or 'auto' | ||||
|  | ||||
| 	UI.Tab.enable(self) | ||||
| end | ||||
|  | ||||
| function tab:eventHandler(event) | ||||
| 	if event.type == 'choice_change' then | ||||
| 		local config = Config.load('os') | ||||
| 		config.wirelessModem = self.modem.value | ||||
| 		Config.update('os', config) | ||||
| 		self:emit({ type = 'success_message', message = 'reboot to take effect' }) | ||||
| 		return true | ||||
| 	end | ||||
| end | ||||
|  | ||||
| return tab | ||||
| } | ||||
|   | ||||
| @@ -2,11 +2,12 @@ local Security = require('opus.security') | ||||
| local SHA      = require('opus.crypto.sha2') | ||||
| local UI       = require('opus.ui') | ||||
|  | ||||
| local colors   = _G.colors | ||||
|  | ||||
| local passwordTab = UI.Tab { | ||||
| return UI.Tab { | ||||
| 	tabTitle = 'Password', | ||||
| 	description = 'Wireless network password', | ||||
| 	[1] = UI.Window { | ||||
| 		x = 2, y = 2, ex = -2, ey = 4, | ||||
| 	}, | ||||
| 	newPass = UI.TextEntry { | ||||
| 		x = 3, ex = -3, y = 3, | ||||
| 		limit = 32, | ||||
| @@ -17,28 +18,28 @@ local passwordTab = UI.Tab { | ||||
| 		}, | ||||
| 	}, | ||||
| 	button = UI.Button { | ||||
| 		x = 3, y = 5, | ||||
| 		text = 'Update', | ||||
| 		x = -8, ex = -2, y = -2, | ||||
| 		text = 'Apply', | ||||
| 		event = 'update_password', | ||||
| 	}, | ||||
| 	info = UI.TextArea { | ||||
| 		x = 3, ex = -3, y = 7, | ||||
| 		textColor = colors.yellow, | ||||
| 		x = 2, ex = -2, y = 5, ey = -4, | ||||
| 		backgroundColor = 'black', | ||||
| 		textColor = 'yellow', | ||||
| 		inactive = true, | ||||
| 		marginLeft = 1, marginRight = 1, marginTop = 1, | ||||
| 		value = 'Add a password to enable other computers to connect to this one.', | ||||
| 	} | ||||
| } | ||||
| function passwordTab:eventHandler(event) | ||||
| 	if event.type == 'update_password' then | ||||
| 		if not self.newPass.value or #self.newPass.value == 0 then | ||||
| 			self:emit({ type = 'error_message', message = 'Invalid password' }) | ||||
| 	}, | ||||
| 	eventHandler = function(self, event) | ||||
| 		if event.type == 'update_password' then | ||||
| 			if not self.newPass.value or #self.newPass.value == 0 then | ||||
| 				self:emit({ type = 'error_message', message = 'Invalid password' }) | ||||
|  | ||||
| 		else | ||||
| 			Security.updatePassword(SHA.compute(self.newPass.value)) | ||||
| 			self:emit({ type = 'success_message', message = 'Password updated' }) | ||||
| 			else | ||||
| 				Security.updatePassword(SHA.compute(self.newPass.value)) | ||||
| 				self:emit({ type = 'success_message', message = 'Password updated' }) | ||||
| 			end | ||||
| 			return true | ||||
| 		end | ||||
| 		return true | ||||
| 	end | ||||
| end | ||||
|  | ||||
| return passwordTab | ||||
| } | ||||
|   | ||||
| @@ -6,8 +6,11 @@ local tab = UI.Tab { | ||||
| 	tabTitle = 'Path', | ||||
| 	description = 'Set the shell path', | ||||
| 	tabClose = true, | ||||
| 	[1] = UI.Window { | ||||
| 		x = 2, y = 2, ex = -2, ey = 4, | ||||
| 	}, | ||||
| 	entry = UI.TextEntry { | ||||
| 		x = 2, y = 2, ex = -2, | ||||
| 		x = 3, y = 3, ex = -3, | ||||
| 		limit = 256, | ||||
| 		shadowText = 'enter new path', | ||||
| 		accelerators = { | ||||
| @@ -16,7 +19,7 @@ local tab = UI.Tab { | ||||
| 		help = 'add a new path', | ||||
| 	}, | ||||
| 	grid = UI.Grid { | ||||
| 		y = 4, ey = -3, | ||||
| 		x = 2, y = 6, ex = -2, ey = -3, | ||||
| 		disableHeader = true, | ||||
| 		columns = { { key = 'value' } }, | ||||
| 		autospace = true, | ||||
|   | ||||
| @@ -2,48 +2,94 @@ local UI = require('opus.ui') | ||||
|  | ||||
| local settings = _G.settings | ||||
|  | ||||
| if settings then | ||||
| 	local settingsTab = UI.Tab { | ||||
| 		tabTitle = 'Settings', | ||||
| 		description = 'Computercraft configurable settings', | ||||
| 		grid = UI.Grid { | ||||
| 			y = 2, | ||||
| 			autospace = true, | ||||
| 			sortColumn = 'name', | ||||
| 			columns = { | ||||
| 				{ heading = 'Setting',   key = 'name' }, | ||||
| 				{ heading = 'Value', key = 'value'  }, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| local transform = { | ||||
| 	string = tostring, | ||||
| 	number = tonumber, | ||||
| } | ||||
|  | ||||
| 	function settingsTab:enable() | ||||
| return settings and UI.Tab { | ||||
| 	tabTitle = 'Settings', | ||||
| 	description = 'Computercraft settings', | ||||
| 	grid = UI.Grid { | ||||
| 		x = 2, y = 2, ex = -2, ey = -2, | ||||
| 		sortColumn = 'name', | ||||
| 		columns = { | ||||
| 			{ heading = 'Setting',   key = 'name' }, | ||||
| 			{ heading = 'Value', key = 'value'  }, | ||||
| 		}, | ||||
| 	}, | ||||
| 	editor = UI.SlideOut { | ||||
| 		y = -6, height = 6, | ||||
| 		titleBar = UI.TitleBar { | ||||
| 			event = 'slide_hide', | ||||
| 			title = 'Enter value', | ||||
| 		}, | ||||
| 		form = UI.Form { | ||||
| 			y = 2, | ||||
| 			value = UI.TextEntry { | ||||
| 				limit = 256, | ||||
| 				formIndex = 1, | ||||
| 				formLabel = 'Value', | ||||
| 				formKey = 'value', | ||||
| 			}, | ||||
| 			validateField = function(self, entry) | ||||
| 				if entry.value then | ||||
| 					return transform[self.type](entry.value) | ||||
| 				end | ||||
| 				return true | ||||
| 			end, | ||||
| 		}, | ||||
| 		accelerators = { | ||||
| 			form_cancel = 'slide_hide', | ||||
| 		}, | ||||
| 		show = function(self, entry) | ||||
| 			self.form.type = type(entry.value) or 'string' | ||||
| 			self.form:setValues(entry) | ||||
| 			self.titleBar.title = entry.name | ||||
| 			UI.SlideOut.show(self) | ||||
| 		end, | ||||
| 		eventHandler = function(self, event) | ||||
| 			if event.type == 'form_complete' then | ||||
| 				if not event.values.value then | ||||
| 					settings.unset(event.values.name) | ||||
| 					self.parent:reload() | ||||
| 				else | ||||
| 					event.values.value = transform[self.form.type](event.values.value) | ||||
| 					settings.set(event.values.name, event.values.value) | ||||
| 				end | ||||
| 				self.parent.grid:draw() | ||||
| 				self:hide() | ||||
| 				settings.save('.settings') | ||||
| 			end | ||||
| 			return UI.SlideOut.eventHandler(self, event) | ||||
| 		end, | ||||
| 	}, | ||||
| 	reload = function(self) | ||||
| 		local values = { } | ||||
| 		for _,v in pairs(settings.getNames()) do | ||||
| 			local value = settings.get(v) | ||||
| 			if not value then | ||||
| 				value = false | ||||
| 			end | ||||
| 			table.insert(values, { | ||||
| 				name = v, | ||||
| 				value = value, | ||||
| 				value = settings.get(v) or false, | ||||
| 			}) | ||||
| 		end | ||||
| 		self.grid:setValues(values) | ||||
| 		self.grid:setIndex(1) | ||||
| 	end, | ||||
| 	enable = function(self) | ||||
| 		self:reload() | ||||
| 		UI.Tab.enable(self) | ||||
| 	end | ||||
|  | ||||
| 	function settingsTab:eventHandler(event) | ||||
| 	end, | ||||
| 	eventHandler = function(self, event) | ||||
| 		if event.type == 'grid_select' then | ||||
| 			if not event.selected.value or type(event.selected.value) == 'boolean' then | ||||
| 			if type(event.selected.value) == 'boolean' then | ||||
| 				event.selected.value = not event.selected.value | ||||
| 				settings.set(event.selected.name, event.selected.value) | ||||
| 				settings.save('.settings') | ||||
| 				self.grid:draw() | ||||
| 			else | ||||
| 				self.editor:show(event.selected) | ||||
| 			end | ||||
| 			settings.set(event.selected.name, event.selected.value) | ||||
| 			settings.save('.settings') | ||||
| 			self.grid:draw() | ||||
| 			return true | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	return settingsTab | ||||
| end | ||||
| 	end, | ||||
| } | ||||
|   | ||||
| @@ -28,7 +28,7 @@ local defaults = { | ||||
| local _colors = config.color or Util.shallowCopy(defaults) | ||||
|  | ||||
| local allSettings = { } | ||||
| for k, v in pairs(defaults) do | ||||
| for k in pairs(defaults) do | ||||
| 	table.insert(allSettings, { name = k }) | ||||
| end | ||||
|  | ||||
| @@ -38,29 +38,34 @@ if not _colors.backgroundColor then | ||||
| 	_colors.fileColor = colors.white | ||||
| end | ||||
|  | ||||
| local tab = UI.Tab { | ||||
| return UI.Tab { | ||||
| 	tabTitle = 'Shell', | ||||
| 	description = 'Shell options', | ||||
| 	grid1 = UI.ScrollingGrid { | ||||
| 		y = 2, ey = -10, x = 3, ex = -16, | ||||
| 		y = 2, ey = -10, x = 2, ex = -17, | ||||
| 		disableHeader = true, | ||||
| 		columns = { { key = 'name' } }, | ||||
| 		values = allSettings, | ||||
| 		sortColumn = 'name', | ||||
| 	}, | ||||
| 	grid2 = UI.ScrollingGrid { | ||||
| 		y = 2, ey = -10, x = -14, ex = -3, | ||||
| 		y = 2, ey = -10, x = -14, ex = -2, | ||||
| 		disableHeader = true, | ||||
| 		columns = { { key = 'name' } }, | ||||
| 		values = allColors, | ||||
| 		sortColumn = 'name', | ||||
| 	}, | ||||
| 	directoryLabel = UI.Text { | ||||
| 		x = 2, y = -2, | ||||
| 		value = 'Display directory', | ||||
| 		getRowTextColor = function(self, row) | ||||
| 			local selected = self.parent.grid1:getSelected() | ||||
| 			if _colors[selected.name] == row.value then | ||||
| 				return colors.yellow | ||||
| 			end | ||||
| 			return UI.Grid.getRowTextColor(self, row) | ||||
| 		end | ||||
| 	}, | ||||
| 	directory = UI.Checkbox { | ||||
| 		x = 20, y = -2, | ||||
| 		x = 2, y = -2, | ||||
| 		labelBackgroundColor = colors.black, | ||||
| 		label = 'Directory', | ||||
| 		value = config.displayDirectory | ||||
| 	}, | ||||
| 	reset = UI.Button { | ||||
| @@ -74,69 +79,57 @@ local tab = UI.Tab { | ||||
| 		event = 'update', | ||||
| 	}, | ||||
| 	display = UI.Window { | ||||
| 		x = 3, ex = -3, y = -8, height = 5, | ||||
| 		x = 2, ex = -2, y = -8, height = 5, | ||||
| 		draw = function(self) | ||||
| 			self:clear(_colors.backgroundColor) | ||||
| 			local offset = 0 | ||||
| 			if config.displayDirectory then | ||||
| 				self:write(1, 1, | ||||
| 					'==' .. os.getComputerLabel() .. ':/dir/etc', | ||||
| 					_colors.directoryBackgroundColor, _colors.directoryTextColor) | ||||
| 				offset = 1 | ||||
| 			end | ||||
|  | ||||
| 			self:write(1, 1 + offset, '$ ', | ||||
| 				_colors.promptBackgroundColor, _colors.promptTextColor) | ||||
|  | ||||
| 			self:write(3, 1 + offset, 'ls /', | ||||
| 				_colors.backgroundColor, _colors.commandTextColor) | ||||
|  | ||||
| 			self:write(1, 2 + offset, 'sys    usr', | ||||
| 				_colors.backgroundColor, _colors.directoryColor) | ||||
|  | ||||
| 			self:write(1, 3 + offset, 'startup', | ||||
| 				_colors.backgroundColor, _colors.fileColor) | ||||
| 		end, | ||||
| 	}, | ||||
| 	eventHandler = function(self, event) | ||||
| 		if event.type =='checkbox_change' then | ||||
| 			config.displayDirectory = not not event.checked | ||||
| 			self.display:draw() | ||||
|  | ||||
| 		elseif event.type == 'grid_focus_row' and event.element == self.grid1 then | ||||
| 			self.grid2:draw() | ||||
|  | ||||
| 		elseif event.type == 'grid_select' and event.element == self.grid2 then | ||||
| 			_colors[self.grid1:getSelected().name] = event.selected.value | ||||
| 			self.display:draw() | ||||
| 			self.grid2:draw() | ||||
|  | ||||
| 		elseif event.type == 'reset' then | ||||
| 			config.color = defaults | ||||
| 			config.displayDirectory = true | ||||
| 			self.directory.value = true | ||||
| 			_colors = Util.shallowCopy(defaults) | ||||
|  | ||||
| 			Config.update('shellprompt', config) | ||||
| 			self:draw() | ||||
|  | ||||
| 		elseif event.type == 'update' then | ||||
| 			config.color = _colors | ||||
| 			Config.update('shellprompt', config) | ||||
|  | ||||
| 		end | ||||
| 		return UI.Tab.eventHandler(self, event) | ||||
| 	end | ||||
| } | ||||
|  | ||||
| function tab.grid2:getRowTextColor(row) | ||||
| 	local selected = tab.grid1:getSelected() | ||||
| 	if _colors[selected.name] == row.value then | ||||
| 		return colors.yellow | ||||
| 	end | ||||
| 	return UI.Grid.getRowTextColor(self, row) | ||||
| end | ||||
|  | ||||
| function tab.display:draw() | ||||
| 	self:clear(_colors.backgroundColor) | ||||
| 	local offset = 0 | ||||
| 	if config.displayDirectory then | ||||
| 		self:write(1, 1, | ||||
| 			'==' .. os.getComputerLabel() .. ':/dir/etc', | ||||
| 			_colors.directoryBackgroundColor, _colors.directoryTextColor) | ||||
| 		offset = 1 | ||||
| 	end | ||||
|  | ||||
| 	self:write(1, 1 + offset, '$ ', | ||||
| 		_colors.promptBackgroundColor, _colors.promptTextColor) | ||||
|  | ||||
| 	self:write(3, 1 + offset, 'ls /', | ||||
| 		_colors.backgroundColor, _colors.commandTextColor) | ||||
|  | ||||
| 	self:write(1, 2 + offset, 'sys    usr', | ||||
| 		_colors.backgroundColor, _colors.directoryColor) | ||||
|  | ||||
| 	self:write(1, 3 + offset, 'startup', | ||||
| 		_colors.backgroundColor, _colors.fileColor) | ||||
| end | ||||
|  | ||||
| function tab:eventHandler(event) | ||||
| 	if event.type =='checkbox_change' then | ||||
| 		config.displayDirectory = not not event.checked | ||||
| 		self.display:draw() | ||||
|  | ||||
| 	elseif event.type == 'grid_focus_row' and event.element == self.grid1 then | ||||
| 		self.grid2:draw() | ||||
|  | ||||
| 	elseif event.type == 'grid_select' and event.element == self.grid2 then | ||||
| 		_colors[tab.grid1:getSelected().name] = event.selected.value | ||||
| 		self.display:draw() | ||||
| 		self.grid2:draw() | ||||
|  | ||||
| 	elseif event.type == 'reset' then | ||||
| 		config.color = defaults | ||||
| 		config.displayDirectory = true | ||||
| 		self.directory.value = true | ||||
| 		_colors = Util.shallowCopy(defaults) | ||||
|  | ||||
| 		Config.update('shellprompt', config) | ||||
| 		self:draw() | ||||
|  | ||||
| 	elseif event.type == 'update' then | ||||
| 		config.color = _colors | ||||
| 		Config.update('shellprompt', config) | ||||
|  | ||||
| 	end | ||||
| 	return UI.Tab.eventHandler(self, event) | ||||
| end | ||||
|  | ||||
| return tab | ||||
|   | ||||
							
								
								
									
										89
									
								
								sys/apps/system/theme.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								sys/apps/system/theme.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| local Config = require('opus.config') | ||||
| local UI     = require('opus.ui') | ||||
| local Util   = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| local allColors = { } | ||||
| for k,v in pairs(colors) do | ||||
| 	if type(v) == 'number' then | ||||
| 		table.insert(allColors, { name = k, value = v }) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| local allSettings = { } | ||||
| for k,v in pairs(UI.colors) do | ||||
| 	allSettings[k] = { name = k, value = v } | ||||
| end | ||||
|  | ||||
| return UI.Tab { | ||||
| 	tabTitle = 'Theme', | ||||
| 	description = 'Theme colors', | ||||
| 	grid1 = UI.ScrollingGrid { | ||||
| 		y = 2, ey = -10, x = 2, ex = -17, | ||||
| 		disableHeader = true, | ||||
| 		columns = { { key = 'name' } }, | ||||
| 		values = allSettings, | ||||
| 		sortColumn = 'name', | ||||
| 	}, | ||||
| 	grid2 = UI.ScrollingGrid { | ||||
| 		y = 2, ey = -10, x = -14, ex = -2, | ||||
| 		disableHeader = true, | ||||
| 		columns = { { key = 'name' } }, | ||||
| 		values = allColors, | ||||
| 		sortColumn = 'name', | ||||
| 		getRowTextColor = function(self, row) | ||||
| 			local selected = self.parent.grid1:getSelected() | ||||
| 			if selected.value == row.value then | ||||
| 				return colors.yellow | ||||
| 			end | ||||
| 			return UI.Grid.getRowTextColor(self, row) | ||||
| 		end | ||||
| 	}, | ||||
| 	button = UI.Button { | ||||
| 		x = -9, y = -2, | ||||
| 		text = 'Update', | ||||
| 		event = 'update', | ||||
| 	}, | ||||
| 	display = UI.Window { | ||||
| 		x = 2, ex = -2, y = -8, height = 5, | ||||
| 		textColor = colors.black, | ||||
| 		backgroundColor = colors.black, | ||||
| 		draw = function(self) | ||||
| 			self:clear() | ||||
|  | ||||
| 			self:write(1, 1, Util.widthify(' Local  Global  Device', self.width), | ||||
| 				allSettings.secondary.value) | ||||
|  | ||||
| 			self:write(2, 2, 'enter command ', | ||||
| 				colors.black, colors.gray) | ||||
|  | ||||
| 			self:write(1, 3, ' Formatted ', | ||||
| 				allSettings.primary.value) | ||||
|  | ||||
| 			self:write(12, 3, Util.widthify(' Output ', self.width - 11), | ||||
| 				allSettings.tertiary.value) | ||||
|  | ||||
| 			self:write(1, 4, Util.widthify(' Key', self.width), | ||||
| 				allSettings.primary.value) | ||||
| 		end, | ||||
| 	}, | ||||
| 	eventHandler = function(self, event) | ||||
| 		if event.type == 'grid_focus_row' and event.element == self.grid1 then | ||||
| 			self.grid2:draw() | ||||
|  | ||||
| 		elseif event.type == 'grid_select' and event.element == self.grid2 then | ||||
| 			self.grid1:getSelected().value = event.selected.value | ||||
| 			self.display:draw() | ||||
| 			self.grid2:draw() | ||||
|  | ||||
| 		elseif event.type == 'update' then | ||||
| 			local config = Config.load('ui.theme', { colors = { } }) | ||||
| 			for k,v in pairs(allSettings) do | ||||
| 				config.colors[k] = v.value | ||||
| 			end | ||||
| 			Config.update('ui.theme', config) | ||||
| 		end | ||||
| 		return UI.Tab.eventHandler(self, event) | ||||
| 	end | ||||
| } | ||||
| @@ -9,7 +9,7 @@ kernel.hook('clipboard_copy', function(_, args) | ||||
| 	keyboard.clipboard =  args[1] | ||||
| end) | ||||
|  | ||||
| keyboard.addHotkey('shift-paste', function() | ||||
| local function queuePaste() | ||||
| 	local data = keyboard.clipboard | ||||
|  | ||||
| 	if type(data) == 'table' then | ||||
| @@ -20,4 +20,7 @@ keyboard.addHotkey('shift-paste', function() | ||||
| 	if data then | ||||
| 		os.queueEvent('paste', data) | ||||
| 	end | ||||
| end) | ||||
| end | ||||
|  | ||||
| kernel.hook('clipboard_paste', queuePaste) | ||||
| keyboard.addHotkey('shift-paste', queuePaste) | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| local fs = _G.fs | ||||
|  | ||||
| local function deleteIfExists(path) | ||||
|   if fs.exists(path) then | ||||
|     fs.delete(path) | ||||
|     print("Deleted outdated file at: "..path) | ||||
|   end | ||||
| 	if fs.exists(path) then | ||||
| 		fs.delete(path) | ||||
| 		print("Deleted outdated file at: "..path) | ||||
| 	end | ||||
| end | ||||
| -- cleanup outdated files | ||||
| deleteIfExists('sys/apps/shell') | ||||
| @@ -21,4 +21,4 @@ deleteIfExists('sys/autorun/apps.lua') | ||||
| deleteIfExists('sys/init/6.tl3.lua') | ||||
|  | ||||
| -- remove this file | ||||
| deleteIfExists('sys/autorun/upgraded.lua') | ||||
| -- deleteIfExists('sys/autorun/upgraded.lua') | ||||
| @@ -50,7 +50,10 @@ | ||||
| 		title = "System", | ||||
| 		category = "System", | ||||
| 		icon = "\030    \0307\031f| \010\0307\031f---o\030 \031  \010\030 \009 \0307\031f| ", | ||||
| 		iconExt = "\030 \0318\138\0308\031 \130\0318\128\031 \129\030 \0318\133\010\030 \0318\143\0308\128\0317\143\0318\128\030 \143\010\030 \0318\138\135\143\139\133", | ||||
| 		iconExt = "22070b02424\ | ||||
| 0277044\ | ||||
| 7071724", | ||||
| 		--iconExt = "\030 \0318\138\0308\031 \130\0318\128\031 \129\030 \0318\133\010\030 \0318\143\0308\128\0317\143\0318\128\030 \143\010\030 \0318\138\135\143\139\133", | ||||
| 		run = "System.lua", | ||||
| 	}, | ||||
| 	[ "2a4d562b1d9a9c90bdede6fac8ce4f7402462b86" ] = { | ||||
|   | ||||
| @@ -6,10 +6,11 @@ Shortcut keys | ||||
|  * l: Lua application | ||||
|  * f: Files | ||||
|  * e: Edit an application (or right-click) | ||||
|  * n: Network | ||||
|  * control-n: Add a new application | ||||
|  * delete: Delete an application | ||||
|  | ||||
| Adding a new application | ||||
| ======================== | ||||
| The run entry can be either a disk file or a URL. | ||||
| Icons must be in NFT format with a height of 3 and a width of 3 to 8 characters. | ||||
| Icons must be in NFT format with a height of 3 and a width of 3 to 8 characters. Magenta is used for transparency. | ||||
| @@ -1,5 +1,3 @@ | ||||
| _G.requireInjector(_ENV) | ||||
|  | ||||
| local Peripheral = require('opus.peripheral') | ||||
|  | ||||
| _G.device = Peripheral.getList() | ||||
|   | ||||
| @@ -4,11 +4,8 @@ if fs.native then | ||||
| 	return | ||||
| end | ||||
|  | ||||
| _G.requireInjector(_ENV) | ||||
| local Util = require('opus.util') | ||||
|  | ||||
| -- TODO: support getDrive for virtual nodes | ||||
|  | ||||
| fs.native = Util.shallowCopy(fs) | ||||
|  | ||||
| local fstypes = { } | ||||
| @@ -23,7 +20,6 @@ for k,fn in pairs(fs) do | ||||
| end | ||||
|  | ||||
| function nativefs.list(node, dir) | ||||
|  | ||||
| 	local files | ||||
| 	if fs.native.isDir(dir) then | ||||
| 		files = fs.native.list(dir) | ||||
| @@ -265,7 +261,6 @@ local function getfstype(fstype) | ||||
| end | ||||
|  | ||||
| function fs.mount(path, fstype, ...) | ||||
|  | ||||
| 	local vfs = getfstype(fstype) | ||||
| 	if not vfs then | ||||
| 		error('Invalid file system type') | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| _G.requireInjector(_ENV) | ||||
|  | ||||
| local Config = require('opus.config') | ||||
|  | ||||
| local device     = _G.device | ||||
|   | ||||
| @@ -32,3 +32,23 @@ end | ||||
|  | ||||
| help.setPath(table.concat(helpPaths, ':')) | ||||
| shell.setPath(table.concat(appPaths, ':')) | ||||
|  | ||||
| local function runDir(directory) | ||||
| 	local files = fs.list(directory) | ||||
| 	table.sort(files) | ||||
|  | ||||
| 	for _,file in ipairs(files) do | ||||
| 		os.sleep(0) | ||||
| 		local result, err = shell.run(directory .. '/' .. file) | ||||
| 		if not result and err then | ||||
| 			_G.printError('\n' .. err) | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| for _, package in pairs(Packages:installedSorted()) do | ||||
| 	local packageDir = 'packages/' .. package.name .. '/init' | ||||
| 	if fs.exists(packageDir) and fs.isDir(packageDir) then | ||||
| 		runDir(packageDir) | ||||
| 	end | ||||
| end | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| _G.requireInjector(_ENV) | ||||
|  | ||||
| local Blit     = require('opus.ui.blit') | ||||
| local Config   = require('opus.config') | ||||
| local trace    = require('opus.trace') | ||||
| local Util     = require('opus.util') | ||||
| @@ -47,6 +46,7 @@ local config = { | ||||
| Config.load('multishell', config) | ||||
|  | ||||
| local _colors = parentTerm.isColor() and config.color or config.standard | ||||
| local palette = parentTerm.isColor() and Blit.colorPalette or Blit.grayscalePalette | ||||
|  | ||||
| local function redrawMenu() | ||||
| 	if not tabsDirty then | ||||
| @@ -207,17 +207,11 @@ end) | ||||
| kernel.hook('multishell_redraw', function() | ||||
| 	tabsDirty = false | ||||
|  | ||||
| 	local function write(x, text, bg, fg) | ||||
| 		parentTerm.setBackgroundColor(bg) | ||||
| 		parentTerm.setTextColor(fg) | ||||
| 		parentTerm.setCursorPos(x, 1) | ||||
| 		parentTerm.write(text) | ||||
| 	end | ||||
|  | ||||
| 	local bg = _colors.tabBarBackgroundColor | ||||
| 	parentTerm.setBackgroundColor(bg) | ||||
| 	parentTerm.setCursorPos(1, 1) | ||||
| 	parentTerm.clearLine() | ||||
| 	local blit = Blit(w, { | ||||
| 		bg = _colors.tabBarBackgroundColor, | ||||
| 		fg = _colors.textColor, | ||||
| 		palette = palette, | ||||
| 	}) | ||||
|  | ||||
| 	local currentTab = kernel.getFocused() | ||||
|  | ||||
| @@ -254,21 +248,26 @@ kernel.hook('multishell_redraw', function() | ||||
| 			tabX = tabX + tab.width | ||||
| 			if tab ~= currentTab then | ||||
| 				local textColor = tab.isDead and _colors.errorColor or _colors.textColor | ||||
| 				write(tab.sx, tab.title:sub(1, tab.width - 1), | ||||
| 				blit:write(tab.sx, tab.title:sub(1, tab.width - 1), | ||||
| 					_colors.backgroundColor, textColor) | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	if currentTab then | ||||
| 		write(currentTab.sx - 1, | ||||
| 			' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ', | ||||
| 			_colors.focusBackgroundColor, _colors.focusTextColor) | ||||
| 		if currentTab.sx then | ||||
| 			blit:write(currentTab.sx - 1, | ||||
| 				' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ', | ||||
| 				_colors.focusBackgroundColor, _colors.focusTextColor) | ||||
| 		end | ||||
| 		if not currentTab.noTerminate then | ||||
| 			write(w, closeInd, _colors.backgroundColor, _colors.focusTextColor) | ||||
| 			blit:write(w, closeInd, nil, _colors.focusTextColor) | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	parentTerm.setCursorPos(1, 1) | ||||
| 	parentTerm.blit(blit.text, blit.fg, blit.bg) | ||||
|  | ||||
| 	if currentTab and currentTab.window then | ||||
| 		currentTab.window.restoreCursor() | ||||
| 	end | ||||
| @@ -334,16 +333,12 @@ kernel.hook('mouse_scroll', function(_, eventData) | ||||
| end) | ||||
|  | ||||
| kernel.hook('kernel_ready', function() | ||||
| 	local env = Util.shallowCopy(shell.getEnv()) | ||||
| 	_G.requireInjector(env) | ||||
|  | ||||
| 	overviewId = multishell.openTab({ | ||||
| 		path = config.launcher or 'sys/apps/Overview.lua', | ||||
| 		isOverview = true, | ||||
| 		noTerminate = true, | ||||
| 		focused = true, | ||||
| 		title = '+', | ||||
| 		env = env, | ||||
| 	}) | ||||
|  | ||||
| 	multishell.openTab({ | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| _G.requireInjector(_ENV) | ||||
|  | ||||
| local Array    = require('opus.array') | ||||
| local Terminal = require('opus.terminal') | ||||
| local Util     = require('opus.util') | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| local Util = require('opus.util') | ||||
|  | ||||
| local Array = { } | ||||
|  | ||||
| function Array.filter(it, f) | ||||
| @@ -14,9 +16,11 @@ function Array.removeByValue(t, e) | ||||
| 	for k,v in pairs(t) do | ||||
| 		if v == e then | ||||
| 			table.remove(t, k) | ||||
| 			break | ||||
| 			return e | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| Array.find = Util.find | ||||
|  | ||||
| return Array | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| local Util = require('opus.util') | ||||
|  | ||||
| local fs    = _G.fs | ||||
| local shell = _ENV.shell | ||||
|  | ||||
| local Config = { } | ||||
|  | ||||
| @@ -25,23 +24,6 @@ function Config.load(fname, data) | ||||
| 	return data | ||||
| end | ||||
|  | ||||
| function Config.loadWithCheck(fname, data) | ||||
| 	local filename = 'usr/config/' .. fname | ||||
|  | ||||
| 	if not fs.exists(filename) then | ||||
| 		Config.load(fname, data) | ||||
| 		print() | ||||
| 		print('The configuration file has been created.') | ||||
| 		print('The file name is: ' .. filename) | ||||
| 		print() | ||||
| 		_G.printError('Press enter to configure') | ||||
| 		_G.read() | ||||
| 		shell.run('edit ' .. filename) | ||||
| 	end | ||||
|  | ||||
| 	return Config.load(fname, data) | ||||
| end | ||||
|  | ||||
| function Config.update(fname, data) | ||||
| 	local filename = 'usr/config/' .. fname | ||||
| 	Util.writeTable(filename, data) | ||||
|   | ||||
| @@ -41,9 +41,9 @@ end | ||||
|  | ||||
| function Entry:updateScroll() | ||||
| 	local ps = self.scroll | ||||
| 	local value = _val(self.value) | ||||
| 	if self.pos > #value then | ||||
| 		self.pos = #value | ||||
| 	local len = #_val(self.value) | ||||
| 	if self.pos > len then | ||||
| 		self.pos = len | ||||
| 		self.scroll = 0 -- ?? | ||||
| 	end | ||||
| 	if self.pos - self.scroll > self.width then | ||||
| @@ -51,6 +51,11 @@ function Entry:updateScroll() | ||||
| 	elseif self.pos < self.scroll then | ||||
| 		self.scroll = self.pos | ||||
| 	end | ||||
| 	if self.scroll > 0 then | ||||
| 		if self.scroll + self.width  > len then | ||||
| 			self.scroll = len - self.width | ||||
| 		end | ||||
| 	end | ||||
| 	if ps ~= self.scroll then | ||||
| 		self.textChanged = true | ||||
| 	end | ||||
| @@ -217,6 +222,10 @@ function Entry:paste(ie) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Entry:forcePaste() | ||||
| 	os.queueEvent('clipboard_paste') | ||||
| end | ||||
|  | ||||
| function Entry:clearLine() | ||||
| 	if #_val(self.value) > 0 then | ||||
| 		self:reset() | ||||
| @@ -363,9 +372,10 @@ local mappings = { | ||||
| 	--[ 'control-d'           ] = Entry.cutNextWord, | ||||
| 	[ 'control-x'           ] = Entry.cut, | ||||
| 	[ 'paste'               ] = Entry.paste, | ||||
| --	[ 'control-y'           ] = Entry.paste,  -- well this won't work... | ||||
| 	[ 'control-y'           ] = Entry.forcePaste,  -- well this won't work... | ||||
|  | ||||
| 	[ 'mouse_doubleclick'   ] = Entry.markWord, | ||||
| 	[ 'mouse_tripleclick'   ] = Entry.markAll, | ||||
| 	[ 'shift-left'          ] = Entry.markLeft, | ||||
| 	[ 'shift-right'         ] = Entry.markRight, | ||||
| 	[ 'mouse_down'          ] = Entry.markAnchor, | ||||
|   | ||||
							
								
								
									
										21
									
								
								sys/modules/opus/fuzzy.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								sys/modules/opus/fuzzy.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| -- Based on Squid's fuzzy search | ||||
| -- https://github.com/SquidDev-CC/artist/blob/vnext/artist/lib/match.lua | ||||
| -- | ||||
| -- not very fuzzy anymore | ||||
|  | ||||
| local SCORE_WEIGHT               = 1000 | ||||
| local LEADING_LETTER_PENALTY     = -30 | ||||
| local LEADING_LETTER_PENALTY_MAX = -90 | ||||
|  | ||||
| local _find = string.find | ||||
| local _max  = math.max | ||||
|  | ||||
| return function(str, pattern) | ||||
| 	local start = _find(str, pattern, 1, true) | ||||
| 	if start then | ||||
| 		-- All letters before the current one are considered leading, so add them to our penalty | ||||
| 		return SCORE_WEIGHT | ||||
| 			+ _max(LEADING_LETTER_PENALTY * (start - 1), LEADING_LETTER_PENALTY_MAX) | ||||
| 			- (#str - #pattern) | ||||
| 	end | ||||
| end | ||||
| @@ -24,9 +24,9 @@ function git.list(repository) | ||||
|  | ||||
| 	local function getContents() | ||||
| 		local dataUrl = string.format(TREE_URL, user, repo, branch) | ||||
| 		local contents, msg = Util.httpGet(dataUrl,TREE_HEADERS) | ||||
| 		local contents, msg = Util.httpGet(dataUrl, TREE_HEADERS) | ||||
| 		if not contents then | ||||
| 			error(_sformat('Failed to download %s\n%s', dataUrl, msg), 2) | ||||
| 			error(string.format('Failed to download %s\n%s', dataUrl, msg), 2) | ||||
| 		else | ||||
| 			return json.decode(contents) | ||||
| 		end | ||||
|   | ||||
| @@ -65,7 +65,7 @@ function GPS.locate(timeout, debug) | ||||
| 	if debug then | ||||
| 		print("Position is "..pos.x..","..pos.y..","..pos.z) | ||||
| 	end | ||||
| 	return vector.new(pos.x, pos.y, pos.z) | ||||
| 	return pos and vector.new(pos.x, pos.y, pos.z) | ||||
| end | ||||
|  | ||||
| function GPS.isAvailable() | ||||
|   | ||||
| @@ -50,18 +50,20 @@ function input:toCode(ch, code) | ||||
| 	   table.insert(result, 'alt') | ||||
| 	end | ||||
|  | ||||
| 	if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or | ||||
| 		 code == keys.leftShift or code == keys.rightShift then | ||||
| 		if code and modifiers[code] then | ||||
| 			table.insert(result, 'shift') | ||||
| 		elseif #ch == 1 then | ||||
| 			table.insert(result, ch:upper()) | ||||
| 		else | ||||
| 			table.insert(result, 'shift') | ||||
| 	if ch then -- some weird things happen with control/command on mac | ||||
| 		if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or | ||||
| 			code == keys.leftShift or code == keys.rightShift then | ||||
| 			if code and modifiers[code] then | ||||
| 				table.insert(result, 'shift') | ||||
| 			elseif #ch == 1 then | ||||
| 				table.insert(result, ch:upper()) | ||||
| 			else | ||||
| 				table.insert(result, 'shift') | ||||
| 				table.insert(result, ch) | ||||
| 			end | ||||
| 		elseif not code or not modifiers[code] then | ||||
| 			table.insert(result, ch) | ||||
| 		end | ||||
| 	elseif not code or not modifiers[code] then | ||||
| 		table.insert(result, ch) | ||||
| 	end | ||||
|  | ||||
| 	return table.concat(result, '-') | ||||
| @@ -118,6 +120,7 @@ function input:translate(event, code, p1, p2) | ||||
| 		local buttons = { 'mouse_click', 'mouse_rightclick' } | ||||
| 		self.mch = buttons[code] | ||||
| 		self.mfired = nil | ||||
| 		self.anchor = { x = p1, y = p2 } | ||||
| 		return { | ||||
| 			code = input:toCode('mouse_down', 255), | ||||
| 			button = code, | ||||
| @@ -132,6 +135,8 @@ function input:translate(event, code, p1, p2) | ||||
| 			button = code, | ||||
| 			x = p1, | ||||
| 			y = p2, | ||||
| 			dx = p1 - self.anchor.x, | ||||
| 			dy = p2 - self.anchor.y, | ||||
| 		} | ||||
|  | ||||
| 	elseif event == 'mouse_up' then | ||||
| @@ -141,18 +146,26 @@ function input:translate(event, code, p1, p2) | ||||
| 				 p1 == self.x and p2 == self.y and | ||||
| 				 (clock - self.timer < .5) then | ||||
|  | ||||
| 				self.mch = 'mouse_doubleclick' | ||||
| 				self.timer = nil | ||||
| 				self.clickCount = self.clickCount + 1 | ||||
| 				if self.clickCount == 3 then | ||||
| 					self.mch = 'mouse_tripleclick' | ||||
| 					self.timer = nil | ||||
| 					self.clickCount = 1 | ||||
| 				else | ||||
| 					self.mch = 'mouse_doubleclick' | ||||
| 				end | ||||
| 			else | ||||
| 				self.timer = os.clock() | ||||
| 				self.x = p1 | ||||
| 				self.y = p2 | ||||
| 				self.clickCount = 1 | ||||
| 			end | ||||
| 			self.mfired = input:toCode(self.mch, 255) | ||||
| 		else | ||||
| 			self.mch = 'mouse_up' | ||||
| 			self.mfired = input:toCode(self.mch, 255) | ||||
| 		end | ||||
|  | ||||
| 		return { | ||||
| 			code = self.mfired, | ||||
| 			button = code, | ||||
|   | ||||
| @@ -1,16 +1,19 @@ | ||||
| local Util = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| local NFT = { } | ||||
|  | ||||
| -- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/ | ||||
|  | ||||
| local tColourLookup = { } | ||||
| local hexToColor = { } | ||||
| for n = 1, 16 do | ||||
| 	tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1) | ||||
| 	hexToColor[string.sub("0123456789abcdef", n, n)] = 2 ^ (n - 1) | ||||
| end | ||||
| local colorToHex = Util.transpose(hexToColor) | ||||
|  | ||||
| local function getColourOf(hex) | ||||
| 	return tColourLookup[hex:byte()] | ||||
| 	return hexToColor[hex] | ||||
| end | ||||
|  | ||||
| function NFT.parse(imageText) | ||||
| @@ -62,8 +65,22 @@ function NFT.parse(imageText) | ||||
| 	return image | ||||
| end | ||||
|  | ||||
| function NFT.load(path) | ||||
| function NFT.transparency(image) | ||||
| 	for y = 1, image.height do | ||||
| 		for _,key in pairs(Util.keys(image.fg[y])) do | ||||
| 			if image.fg[y][key] == colors.magenta then | ||||
| 				image.fg[y][key] = nil | ||||
| 			end | ||||
| 		end | ||||
| 		for _,key in pairs(Util.keys(image.bg[y])) do | ||||
| 			if image.bg[y][key] == colors.magenta then | ||||
| 				image.bg[y][key] = nil | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function NFT.load(path) | ||||
| 	local imageText = Util.readFile(path) | ||||
| 	if not imageText then | ||||
| 		error('Unable to read image file') | ||||
| @@ -71,4 +88,35 @@ function NFT.load(path) | ||||
| 	return NFT.parse(imageText) | ||||
| end | ||||
|  | ||||
| function NFT.save(image, filename) | ||||
| 	local bgcode, txcode = '\30', '\31' | ||||
| 	local output = { } | ||||
|  | ||||
| 	for y = 1, image.height do | ||||
| 		local lastBG, lastFG | ||||
| 		if image.text[y] then | ||||
| 			for x = 1, #image.text[y] do | ||||
| 				local bg = image.bg[y][x] or colors.magenta | ||||
| 				if bg ~= lastBG then | ||||
| 					lastBG = bg | ||||
| 					table.insert(output, bgcode .. colorToHex[bg]) | ||||
| 				end | ||||
|  | ||||
| 				local fg = image.fg[y][x] or colors.magenta | ||||
| 				if fg ~= lastFG then | ||||
| 					lastFG = fg | ||||
| 					table.insert(output, txcode .. colorToHex[fg]) | ||||
| 				end | ||||
|  | ||||
| 				table.insert(output, image.text[y][x]) | ||||
| 			end | ||||
| 		end | ||||
|  | ||||
| 		if y < image.height then | ||||
| 			table.insert(output, '\n') | ||||
| 		end | ||||
| 	end | ||||
| 	Util.writeFile(filename, table.concat(output)) | ||||
| end | ||||
|  | ||||
| return NFT | ||||
|   | ||||
| @@ -36,61 +36,66 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	local maxScroll = 100 | ||||
| 	local cx, cy = 1, 1 | ||||
| 	local blink = false | ||||
| 	local bg, fg = parent.getBackgroundColor(), parent.getTextColor() | ||||
| 	local _bg, _fg = parent.getBackgroundColor(), parent.getTextColor() | ||||
|  | ||||
| 	local canvas = Canvas({ | ||||
| 	win.canvas = Canvas({ | ||||
| 		x       = sx, | ||||
| 		y       = sy, | ||||
| 		width   = w, | ||||
| 		height  = h, | ||||
| 		isColor = parent.isColor(), | ||||
| 		offy    = 0, | ||||
| 		bg      = _bg, | ||||
| 		fg      = _fg, | ||||
| 	}) | ||||
|  | ||||
| 	win.canvas = canvas | ||||
|  | ||||
| 	local function update() | ||||
| 		if isVisible then | ||||
| 			canvas:render(parent) | ||||
| 			win.canvas:render(parent) | ||||
| 			win.setCursorPos(cx, cy) | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	local function scrollTo(y) | ||||
| 		y = math.max(0, y) | ||||
| 		y = math.min(#canvas.lines - canvas.height, y) | ||||
| 		y = math.min(#win.canvas.lines - win.canvas.height, y) | ||||
|  | ||||
| 		if y ~= canvas.offy then | ||||
| 			canvas.offy = y | ||||
| 			canvas:dirty() | ||||
| 		if y ~= win.canvas.offy then | ||||
| 			win.canvas.offy = y | ||||
| 			win.canvas:dirty() | ||||
| 			update() | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	function win.write(str) | ||||
| 		str = tostring(str) or '' | ||||
| 		canvas:write(cx, cy + canvas.offy, str, bg, fg) | ||||
| 		win.canvas:write(cx, cy +  win.canvas.offy, str, win.canvas.bg, win.canvas.fg) | ||||
| 		win.setCursorPos(cx + #str, cy) | ||||
| 		update() | ||||
| 	end | ||||
|  | ||||
| 	function win.blit(str, fg, bg) | ||||
| 		canvas:blit(cx, cy + canvas.offy, str, bg, fg) | ||||
| 		win.canvas:blit(cx, cy + win.canvas.offy, str, bg, fg) | ||||
| 		win.setCursorPos(cx + #str, cy) | ||||
| 		update() | ||||
| 	end | ||||
|  | ||||
| 	function win.clear() | ||||
| 		canvas.offy = 0 | ||||
| 		for i = #canvas.lines, canvas.height + 1, -1 do | ||||
| 			canvas.lines[i] = nil | ||||
| 		win.canvas.offy = 0 | ||||
| 		for i = #win.canvas.lines, win.canvas.height + 1, -1 do | ||||
| 			win.canvas.lines[i] = nil | ||||
| 		end | ||||
| 		canvas:clear(bg, fg) | ||||
| 		win.canvas:clear() | ||||
| 		update() | ||||
| 	end | ||||
|  | ||||
| 	function win.getLine(n) | ||||
| 		local line = win.canvas.lines[n] | ||||
| 		return line.text, line.fg, line.bg | ||||
| 	end | ||||
|  | ||||
| 	function win.clearLine() | ||||
| 		canvas:clearLine(cy + canvas.offy, bg, fg) | ||||
| 		win.canvas:clearLine(cy + win.canvas.offy) | ||||
| 		win.setCursorPos(cx, cy) | ||||
| 		update() | ||||
| 	end | ||||
| @@ -102,10 +107,14 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	function win.setCursorPos(x, y) | ||||
| 		cx, cy = math.floor(x), math.floor(y) | ||||
| 		if isVisible then | ||||
| 			parent.setCursorPos(cx + canvas.x - 1, cy + canvas.y - 1) | ||||
| 			parent.setCursorPos(cx + win.canvas.x - 1, cy + win.canvas.y - 1) | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	function win.getCursorBlink() | ||||
| 		return blink | ||||
| 	end | ||||
|  | ||||
| 	function win.setCursorBlink(b) | ||||
| 		blink = b | ||||
| 		if isVisible then | ||||
| @@ -114,12 +123,12 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	end | ||||
|  | ||||
| 	function win.isColor() | ||||
| 		return canvas.isColor | ||||
| 		return win.canvas.isColor | ||||
| 	end | ||||
| 	win.isColour = win.isColor | ||||
|  | ||||
| 	function win.setTextColor(c) | ||||
| 		fg = c | ||||
| 		win.canvas.fg = c | ||||
| 	end | ||||
| 	win.setTextColour = win.setTextColor | ||||
|  | ||||
| @@ -139,38 +148,38 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	win.setPaletteColour = win.setPaletteColor | ||||
|  | ||||
| 	function win.setBackgroundColor(c) | ||||
| 		bg = c | ||||
| 		win.canvas.bg = c | ||||
| 	end | ||||
| 	win.setBackgroundColour = win.setBackgroundColor | ||||
|  | ||||
| 	function win.getSize() | ||||
| 		return canvas.width, canvas.height | ||||
| 		return win.canvas.width, win.canvas.height | ||||
| 	end | ||||
|  | ||||
| 	function win.scroll(n) | ||||
| 		n = n or 1 | ||||
| 		if n > 0 then | ||||
| 			local lines = #canvas.lines | ||||
| 			local lines = #win.canvas.lines | ||||
| 			for i = 1, n do | ||||
| 				canvas.lines[lines + i] = { } | ||||
| 				canvas:clearLine(lines + i, bg, fg) | ||||
| 				win.canvas.lines[lines + i] = { } | ||||
| 				win.canvas:clearLine(lines + i) | ||||
| 			end | ||||
| 			while #canvas.lines > maxScroll do | ||||
| 				table.remove(canvas.lines, 1) | ||||
| 			while #win.canvas.lines > maxScroll do | ||||
| 				table.remove(win.canvas.lines, 1) | ||||
| 			end | ||||
| 			scrollTo(#canvas.lines) | ||||
| 			canvas:dirty() | ||||
| 			scrollTo(#win.canvas.lines) | ||||
| 			win.canvas:dirty() | ||||
| 			update() | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	function win.getTextColor() | ||||
| 		return fg | ||||
| 		return win.canvas.fg | ||||
| 	end | ||||
| 	win.getTextColour = win.getTextColor | ||||
|  | ||||
| 	function win.getBackgroundColor() | ||||
| 		return bg | ||||
| 		return win.canvas.bg | ||||
| 	end | ||||
| 	win.getBackgroundColour = win.getBackgroundColor | ||||
|  | ||||
| @@ -178,7 +187,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 		if visible ~= isVisible then | ||||
| 			isVisible = visible | ||||
| 			if isVisible then | ||||
| 				canvas:dirty() | ||||
| 				win.canvas:dirty() | ||||
| 				update() | ||||
| 			end | ||||
| 		end | ||||
| @@ -186,7 +195,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
|  | ||||
| 	function win.redraw() | ||||
| 		if isVisible then | ||||
| 			canvas:dirty() | ||||
| 			win.canvas:dirty() | ||||
| 			update() | ||||
| 		end | ||||
| 	end | ||||
| @@ -194,27 +203,27 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	function win.restoreCursor() | ||||
| 		if isVisible then | ||||
| 			win.setCursorPos(cx, cy) | ||||
| 			win.setTextColor(fg) | ||||
| 			win.setTextColor(win.canvas.fg) | ||||
| 			win.setCursorBlink(blink) | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	function win.getPosition() | ||||
| 		return canvas.x, canvas.y | ||||
| 		return win.canvas.x, win.canvas.y | ||||
| 	end | ||||
|  | ||||
| 	function win.reposition(x, y, width, height) | ||||
| 		canvas.x, canvas.y = x, y | ||||
| 		canvas:resize(width or canvas.width, height or canvas.height) | ||||
| 		win.canvas.x, win.canvas.y = x, y | ||||
| 		win.canvas:resize(width or win.canvas.width, height or win.canvas.height) | ||||
| 	end | ||||
|  | ||||
| 	--[[ Additional methods ]]-- | ||||
| 	function win.scrollDown() | ||||
| 		scrollTo(canvas.offy + 1) | ||||
| 		scrollTo(win.canvas.offy + 1) | ||||
| 	end | ||||
|  | ||||
| 	function win.scrollUp() | ||||
| 		scrollTo(canvas.offy - 1) | ||||
| 		scrollTo(win.canvas.offy - 1) | ||||
| 	end | ||||
|  | ||||
| 	function win.scrollTop() | ||||
| @@ -222,7 +231,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	end | ||||
|  | ||||
| 	function win.scrollBottom() | ||||
| 		scrollTo(#canvas.lines) | ||||
| 		scrollTo(#win.canvas.lines) | ||||
| 	end | ||||
|  | ||||
| 	function win.setMaxScroll(ms) | ||||
| @@ -230,37 +239,35 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	end | ||||
|  | ||||
| 	function win.getCanvas() | ||||
| 		return canvas | ||||
| 		return win.canvas | ||||
| 	end | ||||
|  | ||||
| 	function win.getParent() | ||||
| 		return parent | ||||
| 	end | ||||
|  | ||||
| 	canvas:clear() | ||||
| 	win.canvas:clear() | ||||
|  | ||||
| 	return win | ||||
| end | ||||
|  | ||||
| -- get windows contents | ||||
| function Terminal.getContents(win, parent) | ||||
| 	local oblit, oscp = parent.blit, parent.setCursorPos | ||||
| 	local lines = { } | ||||
| function Terminal.getContents(win) | ||||
| 	if not win.getLine then | ||||
| 		error('window is required') | ||||
| 	end | ||||
|  | ||||
| 	parent.blit = function(text, fg, bg) | ||||
| 		lines[#lines + 1] = { | ||||
| 	local lines = { } | ||||
| 	local _, h = win.getSize() | ||||
|  | ||||
| 	for i = 1, h do | ||||
| 		local text, fg, bg = win.getLine(i) | ||||
| 		lines[i] = { | ||||
| 			text = text, | ||||
| 			fg = fg, | ||||
| 			bg = bg, | ||||
| 		} | ||||
| 	end | ||||
| 	parent.setCursorPos = function() end | ||||
|  | ||||
| 	win.setVisible(true) | ||||
| 	win.redraw() | ||||
|  | ||||
| 	parent.blit = oblit | ||||
| 	parent.setCursorPos = oscp | ||||
|  | ||||
| 	return lines | ||||
| end | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| local Array      = require('opus.array') | ||||
| local Blit       = require('opus.ui.blit') | ||||
| local Canvas     = require('opus.ui.canvas') | ||||
| local class      = require('opus.class') | ||||
| local Event      = require('opus.event') | ||||
| local Input      = require('opus.input') | ||||
| @@ -5,7 +8,6 @@ local Transition = require('opus.ui.transition') | ||||
| local Util       = require('opus.util') | ||||
|  | ||||
| local _rep       = string.rep | ||||
| local _sub       = string.sub | ||||
| local colors     = _G.colors | ||||
| local device     = _G.device | ||||
| local fs         = _G.fs | ||||
| @@ -32,11 +34,16 @@ local textutils  = _G.textutils | ||||
| ]] | ||||
|  | ||||
| --[[-- Top Level Manager --]]-- | ||||
| local Manager = class() | ||||
| function Manager:init() | ||||
| local UI = { } | ||||
| function UI:init() | ||||
| 	self.devices = { } | ||||
| 	self.theme = { } | ||||
| 	self.extChars = Util.getVersion() >= 1.76 | ||||
| 	self.colors = { | ||||
| 		primary = colors.green, | ||||
| 		secondary = colors.lightGray, | ||||
| 		tertiary = colors.gray, | ||||
| 	} | ||||
|  | ||||
| 	local function keyFunction(event, code, held) | ||||
| 		local ie = Input:translate(event, code, held) | ||||
| @@ -44,8 +51,7 @@ function Manager:init() | ||||
| 		local currentPage = self:getActivePage() | ||||
| 		if ie and currentPage then | ||||
| 			local target = currentPage.focused or currentPage | ||||
| 			self:inputEvent(target, | ||||
| 				{ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie }) | ||||
| 			target:emit({ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie }) | ||||
| 			currentPage:sync() | ||||
| 		end | ||||
| 	end | ||||
| @@ -53,10 +59,7 @@ function Manager:init() | ||||
| 	local function resize(_, side) | ||||
| 		local dev = self.devices[side or 'terminal'] | ||||
| 		if dev and dev.currentPage then | ||||
| 			-- the parent doesn't have any children set... | ||||
| 			-- that's why we have to resize both the parent and the current page | ||||
| 			-- kinda makes sense | ||||
| 			dev.currentPage.parent:resize() | ||||
| 			dev:resize() | ||||
|  | ||||
| 			dev.currentPage:resize() | ||||
| 			dev.currentPage:draw() | ||||
| @@ -72,17 +75,14 @@ function Manager:init() | ||||
| 		monitor_resize = resize, | ||||
|  | ||||
| 		mouse_scroll = function(_, direction, x, y) | ||||
| 			local ie = Input:translate('mouse_scroll', direction, x, y) | ||||
|  | ||||
| 			local currentPage = self:getActivePage() | ||||
| 			if currentPage then | ||||
| 				local event = currentPage:pointToChild(x, y) | ||||
| 				local directions = { | ||||
| 					[ -1 ] = 'up', | ||||
| 					[  1 ] = 'down' | ||||
| 				} | ||||
| 				-- revisit - should send out scroll_up and scroll_down events | ||||
| 				-- let the element convert them to up / down | ||||
| 				self:inputEvent(event.element, | ||||
| 					{ type = 'key', key = directions[direction] }) | ||||
| 				event.type = ie.code | ||||
| 				event.ie = { code = ie.code, x = event.x, y = event.y } | ||||
| 				event.element:emit(event) | ||||
| 				currentPage:sync() | ||||
| 			end | ||||
| 		end, | ||||
| @@ -92,7 +92,7 @@ function Manager:init() | ||||
| 			if dev and dev.currentPage then | ||||
| 				Input:translate('mouse_click', 1, x, y) | ||||
| 				local ie = Input:translate('mouse_up', 1, x, y) | ||||
| 				self:click(dev.currentPage, ie.code, 1, x, y) | ||||
| 				self:click(dev.currentPage, ie) | ||||
| 			end | ||||
| 		end, | ||||
|  | ||||
| @@ -107,7 +107,7 @@ function Manager:init() | ||||
| 						currentPage:setFocus(event.element) | ||||
| 						currentPage:sync() | ||||
| 					end | ||||
| 					self:click(currentPage, ie.code, button, x, y) | ||||
| 					self:click(currentPage, ie) | ||||
| 				end | ||||
| 			end | ||||
| 		end, | ||||
| @@ -125,7 +125,7 @@ function Manager:init() | ||||
|  | ||||
| 			elseif ie and currentPage then | ||||
| 				if not currentPage.parent.device.side then | ||||
| 					self:click(currentPage, ie.code, button, x, y) | ||||
| 					self:click(currentPage, ie) | ||||
| 				end | ||||
| 			end | ||||
| 		end, | ||||
| @@ -135,7 +135,7 @@ function Manager:init() | ||||
| 			local currentPage = self:getActivePage() | ||||
|  | ||||
| 			if ie and currentPage then | ||||
| 				self:click(currentPage, ie.code, button, x, y) | ||||
| 				self:click(currentPage, ie) | ||||
| 			end | ||||
| 		end, | ||||
|  | ||||
| @@ -156,7 +156,7 @@ function Manager:init() | ||||
| 		end) | ||||
| end | ||||
|  | ||||
| function Manager:configure(appName, ...) | ||||
| function UI:configure(appName, ...) | ||||
| 	local defaults = Util.loadTable('usr/config/' .. appName) or { } | ||||
| 	if not defaults.device then | ||||
| 		defaults.device = { } | ||||
| @@ -190,19 +190,15 @@ function Manager:configure(appName, ...) | ||||
| 	end | ||||
|  | ||||
| 	if defaults.theme then | ||||
| 		for k,v in pairs(defaults.theme) do | ||||
| 			if self[k] and self[k].defaults then | ||||
| 				Util.merge(self[k].defaults, v) | ||||
| 			end | ||||
| 		end | ||||
| 		Util.deepMerge(self.theme, defaults.theme) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:disableEffects() | ||||
| 	self.defaultDevice.effectsEnabled = false | ||||
| function UI:disableEffects() | ||||
| 	self.term.effectsEnabled = false | ||||
| end | ||||
|  | ||||
| function Manager:loadTheme(filename) | ||||
| function UI:loadTheme(filename) | ||||
| 	if fs.exists(filename) then | ||||
| 		local theme, err = Util.loadTable(filename) | ||||
| 		if not theme then | ||||
| @@ -212,8 +208,20 @@ function Manager:loadTheme(filename) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:generateTheme(filename) | ||||
| function UI:generateTheme(filename) | ||||
| 	local t = { } | ||||
|  | ||||
| 	local function getName(d) | ||||
| 		if type(d) == 'string' then | ||||
| 			return string.format("'%s'", d) | ||||
| 		end | ||||
| 		for c, n in pairs(colors) do | ||||
| 			if n == d then | ||||
| 				return 'colors.' .. c | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	for k,v in pairs(self) do | ||||
| 		if type(v) == 'table' then | ||||
| 			if v._preload then | ||||
| @@ -226,72 +234,96 @@ function Manager:generateTheme(filename) | ||||
| 						if not t[k] then | ||||
| 							t[k] = { } | ||||
| 						end | ||||
| 						for c, n in pairs(colors) do | ||||
| 							if n == d then | ||||
| 								t[k][p] = 'colors.' .. c | ||||
| 								break | ||||
| 							end | ||||
| 						end | ||||
| 						t[k][p] = getName(d) | ||||
| 					end | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| 	t.colors = { | ||||
| 		primary = getName(self.colors.primary), | ||||
| 		secondary = getName(self.colors.secondary), | ||||
| 		tertiary = getName(self.colors.tertiary), | ||||
| 	} | ||||
| 	Util.writeFile(filename, textutils.serialize(t):gsub('(")', '')) | ||||
| end | ||||
|  | ||||
| function Manager:emitEvent(event) | ||||
| function UI:emitEvent(event) | ||||
| 	local currentPage = self:getActivePage() | ||||
| 	if currentPage and currentPage.focused then | ||||
| 		return currentPage.focused:emit(event) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:inputEvent(parent, event) -- deprecate ? | ||||
| 	return parent and parent:emit(event) | ||||
| end | ||||
| function UI:click(target, ie) | ||||
| 	local clickEvent | ||||
|  | ||||
| function Manager:click(target, code, button, x, y) | ||||
| 	local clickEvent = target:pointToChild(x, y) | ||||
| 	if ie.code == 'mouse_drag' then | ||||
| 		local function getPosition(element, x, y) | ||||
| 			repeat | ||||
| 				x = x - element.x + 1 | ||||
| 				y = y - element.y + 1 | ||||
| 				element = element.parent | ||||
| 			until not element | ||||
| 			return x, y | ||||
| 		end | ||||
|  | ||||
| 	if code == 'mouse_doubleclick' then | ||||
| 		if self.doubleClickElement ~= clickEvent.element then | ||||
| 		local x, y = getPosition(self.lastClicked, ie.x, ie.y) | ||||
|  | ||||
| 		clickEvent = { | ||||
| 			element = self.lastClicked, | ||||
| 			x = x, | ||||
| 			y = y, | ||||
| 			dx = ie.dx, | ||||
| 			dy = ie.dy, | ||||
| 		} | ||||
| 	else | ||||
| 		clickEvent = target:pointToChild(ie.x, ie.y) | ||||
| 	end | ||||
|  | ||||
| 	-- hack for dropdown menus | ||||
| 	if ie.code == 'mouse_click' and not clickEvent.element.focus then | ||||
| 		self:emitEvent({ type = 'mouse_out' }) | ||||
| 	end | ||||
|  | ||||
| 	if ie.code == 'mouse_doubleclick' then | ||||
| 		if self.lastClicked ~= clickEvent.element then | ||||
| 			return | ||||
| 		end | ||||
| 	else | ||||
| 		self.doubleClickElement = clickEvent.element | ||||
| 		self.lastClicked = clickEvent.element | ||||
| 	end | ||||
|  | ||||
| 	clickEvent.button = button | ||||
| 	clickEvent.type = code | ||||
| 	clickEvent.key = code | ||||
| 	clickEvent.ie = { code = code, x = clickEvent.x, y = clickEvent.y } | ||||
| 	clickEvent.button = ie.button | ||||
| 	clickEvent.type = ie.code | ||||
| 	clickEvent.key = ie.code | ||||
| 	clickEvent.ie = { code = ie.code, x = clickEvent.x, y = clickEvent.y } | ||||
| 	clickEvent.raw = ie | ||||
|  | ||||
| 	if clickEvent.element.focus then | ||||
| 		target:setFocus(clickEvent.element) | ||||
| 	end | ||||
| 	self:inputEvent(clickEvent.element, clickEvent) | ||||
| 	clickEvent.element:emit(clickEvent) | ||||
|  | ||||
| 	target:sync() | ||||
| end | ||||
|  | ||||
| function Manager:setDefaultDevice(dev) | ||||
| 	self.defaultDevice = dev | ||||
| function UI:setDefaultDevice(dev) | ||||
| 	self.term = dev | ||||
| end | ||||
|  | ||||
| function Manager:addPage(name, page) | ||||
| function UI:addPage(name, page) | ||||
| 	if not self.pages then | ||||
| 		self.pages = { } | ||||
| 	end | ||||
| 	self.pages[name] = page | ||||
| end | ||||
|  | ||||
| function Manager:setPages(pages) | ||||
| function UI:setPages(pages) | ||||
| 	self.pages = pages | ||||
| end | ||||
|  | ||||
| function Manager:getPage(pageName) | ||||
| function UI:getPage(pageName) | ||||
| 	local page = self.pages[pageName] | ||||
|  | ||||
| 	if not page then | ||||
| @@ -301,19 +333,18 @@ function Manager:getPage(pageName) | ||||
| 	return page | ||||
| end | ||||
|  | ||||
| function Manager:getActivePage(page) | ||||
| function UI:getActivePage(page) | ||||
| 	if page then | ||||
| 		return page.parent.currentPage | ||||
| 	end | ||||
| 	return self.defaultDevice.currentPage | ||||
| 	return self.term.currentPage | ||||
| end | ||||
|  | ||||
| function Manager:setActivePage(page) | ||||
| function UI:setActivePage(page) | ||||
| 	page.parent.currentPage = page | ||||
| 	page.parent.canvas = page.canvas | ||||
| end | ||||
|  | ||||
| function Manager:setPage(pageOrName, ...) | ||||
| function UI:setPage(pageOrName, ...) | ||||
| 	local page = pageOrName | ||||
|  | ||||
| 	if type(pageOrName) == 'string' then | ||||
| @@ -333,7 +364,6 @@ function Manager:setPage(pageOrName, ...) | ||||
| 			page.previousPage = currentPage | ||||
| 		end | ||||
| 		self:setActivePage(page) | ||||
| 		--page:clear(page.backgroundColor) | ||||
| 		page:enable(...) | ||||
| 		page:draw() | ||||
| 		if page.focused then | ||||
| @@ -344,27 +374,27 @@ function Manager:setPage(pageOrName, ...) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:getCurrentPage() | ||||
| 	return self.defaultDevice.currentPage | ||||
| function UI:getCurrentPage() | ||||
| 	return self.term.currentPage | ||||
| end | ||||
|  | ||||
| function Manager:setPreviousPage() | ||||
| 	if self.defaultDevice.currentPage.previousPage then | ||||
| 		local previousPage = self.defaultDevice.currentPage.previousPage.previousPage | ||||
| 		self:setPage(self.defaultDevice.currentPage.previousPage) | ||||
| 		self.defaultDevice.currentPage.previousPage = previousPage | ||||
| function UI:setPreviousPage() | ||||
| 	if self.term.currentPage.previousPage then | ||||
| 		local previousPage = self.term.currentPage.previousPage.previousPage | ||||
| 		self:setPage(self.term.currentPage.previousPage) | ||||
| 		self.term.currentPage.previousPage = previousPage | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:getDefaults(element, args) | ||||
| function UI:getDefaults(element, args) | ||||
| 	local defaults = Util.deepCopy(element.defaults) | ||||
| 	if args then | ||||
| 		Manager:mergeProperties(defaults, args) | ||||
| 		UI:mergeProperties(defaults, args) | ||||
| 	end | ||||
| 	return defaults | ||||
| end | ||||
|  | ||||
| function Manager:mergeProperties(obj, args) | ||||
| function UI:mergeProperties(obj, args) | ||||
| 	if args then | ||||
| 		for k,v in pairs(args) do | ||||
| 			if k == 'accelerators' then | ||||
| @@ -380,7 +410,7 @@ function Manager:mergeProperties(obj, args) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:pullEvents(...) | ||||
| function UI:pullEvents(...) | ||||
| 	local s, m = pcall(Event.pullEvents, ...) | ||||
| 	self.term:reset() | ||||
| 	if not s and m then | ||||
| @@ -388,21 +418,20 @@ function Manager:pullEvents(...) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| Manager.exitPullEvents = Event.exitPullEvents | ||||
| Manager.quit = Event.exitPullEvents | ||||
| Manager.start = Manager.pullEvents | ||||
| UI.exitPullEvents = Event.exitPullEvents | ||||
| UI.quit = Event.exitPullEvents | ||||
| UI.start = UI.pullEvents | ||||
|  | ||||
| local UI = Manager() | ||||
| UI:init() | ||||
|  | ||||
| --[[-- Basic drawable area --]]-- | ||||
| UI.Window = class() | ||||
| UI.Window = class(Canvas) | ||||
| UI.Window.uid = 1 | ||||
| UI.Window.docs = { } | ||||
| UI.Window.defaults = { | ||||
| 	UIElement = 'Window', | ||||
| 	x = 1, | ||||
| 	y = 1, | ||||
| 	-- z = 0, -- eventually... | ||||
| 	offx = 0, | ||||
| 	offy = 0, | ||||
| 	cursorX = 1, | ||||
| @@ -413,7 +442,9 @@ function UI.Window:init(args) | ||||
| 	local defaults = args | ||||
| 	local m = getmetatable(self)  -- get the class for this instance | ||||
| 	repeat | ||||
| 		defaults = UI:getDefaults(m, defaults) | ||||
| 		if m ~= Canvas then | ||||
| 			defaults = UI:getDefaults(m, defaults) | ||||
| 		end | ||||
| 		m = m._base | ||||
| 	until not m | ||||
| 	UI:mergeProperties(self, defaults) | ||||
| @@ -438,6 +469,9 @@ function UI.Window:init(args) | ||||
| 	until not m | ||||
| end | ||||
|  | ||||
| UI.Window.docs.postInit = [[postInit(VOID) | ||||
| Called once the window has all the properties set. | ||||
| Override to calculate properties or to dynamically add children]] | ||||
| function UI.Window:postInit() | ||||
| 	if self.parent then | ||||
| 		-- this will cascade down the whole tree of elements starting at the | ||||
| @@ -531,47 +565,60 @@ function UI.Window:layout() | ||||
| 	if not self.height then | ||||
| 		self.height = self.parent.height - self.y + 1 | ||||
| 	end | ||||
|  | ||||
| 	self.width = math.max(self.width, 1) | ||||
| 	self.height = math.max(self.height, 1) | ||||
|  | ||||
| 	self:reposition(self.x, self.y, self.width, self.height) | ||||
| end | ||||
|  | ||||
| -- Called when the window's parent has be assigned | ||||
| function UI.Window:setParent() | ||||
| 	self.oh, self.ow = self.height, self.width | ||||
| 	self.ox, self.oy = self.x, self.y | ||||
| 	self.oex, self.oey = self.ex, self.ey | ||||
|  | ||||
| 	self:layout() | ||||
|  | ||||
| 	-- Experimental | ||||
| 	-- Inherit properties from the parent container | ||||
| 	-- does this need to be in reverse order ? | ||||
| 	local m = getmetatable(self)  -- get the class for this instance | ||||
| 	repeat | ||||
| 		if m.inherits then | ||||
| 			for k, v in pairs(m.inherits) do | ||||
| 				local value = self.parent:getProperty(v) | ||||
| 				if value then | ||||
| 					self[k] = value | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 		m = m._base | ||||
| 	until not m | ||||
|  | ||||
| 	self:initChildren() | ||||
| end | ||||
|  | ||||
| function UI.Window:resize() | ||||
| 	self.height, self.width = self.oh, self.ow | ||||
| 	self.x, self.y = self.ox, self.oy | ||||
| 	self.ex, self.ey = self.oex, self.oey | ||||
|  | ||||
| 	self:layout() | ||||
|  | ||||
| 	if self.children then | ||||
| 		for _,child in ipairs(self.children) do | ||||
| 		for child in self:eachChild() do | ||||
| 			child:resize() | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:reposition(x, y, w, h) | ||||
| 	if not self.lines then | ||||
| 		Canvas.init(self, { | ||||
| 			x = x, | ||||
| 			y = y, | ||||
| 			width = w, | ||||
| 			height = h, | ||||
| 			isColor = self.parent.isColor, | ||||
| 		}) | ||||
| 	else | ||||
| 		self:move(x, y) | ||||
| 		Canvas.resize(self, w, h) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| UI.Window.docs.raise = [[raise(VOID) | ||||
| Raise this window to the top]] | ||||
| function UI.Window:raise() | ||||
| 	Array.removeByValue(self.parent.children, self) | ||||
| 	table.insert(self.parent.children, self) | ||||
| 	self:dirty(true) | ||||
| end | ||||
|  | ||||
| UI.Window.docs.add = [[add(TABLE) | ||||
| Add element(s) to a window. Example: | ||||
| page:add({ | ||||
| @@ -584,6 +631,20 @@ function UI.Window:add(children) | ||||
| 	self:initChildren() | ||||
| end | ||||
|  | ||||
| function UI.Window:eachChild() | ||||
| 	local c = self.children and Util.shallowCopy(self.children) | ||||
| 	local i = 0 | ||||
| 	return function() | ||||
| 		i = i + 1 | ||||
| 		return c and c[i] | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:remove() | ||||
| 	Array.removeByValue(self.parent.children, self) | ||||
| 	self.parent:dirty(true) | ||||
| end | ||||
|  | ||||
| function UI.Window:getCursorPos() | ||||
| 	return self.cursorX, self.cursorY | ||||
| end | ||||
| @@ -595,18 +656,20 @@ function UI.Window:setCursorPos(x, y) | ||||
| end | ||||
|  | ||||
| function UI.Window:setCursorBlink(blink) | ||||
| 	self.parent:setCursorBlink(blink) | ||||
| 	self.cursorBlink = blink | ||||
| end | ||||
|  | ||||
| UI.Window.docs.draw = [[draw(VOID) | ||||
| Redraws the window in the internal buffer.]] | ||||
| function UI.Window:draw() | ||||
| 	self:clear(self.backgroundColor) | ||||
| 	if self.children then | ||||
| 		for _,child in pairs(self.children) do | ||||
| 			if child.enabled then | ||||
| 				child:draw() | ||||
| 			end | ||||
| 	self:clear() | ||||
| 	self:drawChildren() | ||||
| end | ||||
|  | ||||
| function UI.Window:drawChildren() | ||||
| 	for child in self:eachChild() do | ||||
| 		if child.enabled then | ||||
| 			child:draw() | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
| @@ -633,19 +696,38 @@ function UI.Window:sync() | ||||
| end | ||||
|  | ||||
| function UI.Window:enable(...) | ||||
| 	self.enabled = true | ||||
| 	if self.children then | ||||
| 		for _,child in pairs(self.children) do | ||||
| 			child:enable(...) | ||||
| 	if not self.enabled then | ||||
| 		self.enabled = true | ||||
| 		if self.transitionHint then | ||||
| 			self:addTransition(self.transitionHint) | ||||
| 		end | ||||
|  | ||||
| 		if self.modal then | ||||
| 			self:raise() | ||||
| 			self:capture(self) | ||||
| 		end | ||||
|  | ||||
| 		for child in self:eachChild() do | ||||
| 			if not child.enabled then | ||||
| 				child:enable(...) | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:disable() | ||||
| 	self.enabled = false | ||||
| 	if self.children then | ||||
| 		for _,child in pairs(self.children) do | ||||
| 			child:disable() | ||||
| 	if self.enabled then | ||||
| 		self.enabled = false | ||||
| 		self.parent:dirty(true) | ||||
|  | ||||
| 		if self.modal then | ||||
| 			self:release(self) | ||||
| 		end | ||||
|  | ||||
| 		for child in self:eachChild() do | ||||
| 			if child.enabled then | ||||
| 				child:disable() | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
| @@ -656,13 +738,9 @@ function UI.Window:setTextScale(textScale) | ||||
| end | ||||
|  | ||||
| UI.Window.docs.clear = [[clear(opt COLOR bg, opt COLOR fg) | ||||
| Clears the window using the either the passed values or the defaults for that window.]] | ||||
| Clears the window using either the passed values or the defaults for that window.]] | ||||
| function UI.Window:clear(bg, fg) | ||||
| 	if self.canvas then | ||||
| 		self.canvas:clear(bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor')) | ||||
| 	else | ||||
| 		self:clearArea(1 + self.offx, 1 + self.offy, self.width, self.height, bg) | ||||
| 	end | ||||
| 	Canvas.clear(self, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor')) | ||||
| end | ||||
|  | ||||
| function UI.Window:clearLine(y, bg) | ||||
| @@ -670,39 +748,28 @@ function UI.Window:clearLine(y, bg) | ||||
| end | ||||
|  | ||||
| function UI.Window:clearArea(x, y, width, height, bg) | ||||
| 	self:fillArea(x, y, width, height, ' ', bg) | ||||
| end | ||||
|  | ||||
| function UI.Window:fillArea(x, y, width, height, fillChar, bg, fg) | ||||
| 	if width > 0 then | ||||
| 		local filler = _rep(' ', width) | ||||
| 		local filler = _rep(fillChar, width) | ||||
| 		for i = 0, height - 1 do | ||||
| 			self:write(x, y + i, filler, bg) | ||||
| 			self:write(x, y + i, filler, bg, fg) | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:write(x, y, text, bg, fg) | ||||
| 	bg = bg or self.backgroundColor | ||||
| 	fg = fg or self.textColor | ||||
|  | ||||
| 	if self.canvas then | ||||
| 		self.canvas:write(x, y, text, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor')) | ||||
| 	else | ||||
| 		x = x - self.offx | ||||
| 		y = y - self.offy | ||||
| 		if y <= self.height and y > 0 then | ||||
| 			self.parent:write( | ||||
| 				self.x + x - 1, self.y + y - 1, tostring(text), bg, fg) | ||||
| 		end | ||||
| 	end | ||||
| 	Canvas.write(self, x, y, text, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor')) | ||||
| end | ||||
|  | ||||
| function UI.Window:centeredWrite(y, text, bg, fg) | ||||
| 	if #text >= self.width then | ||||
| 		self:write(1, y, text, bg, fg) | ||||
| 	else | ||||
| 		local space = math.floor((self.width-#text) / 2) | ||||
| 		local filler = _rep(' ', space + 1) | ||||
| 		local str = _sub(filler, 1, space) .. text | ||||
| 		str = str .. _sub(filler, self.width - #str + 1) | ||||
| 		self:write(1, y, str, bg, fg) | ||||
| 		local x = math.floor((self.width-#text) / 2) + 1 | ||||
| 		self:write(x, y, text, bg, fg) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| @@ -710,89 +777,19 @@ function UI.Window:print(text, bg, fg) | ||||
| 	local marginLeft = self.marginLeft or 0 | ||||
| 	local marginRight = self.marginRight or 0 | ||||
| 	local width = self.width - marginLeft - marginRight | ||||
| 	local cs = { | ||||
| 		bg = bg or self:getProperty('backgroundColor'), | ||||
| 		fg = fg or self:getProperty('textColor'), | ||||
| 		palette = self.palette, | ||||
| 	} | ||||
|  | ||||
| 	local function nextWord(line, cx) | ||||
| 		local result = { line:find("(%w+)", cx) } | ||||
| 		if #result > 1 and result[2] > cx then | ||||
| 			return _sub(line, cx, result[2] + 1) | ||||
| 		elseif #result > 0 and result[1] == cx then | ||||
| 			result = { line:find("(%w+)", result[2]) } | ||||
| 			if #result > 0 then | ||||
| 				return _sub(line, cx, result[1] + 1) | ||||
| 			end | ||||
| 		end | ||||
| 		if cx <= #line then | ||||
| 			return _sub(line, cx, #line) | ||||
| 	local y = (self.marginTop or 0) + 1 | ||||
| 	for _,line in pairs(Util.split(text)) do | ||||
| 		for _, ln in ipairs(Blit(line, cs):wrap(width)) do | ||||
| 			self:blit(marginLeft + 1, y, ln.text, ln.bg, ln.fg) | ||||
| 			y = y + 1 | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	local function pieces(f, bg, fg) | ||||
| 		local pos = 1 | ||||
| 		local t = { } | ||||
| 		while true do | ||||
| 			local s = string.find(f, '\027', pos, true) | ||||
| 			if not s then | ||||
| 				break | ||||
| 			end | ||||
| 			if pos < s then | ||||
| 				table.insert(t, _sub(f, pos, s - 1)) | ||||
| 			end | ||||
| 			local seq = _sub(f, s) | ||||
| 			seq = seq:match("\027%[([%d;]+)m") | ||||
| 			local e = { } | ||||
| 			for color in string.gmatch(seq, "%d+") do | ||||
| 				color = tonumber(color) | ||||
| 				if color == 0 then | ||||
| 					e.fg = fg | ||||
| 					e.bg = bg | ||||
| 				elseif color > 20 then | ||||
| 					e.bg = 2 ^ (color - 21) | ||||
| 				else | ||||
| 					e.fg = 2 ^ (color - 1) | ||||
| 				end | ||||
| 			end | ||||
| 			table.insert(t, e) | ||||
| 			pos = s + #seq + 3 | ||||
| 		end | ||||
| 		if pos <= #f then | ||||
| 			table.insert(t, _sub(f, pos)) | ||||
| 		end | ||||
| 		return t | ||||
| 	end | ||||
|  | ||||
| 	local lines = Util.split(text) | ||||
| 	for k,line in pairs(lines) do | ||||
| 		local fragments = pieces(line, bg, fg) | ||||
| 		for _, fragment in ipairs(fragments) do | ||||
| 			local lx = 1 | ||||
| 			if type(fragment) == 'table' then -- ansi sequence | ||||
| 				fg = fragment.fg | ||||
| 				bg = fragment.bg | ||||
| 			else | ||||
| 				while true do | ||||
| 					local word = nextWord(fragment, lx) | ||||
| 					if not word then | ||||
| 						break | ||||
| 					end | ||||
| 					local w = word | ||||
| 					if self.cursorX + #word > width then | ||||
| 						self.cursorX = marginLeft + 1 | ||||
| 						self.cursorY = self.cursorY + 1 | ||||
| 						w = word:gsub('^ ', '') | ||||
| 					end | ||||
| 					self:write(self.cursorX, self.cursorY, w, bg, fg) | ||||
| 					self.cursorX = self.cursorX + #w | ||||
| 					lx = lx + #word | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 		if lines[k + 1] then | ||||
| 			self.cursorX = marginLeft + 1 | ||||
| 			self.cursorY = self.cursorY + 1 | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	return self.cursorX, self.cursorY | ||||
| end | ||||
|  | ||||
| UI.Window.docs.focus = [[focus(VOID) | ||||
| @@ -822,11 +819,11 @@ function UI.Window:release(child) | ||||
| end | ||||
|  | ||||
| function UI.Window:pointToChild(x, y) | ||||
| 	-- TODO: get rid of this offx/y mess and scroll canvas instead | ||||
| 	x = x + self.offx - self.x + 1 | ||||
| 	y = y + self.offy - self.y + 1 | ||||
| 	if self.children then | ||||
| 		for _,child in pairs(self.children) do | ||||
| 		for i = #self.children, 1, -1 do | ||||
| 			local child = self.children[i] | ||||
| 			if child.enabled and not child.inactive and | ||||
| 				 x >= child.x and x < child.x + child.width and | ||||
| 				 y >= child.y and y < child.y + child.height then | ||||
| @@ -868,7 +865,7 @@ function UI.Window:getFocusables() | ||||
| 	end | ||||
|  | ||||
| 	if self.children then | ||||
| 		getFocusable(self, self.x, self.y) | ||||
| 		getFocusable(self) | ||||
| 	end | ||||
|  | ||||
| 	return focusable | ||||
| @@ -882,36 +879,28 @@ function UI.Window:focusFirst() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:refocus() | ||||
| 	local el = self | ||||
| 	while el do | ||||
| 		local focusables = el:getFocusables() | ||||
| 		if focusables[1] then | ||||
| 			self:setFocus(focusables[1]) | ||||
| 			break | ||||
| 		end | ||||
| 		el = el.parent | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:scrollIntoView() | ||||
| 	local parent = self.parent | ||||
| 	local offx, offy = parent.offx, parent.offy | ||||
|  | ||||
| 	if self.x <= parent.offx then | ||||
| 		parent.offx = math.max(0, self.x - 1) | ||||
| 		parent:draw() | ||||
| 		if offx ~= parent.offx then | ||||
| 			parent:draw() | ||||
| 		end | ||||
| 	elseif self.x + self.width > parent.width + parent.offx then | ||||
| 		parent.offx = self.x + self.width - parent.width - 1 | ||||
| 		parent:draw() | ||||
| 		if offx ~= parent.offx then | ||||
| 			parent:draw() | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	-- TODO: fix | ||||
| 	local function setOffset(y) | ||||
| 		parent.offy = y | ||||
| 		if parent.canvas then | ||||
| 			parent.canvas.offy = parent.offy | ||||
| 		if offy ~= parent.offy then | ||||
| 			parent:draw() | ||||
| 		end | ||||
| 		parent:draw() | ||||
| 	end | ||||
|  | ||||
| 	if self.y <= parent.offy then | ||||
| @@ -921,45 +910,8 @@ function UI.Window:scrollIntoView() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:getCanvas() | ||||
| 	local el = self | ||||
| 	repeat | ||||
| 		if el.canvas then | ||||
| 			return el.canvas | ||||
| 		end | ||||
| 		el = el.parent | ||||
| 	until not el | ||||
| end | ||||
|  | ||||
| function UI.Window:addLayer(bg, fg) | ||||
| 	local canvas = self:getCanvas() | ||||
| 	local x, y = self.x, self.y | ||||
| 	local parent = self.parent | ||||
| 	while parent and not parent.canvas do | ||||
| 		x = x + parent.x - 1 | ||||
| 		y = y + parent.y - 1 | ||||
| 		parent = parent.parent | ||||
| 	end | ||||
| 	canvas = canvas:addLayer({ | ||||
| 		x = x, y = y, height = self.height, width = self.width | ||||
| 	}, bg, fg) | ||||
|  | ||||
| 	canvas:clear(bg or self.backgroundColor, fg or self.textColor) | ||||
| 	return canvas | ||||
| end | ||||
|  | ||||
| function UI.Window:addTransition(effect, args) | ||||
| 	if self.parent then | ||||
| 		args = args or { } | ||||
| 		if not args.x then -- not good | ||||
| 			args.x, args.y = self.x, self.y -- getPosition(self) | ||||
| 			args.width = self.width | ||||
| 			args.height = self.height | ||||
| 		end | ||||
|  | ||||
| 		args.canvas = args.canvas or self.canvas | ||||
| 		self.parent:addTransition(effect, args) | ||||
| 	end | ||||
| function UI.Window:addTransition(effect, args, canvas) | ||||
| 	self.parent:addTransition(effect, args, canvas or self) | ||||
| end | ||||
|  | ||||
| function UI.Window:emit(event) | ||||
| @@ -991,7 +943,16 @@ function UI.Window:getProperty(property) | ||||
| end | ||||
|  | ||||
| function UI.Window:find(uid) | ||||
| 	return self.children and Util.find(self.children, 'uid', uid) | ||||
| 	local el = self.children and Util.find(self.children, 'uid', uid) | ||||
| 	if not el then | ||||
| 		for child in self:eachChild() do | ||||
| 			el = child:find(uid) | ||||
| 			if el then | ||||
| 				break | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| 	return el | ||||
| end | ||||
|  | ||||
| function UI.Window:eventHandler() | ||||
| @@ -1010,18 +971,14 @@ UI.Device.defaults = { | ||||
| function UI.Device:postInit() | ||||
| 	self.device = self.device or term.current() | ||||
|  | ||||
| 	--if self.deviceType then | ||||
| 	--	self.device = device[self.deviceType] | ||||
| 	--end | ||||
|  | ||||
| 	if not self.device.setTextScale then | ||||
| 		self.device.setTextScale = function() end | ||||
| 	end | ||||
|  | ||||
| 	self.device.setTextScale(self.textScale) | ||||
| 	self.width, self.height = self.device.getSize() | ||||
|  | ||||
| 	self.isColor = self.device.isColor() | ||||
| 	Canvas.init(self, { isColor = self.isColor }) | ||||
|  | ||||
| 	UI.devices[self.device.side or 'terminal'] = self | ||||
| end | ||||
| @@ -1031,8 +988,8 @@ function UI.Device:resize() | ||||
| 	self.width, self.height = self.device.getSize() | ||||
| 	self.lines = { } | ||||
| 	-- TODO: resize all pages added to this device | ||||
| 	self.canvas:resize(self.width, self.height) | ||||
| 	self.canvas:clear(self.backgroundColor, self.textColor) | ||||
| 	Canvas.resize(self, self.width, self.height) | ||||
| 	Canvas.clear(self, self.backgroundColor, self.textColor) | ||||
| end | ||||
|  | ||||
| function UI.Device:setCursorPos(x, y) | ||||
| @@ -1046,7 +1003,6 @@ end | ||||
|  | ||||
| function UI.Device:setCursorBlink(blink) | ||||
| 	self.cursorBlink = blink | ||||
| 	self.device.setCursorBlink(blink) | ||||
| end | ||||
|  | ||||
| function UI.Device:setTextScale(textScale) | ||||
| @@ -1061,27 +1017,30 @@ function UI.Device:reset() | ||||
| 	self.device.setCursorPos(1, 1) | ||||
| end | ||||
|  | ||||
| function UI.Device:addTransition(effect, args) | ||||
| function UI.Device:addTransition(effect, args, canvas) | ||||
| 	if not self.transitions then | ||||
| 		self.transitions = { } | ||||
| 	end | ||||
|  | ||||
| 	args = args or { } | ||||
| 	args.ex = args.x + args.width - 1 | ||||
| 	args.ey = args.y + args.height - 1 | ||||
| 	args.canvas = args.canvas or self.canvas | ||||
|  | ||||
| 	if type(effect) == 'string' then | ||||
| 		effect = Transition[effect] | ||||
| 		if not effect then | ||||
| 			error('Invalid transition') | ||||
| 		effect = Transition[effect] or error('Invalid transition') | ||||
| 	end | ||||
|  | ||||
| 	-- there can be only one | ||||
| 	for k,v in pairs(self.transitions) do | ||||
| 		if v.canvas == canvas then | ||||
| 			table.remove(self.transitions, k) | ||||
| 			break | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	table.insert(self.transitions, { update = effect(args), args = args }) | ||||
| 	table.insert(self.transitions, { effect = effect, args = args or { }, canvas = canvas }) | ||||
| end | ||||
|  | ||||
| function UI.Device:runTransitions(transitions, canvas) | ||||
| function UI.Device:runTransitions(transitions) | ||||
| 	for _,k in pairs(transitions) do | ||||
| 		k.update = k.effect(k.canvas, k.args) | ||||
| 	end | ||||
| 	while true do | ||||
| 		for _,k in ipairs(Util.keys(transitions)) do | ||||
| 			local transition = transitions[k] | ||||
| @@ -1089,7 +1048,7 @@ function UI.Device:runTransitions(transitions, canvas) | ||||
| 				transitions[k] = nil | ||||
| 			end | ||||
| 		end | ||||
| 		canvas:render(self.device) | ||||
| 		self.currentPage:render(self, true) | ||||
| 		if Util.empty(transitions) then | ||||
| 			break | ||||
| 		end | ||||
| @@ -1101,17 +1060,19 @@ function UI.Device:sync() | ||||
| 	local transitions = self.effectsEnabled and self.transitions | ||||
| 	self.transitions = nil | ||||
|  | ||||
| 	if self:getCursorBlink() then | ||||
| 		self.device.setCursorBlink(false) | ||||
| 	end | ||||
| 	self.device.setCursorBlink(false) | ||||
|  | ||||
| 	self.canvas:render(self.device) | ||||
| 	if transitions then | ||||
| 		self:runTransitions(transitions, self.canvas) | ||||
| 		self:runTransitions(transitions) | ||||
| 	else | ||||
| 		self.currentPage:render(self, true) | ||||
| 	end | ||||
|  | ||||
| 	if self:getCursorBlink() then | ||||
| 		self.device.setCursorPos(self.cursorX, self.cursorY) | ||||
| 		if self.isColor then | ||||
| 			self.device.setTextColor(colors.orange) | ||||
| 		end | ||||
| 		self.device.setCursorBlink(true) | ||||
| 	end | ||||
| end | ||||
| @@ -1143,7 +1104,7 @@ local function loadComponents() | ||||
| 				return self(...) | ||||
| 			end | ||||
| 		}) | ||||
| 		UI[name]._preload = function(self) | ||||
| 		UI[name]._preload = function() | ||||
| 			return load(name) | ||||
| 		end | ||||
| 	end | ||||
| @@ -1152,6 +1113,12 @@ end | ||||
| loadComponents() | ||||
| UI:loadTheme('usr/config/ui.theme') | ||||
| Util.merge(UI.Window.defaults, UI.theme.Window) | ||||
| UI:setDefaultDevice(UI.Device({ device = term.current() })) | ||||
| Util.merge(UI.colors, UI.theme.colors) | ||||
| UI:setDefaultDevice(UI.Device()) | ||||
|  | ||||
| for k,v in pairs(UI.colors) do | ||||
| 	Canvas.colorPalette[k] = Canvas.colorPalette[v] | ||||
| 	Canvas.grayscalePalette[k] = Canvas.grayscalePalette[v] | ||||
| end | ||||
|  | ||||
| return UI | ||||
|   | ||||
							
								
								
									
										174
									
								
								sys/modules/opus/ui/blit.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								sys/modules/opus/ui/blit.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| local colors = _G.colors | ||||
| local _rep   = string.rep | ||||
| local _sub   = string.sub | ||||
|  | ||||
| local Blit = { } | ||||
|  | ||||
| Blit.colorPalette = { } | ||||
| Blit.grayscalePalette = { } | ||||
|  | ||||
| for n = 1, 16 do | ||||
| 	Blit.colorPalette[2 ^ (n - 1)]     = _sub("0123456789abcdef", n, n) | ||||
| 	Blit.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n) | ||||
| end | ||||
|  | ||||
| -- default palette | ||||
| Blit.palette = Blit.colorPalette | ||||
|  | ||||
| function Blit:init(t, args) | ||||
| 	if args then | ||||
| 		for k,v in pairs(args) do | ||||
| 			self[k] = v | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	if type(t) == 'string' then | ||||
| 		-- create a blit from a string | ||||
| 		self.text, self.bg, self.fg = Blit.toblit(t, args or { }) | ||||
|  | ||||
| 	elseif type(t) == 'number' then | ||||
| 		-- create a fixed width blit | ||||
| 		self.width = t | ||||
| 		self.text = _rep(' ', self.width) | ||||
| 		self.bg = _rep(self.palette[args.bg], self.width) | ||||
| 		self.fg = _rep(self.palette[args.fg], self.width) | ||||
|  | ||||
| 	else | ||||
| 		self.text = t.text | ||||
| 		self.bg = t.bg | ||||
| 		self.fg = t.fg | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Blit:write(x, text, bg, fg) | ||||
| 	self:insert(x, text, | ||||
| 		bg and _rep(self.palette[bg], #text), | ||||
| 		fg and _rep(self.palette[fg], #text)) | ||||
| end | ||||
|  | ||||
| function Blit:insert(x, text, bg, fg) | ||||
| 	if x <= self.width then | ||||
| 		local width = #text | ||||
| 		local tx, tex | ||||
|  | ||||
| 		if x < 1 then | ||||
| 			tx = 2 - x | ||||
| 			width = width + x - 1 | ||||
| 			x = 1 | ||||
| 		end | ||||
|  | ||||
| 		if x + width - 1 > self.width then | ||||
| 			tex = self.width - x + (tx or 1) | ||||
| 			width = tex - (tx or 1) + 1 | ||||
| 		end | ||||
|  | ||||
| 		if width > 0 then | ||||
| 			local function replace(sstr, rstr) | ||||
| 				if tx or tex then | ||||
| 					rstr = _sub(rstr, tx or 1, tex) | ||||
| 				end | ||||
| 				if x == 1 and width == self.width then | ||||
| 					return rstr | ||||
| 				elseif x == 1 then | ||||
| 					return rstr .. _sub(sstr, x + width) | ||||
| 				elseif x + width > self.width then | ||||
| 					return _sub(sstr, 1, x - 1) .. rstr | ||||
| 				end | ||||
| 				return _sub(sstr, 1, x - 1) .. rstr .. _sub(sstr, x + width) | ||||
| 			end | ||||
|  | ||||
| 			self.text = replace(self.text, text) | ||||
| 			if fg then | ||||
| 				self.fg = replace(self.fg, fg) | ||||
| 			end | ||||
| 			if bg then | ||||
| 				self.bg = replace(self.bg, bg) | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Blit:sub(s, e) | ||||
| 	return Blit({ | ||||
| 		text = self.text:sub(s, e), | ||||
| 		bg = self.bg:sub(s, e), | ||||
| 		fg = self.fg:sub(s, e), | ||||
| 	}) | ||||
| end | ||||
|  | ||||
| function Blit:wrap(max) | ||||
| 	local lines = { } | ||||
| 	local data = self | ||||
|  | ||||
|     repeat | ||||
| 		if #data.text <= max then | ||||
| 			table.insert(lines, data) | ||||
| 			break | ||||
| 		elseif data.text:sub(max+1, max+1) == ' ' then | ||||
| 			table.insert(lines, data:sub(1, max)) | ||||
| 			data = data:sub(max + 2) | ||||
| 		else | ||||
| 			local x = data.text:sub(1, max) | ||||
| 			local s = x:match('(.*) ') or x | ||||
| 			table.insert(lines, data:sub(1, #s)) | ||||
| 			data = data:sub(#s + 1) | ||||
| 		end | ||||
| 		local t = data.text:match('^%s*(.*)') | ||||
| 		local spaces = #data.text - #t | ||||
| 		if spaces > 0 then | ||||
| 			data = data:sub(spaces + 1) | ||||
| 		end | ||||
| 	until not data.text or #data.text == 0 | ||||
|  | ||||
|     return lines | ||||
| end | ||||
|  | ||||
| -- convert a string of text to blit format doing color conversion | ||||
| -- and processing ansi color sequences | ||||
| function Blit.toblit(str, cs) | ||||
| 	local text, fg, bg = '', '', '' | ||||
|  | ||||
| 	if not cs.cbg then | ||||
| 		-- reset colors | ||||
| 		cs.rbg = cs.bg or colors.black | ||||
| 		cs.rfg = cs.fg or colors.white | ||||
| 		-- current colors | ||||
| 		cs.cbg = cs.rbg | ||||
| 		cs.cfg = cs.rfg | ||||
|  | ||||
| 		cs.palette = cs.palette or Blit.palette | ||||
| 	end | ||||
|  | ||||
| 	str = str:gsub('(.-)\027%[([%d;]+)m', | ||||
| 		function(k, seq) | ||||
| 			text = text .. k | ||||
| 			bg = bg .. string.rep(cs.palette[cs.cbg], #k) | ||||
| 			fg = fg .. string.rep(cs.palette[cs.cfg], #k) | ||||
| 			for color in string.gmatch(seq, "%d+") do | ||||
| 				color = tonumber(color) | ||||
| 				if color == 0 then | ||||
| 					-- reset to default | ||||
| 					cs.cfg = cs.rfg | ||||
| 					cs.cbg = cs.rbg | ||||
| 				elseif color > 20 then | ||||
| 					cs.cbg = 2 ^ (color - 21) | ||||
| 				else | ||||
| 					cs.cfg = 2 ^ (color - 1) | ||||
| 				end | ||||
| 			end | ||||
| 			return k | ||||
| 		end) | ||||
|  | ||||
| 	local k = str:sub(#text + 1) | ||||
| 	return text .. k, | ||||
| 		bg .. string.rep(cs.palette[cs.cbg], #k), | ||||
| 		fg .. string.rep(cs.palette[cs.cfg], #k) | ||||
| end | ||||
|  | ||||
| return setmetatable(Blit, { | ||||
| 	__call = function(_, ...) | ||||
| 		local obj = setmetatable({ }, { __index = Blit }) | ||||
| 		obj:init(...) | ||||
| 		return obj | ||||
| 	end | ||||
| }) | ||||
| @@ -9,28 +9,34 @@ local colors = _G.colors | ||||
|  | ||||
| local Canvas = class() | ||||
|  | ||||
| Canvas.__visualize = false | ||||
| Canvas.colorPalette = { } | ||||
| Canvas.darkPalette = { } | ||||
| Canvas.grayscalePalette = { } | ||||
|  | ||||
| for n = 1, 16 do | ||||
| 	Canvas.colorPalette[2 ^ (n - 1)]     = _sub("0123456789abcdef", n, n) | ||||
| 	Canvas.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n) | ||||
| 	Canvas.darkPalette[2 ^ (n - 1)]      = _sub("8777777f77fff77f", n, n) | ||||
| local function genPalette(map) | ||||
| 	local t = { } | ||||
| 	local rcolors = Util.transpose(colors) | ||||
| 	for n = 1, 16 do | ||||
| 		local pow = 2 ^ (n - 1) | ||||
| 		local ch = _sub(map, n, n) | ||||
| 		t[pow] = ch | ||||
| 		t[rcolors[pow]] = ch | ||||
| 	end | ||||
| 	return t | ||||
| end | ||||
|  | ||||
| Canvas.colorPalette     = genPalette('0123456789abcdef') | ||||
| Canvas.grayscalePalette = genPalette('088888878877787f') | ||||
|  | ||||
| --[[ | ||||
| 	A canvas can have more lines than canvas.height in order to scroll | ||||
| ]] | ||||
|  | ||||
| 	TODO: finish vertical scrolling | ||||
| ]] | ||||
| function Canvas:init(args) | ||||
| 	self.x = 1 | ||||
| 	self.y = 1 | ||||
| 	self.layers = { } | ||||
| 	self.bg = colors.black | ||||
| 	self.fg = colors.white | ||||
|  | ||||
| 	Util.merge(self, args) | ||||
|  | ||||
| 	self.x = self.x or 1 | ||||
| 	self.y = self.y or 1 | ||||
| 	self.ex = self.x + self.width - 1 | ||||
| 	self.ey = self.y + self.height - 1 | ||||
|  | ||||
| @@ -46,16 +52,31 @@ function Canvas:init(args) | ||||
| 	for i = 1, self.height do | ||||
| 		self.lines[i] = { } | ||||
| 	end | ||||
|  | ||||
| 	self:clear() | ||||
| end | ||||
|  | ||||
| function Canvas:move(x, y) | ||||
| 	self.x, self.y = x, y | ||||
| 	self.ex = self.x + self.width - 1 | ||||
| 	self.ey = self.y + self.height - 1 | ||||
| 	if self.parent then | ||||
| 		self.parent:dirty(true) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Canvas:resize(w, h) | ||||
| 	for i = #self.lines, h do | ||||
| 	self:resizeBuffer(w, h) | ||||
|  | ||||
| 	self.ex = self.x + w - 1 | ||||
| 	self.ey = self.y + h - 1 | ||||
| 	self.width = w | ||||
| 	self.height = h | ||||
| end | ||||
|  | ||||
| -- resize the canvas buffer - not the canvas itself | ||||
| function Canvas:resizeBuffer(w, h) | ||||
| 	for i = #self.lines + 1, h do | ||||
| 		self.lines[i] = { } | ||||
| 		self:clearLine(i) | ||||
| 	end | ||||
| @@ -66,26 +87,24 @@ function Canvas:resize(w, h) | ||||
|  | ||||
| 	if w < self.width then | ||||
| 		for i = 1, h do | ||||
| 			self.lines[i].text = _sub(self.lines[i].text, 1, w) | ||||
| 			self.lines[i].fg = _sub(self.lines[i].fg, 1, w) | ||||
| 			self.lines[i].bg = _sub(self.lines[i].bg, 1, w) | ||||
| 			local ln = self.lines[i] | ||||
| 			ln.text = _sub(ln.text, 1, w) | ||||
| 			ln.fg = _sub(ln.fg, 1, w) | ||||
| 			ln.bg = _sub(ln.bg, 1, w) | ||||
| 		end | ||||
| 	elseif w > self.width then | ||||
| 		local d = w - self.width | ||||
| 		local text = _rep(' ', d) | ||||
| 		local fg = _rep(self.palette[self.fg or colors.white], d) | ||||
| 		local bg = _rep(self.palette[self.bg or colors.black], d) | ||||
| 		local fg = _rep(self.palette[self.fg], d) | ||||
| 		local bg = _rep(self.palette[self.bg], d) | ||||
| 		for i = 1, h do | ||||
| 			self.lines[i].text = self.lines[i].text .. text | ||||
| 			self.lines[i].fg = self.lines[i].fg .. fg | ||||
| 			self.lines[i].bg = self.lines[i].bg .. bg | ||||
| 			local ln = self.lines[i] | ||||
| 			ln.text = ln.text .. text | ||||
| 			ln.fg = ln.fg .. fg | ||||
| 			ln.bg = ln.bg .. bg | ||||
| 			ln.dirty = true | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	self.ex = self.x + w - 1 | ||||
| 	self.ey = self.y + h - 1 | ||||
| 	self.width = w | ||||
| 	self.height = h | ||||
| end | ||||
|  | ||||
| function Canvas:copy() | ||||
| @@ -105,30 +124,26 @@ function Canvas:copy() | ||||
| end | ||||
|  | ||||
| function Canvas:addLayer(layer) | ||||
| 	local canvas = Canvas({ | ||||
| 		x       = layer.x, | ||||
| 		y       = layer.y, | ||||
| 		width   = layer.width, | ||||
| 		height  = layer.height, | ||||
| 		isColor = self.isColor, | ||||
| 	}) | ||||
| 	canvas.parent = self | ||||
| 	table.insert(self.layers, canvas) | ||||
| 	return canvas | ||||
| 	layer.parent = self | ||||
| 	if not self.children then | ||||
| 		self.children = { } | ||||
| 	end | ||||
| 	table.insert(self.children, 1, layer) | ||||
| 	return layer | ||||
| end | ||||
|  | ||||
| function Canvas:removeLayer() | ||||
| 	for k, layer in pairs(self.parent.layers) do | ||||
| 	for k, layer in pairs(self.parent.children) do | ||||
| 		if layer == self then | ||||
| 			self:setVisible(false) | ||||
| 			table.remove(self.parent.layers, k) | ||||
| 			table.remove(self.parent.children, k) | ||||
| 			break | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Canvas:setVisible(visible) | ||||
| 	self.visible = visible | ||||
| 	self.visible = visible  -- TODO: use self.active = visible | ||||
| 	if not visible and self.parent then | ||||
| 		self.parent:dirty() | ||||
| 		-- TODO: set parent's lines to dirty for each line in self | ||||
| @@ -137,11 +152,10 @@ end | ||||
|  | ||||
| -- Push a layer to the top | ||||
| function Canvas:raise() | ||||
| 	if self.parent then | ||||
| 		local layers = self.parent.layers or { } | ||||
| 		for k, v in pairs(layers) do | ||||
| 	if self.parent and self.parent.children then | ||||
| 		for k, v in pairs(self.parent.children) do | ||||
| 			if v == self then | ||||
| 				table.insert(layers, table.remove(layers, k)) | ||||
| 				table.insert(self.parent.children, table.remove(self.parent.children, k)) | ||||
| 				break | ||||
| 			end | ||||
| 		end | ||||
| @@ -161,54 +175,42 @@ end | ||||
| function Canvas:blit(x, y, text, bg, fg) | ||||
| 	if y > 0 and y <= #self.lines and x <= self.width then | ||||
| 		local width = #text | ||||
| 		local tx, tex | ||||
|  | ||||
| 		-- fix ffs | ||||
| 		if x < 1 then | ||||
| 			text = _sub(text, 2 - x) | ||||
| 			if bg then | ||||
| 				bg = _sub(bg, 2 - x) | ||||
| 			end | ||||
| 			if fg then | ||||
| 				fg = _sub(fg, 2 - x) | ||||
| 			end | ||||
| 			tx = 2 - x | ||||
| 			width = width + x - 1 | ||||
| 			x = 1 | ||||
| 		end | ||||
|  | ||||
| 		if x + width - 1 > self.width then | ||||
| 			text = _sub(text, 1, self.width - x + 1) | ||||
| 			if bg then | ||||
| 				bg = _sub(bg, 1, self.width - x + 1) | ||||
| 			end | ||||
| 			if fg then | ||||
| 				fg = _sub(fg, 1, self.width - x + 1) | ||||
| 			end | ||||
| 			width = #text | ||||
| 			tex = self.width - x + (tx or 1) | ||||
| 			width = tex - (tx or 1) + 1 | ||||
| 		end | ||||
|  | ||||
| 		if width > 0 then | ||||
|  | ||||
| 			local function replace(sstr, pos, rstr) | ||||
| 				if pos == 1 and width == self.width then | ||||
| 					return rstr | ||||
| 				elseif pos == 1 then | ||||
| 					return rstr .. _sub(sstr, pos+width) | ||||
| 				elseif pos + width > self.width then | ||||
| 					return _sub(sstr, 1, pos-1) .. rstr | ||||
| 			local function replace(sstr, rstr) | ||||
| 				if tx or tex then | ||||
| 					rstr = _sub(rstr, tx or 1, tex) | ||||
| 				end | ||||
| 				return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width) | ||||
| 				if x == 1 and width == self.width then | ||||
| 					return rstr | ||||
| 				elseif x == 1 then | ||||
| 					return rstr .. _sub(sstr, x + width) | ||||
| 				elseif x + width > self.width then | ||||
| 					return _sub(sstr, 1, x - 1) .. rstr | ||||
| 				end | ||||
| 				return _sub(sstr, 1, x - 1) .. rstr .. _sub(sstr, x + width) | ||||
| 			end | ||||
|  | ||||
| 			local line = self.lines[y] | ||||
| 			if line then | ||||
| 				line.dirty = true | ||||
| 				line.text = replace(line.text, x, text, width) | ||||
| 				if fg then | ||||
| 					line.fg = replace(line.fg, x, fg, width) | ||||
| 				end | ||||
| 				if bg then | ||||
| 					line.bg = replace(line.bg, x, bg, width) | ||||
| 				end | ||||
| 			line.dirty = true | ||||
| 			line.text = replace(line.text, text) | ||||
| 			if fg then | ||||
| 				line.fg = replace(line.fg, fg) | ||||
| 			end | ||||
| 			if bg then | ||||
| 				line.bg = replace(line.bg, bg) | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| @@ -224,15 +226,15 @@ function Canvas:writeLine(y, text, fg, bg) | ||||
| end | ||||
|  | ||||
| function Canvas:clearLine(y, bg, fg) | ||||
| 	fg = _rep(self.palette[fg or colors.white], self.width) | ||||
| 	bg = _rep(self.palette[bg or colors.black], self.width) | ||||
| 	fg = _rep(self.palette[fg or self.fg], self.width) | ||||
| 	bg = _rep(self.palette[bg or self.bg], self.width) | ||||
| 	self:writeLine(y, _rep(' ', self.width), fg, bg) | ||||
| end | ||||
|  | ||||
| function Canvas:clear(bg, fg) | ||||
| 	local text = _rep(' ', self.width) | ||||
| 	fg = _rep(self.palette[fg or colors.white], self.width) | ||||
| 	bg = _rep(self.palette[bg or colors.black], self.width) | ||||
| 	fg = _rep(self.palette[fg or self.fg], self.width) | ||||
| 	bg = _rep(self.palette[bg or self.bg], self.width) | ||||
| 	for i = 1, #self.lines do | ||||
| 		self:writeLine(i, text, fg, bg) | ||||
| 	end | ||||
| @@ -246,13 +248,16 @@ function Canvas:isDirty() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Canvas:dirty() | ||||
| 	for i = 1, #self.lines do | ||||
| 		self.lines[i].dirty = true | ||||
| 	end | ||||
| 	if self.layers then | ||||
| 		for _, canvas in pairs(self.layers) do | ||||
| 			canvas:dirty() | ||||
| function Canvas:dirty(includingChildren) | ||||
| 	if self.lines then | ||||
| 		for i = 1, #self.lines do | ||||
| 			self.lines[i].dirty = true | ||||
| 		end | ||||
|  | ||||
| 		if includingChildren and self.children then | ||||
| 			for _, child in pairs(self.children) do | ||||
| 				child:dirty(true) | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
| @@ -278,115 +283,95 @@ function Canvas:applyPalette(palette) | ||||
| 	self.palette = palette | ||||
| end | ||||
|  | ||||
| function Canvas:render(device) | ||||
| 	local offset = { x = 0, y = 0 } | ||||
| 	local parent = self.parent | ||||
| 	while parent do | ||||
| 		offset.x = offset.x + parent.x - 1 | ||||
| 		offset.y = offset.y + parent.y - 1 | ||||
| 		parent = parent.parent | ||||
| 	end | ||||
| 	if #self.layers > 0 then | ||||
| 		self:__renderLayers(device, offset) | ||||
| 	else | ||||
| 		self:__blitRect(device, nil, { | ||||
| 			x = self.x + offset.x, | ||||
| 			y = self.y + offset.y | ||||
| 		}) | ||||
| 		self:clean() | ||||
| -- either render directly to the device | ||||
| -- or use another canvas as a backing buffer | ||||
| function Canvas:render(device, doubleBuffer) | ||||
| 	self.regions = Region.new(self.x, self.y, self.ex, self.ey) | ||||
| 	self:__renderLayers(device, { x = self.x - 1, y = self.y - 1 }, doubleBuffer) | ||||
|  | ||||
| 	-- doubleBuffering to reduce the amount of | ||||
| 	-- setCursorPos, blits | ||||
| 	if doubleBuffer then | ||||
| 		--[[ | ||||
| 		local drew = false | ||||
| 		local bg = _rep(2,   device.width) | ||||
| 		for k,v in pairs(device.lines) do | ||||
| 			if v.dirty then | ||||
| 				device.device.setCursorPos(device.x, device.y + k - 1) | ||||
| 				device.device.blit(v.text, v.fg, bg) | ||||
| 				drew = true | ||||
| 			end | ||||
| 		end | ||||
| 		if drew then | ||||
| 			local c = os.clock() | ||||
| 			repeat until os.clock()-c > .1 | ||||
| 		end | ||||
| 		]] | ||||
| 		for k,v in pairs(device.lines) do | ||||
| 			if v.dirty then | ||||
| 				device.device.setCursorPos(device.x, device.y + k - 1) | ||||
| 				device.device.blit(v.text, v.fg, v.bg) | ||||
| 				v.dirty = false | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| -- regions are comprised of absolute values that coorespond to the output device. | ||||
| -- regions are comprised of absolute values that correspond to the output device. | ||||
| -- canvases have coordinates relative to their parent. | ||||
| -- canvas layer's stacking order is determined by the position within the array. | ||||
| -- layers in the beginning of the array are overlayed by layers further down in | ||||
| -- the array. | ||||
| function Canvas:__renderLayers(device, offset) | ||||
| 	if #self.layers > 0 then | ||||
| 		self.regions = self.regions or Region.new(self.x + offset.x, self.y + offset.y, self.ex + offset.x, self.ey + offset.y) | ||||
|  | ||||
| 		for i = 1, #self.layers do | ||||
| 			local canvas = self.layers[i] | ||||
| 			if canvas.visible then | ||||
|  | ||||
| 				-- punch out this area from the parent's canvas | ||||
| 				self:__punch(canvas, offset) | ||||
|  | ||||
| function Canvas:__renderLayers(device, offset, doubleBuffer) | ||||
| 	if self.children then | ||||
| 		for i = #self.children, 1, -1 do | ||||
| 			local canvas = self.children[i] | ||||
| 			if canvas.visible or canvas.enabled then | ||||
| 				-- get the area to render for this layer | ||||
| 				canvas.regions = Region.new( | ||||
| 					canvas.x + offset.x, | ||||
| 					canvas.y + offset.y, | ||||
| 					canvas.ex + offset.x, | ||||
| 					canvas.ey + offset.y) | ||||
| 					canvas.x + offset.x - (self.offx or 0), | ||||
| 					canvas.y + offset.y - (self.offy or 0), | ||||
| 					canvas.ex + offset.x - (self.offx or 0), | ||||
| 					canvas.ey + offset.y - (self.offy or 0)) | ||||
|  | ||||
| 				-- contain within parent | ||||
| 				canvas.regions:andRegion(self.regions) | ||||
|  | ||||
| 				-- punch out this area from the parent's canvas | ||||
| 				self.regions:subRect( | ||||
| 					canvas.x + offset.x - (self.offx or 0), | ||||
| 					canvas.y + offset.y - (self.offy or 0), | ||||
| 					canvas.ex + offset.x - (self.offx or 0), | ||||
| 					canvas.ey + offset.y - (self.offy or 0)) | ||||
|  | ||||
| 				-- punch out any layers that overlap this one | ||||
| 				for j  = i + 1, #self.layers do | ||||
| 					if self.layers[j].visible then | ||||
| 						canvas:__punch(self.layers[j], offset) | ||||
| 					end | ||||
| 				end | ||||
| 				if #canvas.regions.region > 0 then | ||||
| 					canvas:__renderLayers(device, { | ||||
| 						x = canvas.x + offset.x - 1, | ||||
| 						y = canvas.y + offset.y - 1, | ||||
| 					}) | ||||
| 						x = canvas.x + offset.x - 1 - (self.offx or 0), | ||||
| 						y = canvas.y + offset.y - 1 - (self.offy or 0), | ||||
| 					}, doubleBuffer) | ||||
| 				end | ||||
| 				canvas.regions = nil | ||||
| 			end | ||||
| 		end | ||||
|  | ||||
| 		self:__blitClipped(device, offset) | ||||
| 		self.regions = nil | ||||
|  | ||||
| 	elseif self.regions and #self.regions.region > 0 then | ||||
| 		self:__blitClipped(device, offset) | ||||
| 		self.regions = nil | ||||
|  | ||||
| 	else | ||||
| 		self:__blitRect(device, nil, { | ||||
| 			x = self.x + offset.x, | ||||
| 			y = self.y + offset.y | ||||
| 		}) | ||||
| 		self.regions = nil | ||||
| 	end | ||||
| 	self:clean() | ||||
| end | ||||
|  | ||||
| function Canvas:__blitClipped(device, offset) | ||||
| 	if self.parent then | ||||
| 		-- contain the rendered region in the parent's region | ||||
| 		local p = Region.new(1, 1, | ||||
| 			self.parent.width + offset.x - self.x + 1, | ||||
| 			self.parent.height + offset.y - self.y + 1) | ||||
| 		self.regions:andRegion(p) | ||||
| 	end | ||||
|  | ||||
| 	for _,region in ipairs(self.regions.region) do | ||||
| 		self:__blitRect(device, | ||||
| 			{ x = region[1] - offset.x, | ||||
| 				y = region[2] - offset.y, | ||||
| 				ex = region[3] - offset.x, | ||||
| 				ey = region[4] - offset.y}, | ||||
| 			{ x = region[1], y = region[2] }) | ||||
| 			  y = region[2] - offset.y, | ||||
| 			  ex = region[3] - offset.x, | ||||
| 			  ey = region[4] - offset.y }, | ||||
| 			{ x = region[1], y = region[2] }, doubleBuffer) | ||||
| 	end | ||||
| 	self.regions = nil | ||||
|  | ||||
| 	self:clean() | ||||
| end | ||||
|  | ||||
| function Canvas:__punch(rect, offset) | ||||
| 	self.regions:subRect( | ||||
| 		rect.x + offset.x, | ||||
| 		rect.y + offset.y, | ||||
| 		rect.ex + offset.x, | ||||
| 		rect.ey + offset.y) | ||||
| end | ||||
|  | ||||
| -- performance can probably be improved by using one more buffer tied to the device | ||||
| function Canvas:__blitRect(device, src, tgt) | ||||
| 	src = src or { x = 1, y = 1, ex = self.ex - self.x + 1, ey = self.ey - self.y + 1 } | ||||
| 	tgt = tgt or self | ||||
|  | ||||
| function Canvas:__blitRect(device, src, tgt, doubleBuffer) | ||||
| 	-- for visualizing updates on the screen | ||||
| 	if Canvas.__visualize then | ||||
| 	--[[ | ||||
| 	if Canvas.__visualize or self.visualize then | ||||
| 		local drew | ||||
| 		local t  = _rep(' ', src.ex-src.x + 1) | ||||
| 		local bg = _rep(2,   src.ex-src.x + 1) | ||||
| @@ -399,10 +384,11 @@ function Canvas:__blitRect(device, src, tgt) | ||||
| 			end | ||||
| 		end | ||||
| 		if drew then | ||||
| 			local t = os.clock() | ||||
| 			repeat until os.clock()-t > .2 | ||||
| 			local c = os.clock() | ||||
| 			repeat until os.clock()-c > .03 | ||||
| 		end | ||||
| 	end | ||||
| 	]] | ||||
| 	for i = 0, src.ey - src.y do | ||||
| 		local line = self.lines[src.y + i + (self.offy or 0)] | ||||
| 		if line and line.dirty then | ||||
| @@ -412,8 +398,13 @@ function Canvas:__blitRect(device, src, tgt) | ||||
| 				fg = _sub(fg, src.x, src.ex) | ||||
| 				bg = _sub(bg, src.x, src.ex) | ||||
| 			end | ||||
| 			device.setCursorPos(tgt.x, tgt.y + i) | ||||
| 			device.blit(t, fg, bg) | ||||
| 			if doubleBuffer then | ||||
| 				Canvas.blit(device, tgt.x, tgt.y + i, | ||||
| 					t, bg, fg) | ||||
| 			else | ||||
| 				device.setCursorPos(tgt.x, tgt.y + i) | ||||
| 				device.blit(t, fg, bg) | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|   | ||||
| @@ -1,32 +0,0 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| UI.ActiveLayer = class(UI.Window) | ||||
| UI.ActiveLayer.defaults = { | ||||
| 	UIElement = 'ActiveLayer', | ||||
| } | ||||
| function UI.ActiveLayer:layout() | ||||
| 	UI.Window.layout(self) | ||||
| 	if not self.canvas then | ||||
| 		self.canvas = self:addLayer() | ||||
| 	else | ||||
| 		self.canvas:resize(self.width, self.height) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.ActiveLayer:enable(...) | ||||
| 	self.canvas:raise() | ||||
| 	self.canvas:setVisible(true) | ||||
| 	UI.Window.enable(self, ...) | ||||
| 	if self.parent.transitionHint then | ||||
| 		self:addTransition(self.parent.transitionHint) | ||||
| 	end | ||||
| 	self:focusFirst() | ||||
| end | ||||
|  | ||||
| function UI.ActiveLayer:disable() | ||||
| 	if self.canvas then | ||||
| 		self.canvas:setVisible(false) | ||||
| 	end | ||||
| 	UI.Window.disable(self) | ||||
| end | ||||
| @@ -2,32 +2,30 @@ local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Button = class(UI.Window) | ||||
| UI.Button.defaults = { | ||||
| 	UIElement = 'Button', | ||||
| 	text = 'button', | ||||
| 	backgroundColor = colors.lightGray, | ||||
| 	backgroundFocusColor = colors.gray, | ||||
| 	textFocusColor = colors.white, | ||||
| 	textInactiveColor = colors.gray, | ||||
| 	textColor = colors.black, | ||||
| 	backgroundColor = 'lightGray', | ||||
| 	backgroundFocusColor = 'gray', | ||||
| 	textFocusColor = 'white', | ||||
| 	textInactiveColor = 'gray', | ||||
| 	textColor = 'black', | ||||
| 	centered = true, | ||||
| 	height = 1, | ||||
| 	focusIndicator = ' ', | ||||
| 	event = 'button_press', | ||||
| 	accelerators = { | ||||
| 		space = 'button_activate', | ||||
| 		[ ' ' ] = 'button_activate', | ||||
| 		enter = 'button_activate', | ||||
| 		mouse_click = 'button_activate', | ||||
| 	} | ||||
| } | ||||
| function UI.Button:setParent() | ||||
| function UI.Button:layout() | ||||
| 	if not self.width and not self.ex then | ||||
| 		self.width = #self.text + 2 | ||||
| 		self.width = self.noPadding and #self.text or #self.text + 2 | ||||
| 	end | ||||
| 	UI.Window.setParent(self) | ||||
| 	UI.Window.layout(self) | ||||
| end | ||||
|  | ||||
| function UI.Button:draw() | ||||
| @@ -35,13 +33,13 @@ function UI.Button:draw() | ||||
| 	local bg = self.backgroundColor | ||||
| 	local ind = ' ' | ||||
| 	if self.focused then | ||||
| 		bg = self.backgroundFocusColor | ||||
| 		fg = self.textFocusColor | ||||
| 		bg = self:getProperty('backgroundFocusColor') | ||||
| 		fg = self:getProperty('textFocusColor') | ||||
| 		ind = self.focusIndicator | ||||
| 	elseif self.inactive then | ||||
| 		fg = self.textInactiveColor | ||||
| 		fg = self:getProperty('textInactiveColor') | ||||
| 	end | ||||
| 	local text = ind .. self.text .. ' ' | ||||
| 	local text = self.noPadding and self.text or ind .. self.text .. ' ' | ||||
| 	if self.centered then | ||||
| 		self:clear(bg) | ||||
| 		self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg) | ||||
| @@ -59,7 +57,7 @@ end | ||||
|  | ||||
| function UI.Button:eventHandler(event) | ||||
| 	if event.type == 'button_activate' then | ||||
| 		self:emit({ type = self.event, button = self }) | ||||
| 		self:emit({ type = self.event, button = self, element = self }) | ||||
| 		return true | ||||
| 	end | ||||
| 	return false | ||||
| @@ -73,7 +71,7 @@ function UI.Button.example() | ||||
| 		}, | ||||
| 		button2 = UI.Button { | ||||
| 			x = 2, y = 4, | ||||
| 			backgroundColor = colors.green, | ||||
| 			backgroundColor = 'green', | ||||
| 			event = 'custom_event', | ||||
| 		}, | ||||
| 		button3 = UI.Button { | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Checkbox = class(UI.Window) | ||||
| UI.Checkbox.defaults = { | ||||
| 	UIElement = 'Checkbox', | ||||
| @@ -11,9 +9,9 @@ UI.Checkbox.defaults = { | ||||
| 	leftMarker = UI.extChars and '\124' or '[', | ||||
| 	rightMarker = UI.extChars and '\124' or ']', | ||||
| 	value = false, | ||||
| 	textColor = colors.white, | ||||
| 	backgroundColor = colors.black, | ||||
| 	backgroundFocusColor = colors.lightGray, | ||||
| 	textColor = 'white', | ||||
| 	backgroundColor = 'black', | ||||
| 	backgroundFocusColor = 'lightGray', | ||||
| 	height = 1, | ||||
| 	width = 3, | ||||
| 	accelerators = { | ||||
| @@ -21,11 +19,9 @@ UI.Checkbox.defaults = { | ||||
| 		mouse_click = 'checkbox_toggle', | ||||
| 	} | ||||
| } | ||||
| UI.Checkbox.inherits = { | ||||
| 	labelBackgroundColor = 'backgroundColor', | ||||
| } | ||||
| function UI.Checkbox:postInit() | ||||
| function UI.Checkbox:layout() | ||||
| 	self.width = self.label and #self.label + 4 or 3 | ||||
| 	UI.Window.layout(self) | ||||
| end | ||||
|  | ||||
| function UI.Checkbox:draw() | ||||
|   | ||||
| @@ -11,8 +11,8 @@ UI.Chooser.defaults = { | ||||
| 	nochoice = 'Select', | ||||
| 	backgroundFocusColor = colors.lightGray, | ||||
| 	textInactiveColor = colors.gray, | ||||
| 	leftIndicator = UI.extChars and '\17' or '<', | ||||
| 	rightIndicator = UI.extChars and '\16' or '>', | ||||
| 	leftIndicator = UI.extChars and '\171' or '<', | ||||
| 	rightIndicator = UI.extChars and '\187' or '>', | ||||
| 	height = 1, | ||||
| 	accelerators = { | ||||
| 		space = 'choice_next', | ||||
| @@ -20,7 +20,7 @@ UI.Chooser.defaults = { | ||||
| 		left  = 'choice_prev', | ||||
| 	} | ||||
| } | ||||
| function UI.Chooser:setParent() | ||||
| function UI.Chooser:layout() | ||||
| 	if not self.width and not self.ex then | ||||
| 		self.width = 1 | ||||
| 		for _,v in pairs(self.choices) do | ||||
| @@ -30,7 +30,7 @@ function UI.Chooser:setParent() | ||||
| 		end | ||||
| 		self.width = self.width + 4 | ||||
| 	end | ||||
| 	UI.Window.setParent(self) | ||||
| 	UI.Window.layout(self) | ||||
| end | ||||
|  | ||||
| function UI.Chooser:draw() | ||||
|   | ||||
| @@ -1,15 +1,11 @@ | ||||
| local Canvas = require('opus.ui.canvas') | ||||
| local class  = require('opus.class') | ||||
| local UI     = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Dialog = class(UI.SlideOut) | ||||
| UI.Dialog.defaults = { | ||||
| 	UIElement = 'Dialog', | ||||
| 	height = 7, | ||||
| 	textColor = colors.black, | ||||
| 	backgroundColor = colors.white, | ||||
| 	noFill = true, | ||||
| 	okEvent ='dialog_ok', | ||||
| 	cancelEvent = 'dialog_cancel', | ||||
| } | ||||
| @@ -18,22 +14,36 @@ function UI.Dialog:postInit() | ||||
| 	self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title }) | ||||
| end | ||||
|  | ||||
| function UI.Dialog:show(...) | ||||
| 	local canvas = self.parent:getCanvas() | ||||
| 	self.oldPalette = canvas.palette | ||||
| 	canvas:applyPalette(Canvas.darkPalette) | ||||
| 	UI.SlideOut.show(self, ...) | ||||
| end | ||||
|  | ||||
| function UI.Dialog:hide(...) | ||||
| 	self.parent:getCanvas().palette = self.oldPalette | ||||
| 	UI.SlideOut.hide(self, ...) | ||||
| 	self.parent:draw() | ||||
| end | ||||
|  | ||||
| function UI.Dialog:eventHandler(event) | ||||
| 	if event.type == 'dialog_cancel' then | ||||
| 		self:hide() | ||||
| 	end | ||||
| 	return UI.SlideOut.eventHandler(self, event) | ||||
| end | ||||
|  | ||||
| function UI.Dialog.example() | ||||
| 	return UI.Dialog { | ||||
| 		title = 'Enter Starting Level', | ||||
| 		height = 7, | ||||
| 		form = UI.Form { | ||||
| 			y = 3, x = 2, height = 4, | ||||
| 			event = 'setStartLevel', | ||||
| 			cancelEvent = 'slide_hide', | ||||
| 			text = UI.Text { | ||||
| 				x = 5, y = 1, width = 20, | ||||
| 				textColor = 'gray', | ||||
| 			}, | ||||
| 			textEntry = UI.TextEntry { | ||||
| 				formKey = 'level', | ||||
| 				x = 15, y = 1, width = 7, | ||||
| 			}, | ||||
| 		}, | ||||
| 		statusBar = UI.StatusBar(), | ||||
| 		enable = function(self) | ||||
| 			require('opus.event').onTimeout(0, function() | ||||
| 				self:show() | ||||
| 				self:sync() | ||||
| 			end) | ||||
| 		end, | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -2,12 +2,10 @@ local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.DropMenu = class(UI.MenuBar) | ||||
| UI.DropMenu.defaults = { | ||||
| 	UIElement = 'DropMenu', | ||||
| 	backgroundColor = colors.white, | ||||
| 	backgroundColor = 'white', | ||||
| 	buttonClass = 'DropMenuItem', | ||||
| } | ||||
| function UI.DropMenu:layout() | ||||
| @@ -32,42 +30,53 @@ function UI.DropMenu:layout() | ||||
| 	self.height = #self.children + 1 | ||||
| 	self.width = maxWidth + 2 | ||||
|  | ||||
| 	if not self.canvas then | ||||
| 		self.canvas = self:addLayer() | ||||
| 	else | ||||
| 		self.canvas:resize(self.width, self.height) | ||||
| 	if self.x + self.width > self.parent.width then | ||||
| 		self.x = self.parent.width - self.width + 1 | ||||
| 	end | ||||
|  | ||||
| 	self:reposition(self.x, self.y, self.width, self.height) | ||||
| end | ||||
|  | ||||
| function UI.DropMenu:enable() | ||||
| end | ||||
| 	local menuBar = self.parent:find(self.menuUid) | ||||
| 	local hasActive | ||||
|  | ||||
| function UI.DropMenu:show(x, y) | ||||
| 	self.x, self.y = x, y | ||||
| 	self.canvas:move(x, y) | ||||
| 	self.canvas:setVisible(true) | ||||
| 	for _,c in pairs(self.children) do | ||||
| 		if not c.spacer and menuBar then | ||||
| 			c.inactive = not menuBar:getActive(c) | ||||
| 		end | ||||
| 		if not c.inactive then | ||||
| 			hasActive = true | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	-- jump through a lot of hoops if all selections are inactive | ||||
| 	-- there's gotta be a better way | ||||
| 	-- lots of exception code just to handle drop menus | ||||
| 	self.focus = not hasActive and function() end | ||||
|  | ||||
| 	UI.Window.enable(self) | ||||
|  | ||||
| 	if self.focus then | ||||
| 		self:setFocus(self) | ||||
| 	else | ||||
| 		self:focusFirst() | ||||
| 	end | ||||
| 	self:draw() | ||||
| 	self:capture(self) | ||||
| 	self:focusFirst() | ||||
| end | ||||
|  | ||||
| function UI.DropMenu:hide() | ||||
| 	self:disable() | ||||
| 	self.canvas:setVisible(false) | ||||
| 	self:release(self) | ||||
| function UI.DropMenu:disable() | ||||
| 	UI.Window.disable(self) | ||||
| 	self:remove() | ||||
| end | ||||
|  | ||||
| function UI.DropMenu:eventHandler(event) | ||||
| 	if event.type == 'focus_lost' and self.enabled then | ||||
| 		if not Util.contains(self.children, event.focused) then | ||||
| 			self:hide() | ||||
| 		if not (Util.contains(self.children, event.focused) or event.focused == self) then | ||||
| 			self:disable() | ||||
| 		end | ||||
| 	elseif event.type == 'mouse_out' and self.enabled then | ||||
| 		self:hide() | ||||
| 		self:refocus() | ||||
| 		self:disable() | ||||
| 		self:setFocus(self.parent:find(self.lastFocus)) | ||||
| 	else | ||||
| 		return UI.MenuBar.eventHandler(self, event) | ||||
| 	end | ||||
| @@ -83,6 +92,15 @@ function UI.DropMenu.example() | ||||
| 					{ spacer = true }, | ||||
| 					{ text = 'Quit        ^q', event = 'quit'   }, | ||||
| 			} }, | ||||
| 			{ text = 'Edit', dropdown = { | ||||
| 				{ text = 'Copy',           event = 'run' }, | ||||
| 				{ text = 'Paste        s', event = 'shell'  }, | ||||
| 			} }, | ||||
| 			{ text = '\187', | ||||
| 				x = -3, | ||||
| 				dropdown = { | ||||
| 					{ text = 'Associations', event = 'associate' }, | ||||
| 			} }, | ||||
| 		} | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -1,20 +1,18 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.DropMenuItem = class(UI.Button) | ||||
| UI.DropMenuItem.defaults = { | ||||
| 	UIElement = 'DropMenuItem', | ||||
| 	textColor = colors.black, | ||||
| 	backgroundColor = colors.white, | ||||
| 	textFocusColor = colors.white, | ||||
| 	textInactiveColor = colors.lightGray, | ||||
| 	backgroundFocusColor = colors.lightGray, | ||||
| 	textColor = 'black', | ||||
| 	backgroundColor = 'white', | ||||
| 	textFocusColor = 'white', | ||||
| 	textInactiveColor = 'lightGray', | ||||
| 	backgroundFocusColor = 'lightGray', | ||||
| } | ||||
| function UI.DropMenuItem:eventHandler(event) | ||||
| 	if event.type == 'button_activate' then | ||||
| 		self.parent:hide() | ||||
| 		self.parent:disable() | ||||
| 	end | ||||
| 	return UI.Button.eventHandler(self, event) | ||||
| end | ||||
|   | ||||
| @@ -1,62 +1,63 @@ | ||||
| local class    = require('opus.class') | ||||
| local Event    = require('opus.event') | ||||
| local Terminal = require('opus.terminal') | ||||
| local UI       = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Embedded = class(UI.Window) | ||||
| UI.Embedded.defaults = { | ||||
| 	UIElement = 'Embedded', | ||||
| 	backgroundColor = colors.black, | ||||
| 	textColor = colors.white, | ||||
| 	backgroundColor = 'black', | ||||
| 	textColor = 'white', | ||||
| 	maxScroll = 100, | ||||
| 	accelerators = { | ||||
| 		up = 'scroll_up', | ||||
| 		down = 'scroll_down', | ||||
| 	} | ||||
| } | ||||
| function UI.Embedded:setParent() | ||||
| 	UI.Window.setParent(self) | ||||
|  | ||||
| 	self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false) | ||||
| 	self.win.setMaxScroll(self.maxScroll) | ||||
|  | ||||
| 	local canvas = self:getCanvas() | ||||
| 	self.win.getCanvas().parent = canvas | ||||
| 	table.insert(canvas.layers, self.win.getCanvas()) | ||||
| 	self.canvas = self.win.getCanvas() | ||||
|  | ||||
| 	self.win.setCursorPos(1, 1) | ||||
| 	self.win.setBackgroundColor(self.backgroundColor) | ||||
| 	self.win.setTextColor(self.textColor) | ||||
| 	self.win.clear() | ||||
| end | ||||
|  | ||||
| function UI.Embedded:layout() | ||||
| 	UI.Window.layout(self) | ||||
| 	if self.win then | ||||
| 		self.win.reposition(self.x, self.y, self.width, self.height) | ||||
|  | ||||
| 	if not self.win then | ||||
| 		local t | ||||
| 		function self.render() | ||||
| 			if not t then | ||||
| 				t = Event.onTimeout(0, function() | ||||
| 					t = nil | ||||
| 					if self.focused then | ||||
| 						self:setCursorPos(self.win.getCursorPos()) | ||||
| 					end | ||||
| 					self:sync() | ||||
| 				end) | ||||
| 			end | ||||
| 		end | ||||
| 		self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false) | ||||
| 		self.win.canvas = self | ||||
| 		self.win.setMaxScroll(self.maxScroll) | ||||
| 		self.win.setCursorPos(1, 1) | ||||
| 		self.win.setBackgroundColor(self.backgroundColor) | ||||
| 		self.win.setTextColor(self.textColor) | ||||
| 		self.win.clear() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Embedded:draw() | ||||
| 	self.canvas:dirty() | ||||
| 	self:dirty() | ||||
| end | ||||
|  | ||||
| function UI.Embedded:focus() | ||||
| 	-- allow scrolling | ||||
| 	if self.focused then | ||||
| 		self:setCursorBlink(self.win.getCursorBlink()) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Embedded:enable() | ||||
| 	self.canvas:setVisible(true) | ||||
| 	self.canvas:raise() | ||||
| 	if self.visible then | ||||
| 		-- the window will automatically update on changes | ||||
| 		-- the canvas does not need to be rendereed | ||||
| 		self.win.setVisible(true) | ||||
| 	end | ||||
| 	UI.Window.enable(self) | ||||
| 	self.canvas:dirty() | ||||
| 	self.win.setVisible(true) | ||||
| 	self:dirty() | ||||
| end | ||||
|  | ||||
| function UI.Embedded:disable() | ||||
| 	self.canvas:setVisible(false) | ||||
| 	self.win.setVisible(false) | ||||
| 	UI.Window.disable(self) | ||||
| end | ||||
| @@ -71,17 +72,12 @@ function UI.Embedded:eventHandler(event) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Embedded:focus() | ||||
| 	-- allow scrolling | ||||
| end | ||||
|  | ||||
| function UI.Embedded.example() | ||||
| 	local Event = require('opus.event') | ||||
| 	local Util  = require('opus.util') | ||||
| 	local term  = _G.term | ||||
|  | ||||
| 	return UI.Embedded { | ||||
| 		visible = true, | ||||
| 		y = 2, x = 2, ex = -2, ey = -2, | ||||
| 		enable = function (self) | ||||
| 			UI.Embedded.enable(self) | ||||
| 			Event.addRoutine(function() | ||||
| @@ -90,10 +86,11 @@ function UI.Embedded.example() | ||||
| 				term.redirect(oterm) | ||||
| 			end) | ||||
| 		end, | ||||
| 		eventHandler = function(_, event) | ||||
| 		eventHandler = function(self, event) | ||||
| 			if event.type == 'key' then | ||||
| 				return true | ||||
| 			end | ||||
| 			return UI.Embedded.eventHandler(self, event) | ||||
| 		end | ||||
| 	} | ||||
| end | ||||
|   | ||||
							
								
								
									
										118
									
								
								sys/modules/opus/ui/components/FileSelect.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								sys/modules/opus/ui/components/FileSelect.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| local class = require('opus.class') | ||||
| local UI   = require('opus.ui') | ||||
| local Util = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
| local fs     = _G.fs | ||||
|  | ||||
| UI.FileSelect = class(UI.Window) | ||||
| UI.FileSelect.defaults = { | ||||
| 	UIElement = 'FileSelect', | ||||
| } | ||||
| function UI.FileSelect:postInit() | ||||
|     self.grid = UI.ScrollingGrid { | ||||
|         x = 2, y = 2, ex = -2, ey = -4, | ||||
|         dir = '/', | ||||
|         sortColumn = 'name', | ||||
|         columns = { | ||||
|             { heading = 'Name', key = 'name' }, | ||||
|             { heading = 'Size', key = 'size', width = 5 } | ||||
|         }, | ||||
|         getDisplayValues = function(_, row) | ||||
|             if row.size then | ||||
|                 row = Util.shallowCopy(row) | ||||
|                 row.size = Util.toBytes(row.size) | ||||
|             end | ||||
|             return row | ||||
|         end, | ||||
|         getRowTextColor = function(_, file) | ||||
|             if file.isDir then | ||||
|                 return colors.cyan | ||||
|             end | ||||
|             if file.isReadOnly then | ||||
|                 return colors.pink | ||||
|             end | ||||
|             return colors.white | ||||
|         end, | ||||
|         sortCompare = function(self, a, b) | ||||
|             if self.sortColumn == 'size' then | ||||
|                 return a.size < b.size | ||||
|             end | ||||
|             if a.isDir == b.isDir then | ||||
|                 return a.name:lower() < b.name:lower() | ||||
|             end | ||||
|             return a.isDir | ||||
|         end, | ||||
|         draw = function(self) | ||||
|             local files = fs.listEx(self.dir) | ||||
|             if #self.dir > 0 then | ||||
|                 table.insert(files, { | ||||
|                     name = '..', | ||||
|                     isDir = true, | ||||
|                 }) | ||||
|             end | ||||
|             self:setValues(files) | ||||
|             self:setIndex(1) | ||||
|             UI.Grid.draw(self) | ||||
|         end, | ||||
|     } | ||||
|     self.path = UI.TextEntry { | ||||
|         x  =  2, | ||||
|         y  = -2, | ||||
|         ex = -11, | ||||
|         limit = 256, | ||||
|         accelerators = { | ||||
|             enter = 'path_enter', | ||||
|         } | ||||
|     } | ||||
|     self.cancel = UI.Button { | ||||
|         text = 'Cancel', | ||||
|         x = -9, | ||||
|         y = -2, | ||||
|         event = 'select_cancel', | ||||
|     } | ||||
| end | ||||
|  | ||||
| function UI.FileSelect:draw() | ||||
|     self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), colors.black, colors.gray) | ||||
|     self:drawChildren() | ||||
| end | ||||
|  | ||||
| function UI.FileSelect:enable(path) | ||||
|     self:setPath(path or '') | ||||
|     UI.Window.enable(self) | ||||
| end | ||||
|  | ||||
| function UI.FileSelect:setPath(path) | ||||
| 	self.grid.dir = path | ||||
| 	while not fs.isDir(self.grid.dir) do | ||||
| 		self.grid.dir = fs.getDir(self.grid.dir) | ||||
| 	end | ||||
| 	self.path.value = self.grid.dir | ||||
| end | ||||
|  | ||||
| function UI.FileSelect:eventHandler(event) | ||||
| 	if event.type == 'grid_select' then | ||||
| 		self.grid.dir = fs.combine(self.grid.dir, event.selected.name) | ||||
| 		self.path.value = self.grid.dir | ||||
| 		if event.selected.isDir then | ||||
| 			self.grid:draw() | ||||
| 			self.path:draw() | ||||
|         else | ||||
|             self:emit({ type = 'select_file', file = '/' .. self.path.value, element = self }) | ||||
|         end | ||||
|         return true | ||||
|  | ||||
|     elseif event.type == 'path_enter' then | ||||
|         if self.path.value then | ||||
|             if fs.isDir(self.path.value) then | ||||
|                 self:setPath(self.path.value) | ||||
|                 self.grid:draw() | ||||
|                 self.path:draw() | ||||
|             else | ||||
|                 self:emit({ type = 'select_file', file = '/' .. self.path.value, element = self }) | ||||
|             end | ||||
|         end | ||||
|         return true | ||||
| 	end | ||||
| end | ||||
							
								
								
									
										16
									
								
								sys/modules/opus/ui/components/FlatButton.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								sys/modules/opus/ui/components/FlatButton.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| UI.FlatButton = class(UI.Button) | ||||
| UI.FlatButton.defaults = { | ||||
| 	UIElement = 'FlatButton', | ||||
| 	textColor = 'black', | ||||
| 	textFocusColor = 'white', | ||||
| 	noPadding = true, | ||||
| } | ||||
| function UI.FlatButton:setParent() | ||||
| 	self.backgroundColor = self.parent:getProperty('backgroundColor') | ||||
| 	self.backgroundFocusColor = self.backgroundColor | ||||
|  | ||||
| 	UI.Button.setParent(self) | ||||
| end | ||||
| @@ -2,8 +2,6 @@ local class = require('opus.class') | ||||
| local Sound = require('opus.sound') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Form = class(UI.Window) | ||||
| UI.Form.defaults = { | ||||
| 	UIElement = 'Form', | ||||
| @@ -68,7 +66,7 @@ function UI.Form:createForm() | ||||
| 				table.insert(self.children, UI.Text { | ||||
| 					x = self.margin, | ||||
| 					y = child.y, | ||||
| 					textColor = colors.black, | ||||
| 					textColor = 'black', | ||||
| 					width = #child.formLabel, | ||||
| 					value = child.formLabel, | ||||
| 				}) | ||||
|   | ||||
| @@ -2,10 +2,8 @@ local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
| local os     = _G.os | ||||
| local _rep   = string.rep | ||||
| local _sub   = string.sub | ||||
|  | ||||
| local function safeValue(v) | ||||
| 	local t = type(v) | ||||
| @@ -23,18 +21,7 @@ function Writer:init(element, y) | ||||
| end | ||||
|  | ||||
| function Writer:write(s, width, align, bg, fg) | ||||
| 	local len = #tostring(s or '') | ||||
| 	if len > width then | ||||
| 		s = _sub(s, 1, width) | ||||
| 	end | ||||
| 	local padding = len < width and _rep(' ', width - len) | ||||
| 	if padding then | ||||
| 		if align == 'right' then | ||||
| 			s = padding .. s | ||||
| 		else | ||||
| 			s = s .. padding | ||||
| 		end | ||||
| 	end | ||||
| 	s = Util.widthify(s, width, align) | ||||
| 	self.element:write(self.x, self.y, s, bg, fg) | ||||
| 	self.x = self.x + width | ||||
| end | ||||
| @@ -56,16 +43,16 @@ UI.Grid.defaults = { | ||||
| 	disableHeader = false, | ||||
| 	headerHeight = 1, | ||||
| 	marginRight = 0, | ||||
| 	textColor = colors.white, | ||||
| 	textSelectedColor = colors.white, | ||||
| 	backgroundColor = colors.black, | ||||
| 	backgroundSelectedColor = colors.gray, | ||||
| 	headerBackgroundColor = colors.cyan, | ||||
| 	headerTextColor = colors.white, | ||||
| 	headerSortColor = colors.yellow, | ||||
| 	unfocusedTextSelectedColor = colors.white, | ||||
| 	unfocusedBackgroundSelectedColor = colors.gray, | ||||
| 	focusIndicator = UI.extChars and '\183' or '>', | ||||
| 	textColor = 'white', | ||||
| 	textSelectedColor = 'white', | ||||
| 	backgroundColor = 'black', | ||||
| 	backgroundSelectedColor = 'gray', | ||||
| 	headerBackgroundColor = 'primary', | ||||
| 	headerTextColor = 'white', | ||||
| 	headerSortColor = 'yellow', | ||||
| 	unfocusedTextSelectedColor = 'white', | ||||
| 	unfocusedBackgroundSelectedColor = 'gray', | ||||
| 	focusIndicator = UI.extChars and '\26' or '>', | ||||
| 	sortIndicator = ' ', | ||||
| 	inverseSortIndicator = UI.extChars and '\24' or '^', | ||||
| 	values = { }, | ||||
| @@ -83,8 +70,8 @@ UI.Grid.defaults = { | ||||
| 		[ 'control-f' ] = 'scroll_pageDown', | ||||
| 	}, | ||||
| } | ||||
| function UI.Grid:setParent() | ||||
| 	UI.Window.setParent(self) | ||||
| function UI.Grid:layout() | ||||
| 	UI.Window.layout(self) | ||||
|  | ||||
| 	for _,c in pairs(self.columns) do | ||||
| 		c.cw = c.width | ||||
| @@ -522,7 +509,7 @@ function UI.Grid.example() | ||||
| 			values = values, | ||||
| 			columns = { | ||||
| 				{ heading = 'key', key = 'key', width = 6,  }, | ||||
| 				{ heading = 'value', key = 'value', textColor = colors.yellow }, | ||||
| 				{ heading = 'value', key = 'value', textColor = 'yellow' }, | ||||
| 			}, | ||||
| 		}, | ||||
| 		autospace = UI.Grid { | ||||
|   | ||||
| @@ -1,19 +1,28 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local lookup = '0123456789abcdef' | ||||
|  | ||||
| -- handle files produced by Paint | ||||
| UI.Image = class(UI.Window) | ||||
| UI.Image.defaults = { | ||||
| 	UIElement = 'Image', | ||||
| 	event = 'button_press', | ||||
| } | ||||
| function UI.Image:setParent() | ||||
| 	if self.image then | ||||
| function UI.Image:postInit() | ||||
| 	if self.filename then | ||||
| 		self.image = Util.readLines(self.filename) | ||||
| 	end | ||||
|  | ||||
| 	if self.image and not (self.height or self.ey) then | ||||
| 		self.height = #self.image | ||||
| 	end | ||||
| 	if self.image and not self.width then | ||||
| 		self.width = #self.image[1] | ||||
| 	if self.image and not (self.width or self.ex) then | ||||
| 		for i = 1, self.height do | ||||
| 			self.width = math.max(self.width or 0, #self.image[i]) | ||||
| 		end | ||||
| 	end | ||||
| 	UI.Window.setParent(self) | ||||
| end | ||||
|  | ||||
| function UI.Image:draw() | ||||
| @@ -22,19 +31,22 @@ function UI.Image:draw() | ||||
| 		for y = 1, #self.image do | ||||
| 			local line = self.image[y] | ||||
| 			for x = 1, #line do | ||||
| 				local ch = line[x] | ||||
| 				if type(ch) == 'number' then | ||||
| 					if ch > 0 then | ||||
| 						self:write(x, y, ' ', ch) | ||||
| 					end | ||||
| 				else | ||||
| 					self:write(x, y, ch) | ||||
| 				local ch = lookup:find(line:sub(x, x)) | ||||
| 				if ch then | ||||
| 					self:write(x, y, ' ', 2 ^ (ch -1)) | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| 	self:drawChildren() | ||||
| end | ||||
|  | ||||
| function UI.Image:setImage(image) | ||||
| 	self.image = image | ||||
| end | ||||
|  | ||||
| function UI.Image.example() | ||||
| 	return UI.Image { | ||||
| 		filename = 'test.paint', | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -13,8 +13,7 @@ function UI.Menu:postInit() | ||||
| 	self.pageSize = #self.menuItems | ||||
| end | ||||
|  | ||||
| function UI.Menu:setParent() | ||||
| 	UI.Grid.setParent(self) | ||||
| function UI.Menu:layout() | ||||
| 	self.itemWidth = 1 | ||||
| 	for _,v in pairs(self.values) do | ||||
| 		if #v.prompt > self.itemWidth then | ||||
| @@ -28,6 +27,7 @@ function UI.Menu:setParent() | ||||
| 	else | ||||
| 		self.width = self.itemWidth + 2 | ||||
| 	end | ||||
| 	UI.Grid.layout(self) | ||||
| end | ||||
|  | ||||
| function UI.Menu:center() | ||||
|   | ||||
| @@ -1,28 +1,15 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| local function getPosition(element) | ||||
| 	local x, y = 1, 1 | ||||
| 	repeat | ||||
| 		x = element.x + x - 1 | ||||
| 		y = element.y + y - 1 | ||||
| 		element = element.parent | ||||
| 	until not element | ||||
| 	return x, y | ||||
| end | ||||
|  | ||||
| UI.MenuBar = class(UI.Window) | ||||
| UI.MenuBar.defaults = { | ||||
| 	UIElement = 'MenuBar', | ||||
| 	buttons = { }, | ||||
| 	height = 1, | ||||
| 	backgroundColor = colors.lightGray, | ||||
| 	textColor = colors.black, | ||||
| 	backgroundColor = 'secondary', | ||||
| 	textColor = 'black', | ||||
| 	spacing = 2, | ||||
| 	lastx = 1, | ||||
| 	showBackButton = false, | ||||
| 	buttonClass = 'MenuItem', | ||||
| } | ||||
| function UI.MenuBar:postInit() | ||||
| @@ -62,10 +49,6 @@ function UI.MenuBar:addButtons(buttons) | ||||
| 			else | ||||
| 				table.insert(self.children, button) | ||||
| 			end | ||||
|  | ||||
| 			if button.dropdown then | ||||
| 				button.dropmenu = UI.DropMenu { buttons = button.dropdown } | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| 	if self.parent then | ||||
| @@ -78,23 +61,28 @@ function UI.MenuBar:getActive(menuItem) | ||||
| end | ||||
|  | ||||
| function UI.MenuBar:eventHandler(event) | ||||
| 	if event.type == 'button_press' and event.button.dropmenu then | ||||
| 		if event.button.dropmenu.enabled then | ||||
| 			event.button.dropmenu:hide() | ||||
| 			self:refocus() | ||||
| 			return true | ||||
| 		else | ||||
| 			local x, y = getPosition(event.button) | ||||
| 			if x + event.button.dropmenu.width > self.width then | ||||
| 				x = self.width - event.button.dropmenu.width + 1 | ||||
| 			end | ||||
| 			for _,c in pairs(event.button.dropmenu.children) do | ||||
| 				if not c.spacer then | ||||
| 					c.inactive = not self:getActive(c) | ||||
| 				end | ||||
| 			end | ||||
| 			event.button.dropmenu:show(x, y + 1) | ||||
| 	if event.type == 'button_press' and event.button.dropdown then | ||||
| 		local function getPosition(element) | ||||
| 			local x, y = 1, 1 | ||||
| 			repeat | ||||
| 				x = element.x + x - 1 | ||||
| 				y = element.y + y - 1 | ||||
| 				element = element.parent | ||||
| 			until not element | ||||
| 			return x, y | ||||
| 		end | ||||
|  | ||||
| 		local x, y = getPosition(event.button) | ||||
|  | ||||
| 		local menu = UI.DropMenu { | ||||
| 			buttons = event.button.dropdown, | ||||
| 			x = x, | ||||
| 			y = y + 1, | ||||
| 			lastFocus = event.button.uid, | ||||
| 			menuUid = self.uid, | ||||
| 		} | ||||
| 		self.parent:add({ dropmenu = menu }) | ||||
|  | ||||
| 		return true | ||||
| 	end | ||||
| end | ||||
| @@ -103,7 +91,8 @@ function UI.MenuBar.example() | ||||
| 	return UI.MenuBar { | ||||
| 		buttons = { | ||||
| 			{ text = 'Choice1', event = 'event1' }, | ||||
| 			{ text = 'Choice2', event = 'event2' }, | ||||
| 			{ text = 'Choice2', event = 'event2', inactive = true }, | ||||
| 			{ text = 'Choice3', event = 'event3' }, | ||||
| 		} | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -1,13 +1,9 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.MenuItem = class(UI.Button) | ||||
| UI.MenuItem = class(UI.FlatButton) | ||||
| UI.MenuItem.defaults = { | ||||
| 	UIElement = 'MenuItem', | ||||
| 	textColor = colors.black, | ||||
| 	backgroundColor = colors.lightGray, | ||||
| 	textFocusColor = colors.white, | ||||
| 	backgroundFocusColor = colors.lightGray, | ||||
| 	noPadding = false, | ||||
| 	textInactiveColor = 'gray', | ||||
| } | ||||
|   | ||||
							
								
								
									
										31
									
								
								sys/modules/opus/ui/components/MiniSlideOut.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								sys/modules/opus/ui/components/MiniSlideOut.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| UI.MiniSlideOut = class(UI.SlideOut) | ||||
| UI.MiniSlideOut.defaults = { | ||||
| 	UIElement = 'MiniSlideOut', | ||||
|     noFill = true, | ||||
|     backgroundColor = 'primary', | ||||
|     height = 1, | ||||
| } | ||||
| function UI.MiniSlideOut:postInit() | ||||
|     self.close_button = UI.Button { | ||||
|         x = -1, | ||||
|         backgroundColor = self.backgroundColor, | ||||
|         backgroundFocusColor = self.backgroundColor, | ||||
|         text = 'x', | ||||
|         event = 'slide_hide', | ||||
|         noPadding = true, | ||||
|     } | ||||
|     if self.label then | ||||
|         self.label_text = UI.Text { | ||||
|             x = 2, | ||||
|             value = self.label, | ||||
|         } | ||||
|     end | ||||
| end | ||||
|  | ||||
| function UI.MiniSlideOut:show(...) | ||||
|     UI.SlideOut.show(self, ...) | ||||
|     self:addTransition('slideLeft', { easing = 'outBounce' }) | ||||
| end | ||||
| @@ -5,17 +5,18 @@ UI.NftImage = class(UI.Window) | ||||
| UI.NftImage.defaults = { | ||||
| 	UIElement = 'NftImage', | ||||
| } | ||||
| function UI.NftImage:setParent() | ||||
| 	if self.image then | ||||
| function UI.NftImage:postInit() | ||||
| 	if self.image and not (self.ey or self.height) then | ||||
| 		self.height = self.image.height | ||||
| 	end | ||||
| 	if self.image and not self.width then | ||||
| 	if self.image and not (self.ex or self.width) then | ||||
| 		self.width = self.image.width | ||||
| 	end | ||||
| 	UI.Window.setParent(self) | ||||
| end | ||||
|  | ||||
| function UI.NftImage:draw() | ||||
| 	self:clear() | ||||
|  | ||||
| 	if self.image then | ||||
| 		-- due to blittle, the background and foreground transparent | ||||
| 		-- color is the same as the background color | ||||
| @@ -25,8 +26,6 @@ function UI.NftImage:draw() | ||||
| 				self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x] or bg) | ||||
| 			end | ||||
| 		end | ||||
| 	else | ||||
| 		self:clear() | ||||
| 	end | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -4,36 +4,34 @@ local Sound = require('opus.sound') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Notification = class(UI.Window) | ||||
| UI.Notification.defaults = { | ||||
| 	UIElement = 'Notification', | ||||
| 	backgroundColor = colors.gray, | ||||
| 	backgroundColor = 'gray', | ||||
| 	closeInd = UI.extChars and '\215' or '*', | ||||
| 	height = 3, | ||||
| 	timeout = 3, | ||||
| 	anchor = 'bottom', | ||||
| } | ||||
| function UI.Notification:draw() | ||||
| function UI.Notification.draw() | ||||
| end | ||||
|  | ||||
| function UI.Notification:enable() | ||||
| function UI.Notification.enable() | ||||
| end | ||||
|  | ||||
| function UI.Notification:error(value, timeout) | ||||
| 	self.backgroundColor = colors.red | ||||
| 	self.backgroundColor = 'red' | ||||
| 	Sound.play('entity.villager.no', .5) | ||||
| 	self:display(value, timeout) | ||||
| end | ||||
|  | ||||
| function UI.Notification:info(value, timeout) | ||||
| 	self.backgroundColor = colors.lightGray | ||||
| 	self.backgroundColor = 'lightGray' | ||||
| 	self:display(value, timeout) | ||||
| end | ||||
|  | ||||
| function UI.Notification:success(value, timeout) | ||||
| 	self.backgroundColor = colors.green | ||||
| 	self.backgroundColor = 'green' | ||||
| 	self:display(value, timeout) | ||||
| end | ||||
|  | ||||
| @@ -43,32 +41,34 @@ function UI.Notification:cancel() | ||||
| 		self.timer = nil | ||||
| 	end | ||||
|  | ||||
| 	if self.canvas then | ||||
| 		self.enabled = false | ||||
| 		self.canvas:removeLayer() | ||||
| 		self.canvas = nil | ||||
| 	end | ||||
| 	self:disable() | ||||
| end | ||||
|  | ||||
| function UI.Notification:display(value, timeout) | ||||
| 	self:cancel() | ||||
| 	self.enabled = true | ||||
| 	local lines = Util.wordWrap(value, self.width - 3) | ||||
|  | ||||
| 	self.enabled = true | ||||
| 	self.height = #lines | ||||
|  | ||||
| 	if self.anchor == 'bottom' then | ||||
| 		self.y = self.parent.height - self.height + 1 | ||||
| 		self.canvas = self:addLayer(self.backgroundColor, self.textColor) | ||||
| 		self:addTransition('expandUp', { ticks = self.height }) | ||||
| 	else | ||||
| 		self.canvas = self:addLayer(self.backgroundColor, self.textColor) | ||||
| 		self.y = 1 | ||||
| 	end | ||||
| 	self.canvas:setVisible(true) | ||||
|  | ||||
| 	self:reposition(self.x, self.y, self.width, self.height) | ||||
| 	self:raise() | ||||
| 	self:clear() | ||||
| 	for k,v in pairs(lines) do | ||||
| 		self:write(2, k, v) | ||||
| 	end | ||||
| 	self:write(self.width, 1, self.closeInd) | ||||
|  | ||||
| 	if self.timer then | ||||
| 		Event.off(self.timer) | ||||
| 		self.timer = nil | ||||
| 	end | ||||
|  | ||||
| 	timeout = timeout or self.timeout | ||||
| 	if timeout > 0 then | ||||
| @@ -77,7 +77,6 @@ function UI.Notification:display(value, timeout) | ||||
| 			self:sync() | ||||
| 		end) | ||||
| 	else | ||||
| 		self:write(self.width, 1, self.closeInd) | ||||
| 		self:sync() | ||||
| 	end | ||||
| end | ||||
| @@ -92,7 +91,7 @@ function UI.Notification:eventHandler(event) | ||||
| end | ||||
|  | ||||
| function UI.Notification.example() | ||||
| 	return UI.ActiveLayer { | ||||
| 	return UI.Window { | ||||
| 		notify1 = UI.Notification { | ||||
| 			anchor = 'top', | ||||
| 		}, | ||||
| @@ -111,7 +110,9 @@ function UI.Notification.example() | ||||
| 			if event.type == 'test_success' then | ||||
| 				self.notify1:success('Example text') | ||||
| 			elseif event.type == 'test_error' then | ||||
| 				self.notify2:error('Example text', 0) | ||||
| 				self.notify2:error([[Example text test test | ||||
| test test test test test | ||||
| test test test]], 0) | ||||
| 			end | ||||
| 		end, | ||||
| 	} | ||||
|   | ||||
| @@ -1,60 +1,31 @@ | ||||
| local Canvas = require('opus.ui.canvas') | ||||
| local class  = require('opus.class') | ||||
| local UI     = require('opus.ui') | ||||
| local Util   = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| -- need to add offsets to this test | ||||
| local function getPosition(element) | ||||
| 	local x, y = 1, 1 | ||||
| 	repeat | ||||
| 		x = element.x + x - 1 | ||||
| 		y = element.y + y - 1 | ||||
| 		element = element.parent | ||||
| 	until not element | ||||
| 	return x, y | ||||
| end | ||||
|  | ||||
| UI.Page = class(UI.Window) | ||||
| UI.Page.defaults = { | ||||
| 	UIElement = 'Page', | ||||
| 	accelerators = { | ||||
| 		down = 'focus_next', | ||||
| 		scroll_down = 'focus_next', | ||||
| 		enter = 'focus_next', | ||||
| 		tab = 'focus_next', | ||||
| 		['shift-tab' ] = 'focus_prev', | ||||
| 		up = 'focus_prev', | ||||
| 		scroll_up = 'focus_prev', | ||||
| 	}, | ||||
| 	backgroundColor = colors.cyan, | ||||
| 	textColor = colors.white, | ||||
| 	backgroundColor = 'primary', | ||||
| 	textColor = 'white', | ||||
| } | ||||
| function UI.Page:postInit() | ||||
| 	self.parent = self.parent or UI.defaultDevice | ||||
| 	self.parent = self.parent or UI.term | ||||
| 	self.__target = self | ||||
| 	self.canvas = Canvas({ | ||||
| 		x = 1, y = 1, width = self.parent.width, height = self.parent.height, | ||||
| 		isColor = self.parent.isColor, | ||||
| 	}) | ||||
| 	self.canvas:clear(self.backgroundColor, self.textColor) | ||||
| end | ||||
|  | ||||
| function UI.Page:enable() | ||||
| 	self.canvas.visible = true | ||||
| 	UI.Window.enable(self) | ||||
|  | ||||
| 	if not self.focused or not self.focused.enabled then | ||||
| 		self:focusFirst() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Page:disable() | ||||
| 	self.canvas.visible = false | ||||
| 	UI.Window.disable(self) | ||||
| end | ||||
|  | ||||
| function UI.Page:sync() | ||||
| 	if self.enabled then | ||||
| 		self:checkFocus() | ||||
| 		self.parent:setCursorBlink(self.focused and self.focused.cursorBlink) | ||||
| 		self.parent:sync() | ||||
| 	end | ||||
| end | ||||
| @@ -73,22 +44,23 @@ function UI.Page:pointToChild(x, y) | ||||
| 	if self.__target == self then | ||||
| 		return UI.Window.pointToChild(self, x, y) | ||||
| 	end | ||||
| 	x = x + self.offx - self.x + 1 | ||||
| 	y = y + self.offy - self.y + 1 | ||||
| --[[ | ||||
| 	-- this is supposed to fix when there are multiple sub canvases | ||||
| 	local absX, absY = getPosition(self.__target) | ||||
| 	if self.__target.canvas then | ||||
| 		x = x - (self.__target.canvas.x - self.__target.x) | ||||
| 		y = y - (self.__target.canvas.y - self.__target.y) | ||||
| 		_syslog({'raw', self.__target.canvas.y, self.__target.y}) | ||||
|  | ||||
| 	local function getPosition(element) | ||||
| 		local x, y = 1, 1 | ||||
| 		repeat | ||||
| 			x = element.x + x - 1 | ||||
| 			y = element.y + y - 1 | ||||
| 			element = element.parent | ||||
| 		until not element | ||||
| 		return x, y | ||||
| 	end | ||||
| 	]] | ||||
| 	return self.__target:pointToChild(x, y) | ||||
|  | ||||
| 	local absX, absY = getPosition(self.__target) | ||||
| 	return self.__target:pointToChild(x - absX + self.__target.x, y - absY + self.__target.y) | ||||
| end | ||||
|  | ||||
| function UI.Page:getFocusables() | ||||
| 	if self.__target == self or self.__target.pageType ~= 'modal' then | ||||
| 	if self.__target == self or not self.__target.modal then | ||||
| 		return UI.Window.getFocusables(self) | ||||
| 	end | ||||
| 	return self.__target:getFocusables() | ||||
| @@ -149,12 +121,17 @@ function UI.Page:setFocus(child) | ||||
| 	if not child.focused then | ||||
| 		child.focused = true | ||||
| 		child:emit({ type = 'focus_change', focused = child }) | ||||
| 		--self:emit({ type = 'focus_change', focused = child }) | ||||
| 	end | ||||
|  | ||||
| 	child:focus() | ||||
| end | ||||
|  | ||||
| function UI.Page:checkFocus() | ||||
| 	if not self.focused or not self.focused.enabled then | ||||
| 		self.__target:focusFirst() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Page:eventHandler(event) | ||||
| 	if self.focused then | ||||
| 		if event.type == 'focus_next' then | ||||
|   | ||||
| @@ -1,39 +1,31 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.ProgressBar = class(UI.Window) | ||||
| UI.ProgressBar.defaults = { | ||||
| 	UIElement = 'ProgressBar', | ||||
| 	backgroundColor = colors.gray, | ||||
| 	backgroundColor = 'gray', | ||||
| 	height = 1, | ||||
| 	progressColor = colors.lime, | ||||
| 	progressColor = 'lime', | ||||
| 	progressChar = UI.extChars and '\153' or ' ', | ||||
| 	fillChar = ' ', | ||||
| 	fillColor = colors.gray, | ||||
| 	textColor = colors.green, | ||||
| 	fillColor = 'gray', | ||||
| 	textColor = 'green', | ||||
| 	value = 0, | ||||
| } | ||||
| function UI.ProgressBar:draw() | ||||
| 	local width = math.ceil(self.value / 100 * self.width) | ||||
|  | ||||
| 	local filler = string.rep(self.fillChar, self.width) | ||||
| 	local progress = string.rep(self.progressChar, width) | ||||
|  | ||||
| 	for i = 1, self.height do | ||||
| 		self:write(1, i, filler, nil, self.fillColor) | ||||
| 		self:write(1, i, progress, self.progressColor) | ||||
| 	end | ||||
| 	self:fillArea(width + 1, 1, self.width - width, self.height, self.fillChar, nil, self.fillColor) | ||||
| 	self:fillArea(1, 1, width, self.height, self.progressChar, self.progressColor) | ||||
| end | ||||
|  | ||||
| function UI.ProgressBar.example() | ||||
| 	local Event = require('opus.event') | ||||
| 	return UI.ProgressBar { | ||||
| 		x = 2, ex = -2, y = 2, | ||||
| 		x = 2, ex = -2, y = 2, height = 2, | ||||
| 		focus = function() end, | ||||
| 		enable = function(self) | ||||
| 			Event.onInterval(.25, function() | ||||
| 			require('opus.event').onInterval(.25, function() | ||||
| 				self.value = self.value == 100 and 0 or self.value + 5 | ||||
| 				self:draw() | ||||
| 				self:sync() | ||||
|   | ||||
							
								
								
									
										27
									
								
								sys/modules/opus/ui/components/Question.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								sys/modules/opus/ui/components/Question.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| UI.Question = class(UI.MiniSlideOut) | ||||
| UI.Question.defaults = { | ||||
|     UIElement = 'Question', | ||||
|     accelerators = { | ||||
|         y = 'question_yes', | ||||
|         n = 'question_no', | ||||
|     } | ||||
| } | ||||
| function UI.Question:postInit() | ||||
|     local x = self.label and #self.label + 3 or 1 | ||||
|  | ||||
|     self.yes_button = UI.Button { | ||||
|         x = x, | ||||
|         text = 'Yes', | ||||
|         backgroundColor = 'primary', | ||||
|         event = 'question_yes', | ||||
|     } | ||||
|     self.no_button = UI.Button { | ||||
|         x = x + 5, | ||||
|         text = 'No', | ||||
|         backgroundColor = 'primary', | ||||
|         event = 'question_no', | ||||
|     } | ||||
| end | ||||
| @@ -17,7 +17,13 @@ UI.ScrollBar.defaults = { | ||||
| 	ey = -1, | ||||
| } | ||||
| function UI.ScrollBar:draw() | ||||
| 	local view = self.parent:getViewArea() | ||||
| 	local parent = self.target or self.parent --self:find(self.target) | ||||
| 	local view = parent:getViewArea() | ||||
|  | ||||
| 	self:clear() | ||||
|  | ||||
| 	-- ... | ||||
| 	self:write(1, 1, ' ', view.fill) | ||||
|  | ||||
| 	if view.totalHeight > view.height then | ||||
| 		local maxScroll = view.totalHeight - view.height | ||||
| @@ -27,7 +33,7 @@ function UI.ScrollBar:draw() | ||||
|  | ||||
| 		local row = view.y | ||||
| 		if not view.static then  -- does the container scroll ? | ||||
| 			self.height = view.totalHeight | ||||
| 			self:reposition(self.x, self.y, self.width, view.totalHeight) | ||||
| 		end | ||||
|  | ||||
| 		for i = 1, view.height - 2 do | ||||
| @@ -56,16 +62,17 @@ end | ||||
| function UI.ScrollBar:eventHandler(event) | ||||
| 	if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then | ||||
| 		if event.x == 1 then | ||||
| 			local view = self.parent:getViewArea() | ||||
| 			local parent = self.target or self.parent --self:find(self.target) | ||||
| 			local view = parent:getViewArea() | ||||
| 			if view.totalHeight > view.height then | ||||
| 				if event.y == view.y then | ||||
| 					self:emit({ type = 'scroll_up'}) | ||||
| 					parent:emit({ type = 'scroll_up'}) | ||||
| 				elseif event.y == view.y + view.height - 1 then | ||||
| 					self:emit({ type = 'scroll_down'}) | ||||
| 					parent:emit({ type = 'scroll_down'}) | ||||
| 				else | ||||
| 					local percent = (event.y - view.y) / (view.height - 2) | ||||
| 					local y = math.floor((view.totalHeight - view.height) * percent) | ||||
| 					self:emit({ type = 'scroll_to', offset = y }) | ||||
| 					parent :emit({ type = 'scroll_to', offset = y }) | ||||
| 				end | ||||
| 			end | ||||
| 			return true | ||||
|   | ||||
| @@ -29,6 +29,7 @@ function UI.ScrollingGrid:getViewArea() | ||||
| 		height      = self.pageSize,           -- viewable height | ||||
| 		totalHeight = Util.size(self.values),  -- total height | ||||
| 		offsetY     = self.scrollOffset,       -- scroll offset | ||||
| 		fill        = not self.disableHeader and self.headerBackgroundColor, | ||||
| 	} | ||||
| end | ||||
|  | ||||
| @@ -57,3 +58,21 @@ function UI.ScrollingGrid:setIndex(index) | ||||
| 	end | ||||
| 	UI.Grid.setIndex(self, index) | ||||
| end | ||||
|  | ||||
| function UI.ScrollingGrid.example() | ||||
| 	local values = { } | ||||
| 	for i = 1, 20 do | ||||
| 		table.insert(values, { key = 'key' .. i, value = 'value' .. i }) | ||||
| 	end | ||||
| 	return UI.ScrollingGrid { | ||||
| 		values = values, | ||||
| 		sortColumn = 'key', | ||||
| 		columns = { | ||||
| 			{ heading = 'key', key = 'key' }, | ||||
| 			{ heading = 'value', key = 'value' }, | ||||
| 		}, | ||||
| 		accelerators = { | ||||
| 			grid_select = 'custom_select', | ||||
| 		} | ||||
| 	} | ||||
| end | ||||
| @@ -4,17 +4,9 @@ local UI    = require('opus.ui') | ||||
| UI.SlideOut = class(UI.Window) | ||||
| UI.SlideOut.defaults = { | ||||
| 	UIElement = 'SlideOut', | ||||
| 	pageType = 'modal', | ||||
| 	transitionHint = 'expandUp', | ||||
| 	modal = true, | ||||
| } | ||||
| function UI.SlideOut:layout() | ||||
| 	UI.Window.layout(self) | ||||
| 	if not self.canvas then | ||||
| 		self.canvas = self:addLayer() | ||||
| 	else | ||||
| 		self.canvas:resize(self.width, self.height) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:enable() | ||||
| end | ||||
|  | ||||
| @@ -27,24 +19,20 @@ function UI.SlideOut:toggle() | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:show(...) | ||||
| 	self:addTransition('expandUp') | ||||
| 	self.canvas:raise() | ||||
| 	self.canvas:setVisible(true) | ||||
| 	UI.Window.enable(self, ...) | ||||
| 	self:draw() | ||||
| 	self:capture(self) | ||||
| 	self:focusFirst() | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:disable() | ||||
| 	self.canvas:setVisible(false) | ||||
| 	UI.Window.disable(self) | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:hide() | ||||
| 	self:disable() | ||||
| 	self:release(self) | ||||
| 	self:refocus() | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:draw() | ||||
| 	if not self.noFill then | ||||
| 		self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), 'black', 'gray') | ||||
| 	end | ||||
| 	self:drawChildren() | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:eventHandler(event) | ||||
| @@ -59,24 +47,27 @@ function UI.SlideOut:eventHandler(event) | ||||
| end | ||||
|  | ||||
| function UI.SlideOut.example() | ||||
| 	-- for the transistion to work properly, the parent must have a canvas | ||||
| 	return UI.ActiveLayer { | ||||
| 		y = 2, | ||||
| 	return UI.Window { | ||||
| 		y = 3, | ||||
| 		backgroundColor = 2048, | ||||
| 		button = UI.Button { | ||||
| 			x = 2, y = 5, | ||||
| 			text = 'show', | ||||
| 		}, | ||||
| 		slideOut = UI.SlideOut { | ||||
| 			backgroundColor = _G.colors.yellow, | ||||
| 			y = -4, height = 4, x = 3, ex = -3, | ||||
| 			backgroundColor = 16, | ||||
| 			y = -7, height = 4, x = 3, ex = -3, | ||||
| 			titleBar = UI.TitleBar { | ||||
| 				title = 'test', | ||||
| 			}, | ||||
| 			button = UI.Button { | ||||
| 				x = 2, y = 2, | ||||
| 				text = 'hide', | ||||
| 				--visualize = true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		eventHandler = function (self, event) | ||||
| 			if event.type == 'button_press' then | ||||
| 				self.slideOut.canvas.xxx = true | ||||
| 				self.slideOut:toggle() | ||||
| 			end | ||||
| 		end, | ||||
|   | ||||
| @@ -2,17 +2,15 @@ local class  = require('opus.class') | ||||
| local UI     = require('opus.ui') | ||||
| local Util   = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Slider = class(UI.Window) | ||||
| UI.Slider.defaults = { | ||||
| 	UIElement = 'Slider', | ||||
| 	height = 1, | ||||
| 	barChar = UI.extChars and '\140' or '-', | ||||
| 	barColor = colors.gray, | ||||
| 	barColor = 'gray', | ||||
| 	sliderChar = UI.extChars and '\143' or '\124', | ||||
| 	sliderColor = colors.blue, | ||||
| 	sliderFocusColor = colors.lightBlue, | ||||
| 	sliderColor = 'blue', | ||||
| 	sliderFocusColor = 'lightBlue', | ||||
| 	leftBorder = UI.extChars and '\141' or '\124', | ||||
| 	rightBorder = UI.extChars and '\142' or '\124', | ||||
| 	value = 0, | ||||
| @@ -57,8 +55,16 @@ end | ||||
|  | ||||
| function UI.Slider:eventHandler(event) | ||||
| 	if event.type == "mouse_down" or event.type == "mouse_drag" then | ||||
|  | ||||
| 		local pos = event.x - 1 | ||||
| 		if event.type == 'mouse_down' then | ||||
| 			self.anchor = event.x - 1 | ||||
| 		else | ||||
| 			pos = self.anchor + event.dx | ||||
| 		end | ||||
|  | ||||
| 		local range = self.max - self.min | ||||
| 		local i = (event.x - 1) / (self.width - 1) | ||||
| 		local i = pos / (self.width - 1) | ||||
| 		self.value = self.min + (i * range) | ||||
| 		self:emit({ type = self.event, value = self.value, element = self }) | ||||
| 		self:draw() | ||||
|   | ||||
| @@ -3,17 +3,16 @@ local Event = require('opus.event') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.StatusBar = class(UI.Window) | ||||
| UI.StatusBar.defaults = { | ||||
| 	UIElement = 'StatusBar', | ||||
| 	backgroundColor = colors.lightGray, | ||||
| 	textColor = colors.gray, | ||||
| 	backgroundColor = 'lightGray', | ||||
| 	textColor = 'gray', | ||||
| 	height = 1, | ||||
| 	ey = -1, | ||||
| } | ||||
| function UI.StatusBar:adjustWidth() | ||||
| function UI.StatusBar:layout() | ||||
| 	UI.Window.layout(self) | ||||
| 	-- Can only have 1 adjustable width | ||||
| 	if self.columns then | ||||
| 		local w = self.width - #self.columns - 1 | ||||
| @@ -31,16 +30,6 @@ function UI.StatusBar:adjustWidth() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.StatusBar:resize() | ||||
| 	UI.Window.resize(self) | ||||
| 	self:adjustWidth() | ||||
| end | ||||
|  | ||||
| function UI.StatusBar:setParent() | ||||
| 	UI.Window.setParent(self) | ||||
| 	self:adjustWidth() | ||||
| end | ||||
|  | ||||
| function UI.StatusBar:setStatus(status) | ||||
| 	if self.values ~= status then | ||||
| 		self.values = status | ||||
| @@ -63,7 +52,7 @@ end | ||||
|  | ||||
| function UI.StatusBar:timedStatus(status, timeout) | ||||
| 	self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor) | ||||
| 	Event.on(timeout or 3, function() | ||||
| 	Event.onTimeout(timeout or 3, function() | ||||
| 		if self.enabled then | ||||
| 			self:draw() | ||||
| 			self:sync() | ||||
| @@ -89,11 +78,13 @@ function UI.StatusBar:draw() | ||||
| 	elseif type(self.values) == 'string' then | ||||
| 		self:write(1, 1, Util.widthify(' ' .. self.values, self.width)) | ||||
| 	else | ||||
| 		local s = '' | ||||
| 		local x = 2 | ||||
| 		self:clear() | ||||
| 		for _,c in ipairs(self.columns) do | ||||
| 			s = s .. ' ' .. Util.widthify(tostring(self.values[c.key] or ''), c.cw) | ||||
| 			local s = Util.widthify(tostring(self.values[c.key] or ''), c.cw) | ||||
| 			self:write(x, 1, s, c.bg, c.fg) | ||||
| 			x = x + c.cw + 1 | ||||
| 		end | ||||
| 		self:write(1, 1, Util.widthify(s, self.width)) | ||||
| 	end | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,16 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| UI.Tab = class(UI.ActiveLayer) | ||||
| UI.Tab = class(UI.Window) | ||||
| UI.Tab.defaults = { | ||||
| 	UIElement = 'Tab', | ||||
| 	tabTitle = 'tab', | ||||
| 	y = 2, | ||||
| } | ||||
|  | ||||
| function UI.Tab:draw() | ||||
| 	if not self.noFill then | ||||
| 		self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), colors.black, colors.gray) | ||||
| 	end | ||||
| 	self:drawChildren() | ||||
| end | ||||
|   | ||||
| @@ -6,9 +6,9 @@ UI.TabBar = class(UI.MenuBar) | ||||
| UI.TabBar.defaults = { | ||||
| 	UIElement = 'TabBar', | ||||
| 	buttonClass = 'TabBarMenuItem', | ||||
| } | ||||
| UI.TabBar.inherits = { | ||||
| 	selectedBackgroundColor = 'backgroundColor', | ||||
| 	backgroundColor = 'black', | ||||
| 	selectedBackgroundColor = 'primary', | ||||
| 	unselectedBackgroundColor = 'tertiary', | ||||
| } | ||||
| function UI.TabBar:enable() | ||||
| 	UI.MenuBar.enable(self) | ||||
| @@ -32,7 +32,7 @@ function UI.TabBar:eventHandler(event) | ||||
| 				self:emit({ type = 'tab_change', current = si, last = pi, tab = selected }) | ||||
| 			end | ||||
| 		end | ||||
| 		UI.MenuBar.draw(self) | ||||
| 		self:draw(self) | ||||
| 	end | ||||
| 	return UI.MenuBar.eventHandler(self, event) | ||||
| end | ||||
|   | ||||
| @@ -1,27 +1,19 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.TabBarMenuItem = class(UI.Button) | ||||
| UI.TabBarMenuItem.defaults = { | ||||
| 	UIElement = 'TabBarMenuItem', | ||||
| 	event = 'tab_select', | ||||
| 	textColor = colors.black, | ||||
| 	selectedBackgroundColor = colors.cyan, | ||||
| 	unselectedBackgroundColor = colors.lightGray, | ||||
| 	backgroundColor = colors.lightGray, | ||||
| } | ||||
| UI.TabBarMenuItem.inherits = { | ||||
| 	selectedBackgroundColor = 'selectedBackgroundColor', | ||||
| 	textInactiveColor = 'lightGray', | ||||
| } | ||||
| function UI.TabBarMenuItem:draw() | ||||
| 	if self.selected then | ||||
| 		self.backgroundColor = self.selectedBackgroundColor | ||||
| 		self.backgroundFocusColor = self.selectedBackgroundColor | ||||
| 		self.backgroundColor = self:getProperty('selectedBackgroundColor') | ||||
| 		self.backgroundFocusColor = self.backgroundColor | ||||
| 	else | ||||
| 		self.backgroundColor = self.unselectedBackgroundColor | ||||
| 		self.backgroundFocusColor = self.unselectedBackgroundColor | ||||
| 		self.backgroundColor = self:getProperty('unselectedBackgroundColor') | ||||
| 		self.backgroundFocusColor = self.backgroundColor | ||||
| 	end | ||||
| 	UI.Button.draw(self) | ||||
| end | ||||
|   | ||||
| @@ -56,12 +56,12 @@ end | ||||
|  | ||||
| function UI.Tabs:enable() | ||||
| 	self.enabled = true | ||||
| 	self.transitionHint = nil | ||||
| 	self.tabBar:enable() | ||||
|  | ||||
| 	local menuItem = Util.find(self.tabBar.children, 'selected', true) | ||||
|  | ||||
| 	for _,child in pairs(self.children or { }) do | ||||
| 	for child in self:eachChild() do | ||||
| 		child.transitionHint = nil | ||||
| 		if child.uid == menuItem.tabUid then | ||||
| 			child:enable() | ||||
| 			self:emit({ type = 'tab_activate', activated = child }) | ||||
| @@ -74,14 +74,11 @@ end | ||||
| function UI.Tabs:eventHandler(event) | ||||
| 	if event.type == 'tab_change' then | ||||
| 		local tab = self:find(event.tab.tabUid) | ||||
| 		if event.current > event.last then | ||||
| 			self.transitionHint = 'slideLeft' | ||||
| 		else | ||||
| 			self.transitionHint = 'slideRight' | ||||
| 		end | ||||
| 		local hint = event.current > event.last and 'slideLeft' or 'slideRight' | ||||
|  | ||||
| 		for _,child in pairs(self.children) do | ||||
| 		for child in self:eachChild() do | ||||
| 			if child.uid == event.tab.tabUid then | ||||
| 				child.transitionHint = hint | ||||
| 				child:enable() | ||||
| 			elseif child.tabTitle then | ||||
| 				child:disable() | ||||
| @@ -89,6 +86,7 @@ function UI.Tabs:eventHandler(event) | ||||
| 		end | ||||
| 		self:emit({ type = 'tab_activate', activated = tab }) | ||||
| 		tab:draw() | ||||
| 		return true | ||||
| 	end | ||||
| end | ||||
|  | ||||
| @@ -102,11 +100,26 @@ function UI.Tabs.example() | ||||
| 		tab2 = UI.Tab { | ||||
| 			index = 2, | ||||
| 			tabTitle = 'tab2', | ||||
| 			button = UI.Button { y = 3 }, | ||||
| 			subtabs = UI.Tabs { | ||||
| 				x = 3, y = 2, ex = -3, ey = -2, | ||||
| 				tab1 = UI.Tab { | ||||
| 					index = 1, | ||||
| 					tabTitle = 'tab4', | ||||
| 					entry = UI.TextEntry { y = 3, shadowText = 'text' }, | ||||
| 				}, | ||||
| 				tab3 = UI.Tab { | ||||
| 					index = 2, | ||||
| 					tabTitle = 'tab5', | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		tab3 = UI.Tab { | ||||
| 			index = 3, | ||||
| 			tabTitle = 'tab3', | ||||
| 		}, | ||||
| 		enable = function(self) | ||||
| 			UI.Tabs.enable(self) | ||||
| 			self:setActive(self.tab3, false) | ||||
| 		end, | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -8,11 +8,11 @@ UI.Text.defaults = { | ||||
| 	value = '', | ||||
| 	height = 1, | ||||
| } | ||||
| function UI.Text:setParent() | ||||
| function UI.Text:layout() | ||||
| 	if not self.width and not self.ex then | ||||
| 		self.width = #tostring(self.value) | ||||
| 	end | ||||
| 	UI.Window.setParent(self) | ||||
| 	UI.Window.layout(self) | ||||
| end | ||||
|  | ||||
| function UI.Text:draw() | ||||
|   | ||||
| @@ -6,36 +6,44 @@ UI.TextArea.defaults = { | ||||
| 	UIElement = 'TextArea', | ||||
| 	marginRight = 2, | ||||
| 	value = '', | ||||
| 	showScrollBar = true, | ||||
| } | ||||
| function UI.TextArea:postInit() | ||||
| 	self.scrollBar = UI.ScrollBar() | ||||
| end | ||||
|  | ||||
| function UI.TextArea:setText(text) | ||||
| 	self:reset() | ||||
| 	self.value = text | ||||
| 	self:draw() | ||||
| end | ||||
|  | ||||
| function UI.TextArea:focus() | ||||
| function UI.TextArea.focus() | ||||
| 	-- allow keyboard scrolling | ||||
| end | ||||
|  | ||||
| function UI.TextArea:draw() | ||||
| 	self:clear() | ||||
| --  self:setCursorPos(1, 1) | ||||
| 	self.cursorX, self.cursorY = 1, 1 | ||||
| 	self:print(self.value) | ||||
|  | ||||
| 	for _,child in pairs(self.children) do | ||||
| 		if child.enabled then | ||||
| 			child:draw() | ||||
| 		end | ||||
| 	end | ||||
| 	self:drawChildren() | ||||
| end | ||||
|  | ||||
| function UI.TextArea.example() | ||||
| 	return UI.TextArea { | ||||
| 		value = 'sample text\nabc' | ||||
| 	local Ansi = require('opus.ansi') | ||||
| 	return UI.Window { | ||||
| 		backgroundColor = 2048, | ||||
| 		t1 = UI.TextArea { | ||||
| 			ey = 3, | ||||
| 			value = 'sample text\nabc' | ||||
| 		}, | ||||
| 		t2 = UI.TextArea { | ||||
| 			y = 5, | ||||
| 			backgroundColor = 'green', | ||||
| 			value = string.format([[now %%is the %stime %sfor%s all good men to come to the aid of their country. | ||||
| 1 | ||||
| 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | ||||
| 3 | ||||
| 4 | ||||
| 5 | ||||
| 6 | ||||
| 7 | ||||
| 8]], Ansi.yellow, Ansi.onred, Ansi.reset), | ||||
| 		} | ||||
| 	} | ||||
| end | ||||
| @@ -3,7 +3,6 @@ local entry = require('opus.entry') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
| local _rep   = string.rep | ||||
|  | ||||
| local function transform(directive) | ||||
| @@ -19,15 +18,16 @@ UI.TextEntry = class(UI.Window) | ||||
| UI.TextEntry.docs = { } | ||||
| UI.TextEntry.defaults = { | ||||
| 	UIElement = 'TextEntry', | ||||
| 	--value = '', | ||||
| 	shadowText = '', | ||||
| 	focused = false, | ||||
| 	textColor = colors.white, | ||||
| 	shadowTextColor = colors.gray, | ||||
| 	backgroundColor = colors.black, -- colors.lightGray, | ||||
| 	backgroundFocusColor = colors.black, --lightGray, | ||||
| 	textColor = 'white', | ||||
| 	shadowTextColor = 'gray', | ||||
| 	markBackgroundColor = 'gray', | ||||
| 	backgroundColor = 'black', | ||||
| 	backgroundFocusColor = 'black', | ||||
| 	height = 1, | ||||
| 	limit = 6, | ||||
| 	cursorBlink = true, | ||||
| 	accelerators = { | ||||
| 		[ 'control-c' ] = 'copy', | ||||
| 	} | ||||
| @@ -74,7 +74,9 @@ function UI.TextEntry:draw() | ||||
| 		text = self.shadowText | ||||
| 	end | ||||
|  | ||||
| 	self:write(1, 1, ' ' .. Util.widthify(text, self.width - 2) .. ' ', bg, tc) | ||||
| 	local ss = self.entry.scroll > 0 and '\183' or ' ' | ||||
| 	self:write(2, 1, Util.widthify(text, self.width - 2) .. ' ', bg, tc) | ||||
| 	self:write(1, 1, ss, bg, self.shadowTextColor) | ||||
|  | ||||
| 	if self.entry.mark.active then | ||||
| 		local tx = math.max(self.entry.mark.x - self.entry.scroll, 0) | ||||
| @@ -85,7 +87,7 @@ function UI.TextEntry:draw() | ||||
| 		end | ||||
|  | ||||
| 		if tx ~= tex then | ||||
| 			self:write(tx + 2, 1, text:sub(tx + 1, tex), colors.gray, tc) | ||||
| 			self:write(tx + 2, 1, text:sub(tx + 1, tex), self.markBackgroundColor, tc) | ||||
| 		end | ||||
| 	end | ||||
| 	if self.focused then | ||||
| @@ -106,13 +108,12 @@ function UI.TextEntry:updateCursor() | ||||
| 	self:setCursorPos(self.entry.pos - self.entry.scroll + 2, 1) | ||||
| end | ||||
|  | ||||
| function UI.TextEntry:markAll() | ||||
| 	self.entry:markAll() | ||||
| end | ||||
|  | ||||
| function UI.TextEntry:focus() | ||||
| 	self:draw() | ||||
| 	if self.focused then | ||||
| 		self:setCursorBlink(true) | ||||
| 	else | ||||
| 		self:setCursorBlink(false) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.TextEntry:eventHandler(event) | ||||
|   | ||||
| @@ -20,24 +20,15 @@ UI.Throttle.defaults = { | ||||
| 		'  //)    (O ). @  \\-d )      (@ ' | ||||
| 	} | ||||
| } | ||||
| function UI.Throttle:setParent() | ||||
| function UI.Throttle:layout() | ||||
| 	self.x = math.ceil((self.parent.width - self.width) / 2) | ||||
| 	self.y = math.ceil((self.parent.height - self.height) / 2) | ||||
| 	UI.Window.setParent(self) | ||||
| 	self:reposition(self.x, self.y, self.width, self.height) | ||||
| end | ||||
|  | ||||
| function UI.Throttle:enable() | ||||
| 	self.c = os.clock() | ||||
| 	self.enabled = false | ||||
| end | ||||
|  | ||||
| function UI.Throttle:disable() | ||||
| 	if self.canvas then | ||||
| 		self.enabled = false | ||||
| 		self.canvas:removeLayer() | ||||
| 		self.canvas = nil | ||||
| 		self.ctr = 0 | ||||
| 	end | ||||
| 	self.ctr = 0 | ||||
| end | ||||
|  | ||||
| function UI.Throttle:update() | ||||
| @@ -46,11 +37,7 @@ function UI.Throttle:update() | ||||
| 		os.sleep(0) | ||||
| 		self.c = os.clock() | ||||
| 		self.enabled = true | ||||
| 		if not self.canvas then | ||||
| 			self.canvas = self:addLayer(self.backgroundColor, self.borderColor) | ||||
| 			self.canvas:setVisible(true) | ||||
| 			self:clear(self.borderColor) | ||||
| 		end | ||||
| 		self:clear(self.borderColor) | ||||
| 		local image = self.image[self.ctr + 1] | ||||
| 		local width = self.width - 2 | ||||
| 		for i = 0, #self.image do | ||||
| @@ -63,3 +50,25 @@ function UI.Throttle:update() | ||||
| 		self:sync() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Throttle.example() | ||||
| 	return UI.Window { | ||||
| 		button1 = UI.Button { | ||||
| 			x = 2, y = 2, | ||||
| 			text = 'Test', | ||||
| 		}, | ||||
| 		throttle = UI.Throttle { | ||||
| 			textColor = colors.yellow, | ||||
| 			borderColor = colors.green, | ||||
| 		}, | ||||
| 		eventHandler = function (self, event) | ||||
| 			if event.type == 'button_press' then | ||||
| 				for _ = 1, 40 do | ||||
| 					self.throttle:update() | ||||
| 					os.sleep(.05) | ||||
| 				end | ||||
| 				self.throttle:disable() | ||||
| 			end | ||||
| 		end, | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -1,59 +1,20 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
| local _rep   = string.rep | ||||
| local _sub   = string.sub | ||||
|  | ||||
| -- For manipulating text in a fixed width string | ||||
| local SB = class() | ||||
| function SB:init(width) | ||||
| 	self.width = width | ||||
| 	self.buf = _rep(' ', width) | ||||
| end | ||||
| function SB:insert(x, str, width) | ||||
| 	if x < 1 then | ||||
| 		x = self.width + x + 1 | ||||
| 	end | ||||
| 	width = width or #str | ||||
| 	if x + width - 1 > self.width then | ||||
| 		width = self.width - x | ||||
| 	end | ||||
| 	if width > 0 then | ||||
| 		self.buf = _sub(self.buf, 1, x - 1) .. _sub(str, 1, width) .. _sub(self.buf, x + width) | ||||
| 	end | ||||
| end | ||||
| function SB:fill(x, ch, width) | ||||
| 	width = width or self.width - x + 1 | ||||
| 	self:insert(x, _rep(ch, width)) | ||||
| end | ||||
| function SB:center(str) | ||||
| 	self:insert(math.max(1, math.ceil((self.width - #str + 1) / 2)), str) | ||||
| end | ||||
| function SB:get() | ||||
| 	return self.buf | ||||
| end | ||||
|  | ||||
| UI.TitleBar = class(UI.Window) | ||||
| UI.TitleBar.defaults = { | ||||
| 	UIElement = 'TitleBar', | ||||
| 	height = 1, | ||||
| 	textColor = colors.white, | ||||
| 	backgroundColor = colors.cyan, | ||||
| 	title = '', | ||||
| 	frameChar = UI.extChars and '\140' or '-', | ||||
| 	closeInd = UI.extChars and '\215' or '*', | ||||
| } | ||||
| function UI.TitleBar:draw() | ||||
| 	local sb = SB(self.width) | ||||
| 	sb:fill(2, self.frameChar, sb.width - 3) | ||||
| 	sb:center(string.format(' %s ', self.title)) | ||||
| 	self:fillArea(2, 1, self.width - 2, 1, self.frameChar) | ||||
| 	self:centeredWrite(1, string.format(' %s ', self.title)) | ||||
| 	if self.previousPage or self.event then | ||||
| 		sb:insert(-1, self.closeInd) | ||||
| 	else | ||||
| 		sb:insert(-2, self.frameChar) | ||||
| 		self:write(self.width - 1, 1, ' ' .. self.closeInd) | ||||
| 	end | ||||
| 	self:write(1, 1, sb:get()) | ||||
| end | ||||
|  | ||||
| function UI.TitleBar:eventHandler(event) | ||||
| @@ -69,5 +30,74 @@ function UI.TitleBar:eventHandler(event) | ||||
| 			end | ||||
| 			return true | ||||
| 		end | ||||
|  | ||||
| 	elseif event.type == 'mouse_down' then | ||||
| 		self.anchor = { x = event.x, y = event.y, ox = self.parent.x, oy = self.parent.y, h = self.parent.height } | ||||
|  | ||||
| 	elseif event.type == 'mouse_drag' then | ||||
| 		if self.expand == 'height' then | ||||
| 			local d = event.dy | ||||
| 			if self.anchor.h - d > 0 and self.anchor.oy + d > 0 then | ||||
| 				self.parent:reposition(self.parent.x, self.anchor.oy + event.dy, self.width, self.anchor.h - d) | ||||
| 			end | ||||
|  | ||||
| 		elseif self.moveable then | ||||
| 			local d = event.dy | ||||
| 			if self.anchor.oy + d > 0 and self.anchor.oy + d <= self.parent.parent.height then | ||||
| 				self.parent:move(self.anchor.ox + event.dx, self.anchor.oy + event.dy) | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.TitleBar.example() | ||||
| 	return UI.Window { | ||||
| 		win1 = UI.Window { | ||||
| 			x = 9, y = 2, ex = -7, ey = -3, | ||||
| 			backgroundColor = 'green', | ||||
| 			titleBar = UI.TitleBar { | ||||
| 				title = 'A really, really, really long title',  moveable = true, | ||||
| 			}, | ||||
| 			button1 = UI.Button { | ||||
| 				x = 2, y = 3, | ||||
| 				text = 'Press', | ||||
| 			}, | ||||
| 			focus = function (self) | ||||
| 				self:raise() | ||||
| 			end, | ||||
| 		}, | ||||
| 		win2 = UI.Window { | ||||
| 			x = 7, y = 3, ex = -9, ey = -2, | ||||
| 			backgroundColor = 'orange', | ||||
| 			titleBar = UI.TitleBar { | ||||
| 				title = 'test', moveable = true, | ||||
| 				event = 'none', | ||||
| 			}, | ||||
| 			button1 = UI.Button { | ||||
| 				x = 2, y = 3, | ||||
| 				text = 'Press', | ||||
| 			}, | ||||
| 			focus = function (self) | ||||
| 				self:raise() | ||||
| 			end, | ||||
| 		}, | ||||
| 		draw = function(self, isBG) | ||||
| 			for i = 1, self.height do | ||||
| 				self:write(1, i, self.filler or '') | ||||
| 			end | ||||
| 			if not isBG then | ||||
| 				for _,v in pairs(self.children) do | ||||
| 					v:draw() | ||||
| 				end | ||||
| 			end | ||||
| 		end, | ||||
| 		enable = function (self) | ||||
| 			require('opus.event').onInterval(.5, function() | ||||
| 				self.filler = string.rep(string.char(math.random(33, 126)), self.width) | ||||
| 				self:draw(true) | ||||
| 				self:sync() | ||||
| 			end) | ||||
| 			UI.Window.enable(self) | ||||
| 		end | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -1,13 +1,11 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.VerticalMeter = class(UI.Window) | ||||
| UI.VerticalMeter.defaults = { | ||||
| 	UIElement = 'VerticalMeter', | ||||
| 	backgroundColor = colors.gray, | ||||
| 	meterColor = colors.lime, | ||||
| 	backgroundColor = 'gray', | ||||
| 	meterColor = 'lime', | ||||
| 	width = 1, | ||||
| 	value = 0, | ||||
| } | ||||
| @@ -18,12 +16,11 @@ function UI.VerticalMeter:draw() | ||||
| end | ||||
|  | ||||
| function UI.VerticalMeter.example() | ||||
| 	local Event = require('opus.event') | ||||
| 	return UI.VerticalMeter { | ||||
| 		x = 2, width = 3, y = 2, ey = -2, | ||||
| 		focus = function() end, | ||||
| 		enable = function(self) | ||||
| 			Event.onInterval(.25, function() | ||||
| 			require('opus.event').onInterval(.25, function() | ||||
| 				self.value = self.value == 100 and 0 or self.value + 5 | ||||
| 				self:draw() | ||||
| 				self:sync() | ||||
| @@ -31,4 +28,4 @@ function UI.VerticalMeter.example() | ||||
| 			return UI.VerticalMeter.enable(self) | ||||
| 		end | ||||
| 	} | ||||
| end | ||||
| end | ||||
|   | ||||
| @@ -1,16 +1,15 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Viewport = class(UI.Window) | ||||
| UI.Viewport.defaults = { | ||||
| 	UIElement = 'Viewport', | ||||
| 	backgroundColor = colors.cyan, | ||||
| 	accelerators = { | ||||
| 		down            = 'scroll_down', | ||||
| 		up              = 'scroll_up', | ||||
| 		home            = 'scroll_top', | ||||
| 		left            = 'scroll_left', | ||||
| 		right           = 'scroll_right', | ||||
| 		[ 'end' ]       = 'scroll_bottom', | ||||
| 		pageUp          = 'scroll_pageUp', | ||||
| 		[ 'control-b' ] = 'scroll_pageUp', | ||||
| @@ -18,53 +17,60 @@ UI.Viewport.defaults = { | ||||
| 		[ 'control-f' ] = 'scroll_pageDown', | ||||
| 	}, | ||||
| } | ||||
| function UI.Viewport:layout() | ||||
| 	UI.Window.layout(self) | ||||
| 	if not self.canvas then | ||||
| 		self.canvas = self:addLayer() | ||||
| 	else | ||||
| 		self.canvas:resize(self.width, self.height) | ||||
| function UI.Viewport:postInit() | ||||
| 	if self.showScrollBar then | ||||
| 		self.scrollBar = UI.ScrollBar() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Viewport:enable() | ||||
| 	UI.Window.enable(self) | ||||
| 	self.canvas:setVisible(true) | ||||
| end | ||||
|  | ||||
| function UI.Viewport:disable() | ||||
| 	UI.Window.disable(self) | ||||
| 	self.canvas:setVisible(false) | ||||
| end | ||||
|  | ||||
| function UI.Viewport:setScrollPosition(offset) | ||||
| 	local oldOffset = self.offy | ||||
| 	self.offy = math.max(offset, 0) | ||||
| 	self.offy = math.min(self.offy, math.max(#self.canvas.lines, self.height) - self.height) | ||||
| 	if self.offy ~= oldOffset then | ||||
| function UI.Viewport:setScrollPosition(offy, offx) -- argh - reverse | ||||
| 	local oldOffy = self.offy | ||||
| 	self.offy = math.max(offy, 0) | ||||
| 	self.offy = math.min(self.offy, math.max(#self.lines, self.height) - self.height) | ||||
| 	if self.offy ~= oldOffy then | ||||
| 		if self.scrollBar then | ||||
| 			self.scrollBar:draw() | ||||
| 		end | ||||
| 		self.canvas.offy = offset | ||||
| 		self.canvas:dirty() | ||||
| 		self.offy = offy | ||||
| 		self:dirty(true) | ||||
| 	end | ||||
|  | ||||
| 	local oldOffx = self.offx | ||||
| 	self.offx = math.max(offx or 0, 0) | ||||
| 	self.offx = math.min(self.offx, math.max(#self.lines[1], self.width) - self.width) | ||||
| 	if self.offx ~= oldOffx then | ||||
| 		if self.scrollBar then | ||||
| 			--self.scrollBar:draw() | ||||
| 		end | ||||
| 		self.offx = offx or 0 | ||||
| 		self:dirty(true) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Viewport:write(x, y, text, bg, tc) | ||||
| 	if y > #self.canvas.lines then | ||||
| 		for i = #self.canvas.lines, y do | ||||
| 			self.canvas.lines[i + 1] = { } | ||||
| 			self.canvas:clearLine(i + 1, self.backgroundColor, self.textColor) | ||||
| 		end | ||||
| function UI.Viewport:blit(x, y, text, bg, fg) | ||||
| 	if y > #self.lines then | ||||
| 		self:resizeBuffer(self.width, y) | ||||
| 	end | ||||
| 	return UI.Window.blit(self, x, y, text, bg, fg) | ||||
| end | ||||
|  | ||||
| function UI.Viewport:write(x, y, text, bg, fg) | ||||
| 	if y > #self.lines then | ||||
| 		self:resizeBuffer(self.width, y) | ||||
| 	end | ||||
| 	return UI.Window.write(self, x, y, text, bg, fg) | ||||
| end | ||||
|  | ||||
| function UI.Viewport:setViewHeight(h) | ||||
| 	if h > #self.lines then | ||||
| 		self:resizeBuffer(self.width, h) | ||||
| 	end | ||||
| 	return UI.Window.write(self, x, y, text, bg, tc) | ||||
| end | ||||
|  | ||||
| function UI.Viewport:reset() | ||||
| 	self.offy = 0 | ||||
| 	self.canvas.offy = 0 | ||||
| 	for i = self.height + 1, #self.canvas.lines do | ||||
| 		self.canvas.lines[i] = nil | ||||
| 	for i = self.height + 1, #self.lines do | ||||
| 		self.lines[i] = nil | ||||
| 	end | ||||
| end | ||||
|  | ||||
| @@ -72,26 +78,33 @@ function UI.Viewport:getViewArea() | ||||
| 	return { | ||||
| 		y           = (self.offy or 0) + 1, | ||||
| 		height      = self.height, | ||||
| 		totalHeight = #self.canvas.lines, | ||||
| 		totalHeight = #self.lines, | ||||
| 		offsetY     = self.offy or 0, | ||||
| 	} | ||||
| end | ||||
|  | ||||
| function UI.Viewport:eventHandler(event) | ||||
| 	if #self.lines <= self.height then | ||||
| 		return | ||||
| 	end | ||||
| 	if event.type == 'scroll_down' then | ||||
| 		self:setScrollPosition(self.offy + 1) | ||||
| 		self:setScrollPosition(self.offy + 1, self.offx) | ||||
| 	elseif event.type == 'scroll_up' then | ||||
| 		self:setScrollPosition(self.offy - 1) | ||||
| 		self:setScrollPosition(self.offy - 1, self.offx) | ||||
| 	elseif event.type == 'scroll_left' then | ||||
| 		self:setScrollPosition(self.offy, self.offx - 1) | ||||
| 	elseif event.type == 'scroll_right' then | ||||
| 		self:setScrollPosition(self.offy, self.offx + 1) | ||||
| 	elseif event.type == 'scroll_top' then | ||||
| 		self:setScrollPosition(0) | ||||
| 		self:setScrollPosition(0, 0) | ||||
| 	elseif event.type == 'scroll_bottom' then | ||||
| 		self:setScrollPosition(10000000) | ||||
| 		self:setScrollPosition(10000000, 0) | ||||
| 	elseif event.type == 'scroll_pageUp' then | ||||
| 		self:setScrollPosition(self.offy - self.height) | ||||
| 		self:setScrollPosition(self.offy - self.height, self.offx) | ||||
| 	elseif event.type == 'scroll_pageDown' then | ||||
| 		self:setScrollPosition(self.offy + self.height) | ||||
| 		self:setScrollPosition(self.offy + self.height, self.offx) | ||||
| 	elseif event.type == 'scroll_to' then | ||||
| 		self:setScrollPosition(event.offset) | ||||
| 		self:setScrollPosition(event.offset, 0) | ||||
| 	else | ||||
| 		return false | ||||
| 	end | ||||
|   | ||||
| @@ -25,9 +25,6 @@ function UI.Wizard:postInit() | ||||
| 	} | ||||
|  | ||||
| 	Util.merge(self, self.pages) | ||||
| 	--for _, child in pairs(self.pages) do | ||||
| 	--	child.ey = -2 | ||||
| 	--end | ||||
| end | ||||
|  | ||||
| function UI.Wizard:add(pages) | ||||
| @@ -50,9 +47,8 @@ end | ||||
| function UI.Wizard:enable(...) | ||||
| 	self.enabled = true | ||||
| 	self.index = 1 | ||||
| 	self.transitionHint = nil | ||||
| 	local initial = self:getPage(1) | ||||
| 	for _,child in pairs(self.children) do | ||||
| 	for child in self:eachChild() do | ||||
| 		if child == initial or not child.index then | ||||
| 			child:enable(...) | ||||
| 		else | ||||
| @@ -93,12 +89,13 @@ function UI.Wizard:eventHandler(event) | ||||
| 	elseif event.type == 'enable_view' then | ||||
| 		local current = event.next or event.prev | ||||
| 		if not current then error('property "index" is required on wizard pages') end | ||||
| 		local hint | ||||
|  | ||||
| 		if event.current then | ||||
| 			if event.next then | ||||
| 				self.transitionHint = 'slideLeft' | ||||
| 				hint = 'slideLeft' | ||||
| 			elseif event.prev then | ||||
| 				self.transitionHint = 'slideRight' | ||||
| 				hint = 'slideRight' | ||||
| 			end | ||||
| 			event.current:disable() | ||||
| 		end | ||||
| @@ -117,6 +114,7 @@ function UI.Wizard:eventHandler(event) | ||||
| 			self.nextButton.event = 'wizard_complete' | ||||
| 		end | ||||
| 		-- a new current view | ||||
| 		current.transitionHint = hint | ||||
| 		current:enable() | ||||
| 		current:emit({ type = 'view_enabled', view = current }) | ||||
| 		self:draw() | ||||
|   | ||||
| @@ -1,11 +1,8 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.WizardPage = class(UI.ActiveLayer) | ||||
| UI.WizardPage = class(UI.Window) | ||||
| UI.WizardPage.defaults = { | ||||
| 	UIElement = 'WizardPage', | ||||
| 	backgroundColor = colors.cyan, | ||||
| 	ey = -2, | ||||
| } | ||||
|   | ||||
| @@ -2,50 +2,85 @@ local Tween  = require('opus.ui.tween') | ||||
|  | ||||
| local Transition = { } | ||||
|  | ||||
| function Transition.slideLeft(args) | ||||
| 	local ticks      = args.ticks or 10 | ||||
| 	local easing     = args.easing or 'outQuint' | ||||
| 	local pos        = { x = args.ex } | ||||
| 	local tween      = Tween.new(ticks, pos, { x = args.x }, easing) | ||||
| function Transition.slideLeft(canvas, args) | ||||
| 	local ticks      = args.ticks or 6 | ||||
| 	local easing     = args.easing or 'inCirc' | ||||
| 	local pos        = { x = canvas.ex } | ||||
| 	local tween      = Tween.new(ticks, pos, { x = canvas.x }, easing) | ||||
|  | ||||
| 	args.canvas:move(pos.x, args.canvas.y) | ||||
| 	canvas:move(pos.x, canvas.y) | ||||
|  | ||||
| 	return function() | ||||
| 		local finished = tween:update(1) | ||||
| 		args.canvas:move(math.floor(pos.x), args.canvas.y) | ||||
| 		args.canvas:dirty() | ||||
| 		canvas:move(math.floor(pos.x), canvas.y) | ||||
| 		canvas:dirty(true) | ||||
| 		return not finished | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Transition.slideRight(args) | ||||
| 	local ticks      = args.ticks or 10 | ||||
| 	local easing     = args.easing or'outQuint' | ||||
| 	local pos        = { x = -args.canvas.width } | ||||
| function Transition.slideRight(canvas, args) | ||||
| 	local ticks      = args.ticks or 6 | ||||
| 	local easing     = args.easing or 'inCirc' | ||||
| 	local pos        = { x = -canvas.width } | ||||
| 	local tween      = Tween.new(ticks, pos, { x = 1 }, easing) | ||||
|  | ||||
| 	args.canvas:move(pos.x, args.canvas.y) | ||||
| 	canvas:move(pos.x, canvas.y) | ||||
|  | ||||
| 	return function() | ||||
| 		local finished = tween:update(1) | ||||
| 		args.canvas:move(math.floor(pos.x), args.canvas.y) | ||||
| 		args.canvas:dirty() | ||||
| 		canvas:move(math.floor(pos.x), canvas.y) | ||||
| 		canvas:dirty(true) | ||||
| 		return not finished | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Transition.expandUp(args) | ||||
| function Transition.expandUp(canvas, args) | ||||
| 	local ticks  = args.ticks or 3 | ||||
| 	local easing = args.easing or 'linear' | ||||
| 	local pos    = { y = args.ey + 1 } | ||||
| 	local tween  = Tween.new(ticks, pos, { y = args.y }, easing) | ||||
| 	local pos    = { y = canvas.ey + 1 } | ||||
| 	local tween  = Tween.new(ticks, pos, { y = canvas.y }, easing) | ||||
|  | ||||
| 	args.canvas:move(args.x, pos.y) | ||||
| 	canvas:move(canvas.x, pos.y) | ||||
|  | ||||
| 	return function() | ||||
| 		local finished = tween:update(1) | ||||
| 		args.canvas:move(args.x, math.floor(pos.y)) | ||||
| 		args.canvas:dirty() | ||||
| 		canvas:move(canvas.x, math.floor(pos.y)) | ||||
| 		canvas.parent:dirty(true) | ||||
| 		return not finished | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Transition.shake(canvas, args) | ||||
| 	local ticks  = args.ticks or 8 | ||||
| 	local i = ticks | ||||
|  | ||||
| 	return function() | ||||
| 		i = -i | ||||
| 		canvas:move(canvas.x + i, canvas.y) | ||||
| 		if i > 0 then | ||||
| 			i = i - 2 | ||||
| 		end | ||||
| 		return i ~= 0 | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Transition.shuffle(canvas, args) | ||||
| 	local ticks  = args.ticks or 4 | ||||
| 	local easing = args.easing or 'linear' | ||||
| 	local t = { } | ||||
|  | ||||
| 	for _,child in pairs(canvas.children) do | ||||
| 		t[child] = Tween.new(ticks, child, { x = child.x, y = child.y }, easing) | ||||
| 		child.x = math.random(1, canvas.parent.width) | ||||
| 		child.y = math.random(1, canvas.parent.height) | ||||
| 	end | ||||
|  | ||||
| 	return function() | ||||
| 		local finished | ||||
| 		for child, tween in pairs(t) do | ||||
| 			finished = tween:update(1) | ||||
| 			child:move(math.floor(child.x), math.floor(child.y)) | ||||
| 		end | ||||
| 		return not finished | ||||
| 	end | ||||
| end | ||||
|   | ||||
| @@ -13,6 +13,7 @@ local _unpack   = table.unpack | ||||
| local _bor      = bit32.bor | ||||
| local _bxor     = bit32.bxor | ||||
|  | ||||
| local byteArrayMT | ||||
| byteArrayMT = { | ||||
| 	__tostring = function(a) return string.char(_unpack(a)) end, | ||||
| 	__index = { | ||||
| @@ -668,42 +669,31 @@ function Util.trimr(s) | ||||
| end | ||||
| -- end http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76 | ||||
|  | ||||
| -- word wrapping based on: | ||||
| -- https://www.rosettacode.org/wiki/Word_wrap#Lua and | ||||
| -- http://lua-users.org/wiki/StringRecipes | ||||
| local function paragraphwrap(text, linewidth, res) | ||||
| 	linewidth = linewidth or 75 | ||||
| 	local spaceleft = linewidth | ||||
| 	local line = { } | ||||
|  | ||||
| 	for word in text:gmatch("%S+") do | ||||
| 		local len = #word + 1 | ||||
|  | ||||
| 		--if colorMode then | ||||
| 		--  word:gsub('()@([@%d])', function(pos, c) len = len - 2 end) | ||||
| 		--end | ||||
|  | ||||
| 		if len > spaceleft then | ||||
| 			table.insert(res, table.concat(line, ' ')) | ||||
| 			line = { word } | ||||
| 			spaceleft = linewidth - len - 1 | ||||
| local function wrap(text, max, lines) | ||||
| 	local index = 1 | ||||
| 	repeat | ||||
| 		if #text <= max then | ||||
| 			table.insert(lines, text) | ||||
| 			text = '' | ||||
| 		elseif text:sub(max+1, max+1) == ' ' then | ||||
| 			table.insert(lines, text:sub(index, max)) | ||||
| 			text = text:sub(max + 2) | ||||
| 		else | ||||
| 			table.insert(line, word) | ||||
| 			spaceleft = spaceleft - len | ||||
| 			local x = text:sub(1, max) | ||||
| 			local s = x:match('(.*) ') or x | ||||
| 			text = text:sub(#s + 1) | ||||
| 			table.insert(lines, s) | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	table.insert(res, table.concat(line, ' ')) | ||||
| 	return table.concat(res, '\n') | ||||
| 		text = text:match('^%s*(.*)') | ||||
| 	until not text or #text == 0 | ||||
| 	return lines | ||||
| end | ||||
| -- end word wrapping | ||||
|  | ||||
| function Util.wordWrap(str, limit) | ||||
| 	local longLines = Util.split(str) | ||||
| 	local lines = { } | ||||
|  | ||||
| 	for _,line in ipairs(longLines) do | ||||
| 		paragraphwrap(line, limit, lines) | ||||
| 	for _,line in ipairs(Util.split(str)) do | ||||
| 		wrap(line, limit, lines) | ||||
| 	end | ||||
|  | ||||
| 	return lines | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 kepler155c
					kepler155c