From dcc76e8f5214f44124cfd93477515050469c6c4f Mon Sep 17 00:00:00 2001 From: ruki Date: Mon, 19 Nov 2018 22:48:45 +0800 Subject: [PATCH] add ui views --- src/core/lcurses/lcurses.c | 1 - src/core/lcurses/xmake.lua | 4 +- src/core/pdcurses/xmake.lua | 4 +- src/core/xmake.lua | 15 +- src/ltui/action.lua | 53 ++++ src/ltui/application.lua | 124 ++++++++++ src/ltui/base/dlist.lua | 213 ++++++++++++++++ src/ltui/base/log.lua | 144 +++++++++++ src/ltui/base/os.lua | 109 ++++++++ src/ltui/base/path.lua | 127 ++++++++++ src/ltui/base/string.lua | 215 ++++++++++++++++ src/ltui/base/table.lua | 344 ++++++++++++++++++++++++++ src/ltui/border.lua | 109 ++++++++ src/ltui/boxdialog.lua | 78 ++++++ src/ltui/button.lua | 105 ++++++++ src/ltui/canvas.lua | 161 ++++++++++++ src/ltui/choicebox.lua | 123 +++++++++ src/ltui/choicedialog.lua | 93 +++++++ src/ltui/curses.lua | 219 ++++++++++++++++ src/ltui/desktop.lua | 46 ++++ src/ltui/dialog.lua | 118 +++++++++ src/ltui/event.lua | 90 +++++++ src/ltui/inputdialog.lua | 77 ++++++ src/ltui/label.lua | 143 +++++++++++ src/ltui/mconfdialog.lua | 313 +++++++++++++++++++++++ src/ltui/menubar.lua | 56 +++++ src/ltui/menuconf.lua | 286 +++++++++++++++++++++ src/ltui/object.lua | 99 ++++++++ src/ltui/panel.lua | 394 +++++++++++++++++++++++++++++ src/ltui/point.lua | 104 ++++++++ src/ltui/program.lua | 456 ++++++++++++++++++++++++++++++++++ src/ltui/rect.lua | 179 ++++++++++++++ src/ltui/statusbar.lua | 58 +++++ src/ltui/textarea.lua | 120 +++++++++ src/ltui/textdialog.lua | 72 ++++++ src/ltui/textedit.lua | 108 ++++++++ src/ltui/view.lua | 481 ++++++++++++++++++++++++++++++++++++ src/ltui/window.lua | 131 ++++++++++ tests/load.lua | 9 + 39 files changed, 5570 insertions(+), 11 deletions(-) create mode 100644 src/ltui/action.lua create mode 100644 src/ltui/application.lua create mode 100644 src/ltui/base/dlist.lua create mode 100644 src/ltui/base/log.lua create mode 100644 src/ltui/base/os.lua create mode 100644 src/ltui/base/path.lua create mode 100644 src/ltui/base/string.lua create mode 100644 src/ltui/base/table.lua create mode 100644 src/ltui/border.lua create mode 100644 src/ltui/boxdialog.lua create mode 100644 src/ltui/button.lua create mode 100644 src/ltui/canvas.lua create mode 100644 src/ltui/choicebox.lua create mode 100644 src/ltui/choicedialog.lua create mode 100644 src/ltui/curses.lua create mode 100644 src/ltui/desktop.lua create mode 100644 src/ltui/dialog.lua create mode 100644 src/ltui/event.lua create mode 100644 src/ltui/inputdialog.lua create mode 100644 src/ltui/label.lua create mode 100644 src/ltui/mconfdialog.lua create mode 100644 src/ltui/menubar.lua create mode 100644 src/ltui/menuconf.lua create mode 100644 src/ltui/object.lua create mode 100644 src/ltui/panel.lua create mode 100644 src/ltui/point.lua create mode 100644 src/ltui/program.lua create mode 100644 src/ltui/rect.lua create mode 100644 src/ltui/statusbar.lua create mode 100644 src/ltui/textarea.lua create mode 100644 src/ltui/textdialog.lua create mode 100644 src/ltui/textedit.lua create mode 100644 src/ltui/view.lua create mode 100644 src/ltui/window.lua create mode 100755 tests/load.lua diff --git a/src/core/lcurses/lcurses.c b/src/core/lcurses/lcurses.c index eeb2ea0..1e8f7e1 100644 --- a/src/core/lcurses/lcurses.c +++ b/src/core/lcurses/lcurses.c @@ -2352,7 +2352,6 @@ __export int luaopen_ltui_curses (lua_State *L) return 1; } - /* initialize the character map table with the known values after ** curses initialization (for ACS_xxx values) */ static void init_ascii_map() diff --git a/src/core/lcurses/xmake.lua b/src/core/lcurses/xmake.lua index 7a7edfa..4aea732 100644 --- a/src/core/lcurses/xmake.lua +++ b/src/core/lcurses/xmake.lua @@ -1,8 +1,8 @@ -- add target target("lcurses") - -- make as a static library - set_kind("static") + -- only make objects + set_kind("object") -- add deps if is_plat("windows") then diff --git a/src/core/pdcurses/xmake.lua b/src/core/pdcurses/xmake.lua index 2d0ff10..9abf533 100644 --- a/src/core/pdcurses/xmake.lua +++ b/src/core/pdcurses/xmake.lua @@ -1,8 +1,8 @@ -- add target target("pdcurses") - -- make as a static library - set_kind("static") + -- only make objects + set_kind("object") -- add the common source files add_files("**.c") diff --git a/src/core/xmake.lua b/src/core/xmake.lua index f3a9bf6..ef0fc9a 100644 --- a/src/core/xmake.lua +++ b/src/core/xmake.lua @@ -31,12 +31,6 @@ end -- add requires add_requires("luajit") --- add projects -includes("lcurses") -if is_plat("windows") then - includes("pdcurses") -end - -- add target target("ltui") @@ -45,3 +39,12 @@ target("ltui") -- add deps add_deps("lcurses") + + -- set target directory + set_targetdir("$(buildir)") + +-- add projects +includes("lcurses") +if is_plat("windows") then + includes("pdcurses") +end diff --git a/src/ltui/action.lua b/src/ltui/action.lua new file mode 100644 index 0000000..78be031 --- /dev/null +++ b/src/ltui/action.lua @@ -0,0 +1,53 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file action.lua +-- + +-- load modules +local log = require("ltui/base/log") +local object = require("ltui/object") + +-- define module +local action = action or object { } + +-- register action types +function action:register(tag, ...) + local base = self[tag] or 0 + local enums = {...} + local n = #enums + for i = 1, n do + self[enums[i]] = i + base + end + self[tag] = base + n +end + +-- register action enums +action:register("ac_max", + "ac_on_text_changed", + "ac_on_selected", + "ac_on_enter", + "ac_on_load", + "ac_on_save", + "ac_on_exit") + +-- return module +return action diff --git a/src/ltui/application.lua b/src/ltui/application.lua new file mode 100644 index 0000000..ea66c89 --- /dev/null +++ b/src/ltui/application.lua @@ -0,0 +1,124 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file application.lua +-- + +-- load modules +local os = require("ltui/base/os") +local log = require("ltui/base/log") +local rect = require("ltui/rect") +local event = require("ltui/event") +local curses = require("ltui/curses") +local program = require("ltui/program") +local desktop = require("ltui/desktop") +local menubar = require("ltui/menubar") +local statusbar = require("ltui/statusbar") + +-- define module +local application = application or program() + +-- init application +function application:init(name, argv) + + -- init log + log:clear() +-- log:enable(false) + + -- trace + log:print(": init ..", name) + + -- init program + program.init(self, name, argv) + + -- trace + log:print(": init ok", name) +end + +-- exit application +function application:exit() + + -- exit program + program.exit(self) + + -- flush log + log:flush() +end + +-- get menubar +function application:menubar() + if not self._MENUBAR then + self._MENUBAR = menubar:new("menubar", rect{0, 0, self:width(), 1}) + end + return self._MENUBAR +end + +-- get desktop +function application:desktop() + if not self._DESKTOP then + self._DESKTOP = desktop:new("desktop", rect{0, 1, self:width(), self:height() - 1}) + end + return self._DESKTOP +end + +-- get statusbar +function application:statusbar() + if not self._STATUSBAR then + self._STATUSBAR = statusbar:new("statusbar", rect{0, self:height() - 1, self:width(), self:height()}) + end + return self._STATUSBAR +end + +-- on event +function application:event_on(e) + program.event_on(self, e) +end + +-- run application +function application:run(...) + + -- init runner + local argv = {...} + local runner = function () + + -- new an application + local app = self:new(argv) + if app then + app:loop() + app:exit() + end + end + + -- run application + local ok, errors = xpcall(runner, debug.traceback) + + -- exit curses + if not ok then + if not curses.isdone() then + curses.done() + end + log:flush() + os.raise(errors) + end +end + +-- return module +return application diff --git a/src/ltui/base/dlist.lua b/src/ltui/base/dlist.lua new file mode 100644 index 0000000..4a9c0ab --- /dev/null +++ b/src/ltui/base/dlist.lua @@ -0,0 +1,213 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file dlist.lua +-- + +-- load modules +local object = require("ltui/object") + +-- define module +local dlist = dlist or object { _init = {"_length"} } {0} + +-- clear list +function dlist:clear() + self._length = 0 + self._first = nil + self._last = nil +end + +-- push item to tail +function dlist:push(t) + assert(t) + if self._last then + self._last._next = t + t._prev = self._last + self._last = t + else + self._first = t + self._last = t + end + self._length = self._length + 1 +end + +-- insert item after the given item +function dlist:insert(t, after) + assert(t) + if not after then + return self:push(t) + end + assert(t ~= after) + if after._next then + after._next._prev = t + t._next = after._next + else + self._last = t + end + t._prev = after + after._next = t + self._length = self._length + 1 +end + +-- pop item from tail +function dlist:pop() + if not self._last then return end + local t = self._last + if t._prev then + t._prev._next = nil + self._last = t._prev + t._prev = nil + else + self._first = nil + self._last = nil + end + self._length = self._length - 1 + return t +end + +-- shift item: 1 2 3 <- 2 3 +function dlist:shift() + if not self._first then return end + local t = self._first + if t._next then + t._next._prev = nil + self._first = t._next + t._next = nil + else + self._first = nil + self._last = nil + end + self._length = self._length - 1 + return t +end + +-- unshift item: 1 2 -> t 1 2 +function dlist:unshift(t) + assert(t) + if self._first then + self._first._prev = t + t._next = self._first + self._first = t + else + self._first = t + self._last = t + end + self._length = self._length + 1 +end + +-- remove item +function dlist:remove(t) + assert(t) + if t._next then + if t._prev then + t._next._prev = t._prev + t._prev._next = t._next + else + assert(t == self._first) + t._next._prev = nil + self._first = t._next + end + elseif t._prev then + assert(t == self._last) + t._prev._next = nil + self._last = t._prev + else + assert(t == self._first and t == self._last) + self._first = nil + self._last = nil + end + t._next = nil + t._prev = nil + self._length = self._length - 1 + return t +end + +-- get first item +function dlist:first() + return self._first +end + +-- get last item +function dlist:last() + return self._last +end + +-- get next item +function dlist:next(last) + if last then + return last._next + else + return self._first + end +end + +-- get the previous item +function dlist:prev(last) + if last then + return last._prev + else + return self._last + end +end + +-- get list size +function dlist:size() + return self._length +end + +-- is empty? +function dlist:empty() + return self:size() == 0 +end + +-- get items +-- +-- .e.g +-- +-- for item in dlist:items() do +-- print(item) +-- end +-- +function dlist:items() + + -- init iterator + local iter = function (list, item) + return list:next(item) + end + + -- return iterator and initialized state + return iter, self, nil +end + +-- get reverse items +function dlist:ritems() + + -- init iterator + local iter = function (list, item) + return list:prev(item) + end + + -- return iterator and initialized state + return iter, self, nil +end + +-- return module: dlist +return dlist diff --git a/src/ltui/base/log.lua b/src/ltui/base/log.lua new file mode 100644 index 0000000..8eb8dcb --- /dev/null +++ b/src/ltui/base/log.lua @@ -0,0 +1,144 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +--o +-- @author ruki +-- @file log.lua +-- + +-- define module: log +local log = log or {} + +-- get the log file +function log:file() + + -- disable? + if self._ENABLE ~= nil and not self._ENABLE then + return + end + + -- get the output file + if self._FILE == nil then + local outputfile = self:outputfile() + if outputfile then + + -- get directory + local i = outputfile:find_last("[/\\]") + if i then + if i > 1 then i = i - 1 end + dir = outputfile:sub(1, i) + else + dir = "." + end + + -- ensure the directory + if not os.isdir(dir) then + os.mkdir(dir) + end + + -- open the log file + self._FILE = io.open(outputfile, 'w+') + end + self._FILE = self._FILE or false + end + return self._FILE +end + +-- get the output file +function log:outputfile() + if self._LOGFILE == nil then + self._LOGFILE = os.getenv("LTUI_LOGFILE") or false + end + return self._LOGFILE +end + +-- clear log +function log:clear(state) + if os.isfile(self:outputfile()) then + io.writefile(self:outputfile(), "") + end +end + +-- enable log +function log:enable(state) + self._ENABLE = state +end + +-- flush log to file +function log:flush() + local file = self:file() + if file then + io.flush(file) + end +end + +-- close the log file +function log:close() + local file = self:file() + if file then + file:close() + end +end + +-- print log to the log file +function log:print(...) + local file = self:file() + if file then + file:write(string.format(...) .. "\n") + end +end + +-- print variables to the log file +function log:printv(...) + local file = self:file() + if file then + local values = {...} + for i, v in ipairs(values) do + -- dump basic type + if type(v) == "string" or type(v) == "boolean" or type(v) == "number" then + file:write(tostring(v)) + else + file:write("<" .. tostring(v) .. ">") + end + if i ~= #values then + file:write(" ") + end + end + file:write('\n') + end +end + +-- printf log to the log file +function log:printf(...) + local file = self:file() + if file then + file:write(string.format(...)) + end +end + +-- write log the log file +function log:write(...) + local file = self:file() + if file then + file:write(...) + end +end + +-- return module: log +return log diff --git a/src/ltui/base/os.lua b/src/ltui/base/os.lua new file mode 100644 index 0000000..9f958ff --- /dev/null +++ b/src/ltui/base/os.lua @@ -0,0 +1,109 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file os.lua +-- + +-- define module +local os = os or {} + +-- load modules +local string = require("ltui/base/string") + +-- raise an exception and abort the current script +-- +-- the parent function will capture it if we uses pcall or xpcall +-- +function os.raise(msg, ...) + + -- raise it + if msg then + error(string.tryformat(msg, ...)) + else + error() + end +end + +-- run program +function os.run(cmd, ...) + return os.execute(string.tryformat(cmd, ...)) +end + +-- run program and get io output +function os.iorun(cmd, ...) + local ok = false + local outs = nil + local file = io.popen(string.tryformat(cmd, ...), "r") + if file then + outs = file:read("*a"):trim() + file:close() + ok = true + end + return ok, outs +end + +-- get host name +function os.host() + if os._HOST == nil then + local ok, result = os.iorun("uname") + if ok then + if result:lower():find("linux", 1, true) then + os._HOST = "linux" + elseif result:lower():find("darwin", 1, true) then + os._HOST = "macosx" + end + elseif os.run("cmd /c ver") == 0 then + os._HOST = "windows" + end + end + return os._HOST +end + +-- read string data from pasteboard +function os.pbpaste() + if os.host() == "macosx" then + local ok, result = os.iorun("pbpaste") + if ok then + return result + end + elseif os.host() == "linux" then + local ok, result = os.iorun("xsel --clipboard --output") + if ok then + return result + end + else + -- TODO + end +end + +-- copy string data to pasteboard +function os.pbcopy(data) + if os.host() == "macosx" then + os.run("bash -c \"echo '" .. data .. "' | pbcopy\"") + elseif os.host() == "linux" then + os.run("bash -c \"echo '" .. data .. "' | xsel --clipboard --input\"") + else + -- TODO + end +end + +-- return module +return os diff --git a/src/ltui/base/path.lua b/src/ltui/base/path.lua new file mode 100644 index 0000000..cbfc69d --- /dev/null +++ b/src/ltui/base/path.lua @@ -0,0 +1,127 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file path.lua +-- + +-- define module: path +local path = path or {} + +-- load modules +local string = require("ltui/base/string") + +-- get the directory of the path +function path.directory(p) + local i = p:find_last("[/\\]") + if i then + if i > 1 then i = i - 1 end + return p:sub(1, i) + else + return "." + end +end + +-- get the filename of the path +function path.filename(p) + local i = p:find_last("[/\\]") + if i then + return p:sub(i + 1) + else + return p + end +end + +-- get the basename of the path +function path.basename(p) + local name = path.filename(p) + local i = name:find_last(".", true) + if i then + return name:sub(1, i - 1) + else + return name + end +end + +-- get the file extension of the path: .xxx +function path.extension(p) + + -- check + assert(p) + + -- get extension + local i = p:find_last(".", true) + if i then + return p:sub(i) + else + return "" + end +end + +-- join path +function path.join(p, ...) + + -- check + assert(p) + + -- join them + for _, name in ipairs({...}) do + p = p .. "/" .. name + end + + -- translate path + return path.translate(p) +end + +-- split path by the separator +function path.split(p) + return p:split("/\\") +end + +-- get the path seperator +function path.seperator() + return xmake._HOST == "windows" and '\\' or '/' +end + +-- the last character is the path seperator? +function path.islastsep(p) + local sep = p:sub(#p, #p) + return xmake._HOST == "windows" and (sep == '\\' or sep == '/') or (sep == '/') +end + +-- convert path pattern to a lua pattern +function path.pattern(pattern) + + -- translate wildcards, .e.g *, ** + pattern = pattern:gsub("([%+%.%-%^%$%(%)%%])", "%%%1") + pattern = pattern:gsub("%*%*", "\001") + pattern = pattern:gsub("%*", "\002") + pattern = pattern:gsub("\001", ".*") + pattern = pattern:gsub("\002", "[^/]*") + + -- case-insensitive filesystem? + if not os.fscase() then + pattern = string.ipattern(pattern, true) + end + return pattern +end + +-- return module: path +return path diff --git a/src/ltui/base/string.lua b/src/ltui/base/string.lua new file mode 100644 index 0000000..abf92e9 --- /dev/null +++ b/src/ltui/base/string.lua @@ -0,0 +1,215 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file string.lua +-- + +-- define module: string +local string = string or {} + +-- find the last substring with the given pattern +function string:find_last(pattern, plain) + + -- find the last substring + local curr = 0 + repeat + local next = self:find(pattern, curr + 1, plain) + if next then + curr = next + end + until (not next) + + -- found? + if curr > 0 then + return curr + end +end + +-- split string with the given characters +-- +-- ("1\n\n2\n3"):split('\n') => 1, 2, 3 +-- ("1\n\n2\n3"):split('\n', true) => 1, , 2, 3 +-- +function string:split(delimiter, strict) + local result = {} + if strict then + for match in (self .. delimiter):gmatch("(.-)" .. delimiter) do + table.insert(result, match) + end + else + self:gsub("[^" .. delimiter .."]+", function(v) table.insert(result, v) end) + end + return result +end + +-- trim the spaces +function string:trim() + return (self:gsub("^%s*(.-)%s*$", "%1")) +end + +-- trim the left spaces +function string:ltrim() + return (self:gsub("^%s*", "")) +end + +-- trim the right spaces +function string:rtrim() + local n = #self + while n > 0 and s:find("^%s", n) do n = n - 1 end + return self:sub(1, n) +end + +-- append a substring with a given separator +function string:append(substr, separator) + + -- check + assert(self) + + -- not substr? return self + if not substr then + return self + end + + -- append it + local s = self + if #s == 0 then + s = substr + else + s = string.format("%s%s%s", s, separator or "", substr) + end + + -- ok + return s +end + +-- encode: ' ', '=', '\"', '<' +function string:encode() + + -- null? + if self == nil then return end + + -- done + return (self:gsub("[%s=\"<]", function (w) return string.format("%%%x", w:byte()) end)) +end + +-- decode: ' ', '=', '\"' +function string:decode() + + -- null? + if self == nil then return end + + -- done + return (self:gsub("%%(%x%x)", function (w) return string.char(tonumber(w, 16)) end)) +end + +-- join array to string with the given separator +function string.join(items, sep) + + -- join them + local str = "" + local index = 1 + local count = #items + for _, item in ipairs(items) do + str = str .. item + if index ~= count and sep ~= nil then + str = str .. sep + end + index = index + 1 + end + + -- ok? + return str +end + +-- try to format +function string.tryformat(format, ...) + + -- attempt to format it + local ok, str = pcall(string.format, format, ...) + if ok then + return str + else + return format + end +end + +-- case-insensitive pattern-matching +-- +-- print(("src/dadasd.C"):match(string.ipattern("sR[cd]/.*%.c", true))) +-- print(("src/dadasd.C"):match(string.ipattern("src/.*%.c", true))) +-- +-- print(string.ipattern("sR[cd]/.*%.c")) +-- [sS][rR][cd]/.*%.[cC] +-- +-- print(string.ipattern("sR[cd]/.*%.c", true)) +-- [sS][rR][cCdD]/.*%.[cC] +-- +function string.ipattern(pattern, brackets) + local tmp = {} + local i = 1 + while i <= #pattern do + + -- get current charactor + local char = pattern:sub(i, i) + + -- escape? + if char == '%' then + tmp[#tmp + 1] = char + i = i + 1 + char = pattern:sub(i,i) + tmp[#tmp + 1] = char + + -- '%bxy'? add next 2 chars + if char == 'b' then + tmp[#tmp + 1] = pattern:sub(i + 1, i + 2) + i = i + 2 + end + -- brackets? + elseif char == '[' then + tmp[#tmp + 1] = char + i = i + 1 + while i <= #pattern do + char = pattern:sub(i, i) + if char == '%' then + tmp[#tmp + 1] = char + tmp[#tmp + 1] = pattern:sub(i + 1, i + 1) + i = i + 1 + elseif char:match("%a") then + tmp[#tmp + 1] = not brackets and char or char:lower() .. char:upper() + else + tmp[#tmp + 1] = char + end + if char == ']' then break end + i = i + 1 + end + -- letter, [aA] + elseif char:match("%a") then + tmp[#tmp + 1] = '[' .. char:lower() .. char:upper() .. ']' + else + tmp[#tmp + 1] = char + end + i = i + 1 + end + return table.concat(tmp) +end + +-- return module: string +return string diff --git a/src/ltui/base/table.lua b/src/ltui/base/table.lua new file mode 100644 index 0000000..dde5fa2 --- /dev/null +++ b/src/ltui/base/table.lua @@ -0,0 +1,344 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file table.lua +-- + +-- define module: table +local table = table or {} + +-- clear the table +function table.clear(self) + for k in next, self do + rawset(self, k, nil) + end +end + +-- join all objects and tables +function table.join(...) + + local result = {} + for _, t in ipairs({...}) do + if type(t) == "table" then + for k, v in pairs(t) do + if type(k) == "number" then table.insert(result, v) + else result[k] = v end + end + else + table.insert(result, t) + end + end + return result +end + +-- join all objects and tables to self +function table.join2(self, ...) + + for _, t in ipairs({...}) do + if type(t) == "table" then + for k, v in pairs(t) do + if type(k) == "number" then table.insert(self, v) + else self[k] = v end + end + else + table.insert(self, t) + end + end + return self +end + +-- append all objects to array +function table.append(array, ...) + for _, value in ipairs({...}) do + table.insert(array, value) + end + return array +end + +-- copy the table to self +function table.copy(copied) + + -- init it + copied = copied or {} + + -- copy it + local result = {} + for k, v in pairs(table.wrap(copied)) do + result[k] = v + end + + -- ok + return result +end + +-- copy the table to self +function table.copy2(self, copied) + + -- check + assert(self) + + -- init it + copied = copied or {} + + -- clear self first + table.clear(self) + + -- copy it + for k, v in pairs(table.wrap(copied)) do + self[k] = v + end + +end + +-- inherit interfaces and create a new instance +function table.inherit(...) + + -- init instance + local classes = {...} + local instance = {} + local metainfo = {} + for _, clasz in ipairs(classes) do + for k, v in pairs(clasz) do + if type(v) == "function" then + if k:startswith("__") then + if metainfo[k] == nil then + metainfo[k] = v + end + else + if instance[k] == nil then + instance[k] = v + else + instance["_super_" .. k] = v + end + end + end + end + end + setmetatable(instance, metainfo) + + -- ok? + return instance +end + +-- inherit interfaces from the given class +function table.inherit2(self, ...) + + -- check + assert(self) + + -- init instance + local classes = {...} + local metainfo = getmetatable(self) or {} + for _, clasz in ipairs(classes) do + for k, v in pairs(clasz) do + if type(v) == "function" then + if k:startswith("__") then + if metainfo[k] == nil then + metainfo[k] = v + end + else + if self[k] == nil then + self[k] = v + else + self["_super_" .. k] = v + end + end + end + end + end + + -- ok? + return self +end + +-- slice table array +function table.slice(self, first, last, step) + + -- slice it + local sliced = {} + for i = first or 1, last or #self, step or 1 do + sliced[#sliced + 1] = self[i] + end + return sliced +end + +-- is array? +function table.is_array(array) + return type(array) == "table" and array[1] ~= nil +end + +-- is dictionary? +function table.is_dictionary(dict) + return type(dict) == "table" and dict[1] == nil +end + +-- dump it with the level +function table._dump(self, exclude, level) + + -- dump basic type + if type(self) == "string" or type(self) == "boolean" or type(self) == "number" then + io.write(tostring(self)) + elseif type(self) == "table" and (getmetatable(self) or {}).__tostring then + io.write(tostring(self)) + -- dump table + elseif type(self) == "table" then + + -- dump head + io.write("\n") + for l = 1, level do + io.write(" ") + end + io.write("{\n") + + -- dump body + local i = 0 + for k, v in pairs(self) do + + -- exclude some keys + if not exclude or type(k) ~= "string" or not k:find(exclude) then + + -- dump spaces and separator + for l = 1, level do + io.write(" ") + end + + if i == 0 then + io.write(" ") + else + io.write(", ") + end + + -- dump key + if type(k) == "string" then + io.write(k, " = ") + end + + -- dump value + table._dump(v, exclude, level + 1) + + -- dump newline + io.write("\n") + i = i + 1 + end + end + + -- dump tail + for l = 1, level do + io.write(" ") + end + io.write("}\n") + elseif self ~= nil then + io.write("<" .. tostring(self) .. ">") + else + io.write("nil") + end +end + +-- dump it +function table.dump(self, exclude, prefix) + + -- dump prefix + if prefix then + io.write(prefix) + end + + -- dump it + table._dump(self, exclude, 0) + + -- end + print("") + + -- return it + return self +end + +-- unwrap object if be only one +function table.unwrap(object) + if type(object) == "table" then + if #object == 1 then + return object[1] + end + end + return object +end + +-- wrap object to table +function table.wrap(object) + + -- no object? + if nil == object then + return {} + end + + -- wrap it if not table + if type(object) ~= "table" then + return {object} + end + + -- ok + return object +end + +-- remove repeat from the given array +function table.unique(array, barrier) + + -- remove repeat + if type(array) == "table" then + + -- not only one? + if table.getn(array) ~= 1 then + + -- done + local exists = {} + local unique = {} + for _, v in ipairs(array) do + + -- exists barrier? clear the current existed items + if barrier and barrier(v) then + exists = {} + end + + -- add unique item + if type(v) == "string" then + if not exists[v] then + exists[v] = true + table.insert(unique, v) + end + else + local key = "\"" .. tostring(v) .. "\"" + if not exists[key] then + exists[key] = true + table.insert(unique, v) + end + end + end + + -- update it + array = unique + end + end + + -- ok + return array +end + +-- return module: table +return table diff --git a/src/ltui/border.lua b/src/ltui/border.lua new file mode 100644 index 0000000..25c058e --- /dev/null +++ b/src/ltui/border.lua @@ -0,0 +1,109 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file border.lua +-- + +-- load modules +local log = require("ltui/base/log") +local rect = require("ltui/rect") +local view = require("ltui/view") +local label = require("ltui/label") +local curses = require("ltui/curses") + +-- define module +local border = border or view() + +-- init border +function border:init(name, bounds) + + -- init view + view.init(self, name, bounds) + + -- check bounds + assert(self:width() > 2 and self:height() > 2, string.format("%s: too small!", self)) +end + +-- draw border +function border:draw(transparent) + + -- draw background (transparent) + view.draw(self, true) + + -- get corner attribute + local cornerattr = self:cornerattr() + + -- the left-upper attribute + local attr_ul = curses.color_pair(cornerattr[1], self:background()) + if self:background() == cornerattr[1] then + attr_ul = {attr_ul, "standout"} + end + + -- the right-lower attribute + local attr_rl = curses.color_pair(cornerattr[2], self:background()) + if self:background() == cornerattr[2] then + attr_rl = {attr_rl, "standout"} + end + + -- the border characters + -- @note acs character will use 2 width on borders (pdcurses), so we use acsii characters instead of them. + local iswin = os.host() == "windows" + local hline = iswin and '-' or "hline" + local vline = iswin and '|' or "vline" + local ulcorner = iswin and ' ' or "ulcorner" + local llcorner = iswin and ' ' or "llcorner" + local urcorner = iswin and ' ' or "urcorner" + local lrcorner = iswin and ' ' or "lrcorner" + + -- draw left and top border + self:canvas():attr(attr_ul) + self:canvas():move(0, 0):putchar(hline, self:width()) + self:canvas():move(0, 0):putchar(ulcorner) + self:canvas():move(0, 1):putchar(vline, self:height() - 1, true) + self:canvas():move(0, self:height() - 1):putchar(llcorner) + + -- draw bottom and right border + self:canvas():attr(attr_rl) + self:canvas():move(1, self:height() - 1):putchar(hline, self:width() - 1) + self:canvas():move(self:width() - 1, 0):putchar(urcorner) + self:canvas():move(self:width() - 1, 1):putchar(vline, self:height() - 1, true) + self:canvas():move(self:width() - 1, self:height() - 1):putchar(lrcorner) +end + +-- get border corner attribute +function border:cornerattr() + return self._CORNERATTR or {"white", "black"} +end + +-- set border corner attribute +function border:cornerattr_set(attr_ul, attr_rl) + self._CORNERATTR = {attr_ul or "white", attr_rl or attr_ul or "black"} + self:invalidate() +end + +-- swap border corner attribute +function border:cornerattr_swap() + local cornerattr = self:cornerattr() + self:cornerattr_set(cornerattr[2], cornerattr[1]) +end + +-- return module +return border diff --git a/src/ltui/boxdialog.lua b/src/ltui/boxdialog.lua new file mode 100644 index 0000000..74d5b62 --- /dev/null +++ b/src/ltui/boxdialog.lua @@ -0,0 +1,78 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file boxdialog.lua +-- + +-- load modules +local log = require("ui/log") +local rect = require("ui/rect") +local action = require("ui/action") +local curses = require("ui/curses") +local window = require("ui/window") +local textdialog = require("ui/textdialog") + +-- define module +local boxdialog = boxdialog or textdialog() + +-- init dialog +function boxdialog:init(name, bounds, title) + + -- init window + textdialog.init(self, name, bounds, title) + + -- insert box + self:panel():insert(self:box()) + + -- resize text + self:text():bounds().ey = 3 + self:text():invalidate(true) + self:text():option_set("selectable", false) + self:text():option_set("progress", false) + + -- text changed + self:text():action_set(action.ac_on_text_changed, function (v) + if v:text() then + local lines = #self:text():splitext(v:text()) + if lines > 0 and lines < self:height() then + self:box():bounds().sy = lines + self:text():bounds().ey = lines + self:box():invalidate(true) + self:text():invalidate(true) + end + end + end) + + -- select buttons by default + self:panel():select(self:buttons()) +end + +-- get box +function boxdialog:box() + if not self._BOX then + self._BOX = window:new("boxdialog.box", rect{0, 3, self:panel():width(), self:panel():height() - 1}) + self._BOX:border():cornerattr_set("black", "white") + end + return self._BOX +end + +-- return module +return boxdialog diff --git a/src/ltui/button.lua b/src/ltui/button.lua new file mode 100644 index 0000000..14fa2a8 --- /dev/null +++ b/src/ltui/button.lua @@ -0,0 +1,105 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file button.lua +-- + +-- load modules +local log = require("ui/log") +local view = require("ui/view") +local event = require("ui/event") +local label = require("ui/label") +local action = require("ui/action") +local curses = require("ui/curses") + +-- define module +local button = button or label() + +-- init button +function button:init(name, bounds, text, on_action) + + -- init label + label.init(self, name, bounds, text) + + -- mark as selectable + self:option_set("selectable", true) + + -- show cursor + self:cursor_show(true) + + -- init action + self:action_set(action.ac_on_enter, on_action) +end + +-- draw button +function button:draw(transparent) + + -- draw background + view.draw(self, transparent) + + -- strip text string + local str = self:text() + if str and #str > 0 then + str = string.sub(str, 1, self:width()) + end + if not str or #str == 0 then + return + end + + -- get the text attribute value + local textattr = self:textattr_val() + + -- selected? + if self:state("selected") and self:state("focused") then + textattr = {textattr, "reverse"} + end + + -- draw text + self:canvas():attr(textattr):move(0, 0):putstr(str) +end + +-- on event +function button:event_on(e) + + -- selected? + if not self:state("selected") then + return + end + + -- enter this button? + if e.type == event.ev_keyboard then + if e.key_name == "Enter" then + self:action_on(action.ac_on_enter) + return true + end + end +end + +-- set state +function button:state_set(name, enable) + if name == "focused" and self:state(name) ~= enable then + self:invalidate() + end + return view.state_set(self, name, enable) +end + +-- return module +return button diff --git a/src/ltui/canvas.lua b/src/ltui/canvas.lua new file mode 100644 index 0000000..b3e851b --- /dev/null +++ b/src/ltui/canvas.lua @@ -0,0 +1,161 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file canvas.lua +-- + +-- load modules +local log = require("ltui/base/log") +local point = require("ltui/point") +local curses = require("ltui/curses") +local object = require("ltui/object") + +-- define module +local line = line or object() +local canvas = canvas or object() + +-- new canvas instance +function canvas:new(view, window) + + -- create instance + self = self() + + -- save view and window + self._view = view + self._window = window + assert(view and window, "cannot new canvas instance without view and window!") + + -- set the default attributes + self:attr() + + -- done + return self +end + +-- clear canvas +function canvas:clear() + self._window:clear() + return self +end + +-- move canvas to the given position +function canvas:move(x, y) + self._window:move(y, x) + return self +end + +-- get the current position +function canvas:pos() + local y, x = self._window:getyx() + return x, y +end + +-- get the canvas size +function canvas:size() + local y, x = self._window:getmaxyx() + return point {x + 1, y + 1} +end + +-- get the canvas width +function canvas:width() + local _, x = self._window:getmaxyx() + return x + 1 +end + +-- get the canvas height +function canvas:height() + local y, _ = self._window:getmaxyx() + return y + 1 +end + +-- put character to canvas +function canvas:putchar(ch, n, vertical) + + -- acs character? + if type(ch) == "string" and #ch > 1 then + ch = curses.acs(ch) + end + + -- draw characters + n = n or 1 + if vertical then + local x, y = self:pos() + while n > 0 do + self:move(x, y) + self._window:addch(ch) + n = n - 1 + y = y + 1 + end + else + while n > 0 do + self._window:addch(ch) + n = n - 1 + end + end + return self +end + +-- put a string to canvas +function canvas:putstr(str) + self._window:addstr(str) + return self +end + +-- put strings to canvas +function canvas:putstrs(strs, startline) + + -- draw strings + local sy, sx = self._window:getyx() + local ey, _ = self._window:getmaxyx() + for idx = startline or 1, #strs do + local _, y = self:pos() + self._window:addstr(strs[idx]) + if y + 1 < ey and idx < #strs then + self:move(sx, y + 1) + else + break + end + end + return self +end + +-- set canvas attributes +-- +-- set attr: canvas:attr("bold") +-- add attr: canvas:attr("bold", true) +-- remove attr: canvas:attr("bold", false) +-- +function canvas:attr(attrs, modify) + + -- calculate the attributes + local attr = curses.calc_attr(attrs) + if modify == nil then + self._window:attrset(attr) + elseif modify == false then + self._window:attroff(attr) + else + self._window:attron(attr) + end + return self +end + +-- return module +return canvas diff --git a/src/ltui/choicebox.lua b/src/ltui/choicebox.lua new file mode 100644 index 0000000..81ad1da --- /dev/null +++ b/src/ltui/choicebox.lua @@ -0,0 +1,123 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file choicebox.lua +-- + +-- load modules +local log = require("ui/log") +local view = require("ui/view") +local rect = require("ui/rect") +local panel = require("ui/panel") +local event = require("ui/event") +local action = require("ui/action") +local curses = require("ui/curses") +local button = require("ui/button") +local object = require("ui/object") + +-- define module +local choicebox = choicebox or panel() + +-- init choicebox +function choicebox:init(name, bounds) + + -- init panel + panel.init(self, name, bounds) + + -- init values + self._VALUES = {} +end + +-- on event +function choicebox:event_on(e) + + -- select config + if e.type == event.ev_keyboard then + if e.key_name == "Down" then + return self:select_next() + elseif e.key_name == "Up" then + return self:select_prev() + elseif e.key_name == "Enter" or e.key_name == " " then + self:_do_select() + return true + end + elseif e.type == event.ev_command and e.command == "cm_enter" then + self:_do_select() + return true + end +end + +-- load values +function choicebox:load(values, selected) + + -- clear the views first + self:clear() + + -- insert values + self._VALUES = values + for idx, value in ipairs(values) do + self:_do_insert(value, idx, idx == selected) + end + + -- select the first item + self:select(self:first()) + + -- invalidate + self:invalidate() +end + +-- do insert a value item +function choicebox:_do_insert(value, index, selected) + + -- init text + local text = (selected and "(X) " or "( ) ") .. tostring(value) + + -- init a value item view + local item = button:new("choicebox.value." .. self:count(), rect:new(0, self:count(), self:width(), 1), text) + + -- attach this index + item:extra_set("index", index) + + -- insert this config item + self:insert(item) +end + +-- do select the current config +function choicebox:_do_select() + + -- get the current item + local item = self:current() + + -- get the current index + local index = item:extra("index") + + -- get the current value + local value = self._VALUES[index] + + -- do action: on selected + self:action_on(action.ac_on_selected, index, value) + + -- update text + item:text_set("(X) " .. tostring(value)) +end + +-- return module +return choicebox diff --git a/src/ltui/choicedialog.lua b/src/ltui/choicedialog.lua new file mode 100644 index 0000000..5762f74 --- /dev/null +++ b/src/ltui/choicedialog.lua @@ -0,0 +1,93 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file choicedialog.lua +-- + +-- load modules +local log = require("ui/log") +local rect = require("ui/rect") +local event = require("ui/event") +local action = require("ui/action") +local curses = require("ui/curses") +local window = require("ui/window") +local choicebox = require("ui/choicebox") +local boxdialog = require("ui/boxdialog") + +-- define module +local choicedialog = choicedialog or boxdialog() + +-- init dialog +function choicedialog:init(name, bounds, title) + + -- init window + boxdialog.init(self, name, bounds, title) + + -- init text + self:text():text_set("Use the arrow keys to navigate this window or press the hotkey of the item you wish to select followed by the . Press for additional information about this") + + -- init buttons + self:button_add("select", "< Select >", function (v, e) + self:choicebox():event_on(event.command {"cm_enter"}) + self:quit() + end) + self:button_add("cancel", "< Cancel >", function (v, e) + self:quit() + end) + self:buttons():select(self:button("select")) + + -- insert choice box + self:box():panel():insert(self:choicebox()) + + -- disable to select to box (disable Tab switch and only response to buttons) + self:box():option_set("selectable", false) +end + +-- get choice box +function choicedialog:choicebox() + if not self._CHOICEBOX then + local bounds = self:box():panel():bounds() + self._CHOICEBOX = choicebox:new("choicedialog.choicebox", rect:new(0, 0, bounds:width(), bounds:height())) + self._CHOICEBOX:state_set("focused", true) -- we can select and highlight selected item + end + return self._CHOICEBOX +end + +-- on event +function choicedialog:event_on(e) + + -- load values first + if e.type == event.ev_idle then + if not self._LOADED then + self:action_on(action.ac_on_load) + self._LOADED = true + end + -- select value + elseif e.type == event.ev_keyboard then + if e.key_name == "Down" or e.key_name == "Up" or e.key_name == " " then + return self:choicebox():event_on(e) + end + end + return boxdialog.event_on(self, e) +end + +-- return module +return choicedialog diff --git a/src/ltui/curses.lua b/src/ltui/curses.lua new file mode 100644 index 0000000..6885471 --- /dev/null +++ b/src/ltui/curses.lua @@ -0,0 +1,219 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file curses.lua +-- + +--[[ Console User Interface (cui) ]----------------------------------------- +Author: Tiago Dionizio (tngd@mega.ist.utl.pt) +$Id: core.lua 18 2007-06-21 20:43:52Z tngd $ +--------------------------------------------------------------------------]] + +-- load modules +local curses = require("ltui.curses") +local os = require("ltui/base/os") +local log = require("ltui/base/log") + +-- get color from the given name +function curses.color(name) + if name == 'black' then return curses.COLOR_BLACK + elseif name == 'red' then return curses.COLOR_RED + elseif name == 'green' then return curses.COLOR_GREEN + elseif name == 'yellow' then return curses.COLOR_YELLOW + elseif name == 'blue' then return curses.COLOR_BLUE + elseif name == 'magenta' then return curses.COLOR_MAGENTA + elseif name == 'cyan' then return curses.COLOR_CYAN + elseif name == 'white' then return curses.COLOR_WHITE + else return curses.COLOR_BLACK + end +end + +-- is color? +local colors = {black = true, red = true, green = true, yellow = true, blue = true, magenta = true, cyan = true, white = true} +function curses.iscolor(name) + return colors[name] or colors[name:sub(3) or ""] +end + +-- get attr from the given name +function curses.attr(name) + if name == 'normal' then return curses.A_NORMAL + elseif name == 'standout' then return curses.A_STANDOUT + elseif name == 'underline' then return curses.A_UNDERLINE + elseif name == 'reverse' then return curses.A_REVERSE + elseif name == 'blink' then return curses.A_BLINK + elseif name == 'dim' then return curses.A_DIM + elseif name == 'bold' then return curses.A_BOLD + elseif name == 'protect' then return curses.A_PROTECT + elseif name == 'invis' then return curses.A_INVIS + elseif name == 'alt' then return curses.A_ALTCHARSET + else return curses.A_NORMAL + end +end + +-- get acs character from the given name +function curses.acs(name) + if name == 'block' then return curses.ACS_BLOCK + elseif name == 'board' then return curses.ACS_BOARD + elseif name == 'btee' then return curses.ACS_BTEE + elseif name == 'bullet' then return curses.ACS_BULLET + elseif name == 'ckboard' then return curses.ACS_CKBOARD + elseif name == 'darrow' then return curses.ACS_DARROW + elseif name == 'degree' then return curses.ACS_DEGREE + elseif name == 'diamond' then return curses.ACS_DIAMOND + elseif name == 'gequal' then return curses.ACS_GEQUAL + elseif name == 'hline' then return curses.ACS_HLINE + elseif name == 'lantern' then return curses.ACS_LANTERN + elseif name == 'larrow' then return curses.ACS_LARROW + elseif name == 'lequal' then return curses.ACS_LEQUAL + elseif name == 'llcorner' then return curses.ACS_LLCORNER + elseif name == 'lrcorner' then return curses.ACS_LRCORNER + elseif name == 'ltee' then return curses.ACS_LTEE + elseif name == 'nequal' then return curses.ACS_NEQUAL + elseif name == 'pi' then return curses.ACS_PI + elseif name == 'plminus' then return curses.ACS_PLMINUS + elseif name == 'plus' then return curses.ACS_PLUS + elseif name == 'rarrow' then return curses.ACS_RARROW + elseif name == 'rtee' then return curses.ACS_RTEE + elseif name == 's1' then return curses.ACS_S1 + elseif name == 's3' then return curses.ACS_S3 + elseif name == 's7' then return curses.ACS_S7 + elseif name == 's9' then return curses.ACS_S9 + elseif name == 'sterling' then return curses.ACS_STERLING + elseif name == 'ttee' then return curses.ACS_TTEE + elseif name == 'uarrow' then return curses.ACS_UARROW + elseif name == 'ulcorner' then return curses.ACS_ULCORNER + elseif name == 'urcorner' then return curses.ACS_URCORNER + elseif name == 'vline' then return curses.ACS_VLINE + elseif type(name) == 'string' and #name == 1 then + return name + else + return ' ' + end +end + +-- calculate attr from the attributes list +-- +-- local attr = curses.calc_attr("bold") +-- local attr = curses.calc_attr("yellow") +-- local attr = curses.calc_attr{ "yellow", "ongreen" } +-- local attr = curses.calc_attr{ "yellow", "ongreen", "bold" } +-- local attr = curses.calc_attr{ curses.color_pair("yellow", "green"), "bold" } +-- +function curses.calc_attr(attrs) + + -- curses.calc_attr(curses.A_BOLD) + -- curses.calc_attr(curses.color_pair("yellow", "green")) + local atype = type(attrs) + if atype == "number" then + return attrs + -- curses.calc_attr("bold") + -- curses.calc_attr("yellow") + elseif atype == "string" then + if curses.iscolor(attrs) then + local color = attrs + if color:startswith("on") then + color = color:sub(3) + end + return curses.color_pair(color, color) + end + return curses.attr(attrs) + -- curses.calc_attr{ "yellow", "ongreen", "bold" } + -- curses.calc_attr{ curses.color_pair("yellow", "green"), "bold" } + elseif atype == "table" then + local v = 0 + local set = {} + local fg = nil + local bg = nil + for _, a in ipairs(attrs) do + if not set[a] and a then + set[a] = true + if type(a) == "number" then + v = v + a + elseif curses.iscolor(a) then + if a:startswith("on") then + bg = a:sub(3) + else + fg = a + end + else + v = v + curses.attr(a) + end + end + end + if fg or bg then + v = v + curses.color_pair(fg or bg, bg or fg) + end + return v + else + return 0 + end +end + +-- get attr from the color pair +curses._color_pair = curses._color_pair or curses.color_pair +function curses.color_pair(fg, bg) + + -- get foreground and backround color + fg = curses.color(fg) + bg = curses.color(bg) + + -- attempt to get color from the cache first + local key = fg .. ':' .. bg + local colors = curses._COLORS or {} + if colors[key] then + return colors[key] + end + + -- no colors? + if not curses.has_colors() then + return 0 + end + + -- update the colors count + curses._NCOLORS = (curses._NCOLORS or 0) + 1 + + -- init the color pair + if not curses.init_pair(curses._NCOLORS, fg, bg) then + os.raise("failed to initialize color pair (%d, %s, %s)", curses._NCOLORS, fg, bg) + end + + -- get the color attr + local attr = curses._color_pair(curses._NCOLORS) + + -- save to cache + colors[key] = attr + curses._COLORS = colors + + -- ok + return attr +end + +-- set cursor state +curses._cursor_set = curses._cursor_set or curses.cursor_set +function curses.cursor_set(state) + if curses._CURSOR_STATE ~= state then + curses._CURSOR_STATE = state + curses._cursor_set(state) + end +end + +-- return module: curses +return curses diff --git a/src/ltui/desktop.lua b/src/ltui/desktop.lua new file mode 100644 index 0000000..234b272 --- /dev/null +++ b/src/ltui/desktop.lua @@ -0,0 +1,46 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file desktop.lua +-- + +-- load modules +local log = require("ltui/base/log") +local rect = require("ltui/rect") +local view = require("ltui/view") +local panel = require("ltui/panel") +local curses = require("ltui/curses") + +-- define module +local desktop = desktop or panel() + +-- init desktop +function desktop:init(name, bounds) + + -- init panel + panel.init(self, name, bounds) + + -- init background + self:background_set("blue") +end + +-- return module +return desktop diff --git a/src/ltui/dialog.lua b/src/ltui/dialog.lua new file mode 100644 index 0000000..089a572 --- /dev/null +++ b/src/ltui/dialog.lua @@ -0,0 +1,118 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file dialog.lua +-- + +-- load modules +local log = require("ui/log") +local rect = require("ui/rect") +local event = require("ui/event") +local label = require("ui/label") +local panel = require("ui/panel") +local action = require("ui/action") +local button = require("ui/button") +local window = require("ui/window") +local curses = require("ui/curses") + +-- define module +local dialog = dialog or window() + +-- init dialog +function dialog:init(name, bounds, title) + + -- init window + window.init(self, name, bounds, title, true) + + -- insert buttons + self:panel():insert(self:buttons()) +end + +-- get buttons +function dialog:buttons() + if not self._BUTTONS then + self._BUTTONS = panel:new("dialog.buttons", rect:new(0, self:panel():height() - 1, self:panel():width(), 1)) + end + return self._BUTTONS +end + +-- get button from the given button name +function dialog:button(name) + return self:buttons():view(name) +end + +-- add button +function dialog:button_add(name, text, command) + + -- init button + local btn = button:new(name, rect:new(0, 0, #text, 1), text, command) + + -- insert button + self:buttons():insert(btn) + + -- update the position of all buttons + local index = 1 + local width = self:buttons():width() + local count = self:buttons():count() + local padding = math.floor(width / 8) + for v in self:buttons():views() do + local x = padding + index * math.floor((width - padding * 2) / (count + 1)) - math.floor(v:width() / 2) + if x + v:width() > width then + x = math.max(0, width - v:width()) + end + v:bounds():move2(x, 0) + v:invalidate(true) + index = index + 1 + end + + -- invalidate + self:invalidate() + + -- ok + return btn +end + +-- select button from the given button name +function dialog:button_select(name) + self:buttons():select(self:button(name)) + return self +end + +-- quit dialog +function dialog:quit() + local parent = self:parent() + if parent then + self:action_on(action.ac_on_exit) + parent:remove(self) + end +end + +-- on event +function dialog:event_on(e) + if e.type == event.ev_keyboard and e.key_name == "Esc" then + self:quit() + return true + end + return window.event_on(self, e) +end + +-- return module +return dialog diff --git a/src/ltui/event.lua b/src/ltui/event.lua new file mode 100644 index 0000000..6628024 --- /dev/null +++ b/src/ltui/event.lua @@ -0,0 +1,90 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file event.lua +-- + +--[[ Console User Interface (cui) ]----------------------------------------- +Author: Tiago Dionizio (tiago.dionizio AT gmail.com) +$Id: event.lua 18 2007-06-21 20:43:52Z tngd $ +--------------------------------------------------------------------------]] + +-- load modules +local log = require("ltui/base/log") +local object = require("ltui/object") + +-- define module +local event = event or object { _init = {"type", "command", "extra"} } + +-- register event types +function event:register(tag, ...) + local base = self[tag] or 0 + local enums = {...} + local n = #enums + for i = 1, n do + self[enums[i]] = i + base + end + self[tag] = base + n +end + +-- is key? +function event:is_key(key_name) + return self.type == event.ev_keyboard and self.key_name == key_name +end + +-- is command event: cm_xxx? +function event:is_command(command) + return self.type == event.ev_command and self.command == command +end + +-- dump event +function event:dump() + if self.type == event.ev_keyboard then + log:print("event(key): %s %s ..", self.key_name, self.key_code) + elseif self.type == event.ev_command then + log:print("event(cmd): %s ..", self.command) + else + log:print("event(%s): ..", self.type) + end +end + +-- register event types, event.ev_keyboard = 1, event.ev_mouse = 2, ... , event.ev_idle = 5, event.ev_max = 5 +event:register("ev_max", "ev_keyboard", "ev_mouse", "ev_command", "ev_text", "ev_idle") + +-- register command event types (ev_command) +event:register("cm_max", "cm_quit", "cm_exit", "cm_enter") + +-- define keyboard event +-- +-- keyname = key name +-- keycode = key code +-- keymeta = ALT key was pressed +-- +event.keyboard = object {_init = { "key_code", "key_name", "key_meta" }, type = event.ev_keyboard} + +-- define command event +event.command = object {_init = { "command", "extra" }, type = event.ev_command} + +-- define idle event +event.idle = object {_init = {}, type = event.ev_idle} + +-- return module +return event diff --git a/src/ltui/inputdialog.lua b/src/ltui/inputdialog.lua new file mode 100644 index 0000000..7e246ef --- /dev/null +++ b/src/ltui/inputdialog.lua @@ -0,0 +1,77 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file inputdialog.lua +-- + +-- load modules +local log = require("ui/log") +local rect = require("ui/rect") +local view = require("ui/view") +local event = require("ui/event") +local action = require("ui/action") +local curses = require("ui/curses") +local window = require("ui/window") +local textedit = require("ui/textedit") +local textdialog = require("ui/textdialog") + +-- define module +local inputdialog = inputdialog or textdialog() + +-- init dialog +function inputdialog:init(name, bounds, title) + + -- init window + textdialog.init(self, name, bounds, title) + + -- insert textedit + self:panel():insert(self:textedit()) + + -- resize text + self:text():bounds().ey = 1 + self:text():invalidate(true) + self:text():option_set("selectable", false) + self:text():option_set("progress", false) + + -- text changed + self:text():action_set(action.ac_on_text_changed, function (v) + if v:text() then + local lines = #self:text():splitext(v:text()) + 1 + if lines > 0 and lines < self:height() then + self:text():bounds().ey = lines + self:textedit():bounds().sy = lines + self:text():invalidate(true) + self:textedit():invalidate(true) + end + end + end) +end + +-- get textedit +function inputdialog:textedit() + if not self._TEXTEDIT then + self._TEXTEDIT = textedit:new("inputdialog.textedit", rect{0, 1, self:panel():width(), self:panel():height() - 1}) + end + return self._TEXTEDIT +end + +-- return module +return inputdialog diff --git a/src/ltui/label.lua b/src/ltui/label.lua new file mode 100644 index 0000000..93f9c5a --- /dev/null +++ b/src/ltui/label.lua @@ -0,0 +1,143 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file label.lua +-- + +-- load modules +local log = require("ltui/base/log") +local view = require("ltui/view") +local event = require("ltui/event") +local action = require("ltui/action") +local curses = require("ltui/curses") + +-- define module +local label = label or view() + +-- init label +function label:init(name, bounds, text) + + -- init view + view.init(self, name, bounds) + + -- init text + self:text_set(text) + + -- init text attribute + self:textattr_set("black") +end + +-- draw view +function label:draw(transparent) + + -- draw background + view.draw(self, transparent) + + -- get the text attribute value + local textattr = self:textattr_val() + + -- draw text string + local str = self:text() + if str and #str > 0 and textattr then + self:canvas():attr(textattr):move(0, 0):putstrs(self:splitext(str)) + end +end + +-- get text +function label:text() + return self._TEXT +end + +-- set text +function label:text_set(text) + + -- set text + text = text or "" + local changed = self._TEXT ~= text + self._TEXT = text + + -- do action + if changed then + self:action_on(action.ac_on_text_changed) + end + self:invalidate() + return self +end + +-- get text attribute +function label:textattr() + return self:attr("textattr") +end + +-- set text attribute, .e.g textattr_set("yellow onblue bold") +function label:textattr_set(attr) + return self:attr_set("textattr", attr) +end + +-- get the current text attribute value +function label:textattr_val() + + -- get text attribute + local textattr = self:textattr() + if not textattr then + return + end + + -- no text background? use view's background + if self:background() and not textattr:find("on") then + textattr = textattr .. " on" .. self:background() + end + + -- attempt to get the attribute value from the cache first + self._TEXTATTR = self._TEXTATTR or {} + local value = self._TEXTATTR[textattr] + if value then + return value + end + + -- update the cache + value = curses.calc_attr(textattr:split("%s+")) + self._TEXTATTR[textattr] = value + return value +end + +-- split text by width +function label:splitext(text, width) + + -- get width + width = width or self:width() + + -- split text first + local result = {} + local lines = text:split('\n', true) + for idx = 1, #lines do + local line = lines[idx] + while #line > width do + table.insert(result, line:sub(1, width)) + line = line:sub(width + 1) + end + table.insert(result, line) + end + return result +end + +-- return module +return label diff --git a/src/ltui/mconfdialog.lua b/src/ltui/mconfdialog.lua new file mode 100644 index 0000000..1d83fce --- /dev/null +++ b/src/ltui/mconfdialog.lua @@ -0,0 +1,313 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file mconfdialog.lua +-- + +-- load modules +local log = require("ui/log") +local rect = require("ui/rect") +local event = require("ui/event") +local action = require("ui/action") +local curses = require("ui/curses") +local window = require("ui/window") +local menuconf = require("ui/menuconf") +local boxdialog = require("ui/boxdialog") +local textdialog = require("ui/textdialog") +local inputdialog = require("ui/inputdialog") +local choicedialog = require("ui/choicedialog") + +-- define module +local mconfdialog = mconfdialog or boxdialog() + +-- init dialog +function mconfdialog:init(name, bounds, title) + + -- init window + boxdialog.init(self, name, bounds, title) + + -- init text + self:text():text_set([[Arrow keys navigate the menu. selects submenus ---> (or empty submenus ----). +Pressing includes, excludes. Enter or to go back, for Help, for Search. Legend: [*] built-in [ ] excluded +]]) + + -- init buttons + self:button_add("select", "< Select >", function (v, e) self:menuconf():event_on(event.command {"cm_enter"}) end) + self:button_add("back", "< Back >", function (v, e) self:menuconf():event_on(event.command {"cm_back"}) end) + self:button_add("exit", "< Exit >", function (v, e) self:quit() end) + self:button_add("help", "< Help >", function (v, e) self:show_help() end) + self:button_add("save", "< Save >", function (v, e) self:action_on(action.ac_on_save) end) + self:buttons():select(self:button("select")) + + -- insert menu config + self:box():panel():insert(self:menuconf()) + + -- disable to select to box (disable Tab switch and only response to buttons) + self:box():option_set("selectable", false) + + -- on selected + self:menuconf():action_set(action.ac_on_selected, function (v, config) + + -- show input dialog + if config.kind == "string" or config.kind == "number" then + local dialog_input = self:inputdialog() + dialog_input:extra_set("config", config) + dialog_input:title():text_set(config:prompt()) + dialog_input:textedit():text_set(tostring(config.value)) + dialog_input:panel():select(dialog_input:textedit()) + if config.kind == "string" then + dialog_input:text():text_set("Please enter a string value. Use the key to move from the input fields to buttons below it.") + else + dialog_input:text():text_set("Please enter a decimal value. Fractions will not be accepted. Use the key to move from the input field to the buttons below it.") + end + self:insert(dialog_input, {centerx = true, centery = true}) + return true + + -- show choice dialog + elseif config.kind == "choice" and config.values and #config.values > 0 then + local dialog_choice = self:choicedialog() + dialog_choice:title():text_set(config:prompt()) + dialog_choice:choicebox():load(config.values, config.value) + dialog_choice:choicebox():action_set(action.ac_on_selected, function (v, index, value) + config.value = index + end) + self:insert(dialog_choice, {centerx = true, centery = true}) + return true + end + end) +end + +-- load configs +function mconfdialog:load(configs) + self._CONFIGS = configs + return self:menuconf():load(configs) +end + +-- get configs +function mconfdialog:configs() + return self._CONFIGS +end + +-- get menu config +function mconfdialog:menuconf() + if not self._MENUCONF then + local bounds = self:box():panel():bounds() + self._MENUCONF = menuconf:new("mconfdialog.menuconf", rect:new(0, 0, bounds:width(), bounds:height())) + self._MENUCONF:state_set("focused", true) -- we can select and highlight selected item + end + return self._MENUCONF +end + +-- get help dialog +function mconfdialog:helpdialog() + if not self._HELPDIALOG then + local helpdialog = textdialog:new("mconfdialog.help", self:bounds(), "help") + helpdialog:button_add("exit", "< Exit >", function (v) helpdialog:quit() end) + self._HELPDIALOG = helpdialog + end + return self._HELPDIALOG +end + +-- get result dialog +function mconfdialog:resultdialog() + if not self._RESULTDIALOG then + local resultdialog = textdialog:new("mconfdialog.result", self:bounds(), "result") + resultdialog:button_add("exit", "< Exit >", function (v) resultdialog:quit() end) + self._RESULTDIALOG = resultdialog + end + return self._RESULTDIALOG +end + +-- get input dialog +function mconfdialog:inputdialog() + if not self._INPUTDIALOG then + local dialog_input = inputdialog:new("mconfdialog.input", rect {0, 0, math.min(80, self:width() - 8), math.min(8, self:height())}, "input dialog") + dialog_input:background_set(self:frame():background()) + dialog_input:frame():background_set("cyan") + dialog_input:textedit():option_set("multiline", false) + dialog_input:button_add("ok", "< Ok >", function (v) + local config = dialog_input:extra("config") + if config.kind == "string" then + config.value = dialog_input:textedit():text() + elseif config.kind == "number" then + local value = tonumber(dialog_input:textedit():text()) + if value ~= nil then + config.value = value + end + end + dialog_input:quit() + end) + dialog_input:button_add("cancel", "< Cancel >", function (v) + dialog_input:quit() + end) + dialog_input:button_select("ok") + self._INPUTDIALOG = dialog_input + end + return self._INPUTDIALOG +end + +-- get choice dialog +function mconfdialog:choicedialog() + if not self._CHOICEDIALOG then + local dialog_choice = choicedialog:new("mconfdialog.choice", rect {0, 0, math.min(80, self:width() - 8), math.min(20, self:height())}, "input dialog") + dialog_choice:background_set(self:frame():background()) + dialog_choice:frame():background_set("cyan") + dialog_choice:box():frame():background_set("cyan") + self._CHOICEDIALOG = dialog_choice + end + return self._CHOICEDIALOG +end + +-- get search dialog +function mconfdialog:searchdialog() + if not self._SEARCHDIALOG then + local dialog_search = inputdialog:new("mconfdialog.input", rect {0, 0, math.min(80, self:width() - 8), math.min(8, self:height())}, "Search Configuration Parameter") + dialog_search:background_set(self:frame():background()) + dialog_search:frame():background_set("cyan") + dialog_search:textedit():option_set("multiline", false) + dialog_search:text():text_set("Enter (sub)string or lua pattern string to search for configuration") + dialog_search:button_add("ok", "< Ok >", function (v) + local configs = self:search(self:configs(), dialog_search:textedit():text()) + local results = "Search('" .. dialog_search:textedit():text() .. "') results:" + for _, config in ipairs(configs) do + results = results .. "\n" .. config:prompt() + if config.kind then + results = results .. "\nkind: " .. config.kind + end + if config.default then + results = results .. "\ndefault: " .. config.default + end + if config.path then + results = results .. "\npath: " .. config.path + end + if config.sourceinfo then + results = results .. "\nposition: " .. (config.sourceinfo.file or "") .. ":" .. (config.sourceinfo.line or "-1") + end + results = results .. "\n" + end + self:show_result(results) + dialog_search:quit() + end) + dialog_search:button_add("cancel", "< Cancel >", function (v) + dialog_search:quit() + end) + dialog_search:button_select("ok") + self._SEARCHDIALOG = dialog_search + end + return self._SEARCHDIALOG +end + +-- search configs via the given text +function mconfdialog:search(configs, text) + local results = {} + for _, config in ipairs(configs) do + local prompt = config:prompt() + if prompt and prompt:find(text) then + table.insert(results, config) + end + if config.kind == "menu" then + table.join2(results, self:search(config.configs, text)) + end + end + return results +end + +-- show help dialog +function mconfdialog:show_help() + if self:parent() then + + -- get the current config item + local item = self:menuconf():current() + + -- get the current config + local config = item:extra("config") + + -- set help title + self:helpdialog():title():text_set(config:prompt()) + + -- set help text + local text = config.description + if type(text) == "table" then + text = table.concat(text, '\n') + end + if config.kind then + text = text .. "\ntype: " .. config.kind + end + if config.default then + text = text .. "\ndefault: " .. tostring(config.default) + end + if config.kind == "choice" then + text = text .. "\nvalues: " + for _, value in ipairs(config.values) do + text = text .. "\n - " .. value + end + end + if config.path then + text = text .. "\npath: " .. config.path + end + if config.sourceinfo then + text = text .. "\nposition: " .. (config.sourceinfo.file or "") .. ":" .. (config.sourceinfo.line or "-1") + end + self:helpdialog():text():text_set(text) + + -- show help + self:parent():insert(self:helpdialog()) + end +end + +-- show search dialog +function mconfdialog:show_search() + local dialog_search = self:searchdialog() + dialog_search:panel():select(dialog_search:textedit()) + self:insert(dialog_search, {centerx = true, centery = true}) +end + +-- show result dialog +function mconfdialog:show_result(text) + local dialog_result = self:resultdialog() + dialog_result:text():text_set(text) + if not self:view("mconfdialog.result") then + self:insert(dialog_result, {centerx = true, centery = true}) + else + self:select(dialog_result) + end +end + +-- on event +function mconfdialog:event_on(e) + + -- select config + if e.type == event.ev_keyboard then + if e.key_name == "Down" or e.key_name == "Up" or e.key_name == " " or e.key_name == "Esc" or e.key_name:lower() == "y" or e.key_name:lower() == "n" then + return self:menuconf():event_on(e) + elseif e.key_name == "?" then + self:show_help() + return true + elseif e.key_name == "/" then + self:show_search() + return true + end + end + return boxdialog.event_on(self, e) +end + +-- return module +return mconfdialog diff --git a/src/ltui/menubar.lua b/src/ltui/menubar.lua new file mode 100644 index 0000000..8fe2c80 --- /dev/null +++ b/src/ltui/menubar.lua @@ -0,0 +1,56 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file menubar.lua +-- + +-- load modules +local log = require("ltui/base/log") +local rect = require("ltui/rect") +local label = require("ltui/label") +local panel = require("ltui/panel") +local curses = require("ltui/curses") + +-- define module +local menubar = menubar or panel() + +-- init menubar +function menubar:init(name, bounds) + + -- init panel + panel.init(self, name, bounds) + + -- init title + self._TITLE = label:new("menubar.title", rect{0, 0, self:width(), self:height()}, "Menu Bar") + self:insert(self:title()) + self:title():textattr_set("red") + + -- init background + self:background_set("white") +end + +-- get title +function menubar:title() + return self._TITLE +end + +-- return module +return menubar diff --git a/src/ltui/menuconf.lua b/src/ltui/menuconf.lua new file mode 100644 index 0000000..cf68a14 --- /dev/null +++ b/src/ltui/menuconf.lua @@ -0,0 +1,286 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file menuconf.lua +-- + +-- load modules +local log = require("ui/log") +local view = require("ui/view") +local rect = require("ui/rect") +local panel = require("ui/panel") +local event = require("ui/event") +local action = require("ui/action") +local curses = require("ui/curses") +local button = require("ui/button") +local object = require("ui/object") + +-- define module +local menuconf = menuconf or panel() + +-- init menuconf +function menuconf:init(name, bounds) + + -- init panel + panel.init(self, name, bounds) + + -- init configs + self._CONFIGS = {} +end + +-- on event +function menuconf:event_on(e) + + -- select config + local back = false + if e.type == event.ev_keyboard then + if e.key_name == "Down" then + return self:select_next() + elseif e.key_name == "Up" then + return self:select_prev() + elseif e.key_name == "Enter" or e.key_name == " " then + self:_do_select() + return true + elseif e.key_name:lower() == "y" then + self:_do_include(true) + return true + elseif e.key_name:lower() == "n" then + self:_do_include(false) + return true + elseif e.key_name == "Esc" then + back = true + end + elseif e.type == event.ev_command then + if e.command == "cm_enter" then + self:_do_select() + return true + elseif e.command == "cm_back" then + back = true + end + end + + -- back? + if back then + -- load the previous menu configs + local configs_prev = self._CONFIGS._PREV + if configs_prev then + self._CONFIGS._PREV = configs_prev._PREV + self:load(configs_prev) + return true + end + end +end + +-- load configs +function menuconf:load(configs) + + -- clear the views first + self:clear() + + -- detach the previous config and view + local configs_prev = self._CONFIGS._PREV + if configs_prev then + for _, config in ipairs(configs_prev) do + config._view = nil + end + end + + -- insert configs + self._CONFIGS = configs + for _, config in ipairs(configs) do + if self:count() < self:height() then + self:_do_insert(config) + end + end + + -- select the first item + self:select(self:first()) + + -- invalidate + self:invalidate() +end + +-- do insert a config item +function menuconf:_do_insert(config) + + -- init a config item view + local item = button:new("menuconf.config." .. self:count(), rect:new(0, self:count(), self:width(), 1), tostring(config)) + + -- attach this config + item:extra_set("config", config) + + -- attach this view + config._view = item + + -- insert this config item + self:insert(item) +end + +-- do select the current config +function menuconf:_do_select() + + -- get the current item + local item = self:current() + + -- get the current config + local config = item:extra("config") + + -- clear new state + config.new = false + + -- do action: on selected + if self:action_on(action.ac_on_selected, config) then + return + end + + -- select the boolean config + if config.kind == "boolean" then + config.value = not config.value + -- show sub-menu configs + elseif config.kind == "menu" and config.configs and #config.configs > 0 then + local configs_prev = self._CONFIGS + self:load(config.configs) + self._CONFIGS._PREV = configs_prev + end +end + +-- do include +function menuconf:_do_include(enabled) + + -- get the current item + local item = self:current() + + -- get the current config + local config = item:extra("config") + + -- clear new state + config.new = false + + -- select the boolean config + if config.kind == "boolean" then + config.value = enabled + end +end + +-- init config object +-- +-- kind +-- - {kind = "number/boolean/string/choice/menu"} +-- +-- description +-- - {description = "config item description"} +-- - {description = {"config item description", "line2", "line3", "more description ..."}} +-- +-- boolean config +-- - {name = "...", kind = "boolean", value = true, default = true, description = "boolean config item", new = true/false} +-- +-- number config +-- - {name = "...", kind = "number", value = 10, default = 0, description = "number config item", new = true/false} +-- +-- string config +-- - {name = "...", kind = "string", value = "xmake", default = "", description = "string config item", new = true/false} +-- +-- choice config (value is index) +-- - {name = "...", kind = "choice", value = 1, default = 1, description = "choice config item", values = {2, 2, 3, 4, 5}} +-- +-- menu config +-- - {name = "...", kind = "menu", description = "menu config item", configs = {...}} +-- +local config = config or object{new = true, + __index = function (tbl, key) + if key == "value" then + local val = rawget(tbl, "_value") + if val == nil then + val = rawget(tbl, "default") + end + return val + end + return rawget(tbl, key) + end, + __newindex = function (tbl, key, val) + if key == "value" then + key = "_value" + end + rawset(tbl, key, val) + if key == "_value" then + local v = rawget(tbl, "_view") -- update the config item text in view + if v then + v:text_set(tostring(tbl)) + end + end + end} + +-- the prompt info +function config:prompt() + + -- get text (first line in description) + local text = self.description or "" + if type(text) == "table" then + text = text[1] or "" + end + return text +end + +-- to string +function config:__tostring() + + -- get text (first line in description) + local text = self:prompt() + + -- get value + local value = self.value + + -- update text + if self.kind == "boolean" or (not self.kind and type(value) == "boolean") then -- boolean config? + text = (value and "[*] " or "[ ] ") .. text + elseif self.kind == "number" or (not self.kind and type(value) == "number") then -- number config? + text = " " .. text .. " (" .. tostring(value or 0) .. ")" + elseif self.kind == "string" or (not self.kind and type(value) == "string") then -- string config? + text = " " .. text .. " (" .. tostring(value or "") .. ")" + elseif self.kind == "choice" then -- choice config? + if self.values and #self.values > 0 then + text = " " .. text .. " (" .. tostring(self.values[value or 1]) .. ")" .. " --->" + else + text = " " .. text .. " () ----" + end + elseif self.kind == "menu" then -- menu config? + text = " " .. text .. (self.configs and #self.configs > 0 and " --->" or " ----") + end + + -- new config? + if self.new and self.kind ~= "choice" and self.kind ~= "menu" then + text = text .. " (NEW)" + end + + -- ok + return text +end + +-- save config objects +menuconf.config = menuconf.config or config +menuconf.menu = menuconf.menu or config { kind = "menu", configs = {} } +menuconf.number = menuconf.number or config { kind = "number", default = 0 } +menuconf.string = menuconf.string or config { kind = "string", default = "" } +menuconf.choice = menuconf.choice or config { kind = "choice", default = 1, values = {} } +menuconf.boolean = menuconf.boolean or config { kind = "boolean", default = false } + +-- return module +return menuconf diff --git a/src/ltui/object.lua b/src/ltui/object.lua new file mode 100644 index 0000000..6720cd8 --- /dev/null +++ b/src/ltui/object.lua @@ -0,0 +1,99 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file object.lua +-- + +-- define module: object +local object = object or {} + +-- taken from 'std' library: http://luaforge.net/projects/stdlib/ +-- and http://lua-cui.sourceforge.net/ +-- +-- local point = object { _init = {"x", "y"} } +-- +-- local p1 = point {1, 2} +-- > p1 {x = 1, y = 2} +-- + +-- permute some indices of a table +local function permute (p, t) + local u = {} + for i, v in pairs (t) do + if p[i] ~= nil then + u[p[i]] = v + else + u[i] = v + end + end + return u +end + +-- make a shallow copy of a table, including any +local function clone (t) + local u = setmetatable ({}, getmetatable (t)) + for i, v in pairs (t) do + u[i] = v + end + return u +end + +-- merge two tables +-- +-- If there are duplicate fields, u's will be used. The metatable of +-- the returned table is that of t +-- +local function merge (t, u) + local r = clone (t) + for i, v in pairs (u) do + r[i] = v + end + return r +end + +-- root object +-- +-- List of fields to be initialised by the +-- constructor: assuming the default _clone, the +-- numbered values in an object constructor are +-- assigned to the fields given in _init +-- +local object = { _init = {} } +setmetatable (object, object) + +-- object constructor +-- +-- @param initial values for fields in +-- +-- @return new object +-- +function object:_clone (values) + local object = merge(self, permute(self._init, values or {})) + return setmetatable (object, object) +end + +-- local x = object {} +function object.__call (...) + return (...)._clone (...) +end + +-- return module: object +return object diff --git a/src/ltui/panel.lua b/src/ltui/panel.lua new file mode 100644 index 0000000..4aa962e --- /dev/null +++ b/src/ltui/panel.lua @@ -0,0 +1,394 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file panel.lua +-- + +-- load modules +local log = require("ltui/base/log") +local view = require("ltui/view") +local rect = require("ltui/rect") +local event = require("ltui/event") +local point = require("ltui/point") +local curses = require("ltui/curses") +local dlist = require("ltui/base/dlist") + +-- define module +local panel = panel or view() + +-- init panel +function panel:init(name, bounds) + + -- init view + view.init(self, name, bounds) + + -- mark as panel + self:type_set("panel") + + -- mark as selectable + self:option_set("selectable", true) + + -- init child views + self._VIEWS = dlist() + + -- init views cache + self._VIEWS_CACHE = {} +end + +-- get all child views +function panel:views() + return self._VIEWS:items() +end + +-- get views count +function panel:count() + return self._VIEWS:size() +end + +-- is empty? +function panel:empty() + return self._VIEWS:empty() +end + +-- get the first view +function panel:first() + return self._VIEWS:first() +end + +-- get the next view +function panel:next(v) + return self._VIEWS:next(v) +end + +-- get the previous view +function panel:prev(v) + return self._VIEWS:prev(v) +end + +-- get the current selected child view +function panel:current() + return self._CURRENT +end + +-- get view from the given name +function panel:view(name) + return self._VIEWS_CACHE[name] +end + +-- insert view +function panel:insert(v, opt) + + -- check + assert(not v:parent() or v:parent() == self) + assert(not self:view(v:name()), v:name() .. " has been in this panel!") + + -- this view has been inserted into this panel? remove it first + if v:parent() == self then + self:remove(v) + end + + -- center this view if centerx or centery are set + local bounds = v:bounds() + local center = false + local org = point {bounds.sx, bounds.sy} + if opt and opt.centerx then + org.x = math.floor((self:width() - v:width()) / 2) + center = true + end + if opt and opt.centery then + org.y = math.floor((self:height() - v:height()) / 2) + center = true + end + if center then + bounds:move(org.x - bounds.sx, org.y - bounds.sy) + v:invalidate(true) + end + + -- insert this view + self._VIEWS:push(v) + + -- cache this view + self._VIEWS_CACHE[v:name()] = v + + -- set it's parent view + v:parent_set(self) + + -- select this view + if v:option("selectable") then + self:select(v) + end + + -- invalidate it + self:invalidate() +end + +-- remove view +function panel:remove(v) + + -- check + assert(v:parent() == self) + + -- remove view + self._VIEWS:remove(v) + self._VIEWS_CACHE[v:name()] = nil + + -- clear parent + v:parent_set(nil) + + -- select next view + if self:current() == v then + self:select_next(nil, true) + end + + -- invalidate it + self:invalidate() +end + +-- clear views +function panel:clear() + + -- clear parents + for v in self:views() do + v:parent_set(nil) + end + + -- clear views and cache + self._VIEWS:clear() + self._VIEWS_CACHE = {} + + -- reset the current view + self._CURRENT = nil + + -- invalidate + self:invalidate() +end + +-- select the child view +function panel:select(v) + + -- check + assert(v == nil or (v:parent() == self and v:option("selectable"))) + + -- get the current selected view + local current = self:current() + if v == current then + return + end + + -- undo the previous selected view + if current then + + -- undo the current view first + if self:state("focused") then + current:state_set("focused", false) + end + current:state_set("selected", false) + end + + -- update the current selected view + self._CURRENT = v + + -- update the new selected view + if v then + + -- select and focus this view + v:state_set("selected", true) + if self:state("focused") then + v:state_set("focused", true) + end + end + + -- ok + return v +end + +-- select the next view +function panel:select_next(start, reset) + + -- is empty? + if self:empty() then + return + end + + -- reset? + if reset then + self._CURRENT = nil + end + + -- get current view + local current = start or self:current() + + -- select the next view + local next = self:next(current) + while next ~= current do + if next and next:option("selectable") and next:state("visible") then + return self:select(next) + end + next = self:next(next) + end +end + +-- select the previous view +function panel:select_prev(start) + + -- is empty? + if self:empty() then + return + end + + -- reset? + if reset then + self._CURRENT = nil + end + + -- get current view + local current = start or self:current() + + -- select the previous view + local prev = self:prev(current) + while prev ~= current do + if prev and prev:option("selectable") and prev:state("visible") then + return self:select(prev) + end + prev = self:prev(prev) + end +end + +-- on event +function panel:event_on(e) + + -- select view? + if e.type == event.ev_keyboard then + if e.key_name == "Right" then + return self:select_next() + elseif e.key_name == "Left" then + return self:select_prev() + end + end +end + +-- set state +function panel:state_set(name, enable) + view.state_set(self, name, enable) + if name == "focused" and self:current() then + self:current():state_set(name, enable) + end + return self +end + +-- draw panel +function panel:draw(transparent) + + -- redraw panel? + local redraw = self:state("redraw") + + -- draw panel background first + if redraw then + view.draw(self, transparent) + end + + -- draw all child views + for v in self:views() do + if redraw then + v:state_set("redraw", true) + end + if v:state("visible") and (v:state("redraw") or v:type() == "panel") then + v:draw(transparent) + end + end +end + +-- resize panel +function panel:resize() + + -- resize panel? + local resize = self:state("resize") + if resize then + view.resize(self) + end + + -- resize all child views + for v in self:views() do + if resize then + v:state_set("resize", true) + end + if v:state("visible") and (v:state("resize") or v:type() == "panel") then + v:resize() + end + end +end + +-- refresh panel +function panel:refresh() + + -- need not refresh? do not refresh it + if not self:state("refresh") or not self:state("visible") then + return + end + + -- refresh all child views + for v in self:views() do + if v:state("refresh") then + v:refresh() + v:state_set("refresh", false) + end + end + + -- refresh it + view.refresh(self) + + -- clear mark + self:state_set("refresh", false) +end + +-- dump all views +function panel:dump() + log:print("%s", self:_tostring(1)) +end + +-- tostring(panel, level) +function panel:_tostring(level) + local str = "" + if self.views then + str = str .. string.format("<%s %s>", self:name(), tostring(self:bounds())) + if not self:empty() then + str = str .. "\n" + end + for v in self:views() do + for l = 1, level do + str = str .. " " + end + str = str .. panel._tostring(v, level + 1) .. "\n" + end + else + str = tostring(self) + end + return str +end + +-- tostring(panel) +function panel:__tostring() + return string.format("", self:name(), tostring(self:bounds())) +end + + +-- return module +return panel diff --git a/src/ltui/point.lua b/src/ltui/point.lua new file mode 100644 index 0000000..7d72349 --- /dev/null +++ b/src/ltui/point.lua @@ -0,0 +1,104 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file point.lua +-- + +--[[ Console User Interface (cui) ]----------------------------------------- +Author: Tiago Dionizio (tiago.dionizio AT gmail.com) +$Id: point.lua 18 2007-06-21 20:43:52Z tngd $ +--------------------------------------------------------------------------]] + +-- load modules +local object = require("ltui/object") + +-- define module +local point = point or object { _init = {"x", "y"} } + +-- add delta x and y +function point:addxy(dx, dy) + self.x = self.x + dx + self.y = self.y + dy + return self +end + +-- add point +function point:add(p) + return self:addxy(p.x, p.y) +end + +-- sub delta x and y +function point:subxy(dx, dy) + return self:addxy(-dx, -dy) +end + +-- sub point +function point:sub(p) + return self:addxy(-p.x, -p.y) +end + +-- p1 + p2 +function point:__add(p) + local np = self() + np.x = np.x + p.x + np.y = np.y + p.y + return np +end + +-- p1 - p2 +function point:__sub(p) + local np = self() + np.x = np.x - p.x + np.y = np.y - p.y + return np +end + +-- -p +function point:__unm() + local p = self() + p.x = -p.x + p.y = -p.y + return p +end + +-- p1 == p2? +function point:__eq(p) + return self.x == p.x and self.y == p.y +end + +-- tostring(p) +function point:__tostring() + return '(' .. self.x .. ', ' .. self.y .. ')' +end + +-- p1 .. p2 +function point.__concat(op1, op2) + if type(op1) == 'string' then + return op1 .. op2:__tostring() + elseif type(op2) == 'string' then + return op1:__tostring() .. op2 + else + return op1:__tostring() .. op2:__tostring() + end +end + +-- return module +return point diff --git a/src/ltui/program.lua b/src/ltui/program.lua new file mode 100644 index 0000000..07600d2 --- /dev/null +++ b/src/ltui/program.lua @@ -0,0 +1,456 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file program.lua +-- + +--[[ Console User Interface (cui) ]----------------------------------------- +Author: Tiago Dionizio (tiago.dionizio AT gmail.com) +$Id: program.lua 18 2007-06-21 20:43:52Z tngd $ +--------------------------------------------------------------------------]] + +-- load modules +local log = require("ltui/base/log") +local rect = require("ltui/rect") +local point = require("ltui/point") +local panel = require("ltui/panel") +local event = require("ltui/event") +local curses = require("ltui/curses") + +-- define module +local program = program or panel() + +-- init program +function program:init(name, argv) + + -- init main window + local main_window = self:main_window() + + -- disable echo + curses.echo(false) + + -- disable input cache + curses.cbreak(true) + + -- disable newline + curses.nl(false) + + -- to filter characters being output to the screen + -- this will filter all characters where a chtype or chstr is used + curses.map_output(true) + + -- on WIN32 ALT keys need to be mapped, so to make sure you get the wanted keys, + -- only makes sense when using keypad(true) and echo(false) + curses.map_keyboard(true) + + -- init colors + if (curses.has_colors()) then + curses.start_color() + end + + -- disable main window cursor + main_window:leaveok(false) + + -- enable special key map + main_window:keypad(true) + + -- non-block for getch() + main_window:nodelay(true) + + -- get 8-bits character for getch() + main_window:meta(true) + + -- save the current arguments + self._ARGV = argv + + -- init panel + panel.init(self, name, rect {0, 0, curses.columns(), curses.lines()}) + + -- init state + self:state_set("focused", true) + self:state_set("selected", true) +end + +-- exit program +function program:exit() + + -- exit panel + panel.exit(self) + + -- (attempt to) make sure the screen will be cleared + -- if not restored by the curses driver + self:main_window():clear() + self:main_window():noutrefresh() + curses.doupdate() + + -- exit curses + assert(not curses.isdone()) + curses.done() +end + +-- get the main window +function program:main_window() + + -- init main window if not exists + local main_window = self._MAIN_WINDOW + if not main_window then + + -- init main window + main_window = curses.init() + assert(main_window, "cannot init main window!") + + -- save main window + self._MAIN_WINDOW = main_window + end + return main_window +end + +-- get the command arguments +function program:argv() + return self._ARGV +end + +-- get the current event +function program:event() + + -- get event from the event queue first + local event_queue = self._EVENT_QUEUE + if event_queue then + local e = event_queue[1] + if e then + table.remove(event_queue, 1) + return e + end + end + + -- get input key + local key_code, key_name, key_meta = self:_input_key() + if key_code then + return event.keyboard{key_code, key_name, key_meta} + end +end + +-- on event +function program:event_on(e) + + -- get the top focused view + local focused_view = self + while focused_view:type() == "panel" and focused_view:current() do + focused_view = focused_view:current() + end + + -- do event for focused views + while focused_view and focused_view ~= self do + local parent = focused_view:parent() + if focused_view:event_on(e) then + return true + end + focused_view = parent + end + + -- quit program? + if e.type == event.ev_keyboard and e.key_name == "CtrlC" then + self:send("cm_exit") + return true + elseif event.is_command(e, "cm_exit") then + self:quit() + return true + end +end + +-- put an event to view +function program:event_put(e) + + -- init event queue + self._EVENT_QUEUE = self._EVENT_QUEUE or {} + + -- put event to queue + table.insert(self._EVENT_QUEUE, e) +end + +-- send command +function program:send(command, extra) + self:event_put(event.command {command, extra}) +end + +-- quit program +function program:quit() + self:send("cm_quit") +end + +-- run program loop +function program:loop(argv) + + -- do message loop + local e = nil + local sleep = true + while true do + + -- get the current event + e = self:event() + + -- do event + if e then + event.dump(e) + self:event_on(e) + sleep = false + else + -- do idle event + self:event_on(event.idle()) + sleep = true + end + + -- quit? + if e and event.is_command(e, "cm_quit") then + break + end + + -- resize views + self:resize() + + -- draw views + self:draw() + + -- refresh views + self:refresh() + + -- wait some time, 50ms + if sleep then + curses.napms(50) + end + end +end + +-- refresh program +function program:refresh() + + -- need not refresh? do not refresh it + if not self:state("refresh") then + return + end + + -- refresh views + panel.refresh(self) + + -- trace + log:print("%s: refresh ..", self) + + -- get main window + local main_window = curses.main_window() + + -- refresh main window + self:window():copy(main_window, 0, 0, 0, 0, self:height() - 1, self:width() - 1) + + -- refresh cursor + self:_refresh_cursor() + + -- mark as refresh + main_window:noutrefresh() + + -- do update + curses.doupdate() +end + +-- get key map +function program:_key_map() + if not self._KEYMAP then + self._KEYMAP = + { + [ 1] = "CtrlA", [ 2] = "CtrlB", [ 3] = "CtrlC", + [ 4] = "CtrlD", [ 5] = "CtrlE", [ 6] = "CtrlF", + [ 7] = "CtrlG", [ 8] = "CtrlH", [ 9] = "CtrlI", + [10] = "CtrlJ", [11] = "CtrlK", [12] = "CtrlL", + [13] = "CtrlM", [14] = "CtrlN", [15] = "CtrlO", + [16] = "CtrlP", [17] = "CtrlQ", [18] = "CtrlR", + [19] = "CtrlS", [20] = "CtrlT", [21] = "CtrlU", + [22] = "CtrlV", [23] = "CtrlW", [24] = "CtrlX", + [25] = "CtrlY", [26] = "CtrlZ", + + [ 8] = "Backspace", + [ 9] = "Tab", + [ 10] = "Enter", + [ 13] = "Enter", + [ 27] = "Esc", + [ 31] = "CtrlBackspace", + [127] = "Backspace", + + [curses.KEY_DOWN ] = "Down", + [curses.KEY_UP ] = "Up", + [curses.KEY_LEFT ] = "Left", + [curses.KEY_RIGHT ] = "Right", + [curses.KEY_HOME ] = "Home", + [curses.KEY_END ] = "End", + [curses.KEY_NPAGE ] = "PageDown", + [curses.KEY_PPAGE ] = "PageUp", + [curses.KEY_IC ] = "Insert", + [curses.KEY_DC ] = "Delete", + [curses.KEY_BACKSPACE ] = "Backspace", + [curses.KEY_F1 ] = "F1", + [curses.KEY_F2 ] = "F2", + [curses.KEY_F3 ] = "F3", + [curses.KEY_F4 ] = "F4", + [curses.KEY_F5 ] = "F5", + [curses.KEY_F6 ] = "F6", + [curses.KEY_F7 ] = "F7", + [curses.KEY_F8 ] = "F8", + [curses.KEY_F9 ] = "F9", + [curses.KEY_F10 ] = "F10", + [curses.KEY_F11 ] = "F11", + [curses.KEY_F12 ] = "F12", + + [curses.KEY_RESIZE ] = "Resize", + [curses.KEY_REFRESH ] = "Refresh", + + [curses.KEY_BTAB ] = "ShiftTab", + [curses.KEY_SDC ] = "ShiftDelete", + [curses.KEY_SIC ] = "ShiftInsert", + [curses.KEY_SEND ] = "ShiftEnd", + [curses.KEY_SHOME ] = "ShiftHome", + [curses.KEY_SLEFT ] = "ShiftLeft", + [curses.KEY_SRIGHT ] = "ShiftRight", + } + end + return self._KEYMAP +end + +-- get input key +function program:_input_key() + + -- get main window + local main_window = self:main_window() + + -- get input character + local ch = main_window:getch() + if not ch then + return + end + + -- this is the time limit in ms within Esc-key sequences are detected as + -- Alt-letter sequences. useful when we can't generate Alt-letter sequences + -- directly. sometimes this pause may be longer than expected since the + -- curses driver may also pause waiting for another key (ncurses-5.3) + local esc_delay = 400 + + -- get key map + local key_map = self:_key_map() + + -- is alt? + local alt = ch == 27 + if alt then + + -- get the next input character + ch = main_window:getch() + if not ch then + + -- since there is no way to know the time with millisecond precision + -- we pause the the program until we get a key or the time limit + -- is reached + local t = 0 + while true do + ch = main_window:getch() + if ch or t >= esc_delay then + break + end + + -- wait some time, 50ms + curses.napms(50) + t = t + 50 + end + + -- nothing was typed... return Esc + if not ch then + return 27, "Esc", false + end + end + if ch > 96 and ch < 123 then + ch = ch - 32 + end + end + + -- map character to key + local key = key_map[ch] + local key_name = nil + if key then + key_name = alt and "Alt".. key or key + elseif (ch < 256) then + key_name = alt and "Alt".. string.char(ch) or string.char(ch) + else + return ch, '(noname)', alt + end + + -- return key info + return ch, key_name, alt +end + +-- refresh cursor +function program:_refresh_cursor() + + -- get the top focused view + local focused_view = self + while focused_view:type() == "panel" and focused_view:current() do + focused_view = focused_view:current() + end + + -- get the cursor state of the top focused view + local cursor_state = 0 + if focused_view and focused_view:state("cursor_visible") then + cursor_state = focused_view:state("block_cursor") and 2 or 1 + end + + -- get the cursor position + local cursor = focused_view and focused_view:cursor()() or point{0, 0} + if cursor_state ~= 0 then + local v = focused_view + while v:parent() do + + -- update the cursor position + cursor:addxy(v:bounds().sx, v:bounds().sy) + + -- is cursor visible? + if cursor.x < 0 or cursor.y < 0 or cursor.x >= v:parent():width() or cursor.y >= v:parent():height() then + cursor_state = 0 + break + end + + -- get the parent view + v = v:parent() + end + end + + -- update the cursor state + curses.cursor_set(cursor_state) + + -- get main window + local main_window = curses.main_window() + + -- trace + log:print("cursor(%s): %s, %d", focused_view, cursor, cursor_state) + + -- move cursor position + if cursor_state ~= 0 then + main_window:move(cursor.y, cursor.x) + else + main_window:move(self:height() - 1, self:width() - 1) + end +end + +-- return module +return program diff --git a/src/ltui/rect.lua b/src/ltui/rect.lua new file mode 100644 index 0000000..3119e7e --- /dev/null +++ b/src/ltui/rect.lua @@ -0,0 +1,179 @@ + +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file rect.lua +-- + +--[[ Console User Interface (cui) ]----------------------------------------- +Author: Tiago Dionizio (tiago.dionizio AT gmail.com) +$Id: rect.lua 18 2007-06-21 20:43:52Z tngd $ +--------------------------------------------------------------------------]] + +-- load modules +local point = require("ltui/point") +local object = require("ltui/object") + +-- define module +local rect = rect or object { _init = {"sx", "sy", "ex", "ey"} } + +-- make rect +function rect:new(x, y, w, h) + return rect { x, y, x + w, y + h } +end + +-- get rect size +function rect:size() + return point { self.ex - self.sx, self.ey - self.sy } +end + +-- get width +function rect:width() + return self.ex - self.sx +end + +-- get height +function rect:height() + return self.ey - self.sy +end + +-- resize rect +function rect:resize(w, h) + self.ex = self.sx + w + self.ey = self.sy + h +end + +-- move rect +function rect:move(dx, dy) + self.sx = self.sx + dx + self.sy = self.sy + dy + self.ex = self.ex + dx + self.ey = self.ey + dy + return self +end + +-- move rect to the given position +function rect:move2(x, y) + local w = self.ex - self.sx + local h = self.ey - self.sy + self.sx = x + self.sy = y + self.ex = x + w + self.ey = y + h + return self +end + +-- move top right corner of the rect +function rect:moves(dx, dy) + self.sx = self.sx + dx + self.sy = self.sy + dy + return self +end + +-- move bottom left corner of the rect +function rect:movee(dx, dy) + self.ex = self.ex + dx + self.ey = self.ey + dy + return self +end + +-- expand rect area +function rect:grow(dx, dy) + self.sx = self.sx - dx + self.sy = self.sy - dy + self.ex = self.ex + dx + self.ey = self.ey + dy + return self +end + +-- is intersect? +function rect:is_intersect(r) + return not self():intersect(r):empty() +end + +-- set rect with shared area between this rect and a given rect +function rect:intersect(r) + self.sx = math.max(self.sx, r.sx) + self.sy = math.max(self.sy, r.sy) + self.ex = math.min(self.ex, r.ex) + self.ey = math.min(self.ey, r.ey) + return self +end + +-- get rect with shared area between two rects: local rect_new = r1 / r2 +function rect:__div(r) + return self():intersect(r) +end + +-- set union rect +function rect:union(r) + self.sx = math.min(self.sx, r.sx) + self.sy = math.min(self.sy, r.sy) + self.ex = math.max(self.ex, r.ex) + self.ey = math.max(self.ey, r.ey) + return self +end + +-- get union rect: local rect_new = r1 + r2 +function rect:__add(r) + return self():union(r) +end + +-- r1 == r1? +function rect:__eq(r) + return + self.sx == r.sx and + self.sy == r.sy and + self.ex == r.ex and + self.ey == r.ey +end + +-- contains the given point in rect? +function rect:contains(x, y) + return x >= self.sx and x < self.ex and y >= self.sy and y < self.ey +end + +-- empty rect? +function rect:empty() + return self.sx >= self.ex or self.sy >= self.ey +end + +-- tostring(r) +function rect:__tostring() + if self:empty() then + return '[]' + end + return string.format("[%d, %d, %d, %d]", self.sx, self.sy, self.ex, self.ey) +end + +-- r1 .. r2 +function rect.__concat(op1, op2) + if type(op1) == 'string' then + return op1 .. op2:__tostring() + elseif type(op2) == 'string' then + return op1:__tostring() .. op2 + else + return op1:__tostring() .. op2:__tostring() + end +end + +-- return module +return rect diff --git a/src/ltui/statusbar.lua b/src/ltui/statusbar.lua new file mode 100644 index 0000000..803842b --- /dev/null +++ b/src/ltui/statusbar.lua @@ -0,0 +1,58 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file statusbar.lua +-- + +-- load modules +local log = require("ltui/base/log") +local rect = require("ltui/rect") +local panel = require("ltui/panel") +local label = require("ltui/label") +local event = require("ltui/event") +local curses = require("ltui/curses") + +-- define module +local statusbar = statusbar or panel() + +-- init statusbar +function statusbar:init(name, bounds) + + -- init panel + panel.init(self, name, bounds) + + -- init info + self._INFO = label:new("statusbar.info", rect{0, 0, self:width(), self:height()}) + self:insert(self:info()) + self:info():text_set("Status Bar") + self:info():textattr_set("blue") + + -- init background + self:background_set("white") +end + +-- get status info +function statusbar:info() + return self._INFO +end + +-- return module +return statusbar diff --git a/src/ltui/textarea.lua b/src/ltui/textarea.lua new file mode 100644 index 0000000..b11132a --- /dev/null +++ b/src/ltui/textarea.lua @@ -0,0 +1,120 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file textarea.lua +-- + +-- load modules +local log = require("ui/log") +local view = require("ui/view") +local label = require("ui/label") +local event = require("ui/event") +local curses = require("ui/curses") + +-- define module +local textarea = textarea or label() + +-- init textarea +function textarea:init(name, bounds, text) + + -- init label + label.init(self, name, bounds, text) + + -- mark as selectable + self:option_set("selectable", true) + + -- enable progress + self:option_set("progress", true) + + -- init start line + self._STARTLINE = 0 + self._LINECOUNT = 0 +end + +-- draw textarea +function textarea:draw(transparent) + + -- draw background + view.draw(self, transparent) + + -- get the text attribute value + local textattr = self:textattr_val() + + -- draw text string + local strs = self._SPLITTEXT + if strs and #strs > 0 and textattr then + self:canvas():attr(textattr):move(0, 0):putstrs(strs, self._STARTLINE + 1) + end + + -- draw progress + if self:option("progress") then + local progress = (self._STARTLINE + math.min(self:height(), self._LINECOUNT)) * 100 / self._LINECOUNT + if (self._STARTLINE > 0 or progress < 100) and self:width() > 20 then + self:canvas():move(self:width() - 10, self:height() - 1):putstr(string.format("(%%%d)", progress)) + end + end +end + +-- set text +function textarea:text_set(text) + self._STARTLINE = 0 + self._SPLITTEXT = text and self:splitext(text) or {} + self._LINECOUNT = #self._SPLITTEXT + return label.text_set(self, text) +end + +-- scroll +function textarea:scroll(lines) + if self._LINECOUNT > self:height() then + self._STARTLINE = self._STARTLINE + lines + if self._STARTLINE < 0 then + self._STARTLINE = 0 + end + if self._STARTLINE > self._LINECOUNT - self:height() then + self._STARTLINE = self._LINECOUNT - self:height() + end + self:invalidate() + end +end + +-- scroll to end +function textarea:scroll_to_end() + if self._LINECOUNT > self:height() then + self._STARTLINE = self._LINECOUNT - self:height() + self:invalidate() + end +end + +-- on event +function textarea:event_on(e) + if e.type == event.ev_keyboard then + if e.key_name == "Up" then + self:scroll(-5) + return true + elseif e.key_name == "Down" then + self:scroll(5) + return true + end + end +end + +-- return module +return textarea diff --git a/src/ltui/textdialog.lua b/src/ltui/textdialog.lua new file mode 100644 index 0000000..517473c --- /dev/null +++ b/src/ltui/textdialog.lua @@ -0,0 +1,72 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file textdialog.lua +-- + +-- load modules +local log = require("ui/log") +local rect = require("ui/rect") +local event = require("ui/event") +local dialog = require("ui/dialog") +local curses = require("ui/curses") +local textarea = require("ui/textarea") + +-- define module +local textdialog = textdialog or dialog() + +-- init dialog +function textdialog:init(name, bounds, title) + + -- init window + dialog.init(self, name, bounds, title) + + -- insert text + self:panel():insert(self:text()) + + -- select buttons by default + self:panel():select(self:buttons()) +end + +-- get text +function textdialog:text() + if not self._TEXT then + self._TEXT = textarea:new("textdialog.text", rect:new(0, 0, self:panel():width(), self:panel():height() - 1)) + end + return self._TEXT +end + +-- on event +function textdialog:event_on(e) + + -- pass event to dialog + if dialog.event_on(self, e) then + return true + end + + -- pass keyboard event to text area to scroll + if e.type == event.ev_keyboard then + return self:text():event_on(e) + end +end + +-- return module +return textdialog diff --git a/src/ltui/textedit.lua b/src/ltui/textedit.lua new file mode 100644 index 0000000..ed1a922 --- /dev/null +++ b/src/ltui/textedit.lua @@ -0,0 +1,108 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file textedit.lua +-- + +-- load modules +local log = require("ui/log") +local view = require("ui/view") +local label = require("ui/label") +local event = require("ui/event") +local border = require("ui/border") +local curses = require("ui/curses") +local textarea = require("ui/textarea") + +-- define module +local textedit = textedit or textarea() + +-- init textedit +function textedit:init(name, bounds, text) + + -- init label + textarea.init(self, name, bounds, text) + + -- show cursor + self:cursor_show(true) + + -- mark as selectable + self:option_set("selectable", true) + + -- disable progress + self:option_set("progress", false) + + -- enable multiple line + self:option_set("multiline", true) +end + +-- draw textedit +function textedit:draw(transparent) + + -- draw label + textarea.draw(self, transparent) + + -- move cursor + if not self:text() or #self:text() == 0 then + self:cursor_move(0, 0) + else + self:cursor_move(self:canvas():pos()) + end +end + +-- set text +function textedit:text_set(text) + textarea.text_set(self, text) + self:scroll_to_end() + return self +end + +-- on event +function textedit:event_on(e) + + -- update text + if e.type == event.ev_keyboard then + if e.key_code > 0x1f and e.key_code < 0x7f then + self:text_set(self:text() .. e.key_name) + return true + elseif e.key_name == "Enter" and self:option("multiline") then + self:text_set(self:text() .. '\n') + return true + elseif e.key_name == "Backspace" then + local text = self:text() + if #text > 0 then + self:text_set(text:sub(1, #text - 1)) + end + return true + elseif e.key_name == "CtrlV" then + local pastetext = os.pbpaste() + if pastetext then + self:text_set(self:text() .. pastetext) + end + return true + end + end + + -- do textarea event + return textarea.event_on(self, e) +end + +-- return module +return textedit diff --git a/src/ltui/view.lua b/src/ltui/view.lua new file mode 100644 index 0000000..3e73f99 --- /dev/null +++ b/src/ltui/view.lua @@ -0,0 +1,481 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file view.lua +-- + +-- load modules +local log = require("ltui/base/log") +local rect = require("ltui/rect") +local point = require("ltui/point") +local object = require("ltui/object") +local canvas = require("ltui/canvas") +local curses = require("ltui/curses") + +-- define module +local view = view or object() + +-- new view instance +function view:new(name, bounds, ...) + + -- create instance + self = self() + + -- init view + self:init(name, bounds, ...) + + -- done + return self +end + +-- init view +function view:init(name, bounds) + + -- check + assert(name and type(bounds) == 'table') + + -- init type + self._TYPE = "view" + + -- init state + local state = object() + state.visible = true -- view visibility + state.cursor_visible = false -- cursor visibility + state.block_cursor = false -- block cursor + state.selected = false -- is selected? + state.focused = false -- is focused? + state.redraw = true -- need redraw + state.refresh = true -- need refresh + state.resize = true -- need resize + self._STATE = state + + -- init options + local options = object() + options.selectable = false -- true if window can be selected + self._OPTIONS = options + + -- init attributes + self._ATTRS = object() + + -- init actions + self._ACTIONS = object() + + -- init extras + self._EXTRAS = object() + + -- init name + self._NAME = name + + -- init cursor + self._CURSOR = point{0, 0} + + -- init bounds and window + self:bounds_set(bounds) +end + +-- exit view +function view:exit() + + -- close window + if self:window() then + self:window():close() + self._WINDOW = nil + end +end + +-- get view name +function view:name() + return self._NAME +end + +-- get view bounds +function view:bounds() + return self._BOUNDS +end + +-- set window bounds +function view:bounds_set(bounds) + if bounds and self:bounds() ~= bounds then + self._BOUNDS = bounds() + self:invalidate(true) + end +end + +-- get view width +function view:width() + return self:bounds():width() +end + +-- get view height +function view:height() + return self:bounds():height() +end + +-- get view size +function view:size() + return self:bounds():size() +end + +-- get the parent view +function view:parent() + return self._PARENT +end + +-- set the parent view +function view:parent_set(parent) + self._PARENT = parent +end + +-- get the application +function view:application() + if not self._APPLICATION then + local app = self + while app:parent() do + app = app:parent() + end + self._APPLICATION = app + end + return self._APPLICATION +end + +-- get the view window +function view:window() + return self._WINDOW +end + +-- get the view canvas +function view:canvas() + if not self._CANVAS then + self._CANVAS = canvas:new(self, self:window()) + end + return self._CANVAS +end + +-- draw view +function view:draw(transparent) + + -- trace + log:print("%s: draw ..", self) + + -- draw background + if not transparent then + local background = self:background() + if background then + background = curses.color_pair(background, background) + self:canvas():attr(background):move(0, 0):putchar(' ', self:width() * self:height()) + else + self:canvas():clear() + end + end + + -- clear mark + self:state_set("redraw", false) + self:_mark_refresh() +end + +-- refresh view +function view:refresh() + + -- refresh to the parent view + local parent = self:parent() + if parent and self:state("visible") then + + -- clip bounds with the parent view + local bounds = self:bounds() + local r = bounds():intersect(rect{0, 0, parent:width(), parent:height()}) + if not r:empty() then + + -- trace + log:print("%s: refresh to %s(%d, %d, %d, %d) ..", self, parent:name(), r.sx, r.sy, r.ex, r.ey) + + -- copy this view to parent view + self:window():copy(parent:window(), 0, 0, r.sy, r.sx, r.ey - 1, r.ex - 1) + end + end +end + +-- resize bounds of inner child views (abstract) +function view:resize() + + -- trace + log:print("%s: resize ..", self) + + -- close the previous windows first + if self:window() then + self:window():close() + self._WINDOW = nil + end + + -- need renew canvas + self._CANVAS = nil + + -- create a new window + self._WINDOW = curses.new_pad(self:height() > 0 and self:height() or 1, self:width() > 0 and self:width() or 1) + assert(self._WINDOW, "cannot create window!") + + -- disable cursor + self:window():leaveok(true) + + -- clear mark + self:state_set("resize", false) +end + +-- show view? +-- +-- .e.g +-- v:show(false) +-- v:show(true, {focused = true}) +-- +function view:show(visible, opt) + if self:state("visible") ~= visible then + local parent = self:parent() + if parent and parent:current() == self and not visible then + parent:select_next(nil, true) + elseif parent and visible and opt and opt.focused then + parent:select(self) + end + self:state_set("visible", visible) + self:invalidate() + end +end + +-- invalidate view to redraw it +function view:invalidate(bounds) + if bounds then + self:_mark_resize() + end + self:_mark_redraw() +end + +-- on event (abstract) +-- +-- @return true: done and break dispatching, false/nil: continous to dispatch to other views +-- +function view:event_on(e) +end + +-- get the current event +function view:event() + return self:parent() and self:parent():event() +end + +-- put an event to view +function view:event_put(e) + return self:parent() and self:parent():event_put(e) +end + +-- get type +function view:type() + return self._TYPE +end + +-- set type +function view:type_set(t) + self._TYPE = t or "view" + return self +end + +-- get state +function view:state(name) + return self._STATE[name] +end + +-- set state +function view:state_set(name, enable) + + -- state is not changed? + enable = enable or false + if self:state(name) == enable then + return self + end + + -- change state + self._STATE[name] = enable + return self +end + +-- get option +function view:option(name) + return self._OPTIONS[name] +end + +-- set option +function view:option_set(name, enable) + + -- state is not changed? + enable = enable or false + if self:option(name) == enable then + return + end + + -- set option + self._OPTIONS[name] = enable +end + +-- get attribute +function view:attr(name) + return self._ATTRS[name] +end + +-- set attribute +function view:attr_set(name, value) + self._ATTRS[name] = value + self:invalidate() + return self +end + +-- get extra data +function view:extra(name) + return self._EXTRAS[name] +end + +-- set extra data +function view:extra_set(name, value) + self._EXTRAS[name] = value + return self +end + +-- get action +function view:action(name) + return self._ACTIONS[name] +end + +-- set action +function view:action_set(name, on_action) + self._ACTIONS[name] = on_action + return self +end + +-- do action +function view:action_on(name, ...) + local on_action = self:action(name) + if on_action then + if type(on_action) == "string" then + -- send command + if self:application() then + self:application():send(on_action) + end + elseif type(on_action) == "function" then + -- do action script + return on_action(self, ...) + end + end +end + +-- get cursor position +function view:cursor() + return self._CURSOR +end + +-- move cursor to the given position +function view:cursor_move(x, y) + self._CURSOR = point{ self:_limit(x, 0, self:width() - 1), self:_limit(y, 0, self:height() - 1) } + return self +end + +-- show cursor? +function view:cursor_show(visible) + if self:state("cursor_visible") ~= visible then + self:state_set("cursor_visible", visible) + end + return self +end + +-- get background +function view:background() + local background = self:attr("background") + if not background and self:parent() then + background = self:parent():background() + end + return background +end + +-- set background, .e.g background_set("blue") +function view:background_set(color) + return self:attr_set("background", color) +end + +-- limit value range +function view:_limit(value, minval, maxval) + return math.min(maxval, math.max(value, minval)) +end + +-- need resize view +function view:_mark_resize() + + -- have been marked? + if self:state("resize") then + return + end + + -- trace + log:print("%s: mark as resize", self) + + -- need resize it + self:state_set("resize", true) +end + +-- need redraw view +function view:_mark_redraw() + + -- have been marked? + if self:state("redraw") then + return + end + + -- trace + log:print("%s: mark as redraw", self) + + -- need redraw it + self:state_set("redraw", true) + + -- need redraw it's parent view if this view is invisible + if not self:state("visible") and self:parent() then + self:parent():_mark_redraw() + end +end + +-- need refresh view +function view:_mark_refresh() + + -- have been marked? + if self:state("refresh") then + return + end + + -- need refresh it + if self:state("visible") then + self:state_set("refresh", true) + end + + -- need refresh it's parent view + if self:parent() then + self:parent():_mark_refresh() + end +end + +-- tostring(view) +function view:__tostring() + return string.format("", self:name(), tostring(self:bounds())) +end + +-- return module +return view diff --git a/src/ltui/window.lua b/src/ltui/window.lua new file mode 100644 index 0000000..4f07c09 --- /dev/null +++ b/src/ltui/window.lua @@ -0,0 +1,131 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2018, TBOOX Open Source Group. +-- +-- @author ruki +-- @file window.lua +-- + +-- load modules +local log = require("ui/log") +local rect = require("ui/rect") +local view = require("ui/view") +local label = require("ui/label") +local panel = require("ui/panel") +local event = require("ui/event") +local border = require("ui/border") +local curses = require("ui/curses") +local action = require("ui/action") + +-- define module +local window = window or panel() + +-- init window +function window:init(name, bounds, title, shadow) + + -- init panel + panel.init(self, name, bounds) + + -- check bounds + assert(self:width() > 4 and self:height() > 3, string.format("%s: too small!", self)) + + -- insert shadow + if shadow then + self:insert(self:shadow()) + self:frame():bounds():movee(-2, -1) + self:frame():invalidate(true) + end + + -- insert border + self:frame():insert(self:border()) + + -- insert title + if title then + self._TITLE = label:new("window.title", rect{0, 0, #title, 1}, title) + self:title():textattr_set("blue bold") + self:title():action_set(action.ac_on_text_changed, function (v) + if v:text() then + local bounds = v:bounds() + v:bounds():resize(#v:text(), v:height()) + bounds:move2(math.max(0, math.floor((self:frame():width() - v:width()) / 2)), bounds.sy) + v:invalidate(true) + end + end) + self:frame():insert(self:title(), {centerx = true}) + end + + -- insert panel + self:frame():insert(self:panel()) + + -- insert frame + self:insert(self:frame()) +end + +-- get frame +function window:frame() + if not self._FRAME then + self._FRAME = panel:new("window.frame", rect{0, 0, self:width(), self:height()}):background_set("white") + end + return self._FRAME +end + +-- get panel +function window:panel() + if not self._PANEL then + self._PANEL = panel:new("window.panel", self:frame():bounds()) + self._PANEL:bounds():grow(-1, -1) + self._PANEL:invalidate(true) + end + return self._PANEL +end + +-- get title +function window:title() + return self._TITLE +end + +-- get shadow +function window:shadow() + if not self._SHADOW then + self._SHADOW = view:new("window.shadow", rect{2, 1, self:width(), self:height()}):background_set("black") + end + return self._SHADOW +end + +-- get border +function window:border() + if not self._BORDER then + self._BORDER = border:new("window.border", self:frame():bounds()) + end + return self._BORDER +end + +-- on event +function window:event_on(e) + + -- select panel? + if e.type == event.ev_keyboard then + if e.key_name == "Tab" then + return self:panel():select_next() + end + end +end + +-- return module +return window diff --git a/tests/load.lua b/tests/load.lua new file mode 100755 index 0000000..29bbb73 --- /dev/null +++ b/tests/load.lua @@ -0,0 +1,9 @@ +-- init load directories +package.path = package.path .. ';./src/?.lua' +package.cpath = package.cpath .. ';./build/ltui.dll;./build/libltui.so;./build/libltui.dylib' + +local os = require("ltui/base/os") +local ltui = require("ltui.curses") +print(ltui) + +print(os.host())