luaforwindows/files/lua/CLRForm.lua

668 lines
16 KiB
Lua
Executable File

require "CLRPackage"
import "System.Windows.Forms"
import "System.Drawing"
local Directory = luanet.import_type("System.IO.Directory")
local Path = luanet.import_type("System.IO.Path")
local File = luanet.import_type("System.IO.File")
local append = table.insert
local ferr = io.stderr -- for debugging
----------- some generally useful functions -----------------
--- Can be used to set multiple properties of an object, by supplying a table.
-- e.g. set(button,{Text="Click Me",Dock = DockStyle.Fill})
function set (obj,props)
for k,v in pairs(props) do
if type(k) == 'string' then
obj[k] = v
end
end
end
--- works like AddRange, except it takes a table of controls
-- e.g add_controls(form,{button1,button2})
function add_controls (ctrl,ctrls)
for k,v in pairs(ctrls) do
ctrl.Controls:Add(v)
end
end
function ShowMessageBox (caption,icon)
icon = icon or MessageBoxIcon.Information
MessageBox.Show(caption,arg[0],MessageBoxButtons.OK,icon)
end
function ShowError (caption)
ShowMessageBox(caption,MessageBoxIcon.Error)
end
---------- Utility function for creating classes -------------
--- Does single-inheritance and _delegation_
function class(base)
local c = {} -- a new class instance, which is the metatable for all objects of this type
local mt = {} -- a metatable for the class instance
local userdata_base
if base == nil then
--nada
elseif type(base) == 'table' then
-- our new class is a shallow copy of the base class!
for i,v in pairs(base) do
c[i] = v
end
c._base = base
-- inherit the 'not found' handler, if present
if c._handler then mt.__index = c._handler end
end
-- the class will be the metatable for all its objects,
-- and they will look up their methods in it.
c.__index = c
-- expose a ctor which can be called by <classname>(<args>)
mt.__call = function(class_tbl,...)
local obj= {}
setmetatable(obj,c)
-- nice alias for the base class ctor (which you have to call explicitly if you have a ctor)
if base then c.super = base._init end
if c._init then
c._init(obj,...)
else
-- make sure that any stuff from the base class is initialized!
if base and base._init then
base._init(obj,...)
end
end
return obj
end
-- Call Class.catch to set a handler for methods/properties not found in the class!
c.catch = function(handler)
c._handler = handler
mt.__index = handler
end
c._init = ctor
c.is_a = function(self,klass)
local m = getmetatable(self)
if not m then return false end --*can't be an object!
while m do
if m == klass then return true end
m = rawget(m,'_base')
end
return false
end
c.class_of = function(obj)
return c.is_a(obj,c)
end
-- any object can have a specified delegate which is called with unrecognized methods
-- if _handler exists and obj[key] is nil, then pass onto handler!
c.delegate = function(self,obj)
local me = self
mt.__index = function(tbl,key)
-- handling fields!
local getter = rawget(c,"Get_"..key)
if getter then return getter(me) end
getter = rawget(me,"_"..key)
if getter then return getter end
local method = obj[key]
if method then
-- it exists in the delegate! First check if it's callable
if type(method) == 'function' or getmetatable(method).__call then
return function(self,...)
return method(obj,...)
end
else -- otherwise, just return
return method
end
elseif self._handler then
return self._handler(tbl,key)
end
end
c.__newindex = function(self,key,val)
local setter = rawget(c,"Set_"..key)
if setter then
setter(self,val)
else
obj[key] = val
end
end
end
setmetatable(c,mt)
return c
end
----------- Creating Menus -----------------------------
local ShortcutType = Shortcut.F1:GetType()
local function parse_shortcut (s)
local res
if pcall(function() -- we have to catch the exception!
res = Enum.Parse(ShortcutType,s,false)
end) then return res end
end
local function add_menu_items (item,tbl)
for i = 1,#tbl,2 do
item.MenuItems:Add(create_menu_item(tbl[i],tbl[i+1]))
end
end
function create_menu_item (label,action)
local item = MenuItem()
local shortcut = label:match('%((%w+)%)')
if shortcut then
local shortcut = parse_shortcut(shortcut)
if shortcut then item.Shortcut = shortcut end
label = label:match('(.+)%(')
end
item.Text = label
if type(action) == 'function' then
item.Click:Add(action)
else
add_menu_items(item,action)
end
return item
end
function main_menu (tbl)
local mm = MainMenu()
add_menu_items(mm,tbl)
return mm
end
function popup_menu (tbl)
local mm = ContextMenu()
add_menu_items(mm,tbl)
return mm
end
-- a useful function for creating menu callbacks to methods of a given object.
function method (obj,fun)
return function()
fun(obj)
end
end
local function populate_control (form,tbl)
set(form,tbl)
if #tbl > 0 then -- has an array part, containing controls
if #tbl == 1 then
table.insert(tbl,1,"Fill")
end
local i = 1
while i <= #tbl do
local c = tbl[i]
local dock
if type(c) == 'string' then
dock = c
c = tbl[i+1]
i = i + 1
c.Dock = DockStyle[dock]
end
form.Controls:Add(c)
i = i + 1
end
end
return form
end
function LuaForm (tbl)
return populate_control(Form(),tbl)
end
function LuaPanel (tbl)
return populate_control(Panel(),tbl)
end
---------------- Stream Layout --------------
StreamLayout = class()
function StreamLayout:_init(panel)
self.xsep = 10
self.X = self.xsep
self.Y = self.xsep
self.panel = panel
self.newline = true
self.maxX = 0
self.maxHeight = 0
self.labels = {}
self.panel:SuspendLayout()
end
function StreamLayout:Add(c,like)
if like then self.X = like.Left end
c.Location = Point(self.X,self.Y)
self.panel.Controls:Add(c)
self.X = self.X + c.Width + self.xsep
self.maxX = math.max(self.maxX,self.X)
self.maxHeight = math.max(self.maxHeight,c.Height)
if self.newline then
self.firstC = c
self.newline = false
self.maxHeight = 0
end
end
function StreamLayout:AddRow(lbl,...)
local row = {...}
if lbl then
local label = Label()
label.AutoSize = true
label.Text = lbl
row.label = label
append(self.labels,row)
self:Add(label)
end
for i,c in ipairs(row) do
self:Add(c)
end
self:NextRow()
end
function StreamLayout:Height()
return self.Y + self.maxHeight + self.xsep
end
function StreamLayout:Width()
return self.maxX
end
function StreamLayout:NextRow()
self.Y = self:Height()
self.X = self.xsep
self.newline = true
end
function StreamLayout:Finish ()
local width = 0
for i,row in ipairs(self.labels) do
width = math.max(width,row.label.Width)
end
if width > 0 then -- i.e there is an explicit row of labels
for i,row in ipairs(self.labels) do
local lbl = row.label
for j,c in ipairs(row) do
c.Left = c.Left + (width - lbl.Width)
self.maxX = math.max(self.maxX,c.Left+c.Width+self.xsep)
end
end
end
self.panel:ResumeLayout(false)
end
LayoutForm = class()
function LayoutForm:_init ()
self.form = Form()
self.layout = StreamLayout(self.form)
self.hasButtons = false
self.ok = false
self.cancel = false
self.finishedLayout = false
-- this method can only be called once we've set up our own fields!
self:delegate(self.form)
self.FormBorderStyle = FormBorderStyle.FixedDialog
self.MaximizeBox = false
self.MinimizeBox = false
end
function LayoutForm:AddControl(c)
self.layout:Add(c)
end
function LayoutForm:AddControlRow(lbl,...)
self.layout:AddRow(lbl,...)
end
function LayoutForm:AddTextBoxRow(lbl)
local textBox = TextBox()
self:AddControlRow(lbl,textBox)
return textBox
end
function LayoutForm:Btn (title,res)
local b = Button()
b.Text = title
if res == DialogResult.OK then
self.AcceptButton = b
elseif res == DialogResult.Cancel then
self.CancelButton = b
end
self.layout:Add(b)
self.hasButtons = true
return b
end
function LayoutForm:OkBtn (title)
return self:Btn(title,DialogResult.OK)
end
function LayoutForm:CancelBtn (title)
return self:Btn(title,DialogResult.Cancel)
end
function LayoutForm:NextRow()
self.layout:NextRow()
end
function LayoutForm:OkCancel ()
if not self.layout.newline then self:NextRow() end
self.ok = self:OkBtn "OK"
self.cancel = self:CancelBtn "Cancel"
end
function LayoutForm:OnOK()
return true
end
function LayoutForm:CenterControls (...)
local w = 0
local ctrls = {...}
for _,c in ipairs(ctrls) do
w = w + c.Width
end
local diff = (self.layout:Width() - w)/(#ctrls + 1)
local xx = diff
for _,c in ipairs(ctrls) do
c.Left = xx
xx = xx + c.Width + diff
end
end
function LayoutForm:FinishLayout()
if not self.hasButtons then
self:OkCancel()
self:CenterControls(self.ok,self.cancel)
self.ok.Click:Add(function()
if self:OnOK() then
self.DialogResult = DialogResult.OK
else
self.DialogResult = DialogResult.None
end
end)
end
local layout = self.layout
layout:Finish()
self.ClientSize = Size(layout:Width(), layout:Height())
self.finishedLayout = true
end
function LayoutForm:ShowDialogOK ()
if not self.finishedLayout then
self:FinishLayout()
end
return self:ShowDialog() == DialogResult.OK
end
------------------- Converters ------------------------------
-- These classes convert values between controls and Lua values, and provide basic verification,
-- like ensuring that a string is a valid number, for instance.
-- They provide an appropriate control for editing the particular value.
Converter = class()
function Converter:Control ()
self.box = TextBox()
return self.box
end
function Converter:Read (c)
return c.Text
end
function Converter:Write (c,text)
c.Text = text
end
NumberConverter = class(Converter)
function NumberConverter:Read (c)
local txt = c.Text
local value = tonumber(txt)
if not value then return nil, "Cannot convert '"..txt.."' to a number" end
return value
end
BoolConverter = class(Converter)
function BoolConverter:Control ()
return CheckBox()
end
function BoolConverter:Read (c)
return c.Checked
end
function BoolConverter:Write (c,val)
c.Checked = val
end
ListConverter = class(Converter)
function ListConverter:_init (list)
self.list = list
end
function ListConverter:Control ()
local c = ComboBox()
if not self.list.Editable then
c.DropDownStyle = ComboBoxStyle.DropDownList
end
for i,item in ipairs(self.list) do
c.Items:Add(item)
end
return c
end
function ListConverter:Read (c)
local val = c.SelectedItem
if not val then val = c.Text end
return val
end
function ListConverter:Write (c,val)
c.SelectedItem = val
end
FileConverter = class(Converter)
function FileConverter:_init (reading,mask)
-- the filter is in a simplified form like 'Lua Files (*.lua)|C# Files (*.cs)"
-- this will expand it into the required form.
self.filter = mask:gsub("%((.-)%)",function(pat)
return "("..pat..")|"..pat
end)
self.reading = reading
end
-- ExtraControl is an optional method which gives a converter the opportunity of adding another
-- control to the row after the primary control. In this case, we create a file browser button.
function FileConverter:ExtraControl ()
local btn = Button()
local box = self.box
btn.Width = 30
btn.Text = ".."
btn.Click:Add(function()
-- if possible, open the file browser in the same directory as the filename
local path = self:Read(box)
if not File.Exists(path) then
path = Directory.GetCurrentDirectory()
else
path = Path.GetDirectoryName(path)
end
-- depending on whether we want a file to read or write ("Save as"), pick the file dialog.
local filebox
if self.reading then filebox = OpenFileDialog
else filebox = SaveFileDialog end
local dlg = filebox()
dlg.Filter = self.filter
dlg.InitialDirectory = path
if dlg:ShowDialog() == DialogResult.OK then
self:Write(box,dlg.FileName)
end
end)
return btn
end
-- Note an important convention: this converter puts the full file path in the text box's Tag field,
function FileConverter:Write (c,val)
c.Text = Path.GetFileName(val)
c.Tag = val
end
function FileConverter:Read (c)
return c.Tag
end
-- there are then two subclasses, depending if you want to open a file for reading or writing.
FileIn = class(FileConverter)
function FileIn:_init (mask)
self:super(true,mask)
end
FileOut = class(FileConverter)
function FileOut:_init (mask)
self:super(false,mask)
end
local converters = {
number = NumberConverter(),
string = Converter(),
boolean = BoolConverter(),
}
function Converter.AddConverter (typename,conv)
converters[typename] = conv
end
---------------- AutoVarDialog ------------------------------
local function callable (method)
local mt = getmetatable(method)
return type(method) == 'function' or (mt and mt.__call)
end
local function simple_list (nxt)
return type(nxt) == 'table' and #nxt > 0
end
AutoVarDialog = class(LayoutForm)
function AutoVarDialog:_init (tbl)
self.rows = {}
self.T = tbl.Object or _G
self.verify = tbl.Verify
self.verify_exists = tbl.Verify ~= nil
--end of local fields; NOW we can initialize the form!
self.super(self)
self.Text = tbl.Text or "untitled"
local i,n = 1,#tbl
while i <= n do
local converter,constraint,extra
local lbl = tbl[i]
local var = tbl[i+1]
local value = self.T[var]
local vtype = type(value)
-- is there a particular default or constraint set?
if i+1 < n then
local nxt = tbl[i+2]
if type(nxt) ~= 'string' then
if callable(nxt) then
constraint = nxt
elseif simple_list(nxt) then
-- have been given a list of possible values
converter = ListConverter(nxt)
elseif Converter.class_of(nxt) then
converter = nxt
else
ShowError("Unknown converter or verify function: "..nxt)
return
end
i = i + 1
end
end
if not converter then
-- use a default converter appropriate to this type
converter = converters[vtype]
if not converter then
ShowError("Cannot find a converter for type: "..vtype)
return
end
end
local c = converter:Control()
self:AddControlRow(lbl,c,converter.ExtraControl and converter:ExtraControl())
converter:Write(c,value)
append(self.rows,{cntrl=c,converter=converter,var=var, constraint=constraint})
i = i + 2
end
end
function AutoVarDialog:OnOK ()
local T = {}
for _,t in ipairs(self.rows) do
local value,err = t.converter:Read(t.cntrl)
if not err and t.constraint then
err = t.constraint(value)
end
if err then
ShowError(err)
t.cntrl:Focus()
return false
end
T[t.var] = value
end
-- a function to verify the fields has been supplied
if self.verify_exists then
local err = self.verify(T)
if err then
ShowError(err)
return false
end
end
-- NOW we can finally copy the changed values into the target table!
for k,v in pairs(T) do
self.T[k] = v
end
return true
end
function Match (pat,err)
return function (s)
--ferr:write(',',s,',',pat,'\n')
if not s:find(pat) then return err end
end
end
function Range (x1,x2)
if not x2 then -- unbound upper range
return function(x)
if x < x1 then return "Must be greater than "..x1 end
end
elseif not x1 then -- unbound lower range
return function(x)
if x > x2 then return "Must be less than "..x2 end
end
else
return function(x)
if x < x1 or x > x2 then return "Must be in range "..x1.." to "..x2 end
end
end
end
NonBlank = Match ('%S+','Must be a non-blank string')
Word = Match('^%w+$','Must be a word')
--- A useful function for prompting a user for a single value.
-- returns a non-nil value if the user clicks on OK or presses <enter>.
function PromptForString (caption,prompt,default)
local tbl = {val = default or ""}
local form = AutoVarDialog {Text = caption, Object = tbl;
prompt,"val"
}
if form:ShowDialogOK() then
return tbl.val
end
end