Multifact.fh_lua--[[
@Title: Multifact
@Author: Helen Wright
@Version: 1.9.1
@LastUpdated: 23 December 2019
@Description: Create multiple Individual facts for a set of individuals
all linked to a single existing source with optional common data and citation elements
]]--
--[[Licence:
All code within this plugin is released under the Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) licence: https://creativecommons.org/licenses/by-nc-sa/4.0/
except where indicated. Specifically, rights to the encoding functions are reserved to Mike Tate; and rights to the function ProgressBar are reserved to Mike Tate and Jane Taubman
In practice, this means you can steal plunder or reuse any elements of my code (modified if you wish) as long as you credit me by name, link to the licence, and indicate what changes were made. You can't sell any code derived from mine (as if anyone would pay for it!) and you must release it under the same Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) licence: https://creativecommons.org/licenses/by-nc-sa/4.0/
]]--
--[[ChangeLog:
Version 1: Initial version
1.1, 1.2: Fixed initial crop of bugs
1.3 More bug fixes; create Automatic notes if specific note not specified.
1.4 More bug fixes; better dialog size management; option to specify note appended to autonote
1.5 Bug fix (not all places included)
1.6 Better handling of small screens (less than 999 pixels deep. It's usable down to 864 pixels and just about usable down to 800, depending on text scaling, but anything below that won't work.
1.7 Minor layout adjustment
1.8 Changes to boilerplate and further minor layout adjustments
1.9 Minor bug fixes
1.9.1 Bug fix to handle odd fact definitions
]]--
--[[Variable type naming conventions
g prefix for global variables
c for constants
int for integer numbers
n for fractional numbers
str for strings
boo for boolean
tbl for tables
func for functions as parameters
FH API data types
ptr for pointers
dt for date objects
In IUP:
Assume all IUP variables are global
b for "ON"/"OFF" value
tab for tab
dlg for dialogs
norm for normalizer
dlg for dialog
btn for button
txt for text
lab for label
box for hbox or vbox
list for list
Note: where code snippets have been imported these conventions may not be followed
]]--
--------------------------------------------------------------
--EXTERNAL LIBRARIES AND FH VERSION CHECK
--------------------------------------------------------------
do
if fhInitialise(5,0,9) == false then return end-- requires FH 5.0.9 due to use of folder option in fhGetPluginDataFile and the use of Labelled Text
require("iuplua") -- GUI
require("lfs") -- file system access
require ("luacom") --Microsoft's Component Object Model
function fhloadrequire(module,extended)
local function httpRequest(url)
local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
http:Open("GET",url,false)
http:Send()
http:WaitForResponse(30)
return http
end -- local function httpRequest
if not(extended) then extended = module end
local function installmodule(module,filename,size)
local bmodule = false
if not(filename) then
filename = module..'.mod'
bmodule = true
end
local storein = fhGetContextInfo('CI_APP_DATA_FOLDER')..'\\Plugins\\'
-- Check if subdirectory needed
local path = string.match(filename, "(.-)[^/]-[^%.]+$")
if path ~= "" then
path = path:gsub('/','\\')
-- Create sub-directory
lfs.mkdir(storein..path)
end
local attr = lfs.attributes(storein..filename)
if attr and attr.mode == 'file' and attr.size == size then return true end
-- Get file down and install it
local url = "http://www.family-historian.co.uk/lnk/getpluginmodule.php?file="..filename
local isOK, reply = pcall(httpRequest,url)
if not isOK then
fhMessageBox(reply.."\nLoad Require module finds the Internet inaccessible.")
return false
end
local http = reply
local status = http.StatusText
if status == 'OK' then
-- local length = http:GetResponseHeader('Content-Length')
local data = http.ResponseBody
if bmodule then
local modlist = loadstring(http.ResponseBody)
for x,y in pairs(modlist()) do
if type(x) == 'number' and type(y) == 'string' then
x = y -- revert to original 'filename' ipairs modlist
y = 0
end -- otherwise use ['filename']=size pairs modlist
if not(installmodule(module,x,y)) then
break
end
end
else
local function OpenFile(strFileName,strMode)
local fileHandle, strError = io.open(strFileName,strMode)
if not fileHandle then
error("\n Unable to open file in \""..strMode.."\" mode. \n "..
strFileName.." \n "..tostring(strError).." \n")
end
return fileHandle
end -- OpenFile
local function SaveStringToFile(strString,strFileName)
local fileHandle = OpenFile(strFileName,"wb")
fileHandle:write(strString)
assert(fileHandle:close())
end -- SaveStringToFile
SaveStringToFile(data,storein..filename)
end
return true
else
fhMessageBox('An error occurred in Download please try later')
return false
end
end
local function requiref(module)
require(module)
end
local _, err = pcall(requiref,extended)
if err then
if err:match("module '"..extended:gsub("(%W)","%%%1").."' not found") then
local ans = fhMessageBox(
'This plugin requires '..module..' support, please click OK to download and install the module',
'MB_OKCANCEL','MB_ICONEXCLAMATION')
if ans ~= 'OK' then
return false
end
if installmodule(module) then
package.loaded[extended] = nil -- Reset Failed Load
require(extended)
else
return false
end
else
fhMessageBox('Error from require("'..module..'") command:\n'..(err or ''))
return false
end
end
return true
end
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
end
--------------------------------------------------------------
--ENVIRONMENT VARIABLES
--------------------------------------------------------------
local cstrPluginDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local cstrPluginVersion = "1.8"
-- any other constants go here
local myResults = nil
local myActivity = nil
local mySource = nil
local myIndividuals = nil
local myFacts = nil
--------------------------------------------------------------
--UTILITY MODULES AND CLASSES
--------------------------------------------------------------
--[[ Dialog
@Author: Helen Wright
@Version: 1.0
@LastUpdated: 29 October 2019
@Description: Helper functions for IUP dialogs
@V1.0: Initial version.
]]--
do
--Prerequisites:
do
require("iuplua") -- GUI
-- also requires fh API for date handling
end
--useful constants
colorred = "255 0 0" -- used to flag erroneous data
strMin1Char = "/S+" --useful text mask
strMin1Letter = "/l+" --useful text mask
strAlphaNumeric = "[0-9a-zA-Z ()]+" --useful text mask
-- font sizing, normalisation and other layout functions
do
function SetTextSize()
local _,_, height = stringx.partition(iup.GetGlobal("SCREENSIZE"),"x")
if tonumber(height) > 999 then
iup.SetGlobal("DEFAULTFONT", "Arial Unicode MS, 11")
else
iup.SetGlobal("DEFAULTFONT", "Arial Unicode MS, 8") --reduce font size for smaller screens
end
end
function SetUTF8IfPossible()
if fhGetAppVersion() > 5 then -- make interface UTF8; fh API will handle conversion if necessary
iup.SetGlobal("UTF8MODE", "YES")
iup.SetGlobal ("UTF8MODE_FILE", "NO") --use file names in the current locale
fhSetStringEncoding("UTF-8")
end
end
function SetMinimumSize(dlg)
dlg.minsize = iup.NULL
iup.Refresh(dlg)
dlg.minsize = dlg.naturalsize
end
norm = iup.normalizer{} -- normalizer used to layout buttons and labels neatly
norm2 = iup.normalizer{} -- normalizer used to layout lists and text neatly
norm3 = iup.normalizer{} --normalizer used to layout popup dialogs neatly
function DoNormalize()
norm.normalize = "HORIZONTAL"
norm2.normalize = "HORIZONTAL"
norm3.normalize = "HORIZONTAL"
end
end
--dialog handling
do
function MakeDialog(content, options)
local d = iup.dialog{content}
d.title = options.title or ""
if options.expand then d.expand = options.expand end --default is "YES"
if options.resize then d.resize = options.resize end --default is "YES"
if options.size ~= nil then d.size= options.size else d.size = iup.NULL end
if options.show ~= nil then
d.show_cb = function(self, state)
if state == iup.SHOW then SetMinimumSize(self) options.show(state) else return iup.IGNORE end
end
else
d.show_cb = function(self, state)
if state == iup.SHOW then SetMinimumSize(self) else return iup.IGNORE end
end
end
d.margin = options.margin or "3x3"
d.cgap = options.cgap or "1"
d.menubox = options.menubox or "YES"
iup.SetHandle(options.name or d.title, d) --will be used to help identify active window
return d
end
function IdentifyActiveWindow()
--depends on having names associated with all dialogs
local tblD, _ = iup.GetAllDialogs() --get dialog names, which are not the same as their handles
local d = nil
for _, v in ipairs(tblD) do
d = iup.GetHandle(v)
if d.ACTIVEWINDOW == "YES" then
return d
end
end
end
function DestroyAllDialogs()
--depends on having names associated with all dialogs
local tblD, _ = iup.GetAllDialogs() --get dialog names, which are not the same as their handles
local d = nil
for _, v in ipairs(tblD) do
d = iup.GetHandle(v)
d:destroy()
end
end
end
--container handling
do
--local constants used when dealing with containers
local dialogs = dialogs or tablex.index_map({"dialog", "messagedlg", "progesssdlg", "fontdg", "filedlg", "colordlg"})
local containers = containers or tablex.index_map({"frame", "hbox", "vbox", "zbox", "tabs", "radio", "sbox", "cbox", "gridbox", "multibox", "scrollbox", "detachbox", "expander", "detachbox", "split", "backgroundbox", "spinbox"})
local datacontrols = datacontrols or tablex.index_map({"list", "text", "val", "link", "multiline", "toggle" })
local static = static or tablex.index_map({"fill", "normalizer", "button", "label", "menu", "submenu", "item", "separator", "imagergb", "imagergba", "image", "matrix", "cells", "clipboard", "timer", "user", "link"})-- these will be ignored when clearing the dialog
local toohardtohandle = toohardtohandle or tablex.index_map({"tree", "spin", "canvas"}) --only g*d knows
function EnableContainer(ih, tblexcludeih, strstate)
local element = nil
local e = 1 --element index
while ih[e]~= nil do -- loop the elements in the parent
element = ih[e]
if tblexcludeih[element] == nil then
if dialogs[iup.GetClassName(element)] ~= nil or containers[iup.GetClassName(element)] ~= nil then -- this is a container
EnableContainer(element,tblexcludeih, strstate)
elseif datacontrols[iup.GetClassName(element)] ~= nil or iup.GetClassName(element) == "button" then --this is a data control or a button
ih.active = strstate
end
end
e=e+1
end
end
function ClearContainer(ih, tblexcludeih)
local function ClearDataControl(ih)
local cclass = iup.GetClassName(ih)
ih.fgcolor = TXTFGCOLOR
if cclass == "list" then
if ih.editbox=="YES" or iup.multiple=="YES" then
ih.value =""
else
ih.value = "0"
end
elseif cclass == "text" or cclass == "multiline" then
ih.value = ""
elseif cclass == "val" then
ih.value = "0.0"
elseif cclass == "toggle" then
ih.value = "OFF"
end
end
local element = nil
local e = 1 --element index
while ih[e]~= nil do -- loop the elements in the parent
element = ih[e]
if tblexcludeih[element] == nil then
if dialogs[iup.GetClassName(element)] ~= nil or containers[iup.GetClassName(element)] ~= nil then -- this is a container
ClearContainer(element,tblexcludeih)
elseif datacontrols[iup.GetClassName(element)] ~= nil then --this is a data control
ClearDataControl(element)
end
end
e=e+1
end
end
function HideContainer(ih, boostatus)
local strVisible = "NO"
local strFloating = "YES"
if boostatus == false then
strVisible = "YES"
strFloating = "NO"
end
local element = nil
local e = 1 --element index
while ih[e]~= nil do -- loop the elements in the parent
element = ih[e]
if dialogs[iup.GetClassName(element)] ~= nil or containers[iup.GetClassName(element)] ~= nil then -- this is a container
HideContainer(element, booStatus)
else --hide a control
element.visible = strVisible
element.floating = strFloating
iup.Refresh(element)
end
e=e+1
end
end
function GetValueFromControl(ctl)
if type(ctl.value) == "string" or type(ctl.value) == "number" then
return ctl.value
else
return ""
end
end
function GetContainerData(tblCtls)
local is_indexed = (rawget( tblCtls, 1 ) ~= nil)
local tblVals ={}
if not is_indexed then
for k, v in pairs(tblCtls) do
tblVals[k] = GetValueFromControl(v)
end
else
for k,v in ipairs(tblCtls) do
tblVals[k] = GetValueFromControl(v)
end
end
return tblVals
end
function GetDataElements(ih, tblexcludeih, tblElements, keyname)
if not keyname then keyname=false end
local child = iup.GetChild(ih,0)
while child ~= nil do -- loop the elements in the parent and add any data controls to the end of tblElements
if tblexcludeih[child] == nil then
if dialogs[iup.GetClassName(child)] ~= nil or containers[iup.GetClassName(child)] ~= nil then -- this is a container
GetDataElements(child, tblexcludeih, tblElements, keyname)
elseif datacontrols[iup.GetClassName(child)] ~= nil then --this is a data control
if keyname then
tblElements[iup.GetName(child)]=child
else
tblElements[#tblElements+1]=child
end
end
end
child = iup.GetNextChild(ih,child)
end
return tblElements -- a table of data controls
end
end
--message boxes and parameter prompts
do
function Messagebox(strTitle, strMessage, bIsAlert)
local msg = iup.messagedlg{}
msg.TITLE = strTitle
msg.VALUE = strMessage
bIsAlert = bIsAlert or false
if bIsAlert == true then
msg.BUTTONS = "OKCANCEL"
msg.DIALOGTYPE="WARNING"
else
msg.BUTTONS = "OK"
msg.DIALOGTYPE="INFORMATION"
end
msg:popup(iup.CENTERPARENT, iup.CENTERPARENT)--display the dialog
local btnResponse = msg.BUTTONRESPONSE
msg:destroy()
if btnResponse == "1" then return "OK" else return "CANCEL" end
end
function GetText(strPrompt, strDefault, strMask)
local function param_action()
return 1
end
local pstrText = strDefault
local booOK = false
local pstrParam = strPrompt..": %s"..strMask.."\n" --string with mask
booOK, pstrText = iup.GetParam(strPrompt, param_action, pstrParam, pstrText)
pstrText = utils.choose(type(pstrText) == 'string', pstrText, "")
return booOK, utils.choose(booOK == true, pstrText, "")
end
function GetTextandTick(strPrompt, strDefault, strMask, strTickPrompt)
local function param_action()
return 1
end
local booTick = 0
local pstrText = strDefault
local booOK = false
local pstrParam = strPrompt..": %s"..strMask.."\n"..strTickPrompt.." %b\n" --string with mask plus boolean
booOK, pstrText, booTick = iup.GetParam(strPrompt, param_action, pstrParam, pstrText, booTick)
pstrText = utils.choose(type(pstrText) == 'string', pstrText, "")
booTick = utils.choose(booTick == 0, false, true)
return booOK, utils.choose(booOK == true, pstrText, ""), utils.choose(booOK == true, booTick, false)
end
end
--button handling
do
function EnableButtons(tblButtons,strSetting)
-- strSetting can be "YES" or "NO"
for _,v in ipairs(tblButtons) do
v.ACTIVE = strSetting
end
end
function MakeButton(options)
local b=iup.button{}
local callback = options.callback
if callback ~= nil then
if options.close ~= nil then --this is a close button
b=iup.button{action = function(self) if callback() == true then return iup.CLOSE end end}
else
b=iup.button{action = function(self) callback() end}
end
else -- if callback isn't specified this is a cancel button
b=iup.button{action = function(self) return iup.CLOSE end}
end
b.alignment="ALEFT"
b.padding ="10x0"
b.normalizergroup = options.norm or norm
b.title = options.title or ""
if options.tip then b.tip = options.tip end
if options.name then iup.SetHandle(options.name, b) end
return b
end
end
--list handling
do
function PopulateList(l, tblVals)
local is_indexed = (rawget( tblVals, 1 ) ~= nil)
l.REMOVEITEM = "ALL"
if not is_indexed then
local i=1
for k, _ in pairs(tblVals) do
l[tostring(i)]=k
i=i+1
end
else
for i, v in ipairs(tblVals) do
l[tostring(i)]=v
end
end
end
function MultiListSelectionTrue(l)
return l.value:match("%+") ~= nil
end
function MultiListSelectionClear(l)
l.value = string.rep('%-',l.count)
end
function MakeList(options)
local l = iup.list{}
l.editbox = options.editbox or "NO"
l.sort = options.sort or "YES"
l.dropdown=options.dropdown or "YES"
l.multiple=options.multiple or "NO"
if l.dropdown == "YES" then
l.visibleitems = options.visibleitems or "5"
l.expand = "HORIZONTAL"
else
l.visiblelines = options.visiblelines or "9"
if options.visiblecolumns then l.visiblecolumns = options.visiblecolumns end
l.expand = "YES"
end
if options.norm then l.normalizergroup = options.norm end
if options.tip then l.tip = options.tip end
if type(options.values) == "table" then PopulateList(l,options.values) end
--now handle callbacks
if options.action ~= nil then
l.action = function(self, text, item, state) options.action(l, text,item,state) end
end
if options.killfocus ~= nil then
l.killfocus_cb = function(self) options.killfocus() end
end
if options.name then iup.SetHandle(options.name, l) end
return l
end
function GoToInList(str,l)
--find a value within a list and navigate there
local intLength = tonumber(l.COUNT)
if intLength > 0 then
for intPosition = 1, intLength do
if l[tostring(intPosition)]==str then
l.value = intPosition
return intPosition
end
end
end
return 0 --not found
end
function GetSingleValue(list)
return utils.choose(list.value ~=0, list[tostring(list.value)], "")
end
function GetSelectedValues(list)
local tbl = {} --assume nothing selected
local intLength = tonumber(list.COUNT)
if intLength > 0 then
local strSelectionState = list.value -- a sequence of + and -
for i = 1, intLength do
if strSelectionState:sub(i,i) == '\+' then --item is selected
table.insert(tbl, list[tostring(i)]) --insert the list item text in the table
end
end
end
return tbl --an indexed list of strings
end
function SetSelectedValues(list, tblselected) --tbl selected is an indexed list of strings
local tbl = tablex.index_map(tblselected) -- a table keyed on the strings
local strselection = ""
local intLength = tonumber(list.COUNT)
if intLength >0 then
for i = 1, intLength do
strselection = strselection..utils.choose(tbl[list[tostring(i)]], "+", "-")
end
list.value = strselection
end
end
end
--other controls
do
function MakeText(options)
local function CheckTextNotBlank(self)
if self.count == 0 or type(self.value) ~= 'string' then
self.value = options.default or ""
end
end
-- there are no mandatory options
local t= iup.text{wordwrap = options.wordwrap or "YES",
append = options.append or "YES", scrollbar = options.scrollbar or "NO",
multiline = options.multiline or "NO",
visiblelines = options.visiblelines or "2", readonly=options.readonly or "NO",padding ="10x2"}
if options.tip then t.tip = options.tip end
if options.expand then t.expand = options.expand end
if options.norm then t.normalizergroup = options.norm end
if t.multiline == "YES" then
t.expand = options.expand or "YES"
else
t.expand = options.expand or "HORIZONTAL"
end
if options.filter then t.filter = options.filter end
if options.killfocus ~= nil then
t.killfocus_cb = function(self) CheckTextNotBlank(self) options.killfocus() end
else
t.killfocus_cb = function(self) CheckTextNotBlank(self) end
end
if options.mask ~= nil then t.mask = options.mask end
if options.name then iup.SetHandle(options.name, t) end
if options.default then t.value = options.default else t.value = "" end
return t
end
function MakeLabel(options)
local l = iup.label{title = options.title or "", normalizergroup = options.norm or norm, wordwrap = options.wordwrap or "NO"}
if options.tip then l.tip = options.tip end
return l
end
function MakeToggle(options)
local t=iup.toggle{}
local action = options.action
if action ~= nil then
t = iup.toggle{action = function(state) action(state) end}
end
t.alignment="ALEFT"
t.title = options.title or ""
if options.tip then t.tip = options.tip end
if options.name then iup.SetHandle(options.name, t) end
if options.value then t.value = options.value end
return t
end
function MakeExpander(content, title, state)
local e = iup.expander{content}
e.title = title
e.state = state
e.visible = "YES"
return e
end
function MakeGridbox(options)
local gbox = iup.gridbox{}
gbox.expandchildren = options.expandchildren or "HORIZONTAL"
gbox.alignmentlin = options.alignmentlin or "ACENTER"
gbox.orientation = options.orientation or "HORIZONTAL"
gbox.numdiv = options.numdiv or "2"
gbox.gaplin = "10"
gbox.gapcol = "10"
gbox.normalizesize = "YES"
return gbox
end
function MakeDateField(options)
local txtEntryDate = nil
local function ValidateDate()
txtEntryDate.fgcolor = TXTFGCOLOR
local booDateValid = true
local d = txtEntryDate.value
if type(d) == "string" then
_, booDateValid = TestTextDate(d)
end
if not booDateValid then
txtEntryDate.fgcolor = colorred
end
end
txtEntryDate = MakeText{visiblelines = "1", killfocus=ValidateDate, norm = options.norm or norm2, tip = "Enter date. If you type in an invalid date the text will turn red.", norm = options.norm or norm, expand = "NO"}
return iup.hbox{MakeLabel{title=options.title or "", tip = "Enter date. If you type in an invalid date the text will turn red."}, txtEntryDate; alignment = "ACENTER"}
end
end
end
--[[FH Data API
@Author: Helen Wright
@Version: 1.1
@LastUpdated: 29 October 2019
@Description: Helper functions to extend the FH API
]]--
do
--pre-requisites
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
require("iuplua") -- GUI
-- also requires
-- fh API
-- Dialog boilerplate
function PromptForRecords(options)
local tblrec = {}
local tbltxt = {}
local tblptr = {}
local currdlg=IdentifyActiveWindow()
if options.recordcount then
tblptr = fhPromptUserForRecordSel(options.recordtype or "INDI", options.recordcount)
else
tblptr = fhPromptUserForRecordSel(options.recordtype or "INDI")
end
currdlg.bringfront = "YES"
if #tblptr > 0 then
for i, p in ipairs(tblptr) do
tblrec[i]=fhGetRecordId(p)
tbltxt[i]=fhGetDisplayText(p)
end
else
tblrec = {}
tbltxt={}
end
return tblptr, tblrec, tbltxt
end
function GetCurrentRecord(options)
local tblrec = {}
local tbltxt = {}
local tblptr = fhGetCurrentRecordSel(options.recordtype or "INDI")
if #tblptr > 0 then
for i, p in ipairs(tblptr) do
tblrec[i]=fhGetRecordId(p)
tbltxt[i]=fhGetDisplayText(p)
end
end
return tblptr, tblrec, tbltxt
end
function Date()
local currdlg=IdentifyActiveWindow()
local date = fhPromptUserForDate()
currdlg.bringfront = "YES"
return date
end
function TestTextDate(strDateText)
local dt = fhNewDate()
if strDateText == "" then
return dt, true
elseif (stringx.startswith(strDateText,'"') and stringx.endswith(strDateText, '"'))
or (stringx.startswith(strDateText,"'") and stringx.endswith(strDateText, "'")) then
return dt, dt:SetValueAsText(strDateText, true) -- a date phrase
else
return dt, dt:SetValueAsText(strDateText, false) --not a date phrase
end
end
function Places()
local tblPlaces = {}
local ptrPlace = fhNewItemPtr()
ptrPlace:MoveToFirstRecord("_PLAC")
while ptrPlace:IsNotNull() do -- Loop through all Place Records
local strPlace = fhGetValueAsText(fhGetItemPtr(ptrPlace,"~.TEXT"))
tblPlaces[strPlace]=1
ptrPlace:MoveNext()
end
return tblPlaces
end
function Addresses()
local tblAddresses = {}
local ptr = fhNewItemPtr()
local pplace = fhNewItemPtr()
for _, rectype in pairs({"INDI","FAM","REPO","SUBM"}) do
ptr:MoveToFirstRecord(rectype)
while ptr:IsNotNull() do
if fhGetTag(ptr) == "ADDR" then
local strAddress = fhGetValueAsText(ptr)
pplace:MoveToParentItem(ptr) -- i.e. Fact for current address
pplace:MoveTo(pplace,"~.PLAC") -- pplace is place pointer
local strPlace= fhGetValueAsText(pplace)
if strPlace == nil then strPlace = "" end
if tblAddresses[strPlace..strAddress] == nil then --place address combination is unique
tblAddresses[strPlace..strAddress]={strAddress, strPlace}
end
end
ptr:MoveNextSpecial()
end
end
return tblAddresses
end
function AddressesForPlace(place, tblAddress)
local tblresult = {}
for _,a in pairs(tblAddress) do
if a[2]==place then
table.insert(tblresult,a[1])
end
end
return tblresult
end
function CreateNote(ptr, value)
local ptrNew = fhCreateItem("NOTE2",ptr)
fhSetValueAsText(ptrNew,value)
return ptrNew
end
function CreateSharedNote(value)
local ptrNew = fhCreateItem("NOTE")
local Text = fhCreateItem("TEXT",ptrNew)
fhSetValueAsText(Text,value)
return ptrNew
end
function CreateNoteLink(ptrParent,ptrNote)
local ptrWork = fhCreateItem("NOTE", ptrParent)
fhSetValueAsLink(ptrWork, ptrNote)
end
function CreateTagAsText(ptrParent, strTag, strText)
local ptrNew = fhCreateItem(strTag, ptrParent)
fhSetValueAsText(ptrNew,strText)
return ptrNew
end
function SetDate(ptrParent, dtDate)
local ptrDate = fhCreateItem("DATE", ptrParent)
fhSetValueAsDate(ptrDate, dtDate)
return ptrDate
end
function allItems(...)
local iTypeCount = nil
local iPos = 1
local p1 = fhNewItemPtr()
local p2 = fhNewItemPtr()
local tblRecTypes = {}
if arg['n'] == 0 then
-- No parameter do all Record Types
iTypeCount = fhGetRecordTypeCount() -- Get Count of Record types
for i = 1,iTypeCount do
tblRecTypes[i] = fhGetRecordTypeTag(i)
end
else
-- Got Parameters Process them instead
tblRecTypes = arg
iTypeCount = arg['n']
end
p1:MoveToFirstRecord(tblRecTypes[iPos])
return function()
repeat
while p1:IsNotNull() do
p2:MoveTo(p1)
p1:MoveNextSpecial()
if p2:IsNotNull() then
return p2
end
end
-- Loop through Record Types
iPos = iPos + 1
if iPos <= iTypeCount then
p1:MoveToFirstRecord(tblRecTypes[iPos])
end
until iPos > iTypeCount
end
end
function records(type)
local pi = fhNewItemPtr()
local p2 = fhNewItemPtr()
pi:MoveToFirstRecord(type)
return function ()
p2:MoveTo(pi)
pi:MoveNext()
if p2:IsNotNull() then return p2 end
end
end
function CopyBranch(ptrSource,ptrTarget)
local ptrNew = fhCreateItem(fhGetTag(ptrSource),ptrTarget,true)
fhSetValue_Copy(ptrNew,ptrSource)
CopyChildren(ptrSource,ptrNew)
end -- function CopyBranch
function CopyChildren(ptrSource,ptrTarget)
local ptrFrom = fhNewItemPtr()
ptrFrom = ptrSource:Clone()
ptrFrom:MoveToFirstChildItem(ptrFrom)
while ptrFrom:IsNotNull() do
CopyBranch(ptrFrom,ptrTarget)
ptrFrom:MoveNext()
end
end -- function CopyChildren
end
--[[Unicode conversion
@Author: Mike Tate
@Description: Convert betweeen UTF16 and UF8/ANSI
]]
do
--Prerequisites:
-- fh API
-- fh version > 5
function StrANSI_UTF8(strText)
return fhConvertANSItoUTF8(strText) --requires FH version >= 5
end -- function StrANSI_UTF8
function StrUTF8_ANSI(strText)
return fhConvertUTF8toANSI(strText) --requires FH version >= 5
end
-- UTF8 <=> UTF16
local intTop10 = 0
function StrUtf16toUtf8(strChar1,strChar2) -- Convert a UTF-16 word or pair to UTF-8 bytes --
local intUtf16 = string.byte(strChar2) * 0x100 + string.byte(strChar1)
if intUtf16 < 0x80 then -- U+0000 to U+007F (ASCII)
return string.char(intUtf16)
end
if intUtf16 < 0x800 then -- U+0080 to U+07FF
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16
return string.char( intByte2 + 0xC0, intByte1 + 0x80 )
end
if intUtf16 < 0xD800 -- U+0800 to U+FFFF
or intUtf16 > 0xDFFF then
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte3 = intUtf16
return string.char( intByte3 + 0xE0, intByte2 + 0x80, intByte1 + 0x80 )
end
if intUtf16 < 0xDC00 then -- U+10000 to U+10FFFF High 16-bit Surrogate Supplementary Planes -- V2.6
intTop10 = ( intUtf16 - 0xD800 ) * 0x400 + 0x10000
return ""
end
intUtf16 = intUtf16 - 0xDC00 + intTop10 -- U+10000 to U+10FFFF Low 16-bit Surrogate Supplementary Planes -- V2.6
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte3 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte4 = intUtf16
return string.char( intByte4 + 0xF0, intByte3 + 0x80, intByte2 + 0x80, intByte1 + 0x80 )
end -- function StrUtf16toUtf8
function StrUTF16_UTF8(strText) --Encode UTF16 words into UTF8 bytes
return ( (strText or ""):gsub("(.)(.)",StrUtf16toUtf8) )
end -- function StrUTF16_UTF8
local tblByte = {}
local tblLead = { 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }
function StrUtf8toUtf16(strChar) -- Convert UTF-8 bytes to a UTF-16 word or pair
-- Convert any UTF-8 multibytes to UTF-16 --
local function strUtf8()
if #tblByte > 0 then
local intUtf16 = 0
for intIndex, intByte in ipairs (tblByte) do -- Convert UTF-8 bytes to UNICODE U+0080 to U+10FFFF
if intIndex == 1 then
intUtf16 = intByte - tblLead[#tblByte]
else
intUtf16 = intUtf16 * 0x40 + intByte - 0x80
end
end
if intUtf16 > 0xFFFF then -- U+10000 to U+10FFFF Supplementary Planes -- V2.6
tblByte = {}
intUtf16 = intUtf16 - 0x10000
local intLow10 = 0xDC00 + ( intUtf16 % 0x400 ) -- Low 16-bit Surrogate
local intTop10 = 0xD800 + math.floor( intUtf16 / 0x400 ) -- High 16-bit Surrogate
local intChar1 = intTop10 % 0x100
local intChar2 = math.floor( intTop10 / 0x100 )
local intChar3 = intLow10 % 0x100
local intChar4 = math.floor( intLow10 / 0x100 )
return string.char(intChar1,intChar2,intChar3,intChar4) -- Surrogate 16-bit Pair
end
if intUtf16 < 0xD800 -- U+0080 to U+FFFF (except U+D800 to U+DFFF) -- V2.6
or intUtf16 > 0xDFFF then -- Basic Multilingual Plane
tblByte = {}
local intChar1 = intUtf16 % 0x100
local intChar2 = math.floor( intUtf16 / 0x100 )
return string.char(intChar1,intChar2) -- BPL 16-bit
end
local strUtf8 = "" -- U+D800 to U+DFFF Reserved Code Points -- V2.6
for _, intByte in ipairs (tblByte) do
strUtf8 = strUtf8..string.format("%.2X ",intByte)
end
local strUtf16 = string.format("%.4X ",intUtf16)
fhMessageBox("\n UTF-16 Reserved Code Point U+D800 to U+DFFF \n UTF-16 = "..strUtf16.." UTF-8 = "..strUtf8.."\n Character will be replaced by a question mark. \n")
tblByte = {}
return "?\0"
end
return ""
end -- local function strUtf8
local intUtf8 = string.byte(strChar)
if intUtf8 < 0x80 then -- U+0000 to U+007F (ASCII)
return strUtf8()..strChar.."\0" -- Previous UTF-8 multibytes + current ASCII char
end
if intUtf8 >= 0xC0 then -- Next UTF-8 multibyte start
local strUtf16 = strUtf8()
table.insert(tblByte,intUtf8)
return strUtf16 -- Previous UTF-8 multibytes
end
table.insert(tblByte,intUtf8)
return ""
end -- function StrUtf8toUtf16
function StrUTF8_UTF16(strText) -- Encode UTF-8 bytes into UTF-16 words
tblByte = {} -- (0xFF) flushes last UTF-8 character
return ((strText or "")..string.char(0xFF)):gsub("(.)",StrUtf8toUtf16)
end
end
--[[File handling
@Author: Helen Wright
@Version: 1.0
@LastUpdated: 29 October 2019
@Description: Helper functions for File handling
@V1.0: Initial version.
]]
do
--prerequisites
require ("luacom") --Microsoft's Component Object Model
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
-- also requires
-- Unicode boilerplate
function FileExists(strFile)
return path.isfile(strFile)
end
function FileDownload(strURL, strFile)
-- retrieve the content of a URL
local function httpRequest(strURL)
local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
http:Open("GET",strURL,false)
http:Send()
http:WaitForResponse(30)
return http.Responsebody
end
local isOK, body = pcall(httpRequest, strURL)
if isOK then
if body ~= nil then
-- save the content to a file (assume it's binary)
local f = assert(io.open(strFile, "wb")) -- open in "binary" mode -- throws an error if the open fails
if f ~= nil then --open succeeded
f:write(body)
f:close()
end
return true
end
end
return false,'An error occurred in Download. Please try later'
end
function WriteFile(strContents, strTarget)
local f = assert(io.open(strTarget, "wb")) -- open for writing in "binary" mode -- throws an error if the open fails
if f ~= nil then --open succeeded
f:write(strContents)
f:close()
end
end
function ReadFile(strSource)
local strContent = ""
local f = assert(io.open(strSource, "rb")) -- open for reading in "binary" mode -- throws an error if the open fails
if f ~= nil then --open succeeded
strContent = f:read("*all")
f:close()
end
return strContent
end
function CopyFile(old, new)
WriteFile(ReadFile(old), new) --doing it this way preserve file name case
end
local bomUtf16= string.char(0xFF,0xFE) -- "ÿþ"
function WriteUTF16File(strUTF8, strTarget)
--strUTF8 is a UTF8 or ANSI string
WriteFile(bomUtf16 .. StrUTF8_UTF16(strUTF8:gsub("\n","\r\n") ), strTarget)
end
function ReadUTF16File(strSource)
local strUTF8 = ReadFile(strSource)
return StrUTF16_UTF8(string.gsub(string.gsub(strUTF8,bomUtf16,""), "\r\n","\n")) --remove BOM, rationalise newlines and convert to UTF8
end
function DeleteFile(strFile)
os.remove(strFile)
end
function RenameFile(oldname,newname)
os.rename(oldname, newname)
end
--Chillcode code snippets for loading/saving tables to a file -- used for storage of template definitions
--[[
Save Table to File
Load Table from File
v 1.0
Lua 5.2 compatible
Only Saves Tables, Numbers and Strings
Insides Table References are saved
Does not save Userdata, Metatables, Functions and indices of these
----------------------------------------------------
table.save( table , filename )
on failure: returns an error msg
----------------------------------------------------
table.load( filename or stringtable )
Loads a table that has been saved via the table.save function
on success: returns a previously saved table
on failure: returns as second argument an error msg
----------------------------------------------------
Licensed under the same terms as Lua itself.
]]--
-- declare local variables
--// exportstring( string )
--// returns a "Lua" portable version of the string
local function exportstring( s )
return string.format("%q", s)
end
--// The Save Function
function table.save(tbl,filename )
local charS,charE = " ","\n"
local file,err = io.open( filename, "wb" )
if err then return err end
-- initiate variables for save procedure
local tables,lookup = { tbl },{ [tbl] = 1 }
file:write( "return {"..charE )
for idx,t in ipairs( tables ) do
file:write( "-- Table: {"..idx.."}"..charE )
file:write( "{"..charE )
local thandled = {}
for i,v in ipairs( t ) do
thandled[i] = true
local stype = type( v )
-- only handle value
if stype == "table" then
if not lookup[v] then
table.insert( tables, v )
lookup[v] = #tables
end
file:write( charS.."{"..lookup[v].."},"..charE )
elseif stype == "string" then
file:write( charS..exportstring( v )..","..charE )
elseif stype == "number" then
file:write( charS..tostring( v )..","..charE )
end
end
for i,v in pairs( t ) do
-- escape handled values
if (not thandled[i]) then
local str = ""
local stype = type( i )
-- handle index
if stype == "table" then
if not lookup[i] then
table.insert( tables,i )
lookup[i] = #tables
end
str = charS.."[{"..lookup[i].."}]="
elseif stype == "string" then
str = charS.."["..exportstring( i ).."]="
elseif stype == "number" then
str = charS.."["..tostring( i ).."]="
end
if str ~= "" then
stype = type( v )
-- handle value
if stype == "table" then
if not lookup[v] then
table.insert( tables,v )
lookup[v] = #tables
end
file:write( str.."{"..lookup[v].."},"..charE )
elseif stype == "string" then
file:write( str..exportstring( v )..","..charE )
elseif stype == "number" then
file:write( str..tostring( v )..","..charE )
end
end
end
end
file:write( "},"..charE )
end
file:write( "}" )
file:close()
end
--// The Load Function
function table.load( sfile )
local ftables,err = loadfile( sfile )
if err then return _,err end
local tables = ftables()
if tables then
for idx = 1,#tables do
local tolinki = {}
for i,v in pairs( tables[idx] ) do
if type( v ) == "table" then
tables[idx][i] = tables[v[1]]
end
if type( i ) == "table" and tables[i[1]] then
table.insert( tolinki,{ i,tables[i[1]] } )
end
end
-- link indices
for _,v in ipairs( tolinki ) do
tables[idx][v[2]],tables[idx][v[1]] = tables[idx][v[1]],nil
end
end
return tables[1]
else
iup.Message("Table load fail","No data returned")
end
end
end
--[[Progress Bar
@Title: Progress Bar (drop in)
@Author: Jane Taubman / Mike Tate
@LastUpdated: January 2013
@Description: Allows easy adding of a Progress Bar to any long running Plugin
]]
do
--prerequisites
require("iuplua") -- GUI
function ProgressBar(tblGauge)
local tblGauge = tblGauge or {} -- Optional table of external parameters
local strFont = tblGauge.Font or nil -- Font dialogue default is current font
local strButton = tblGauge.Button or "255 0 0" -- Button colour default is red
local strBehind = tblGauge.Behind or "255 255 255" -- Background colour default is white
local intShowX = tblGauge.ShowX or iup.CENTER -- Show window default position is central
local intShowY = tblGauge.ShowY or iup.CENTER
local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
local lblText, barGauge, lblDelta, btnStop, dlgGauge
local function doFocus() -- Bring the Progress Bar window into Focus
dlgGauge.bringfront="YES" -- If used too often, inhibits other windows scroll bars, etc
end -- local function doFocus
local function doUpdate() -- Update the Progress Gauge and the Delta % with clock
barGauge.value = intVal
lblDelta.title = string.format("%4d %% %s ",intPercent,strClock)
end -- local function doUpdate
local function doReset() -- Reset all dialogue variables and Update display
intVal = 0 -- Current value of Progress Bar
intPercent = 0.01 -- Percentage of progress
intStart = os.time() -- Start time of progress
intDelta = 0 -- Delta time of progress
intScale = math.ceil( intMax / 1000 ) -- Scale of percentage per second (this guess is corrected in Step function)
strClock = "00 : 00 : 00" -- Clock delta time display
isBarStop = false -- Stop button pressed signal
doUpdate()
doFocus()
end -- local function doReset
local tblProgressBar = {
Start = function(strTitle,intMaximum) -- Create & start Progress Bar window
if not dlgGauge then
strTitle = strTitle or "" -- Dialogue and button title
intMax = intMaximum or 100 -- Maximun range of Progress Bar, default is 100
local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30" -- Adjust Stop button size to Title
lblText = iup.label { title=" ", expand="YES", alignment="ACENTER", tip="Progress Message" }
barGauge = iup.progressbar { rastersize="400x30", value=0, max=intMax, tip="Progress Bar" }
lblDelta = iup.label { title=" ", expand="YES", alignment="ACENTER", tip="Percentage and Elapsed Time" }
btnStop = iup.button { title=" Stop "..strTitle, rastersize=strSize, fgcolor=strButton, tip="Stop Progress Button", action=function() isBarStop = true end } -- Signal Stop button pressed return iup.CLOSE -- Often caused main GUI to close !!!
dlgGauge = iup.dialog { title=strTitle.." Progress ", font=strFont, dialogframe="YES", background=strBehind, -- Remove Windows minimize/maximize menu
iup.vbox{ alignment="ACENTER", gap="10", margin="10x10",
lblText,
barGauge,
lblDelta,
btnStop,
},
move_cb = function(self,x,y) tblGauge.ShowX = x tblGauge.ShowY = y end,
close_cb = btnStop.action, -- Windows Close button = Stop button
}
dlgGauge:showxy(intShowX,intShowY) -- Show the Progress Bar window
doReset() -- Reset the Progress Bar display
end
end,
Message = function(strText) -- Show the Progress Bar message
if dlgGauge then lblText.title = strText end
end,
Step = function(intStep) -- Step the Progress Bar forward
if dlgGauge then
intVal = intVal + ( intStep or 1 ) -- Default step is 1
local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
if intPercent ~= intNew then -- Update progress once per percent or per second, whichever is smaller
intPercent = math.max( 0.1, intNew ) -- Ensure percentage is greater than zero
if intVal > intMax then intVal = intMax intPercent = 100 end -- Ensure values do not exceed maximum
intNew = os.difftime(os.time(),intStart)
if intDelta < intNew then -- Update clock of elapsed time
intDelta = intNew
intScale = math.ceil( intDelta / intPercent ) -- Scale of seconds per percentage step
local intHour = math.floor( intDelta / 3600 )
local intMins = math.floor( intDelta / 60 - intHour * 60 )
local intSecs = intDelta - intMins * 60 - intHour * 3600
strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
end
doUpdate() -- Update the Progress Bar display
end
iup.LoopStep()
end
end,
Focus = function()
if dlgGauge then doFocus() end -- Bring the Progress Bar window to front
end,
Reset = function() -- Reset the Progress Bar display
if dlgGauge then doReset() end
end,
Stop = function() -- Check if Stop button pressed
iup.LoopStep()
return isBarStop
end,
Close = function() -- Close the Progress Bar window
isBarStop = false
if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
end,
} -- end newProgressBar
return tblProgressBar
end -- function ProgressBar
end
---------------------------------------------------------------
--DATA CLASSES
---------------------------------------------------------------
--[[Various source related classes
@Author: Helen Wright
@V1.1: Initial version.
@LastUpdated: 5 November 2019
A UI class that implement:
1. A source selector optionally with a source definition control, with options to edit the selected source or create a new source
2. A citation definition control
Multiple instances of the class can be created by a plugin.
2. a Class that implements a number of source-related utility functions
]]--
function SourceTools ()
--a class containing a number of utility routines for dealing with Sources
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
utils = require("pl.utils")
--also requires progress boilerplate and fh API
local Link = function(ptrParent,ptrSour)
local ptrWork = fhCreateItem("SOUR", ptrParent)
fhSetValueAsLink(ptrWork, ptrSour)
return ptrWork
end
local CitationDetail = function(ptrParent, values) --values: entrydate, quality, where, from, citationnote
local ptrData = nil
if not types.is_empty(values.where) then
local ptrP = fhCreateItem('PAGE',ptrParent,true)
fhSetValueAsText(ptrP, values.where)
end
if not types.is_empty(values.entrydate) then
if ptrData == nil then ptrData = fhCreateItem('DATA', ptrParent, true) end
local ptrD = fhCreateItem('DATE',ptrData,true)
fhSetValueAsText(ptrD, values.entrydate)
end
if not types.is_empty(values.from) then
if ptrData == nil then ptrData = fhCreateItem('DATA', ptrParent, true) end
local ptrT = fhCreateItem('TEXT',ptrData, true)
fhSetValueAsText(ptrT, values.from)
end
if not types.is_empty(values.quality) then
local ptrQ = fhCreateItem('QUAY',ptrParent,true)
fhSetValueAsText(ptrQ,values.quality)
end
if not types.is_empty(values.citationnote) then
local ptrN = fhCreateItem('NOTE2',ptrParent,true)
fhSetValueAsText(ptrN,values.citationnote)
end
end
return{Link = Link, CitationDetail = CitationDetail}
end
local mySourceTools = SourceTools()
function Source (imax, booE, booN, booC) --optional parameters to allow editing or creation of sources and citations
--prerquisities: Penlight libraries, DH Data api
--TODO: Include making new sources and displaying/editing source fields
--private state variables
local booEdit = booE or false --is editing source fields allowed
local booNew = booN or false --is creating new sources allowed
local imaxSource = imax or 1 --how many sources can be selected at one (0 = unlimited)
local booCitation = booC or false --is a citation container required
if booCitation then imaxSource = 1 end --only one source allowed if a citation is required
local caller = CallerSources()
local txtSources = nil --text field to display selected sources
local tblSources = {} --table of selected source pointers
local btnSources = nil --button to select a source
local boxSource = nil
local boxCitation = nil --will be used to hold citation details if required
local booEntryDateValid = true
local tblAssessment = {"Unreliable", "Questionable", "Secondary Evidence", "Primary Evidence", "" } --Source assessment text values
--public methods
local function Populate(tbltxt)
txtSources.VALUE = ""
if #tbltxt > 0 then txtSources.value = table.concat(tbltxt,"\n") end
end
local function ClearCitation()
if boxCitation then
ClearContainer(boxCitation, {})
booEntryDateValid = true
end
end
local Selector = function (strSourceTip, strOrientation, strCitationTip) --returns an iup control
local function Initialise()
local tbltxt = {}
tblSources, _, tbltxt = GetCurrentRecord {recordtype="SOUR"}
Populate(tbltxt)
end
local function MakeCitation()
local function DateDetails()
local dtEntryDate = nil --result of entry date operations
local txtEntryDate = nil
local function DateChoose()
dtEntryDate = Date()
if dtEntryDate ~= nil then
txtEntryDate.value = dtEntryDate:GetDisplayText()
else
txtEntryDate.value= ""
end
booEntryDateValid = true
txtEntryDate.fgcolor = TXTFGCOLOR
end
local function ValidateDate()
txtEntryDate.fgcolor = TXTFGCOLOR
booEntryDateValid = true
local d = txtEntryDate.value
if type(d) == "string" then
dtEntryDate, booEntryDateValid = TestTextDate(d)
end
if not booEntryDateValid then
txtEntryDate.fgcolor = colorred
end
end
local btnGetEntryDate = MakeButton{title="Date...", callback=DateChoose, tip = "Use the button to call Family Historian's Date Entry Assistant, or type directly into the Date box. \n If you type in an invalid date the text will turn red."}
txtEntryDate = MakeText{visiblelines = "1", killfocus=ValidateDate, norm = norm2, tip = "Use the button to call Family Historian's Date Entry Assistant, or type directly into the Date box. \n If you type in an invalid date the text will turn red.", name = "entrydate"}
return iup.hbox{btnGetEntryDate, txtEntryDate; alignment = "ACENTER"}
--txtEntryDate.value will be returned as part of the Citation Data
end
local function QualityDetails()
local listQuality = nil
local function CheckQualityValue(self, text, item, state)
if item == 5 and state == 1 then listQuality.value = "0" end
end
listQuality = MakeList{sort = "NO", action = CheckQualityValue, values = tblAssessment, norm = norm2, name = "quality"}
return iup.hbox{MakeLabel{title = "Assessment"},listQuality; alignment = "ACENTER"}
--listquality.value will be returned as part of the Citation Data
end
local txtWhere = MakeText{visiblelines="1", name = "where"}
local txtFrom = MakeText{scrollbar="VERTICAL", multiline="YES", name = "from"}
local txtCitationNote = MakeText{scrollbar="VERTICAL", multiline="YES", name = "citationnote"}
local box = iup.vbox{
iup.hbox{DateDetails(),QualityDetails()},
iup.vbox{MakeLabel{title = "Where in Source"}, txtWhere},
iup.vbox{MakeLabel{title = "Text from Source"}, txtFrom},
iup.vbox{MakeLabel{title = "Citation Note"}, txtCitationNote};
}
return MakeExpander(box,
strCitationTip or "Citation (Optional).",
"OPEN")
end
local function Choose()
local PreviousSource = tblSources[1] --will only be used if booCitation is true
local tbltxt = {}
if imaxSource == 0 then
tblSources, _, tbltxt = PromptForRecords {recordtype="SOUR"}
else
tblSources, _, tbltxt = PromptForRecords {recordtype="SOUR", recordcount = imaxSource}
end
Populate(tbltxt)
if booCitation and tblSources[1] ~= PreviousSource then
ClearCitation()
end
caller.CheckUIStatus()
end
strSourceTip = strSourceTip or "Use the Source button to choose sources(s) from within Family Historian."
strOrientation = strOrientation or "HORIZONTAL"
txtSources = MakeText{readonly = "YES", expand = "HORIZONTAL", norm = norm2, tip = strSourceTip, multiline = utils.choose(imaxSource ~= 1, "YES", "NO")}
Initialise()
btnSources = MakeButton{title="Source...", callback=Choose, tip = strSourceTip}
if strOrientation == "HORIZONTAL" then
boxSource = iup.hbox{btnSources, txtSources; alignment = "ACENTER"}
else
boxSource = iup.vbox{btnSources, txtSources; alignment = "ACENTER"}
end
if booCitation then
boxCitation = MakeCitation()
return iup.vbox{boxSource, boxCitation}
else
return boxSource
end
end
local SourceList = function()-- returns a table of source pointers
return tblSources
end
local ClearSelector = function()
tblSources = {}
txtSources.value = ""
if booCitation then ClearCitation() end
end
local DisableSelector = function (boo)
local strStatus = utils.choose(boo, "NO", "YES")
btnSources.active = strStatus
txtSources.active = strStatus
if boo then ClearSelector() end
end
local CitationData = function() --returns a table of Citation Data
local tblCitationElements = GetDataElements(boxCitation,{},{},true)
local tbldata = GetContainerData(tblCitationElements) --entrydate, quality, where, from, citationnote
if not types.is_empty(tbldata.quality) then tbldata.quality = tblAssessment[tonumber(tbldata.quality)] end
return tbldata
end
local CitationState = function(state)
boxCitation.state = state
end
local CitationOK = function()
return booEntryDateValid
end
--expose public methods
return{
Selector = Selector,
SourceList = SourceList,
ClearSelector = ClearSelector,
DisableSelector = DisableSelector,
CitationData = CitationData,
CitationState = CitationState,
CitationOK = CitationOK
}
end
function CallerSources ()
local CheckUIStatus = function()
AdjustButtons()
end
return {CheckUIStatus = CheckUIStatus}
end
--[[Individuals class
@Author: Helen Wright
@V1.0: Initial version.
@LastUpdated: 27 October 2019
A class that implements a field to prompt for one or more individuals
Multiple instances of the Individuals class can be created by a plugin
]]--
function Individuals (iMax)
--local constants go here
--private state variables
local tblIndividuals = {}
local imaxIndividuals = iMax or 0 --0 for unlimited
local txtIndividuals = nil
local btnIndividuals = nil
local caller = nil --customised class in caller plugin
--private methods
local function Populate(tbltxt)
txtIndividuals.VALUE = ""
if #tbltxt > 0 then txtIndividuals.value = table.concat(tbltxt,"\n") end
end
--public methods
local Selector = function (strIndividualTip, strOrientation)
local function Initialise()
local tbltxt = {}
tblIndividuals, _, tbltxt = GetCurrentRecord {recordtype="INDI"}
Populate(tbltxt)
end
local function Choose()
local tbltxt = {}
if imaxIndividuals == 0 then
tblIndividuals, _, tbltxt = PromptForRecords {recordtype="INDI"}
else
tblIndividuals, _, tbltxt = PromptForRecords {recordtype="INDI", recordcount = imaxIndividuals}
end
Populate(tbltxt)
caller.CheckUIStatus()
end
local strTip = strIndividualTip or "Use the button to choose individual(s) from within Family Historian."
local Orientation = strOrientation or "HORIZONTAL"
txtIndividuals = MakeText{readonly = "YES", expand = "YES", norm = norm2, tip = strTip, multiline = utils.choose(imaxIndividuals ~= 1, "YES", "NO")}
Initialise()
btnIndividuals = MakeButton{title="Individual(s)...", callback=Choose, tip = strTip}
if Orientation == "HORIZONTAL" then
return iup.hbox{btnIndividuals, txtIndividuals; alignment = "ALEFT"}
else
return iup.vbox{btnIndividuals, txtIndividuals; alignment = "ALEFT"}
end
end
local IndividualList= function () --returns a table of Individual pointers
return tblIndividuals
end
local ClearSelector = function()
tblIndividuals = {}
txtIndividuals.value = ""
end
local DisableSelector = function (boo)
local strStatus = utils.choose(boo, "NO", "YES")
btnIndividuals.active = strStatus
txtIndividuals.active = strStatus
if boo then ClearSelector() end
end
local Load = function(tblptr)
tblIndividuals = tablex.copy(tblptr)
local tbltxt = {}
for i, p in ipairs(tblptr) do
tbltxt[i]=fhGetDisplayText(p)
end
Populate(tbltxt)
end
caller = CallerIndividuals(iMax)
--expose public methods
return{
Selector = Selector,
IndividualList = IndividualList,
ClearSelector = ClearSelector,
DisableSelector = DisableSelector,
Load = Load
}
end
function CallerIndividuals(iMax)
local CheckUIStatus = function()
AdjustButtons()
end
return {CheckUIStatus = CheckUIStatus}
end
--[[Various fact-related classes
@Author: Helen Wright
@V1.0: Initial version.
@LastUpdated: 6 November 2019
]]--
function FactTools ()
--a class containing a number of utility routines for dealing with Facts
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
utils = require("pl.utils")
--also requires progress boilerplate and fh API
local Exists = function(strname, booattr, booIndi)
booIndi = booIndi or true
booattr = booattr or false
local strTag, _ = fhGetFactTag(strname,
utils.choose(booattr, "Attribute", "Event"), utils.choose(booIndi, "INDI", "FAM"), false)
return strTag ~= "", strTag
end
local Create = function(strFactTag, ptr, values)
--values should be a table containing text items (or nil or ""):
--date, age, place, address, place2, attribute, cause and note
--assumes that the calling function has done validation
local ptrFact = fhCreateItem(strFactTag, ptr) --ptr can be to an Indi or Fam record
if not types.is_empty(values.factdate) then --add a fact date
local dt = fhNewDate()
local DateOK = dt:SetValueAsText(values.factdate, true)
SetDate(ptrFact, dt)
end
if not types.is_empty(values.age) then --add an age
CreateTagAsText(ptrFact, "AGE", values.age)
end
if not types.is_empty(values.place) then -- add a place
CreateTagAsText(ptrFact, "PLAC", values.place)
end
if not types.is_empty(values.address) then -- add address
CreateTagAsText(ptrFact, "ADDR", values.address)
end
if not types.is_empty(values.place2) then --add place2
CreateTagAsText(ptrFact, "_PLAC", values.place2)
end
if not types.is_empty(values.attributevalue) then
fhSetValueAsText(ptrFact,values.attributevalue)
end
if not types.is_empty(values.cause) then
CreateTagAsText(ptrFact, "CAUS", values.cause)
end
if not types.is_empty(values.factnote) then --add a note
CreateNote(ptrFact, values.factnote)
end
return ptrFact --pointer to the fact
end
local Iterate = function(pi) --iterate over the facts for an individual or family, pointed to by pi
local pf = fhNewItemPtr()
local pf2 = fhNewItemPtr()
pf:MoveToFirstChildItem(pi)
return function ()
while pf:IsNotNull() do
pf2:MoveTo(pf)
pf:MoveNext()
if fhIsFact(pf2) then return pf2 end
end
end
end
return{Exists = Exists, Create = Create, Iterate = Iterate}
end
local myFactTools = FactTools()
function Facts(imax)
local imaxFact = imax or 1 --how many facts can be selected at one (0 = unlimited)
local caller = CallerFacts()
local tblSelectedFacts = {}
local listFacts = nil
local oldfacts = ""
local txtFacts = nil
local dlgFacts = nil
local boxDetails = nil
local booFactDateValid = true
--public methods
local function Parse()
local tblFactsets = {} --Fact sets found
local strfactsetfiletype = ".fhf"
local cstrFactTypeRoot = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Fact Types"
local cstrCustomFactsDir = cstrFactTypeRoot.."\\Custom\\"
local cstrStandardFactsDir = cstrFactTypeRoot.."\\Standard\\"
local cstrFactSetIndex = cstrStandardFactsDir.."GroupIndex.fhdata"
local tblFacts = {}
local function ParseFile(strFacts)
--returns individual facts that are not hidden; custom facts override standard ones
local function FactIsUnique(fctname)
return tblFacts[fctname]==nil
end
local function ParseAutonote(ident)
local noteparse="%[Text%-"..utils.escape(ident).."%-Auto Note%]%c+(Count=%d+%c+.-)" --escape magic characters in ident string
local strParsed = strFacts:match(noteparse.."%c+%[")
or strFacts:match(noteparse.."$")
if strParsed ~= nil then
local strlines, content = strParsed:match("Count=(%d+)%c+(.*)")
if strlines ~= "0" then
local strAutonote = content:gsub("Line%d+=[n0];","") --remove preamble from all lines
strAutonote = strAutonote:gsub(";(%c+)","%1") --remove final ; at end of lines
return strAutonote:gsub(";$","") --and lose the very last line end
end
end
return "" --Autonote not found or contains no lines
end
local foundCount=0 --to hold the running count of facts in the file
local factCount=tonumber(strFacts:match("Count=(%d+)%c")) -- find the first instance of a Count in the file and get the total count of facts in the file from it
local strParseFact = "%[(FCT%-[_%u%-]+%-([IF])([EA]))%]%c+Name=(.-)%c.-Label=(.-)%c(.-)\r\n{END}%c" --order of these elements in the fact file seems standard
local strFacts = strFacts:gsub("(%c+%[FCT)","\r\n{END}%1").."\r\n{END}"
for ident, recordtype, facttype, name, label, strdetail in strFacts:gmatch(strParseFact) do
local hidden = strdetail:match("Hidden=(%a)%c") or "N"
if recordtype == "I" and hidden == "N" then --this is an individual fact which isn't hidden so process it
if label ~= name then pref=label else pref=name end
if FactIsUnique(pref) then -- need to check that it is unique
--extract details
local facttag = fhGetFactTag(name, utils.choose(facttype=="A","Attribute","Event"), "INDI", false)
local abbr=strdetail:match("Abbr=(.-)%c") or "" --may be blank
local date=strdetail:match("Field Date=(%d)") or "1"
local age=strdetail:match("Field Age=(%d)") or "1"
local place=strdetail:match("Field Place=(%d)") or "1"
local address=strdetail:match("Field Address=(%d)") or "1"
local note=strdetail:match("Field Note=(%d)") or "1"
--need to sanitise ident which could have a bunch of extraneous material tacked on the front and use it to get the autonote
-- local ident=sident:match(".*%[(FCT%-.-%-)$")..recordtype..facttype
local autonote=ParseAutonote(ident)
tblFacts[pref] = {}
tblFacts[pref]["ident"] = ident
tblFacts[pref]["facttype"] = utils.choose(facttype=="A","attribute","event")
tblFacts[pref]["facttag"] = facttag
tblFacts[pref]["factname"] = name
tblFacts[pref]["factlabel"] = label
tblFacts[pref]["factabbr"] = abbr
tblFacts[pref]["factdate"] = date
tblFacts[pref]["age"] = age
tblFacts[pref]["place"] = place
tblFacts[pref]["place2"] = utils.choose(facttag == "IMMI" or facttag == "EMIG", "1", "0")
tblFacts[pref]["cause"] = utils.choose(facttag == "DEAT", "1", "0")
tblFacts[pref]["attributevalue"] = utils.choose(facttype=="A","1","0")
tblFacts[pref]["address"] = address
tblFacts[pref]["factnote"] = note
tblFacts[pref]["autonote"] = autonote
--add to the Facts table (keyed on pref)
-- end
end
foundCount = foundCount+1
if foundCount == factCount then return end --decide whether to break: have all facts been found (avoids unnecessary searching of file tail)?
end
end
end
local function GetFactSize(tblFactsets)
local intSize = 0
for _, filename in ipairs(tblFactsets) do -- Get facts sets file sizes
local attr = lfs.attributes(utils.choose(filename=="Standard",cstrStandardFactsDir, cstrCustomFactsDir)..filename..strfactsetfiletype)
intSize = intSize + attr.size
end
return intSize
end
-- get list of files to process
local strFactsSetList = ReadUTF16File(cstrFactSetIndex)
for fs,fileind in strFactsSetList:gmatch("([%w_ ]+)=(%d+)") do
tblFactsets[tonumber(fileind)]=fs
end
local progbar = ProgressBar()
if GetFactSize(tblFactsets) > 400000 then -- start progressbar
progbar.Start("Loading Fact Sets",#tblFactsets)
end
for _, filename in ipairs(tblFactsets) do --process facts sets in ascending fileind order to ensure older definitions don't overwrite new
progbar.Message("Loading "..filename)
progbar.Step(1)
local strFactsSet = ReadUTF16File(utils.choose(filename=="Standard",cstrStandardFactsDir, cstrCustomFactsDir)..filename..strfactsetfiletype)
ParseFile(strFactsSet)
end
progbar.Close()
--tblFacts now holds the list of facts and their associated parameters
return tblFacts
end
local tblFacts = Parse()
local function RestrictedFields()
if boxDetails ~= nil then
local tblboxDetails = GetDataElements(boxDetails, {}, {})
tblboxDetails[5].ACTIVE = "NO" -- Place 2
tblboxDetails[6].ACTIVE = "NO" -- Attribute
tblboxDetails[7].ACTIVE = "NO" -- Cause
local tblSelFacts = utils.choose (imax ~= 1, tblSelectedFacts, {listFacts[1]})
for _, pref in pairs(tblSelFacts) do
local tblDetails = tblFacts[pref]
if tblDetails["place2"]== "1" then tblboxDetails[5].ACTIVE = "YES" end
if tblDetails["facttype"] == "attribute" then tblboxDetails[6].ACTIVE = "YES" end
if tblDetails["cause"] == "1" then tblboxDetails[7].ACTIVE = "YES" end
end
end
end
local EnableRestrictedFactFields = function()
RestrictedFields()
end
local Selector = function (strT, strO)
--returns an iup control which may be:
-- a dropdown list if imax == 1
-- a text box plus button plus popup behind it if imax ~= 1
strOrientation = strO or "HORIZONTAL"
local function MakeBoxChooseFacts()
local function FactsConfirm()
txtFacts.value = "" -- empty existing fact selections if anything
tblSelectedFacts = {}
--populate txtFacts and tblSelectedTasks from listFacts
local strSelectionState = listFacts.value -- a sequence of + and -
for i = 1, #strSelectionState do
if strSelectionState:sub(i,i) == "\+" then --Template is selected
table.insert(tblSelectedFacts, listFacts[i])
txtFacts.APPEND = listFacts[i]
end
end
return true
end
local function FactsClear()
listFacts.value = ""
end
local function FactsRestore()
listFacts.value = oldfacts
return FactsConfirm()
end
local function FactsShow(state)
oldfacts = listFacts.value
end
local btnChooseFacts = MakeButton{title="Confirm Facts", callback=FactsConfirm, close="YES", tip = "Confirm that you have selected all the relevant facts."}
local btnCancelChoice = MakeButton{title="Cancel", tip = "Discard changes and exit.", callback = FactsRestore, close = "YES"}
local btnClear = MakeButton{title="Clear", callback=FactsClear, tip = "Clear fact selection."}
listFacts = MakeList{dropdown="NO", visiblelines="20", multiple="YES", values = tblFacts}
d = MakeDialog(
iup.vbox{listFacts,
iup.hbox{iup.fill{}, btnClear, btnChooseFacts, btnCancelChoice};expandchildren = "HORIZONTAL"
},
{title = "Choose Facts", menubox="NO", show = FactsShow })
d.close_cb = function()
d.show_cb = nil --avoid iup bug
return iup.CLOSE
end
return d
end
local function FactsChoose()
--pop up a multiselect combolist
dlgFacts:popup(iup.CENTERPARENT, iup.CENTERPARENT) --display the fact choice dialog
caller.CheckUIStatus()
end
if imaxFact == 1 then
--TODO: Not tested -- single fact selector
local function Updated(l, text,item,state)
caller.CheckUIStatus()
end
local strTip = strT or "Use the dropdown to choose a fact."
local lblChooseFacts = MakeLabel{title = "Choose fact", tip = strTip}
listFacts = MakeList{tip = strTip, values = tblFacts, action = Updated}
return utils.choose(strOrientation == "HORIZONTAL",
iup.hbox{lblChooseFacts, lstFacts},
iup.vbox{lblChooseFacts, lstFacts}
)
else
dlgFacts = MakeBoxChooseFacts()
local strTip = strT or "Use the button to choose one or more facts."
local btnChooseFacts = MakeButton{title="Fact(s)...", callback=FactsChoose, tip = strTip}
txtFacts=MakeText{multiline="YES", readonly = "YES", expand = "YES", norm = norm2, tip = strTip}
return utils.choose(strOrientation == "HORIZONTAL",
iup.hbox{btnChooseFacts, txtFacts},
iup.vbox{btnChooseFacts, txtFacts}
)
end
end
local ClearSelector = function()
if imaxFact == 1 then
listFacts.value = 0
else
listFacts.value = ""
txtFacts.value = ""
tblSelectedFacts = {}
oldfacts = {}
end
if boxDetails then
ClearContainer(boxDetails,{})
RestrictedFields()
end
end
local FactList = function()-- returns a table of facts
return utils.choose (imax ~= 1, tblSelectedFacts, {listFacts[1]}) --return a table of fact names
end
local Details = function(strTitle, expanderstate, booNoteToggle, strNoteTip)
local function DateDetails()
local dtFactDate = nil --result of entry date operations
local txtFactDate = nil
local function DateChoose()
dtFactDate = Date()
if dtFactDate ~= nil then
txtFactDate.value = dtFactDate:GetDisplayText()
else
txtFactDate.value= ""
end
booFactDateValid = true
txtFactDate.fgcolor = TXTFGCOLOR
end
local function ValidateDate()
txtFactDate.fgcolor = TXTFGCOLOR
booFactDateValid = true
local d = txtFactDate.value
if type(d) == "string" then
dtFactDate, booFactDateValid = TestTextDate(d)
end
if not booFactDateValid then
txtFactDate.fgcolor = colorred
end
end
local btnGetFactDate = MakeButton{title="Date...", callback=DateChoose, tip = "Use the button to call Family Historian's Date Entry Assistant, or type directly into the Date box.\nIf you type in an invalid date the text will turn red and you will not be able to use the fact details."}
txtFactDate = MakeText{visiblelines = "1", killfocus=ValidateDate, norm = norm2, tip = "Use the button to call Family Historian's Date Entry Assistant, or type directly into the Date box.\nIf you type in an invalid date the text will turn red and you will not be able to use the fact details.", name = "factdate"}
return iup.hbox{btnGetFactDate, txtFactDate; alignment = "ACENTER"}
--txtEntryDate.value will be returned as part of the Citation Data
end
local function AgeDetails()
local dlgage = nil
local txtage = nil
local function MakeBoxChooseAge()
local child, infant, stillborn, agebelow, younger, older = nil
local txtyears, txtmonths, txtdays = nil
local function AgeConfirm()
local strAge = ""
if child.value == "ON" then txtage.value = "Child" return end
if infant.value == "ON" then txtage.value = "Infant" return end
if stillborn.value == "ON" then txtage.value = "Stillborn" return end
local y = txtyears.VALUE
local m = txtmonths.VALUE
local d = txtdays.VALUE
if type(y) == "string" and (y ~= "" and y ~= "0") then
strAge = strAge..y.." yr"
if tonumber(y) > 1 then strAge = strAge.."s" end
strAge = strAge.." "
end
if type(m) == "string" and (m ~= "" and m ~= "0") then
strAge = strAge..m.." mn"
if tonumber(m) > 1 then strAge = strAge.."s" end
strAge = strAge.." "
end
if type(d) == "string" and (d ~= "" and d ~= "0") then
strAge = strAge..d.." dy"
if tonumber(d) > 1 then strAge = strAge.."s" end
end
strAge = stringx.strip(strAge)
if strAge ~= "" then
if younger.value == "ON" then
strAge = "< "..strAge
elseif older.value == "ON" then
strAge = "> "..strAge
end
end
txtage.value = strAge
return true
end
local btnChooseAge = MakeButton{title="Confirm Age", callback=AgeConfirm, close="YES"}
local btnCancelAge = MakeButton{title="Cancel"}
txtyears = MakeText{visiblelines="1", filter="NUMBER", norm=norm}
txtmonths = MakeText{visiblelines="1", filter="NUMBER", norm=norm}
txtdays = MakeText{visiblelines="1", filter="NUMBER", norm=norm}
local boxYears= iup.hbox{MakeLabel{title = "Years"}, txtyears;}
local boxMonths = iup.hbox{MakeLabel{title = "Months"}, txtmonths;}
local boxDays = iup.hbox{MakeLabel{title = "Days"}, txtdays;}
local agespec = iup.vbox{boxYears, boxMonths, boxDays;}
agespec.active = "YES"
txtyears.value = ""
txtmonths.value = ""
txtdays.value = ""
local function ToggleAction1(state)
if state == 1 then agespec.active = "YES" else agespec.active = "NO" end
end
local function ToggleAction2(state)
if state == 1 then agespec.active = "NO" else agespec.active = "YES" end
end
child = MakeToggle{title="Child", action = ToggleAction1}
infant = MakeToggle{title="Infant", action = ToggleAction1}
stillborn = MakeToggle{title="Stillborn", action = ToggleAction1}
agebelow = MakeToggle{title="Age given below", action = ToggleAction2}
younger = MakeToggle{title="Younger than age given below", action = ToggleAction2}
older = MakeToggle{title="Older than age given below", action = ToggleAction2}
return MakeDialog(
iup.vbox{
iup.radio{iup.vbox{child,infant,stillborn,agebelow,younger,older}; value=agebelow},
agespec,
iup.hbox{iup.fill{}, btnChooseAge, btnCancelAge};
},
{title = "Enter Age", menubox="NO", resize="NO"})
end
local function AgeChoose()
dlgage:popup(iup.CENTERPARENT, iup.CENTERPARENT)
end
dlgage = MakeBoxChooseAge()
local btnSetAge = MakeButton{title="Age...", callback=AgeChoose, tip = "Use the button to open an Age Entry Assistant, or type directly into the age box.\nYou will not be able to enter an invalid age."}
txtage = MakeText{visiblelines = "1", norm = norm2, tip = "Use the button to open an Age Entry Assistant, or type directly into the age box.\nYou will not be able to enter an invalid age.", name = "age"}
txtage.mask = "^(Child|Infant|Stillborn|([<>]?((/d+)|(/d+y)|(/d+y/s/d+m)|(/d+y/s/d+m/s/d+d)|(/d+y/s/d+d)|(/d+m/s/d+d)|(/d+d))))"
return iup.hbox{btnSetAge, txtage; alignment = "ACENTER"}
end
local function PlaceandAddressDetails()
local txtSelPlace = "" -- selected place
local txtSelPlace2 = "" --selected place2
local txtSelAddress = "" --selected address
local listPlace1 = nil
local listPlace2 = nil
local listAddress = nil
local tblPlaces = Places() --place lookup data
local tblAddresses = Addresses() -- address lookup data
local function PlaceNew(p)
if p ~= "" then --ignore empty strings
tblPlaces[p] = 1
listPlace1.APPENDITEM = p
listPlace2.APPENDITEM = p
listAddress.value = ""
end
end
local function Place1LostFocus()
--Place has lost focus so populate the address list if the place has changed
local placevalue = stringx.strip(listPlace1.value)
if placevalue == txtSelPlace then return end --place hasn't changed
if type(placevalue) == "string" then
txtSelPlace = placevalue -- includes an empty string
if tblPlaces[txtSelPlace] == nil then
--this is a new place or an empty string
PlaceNew(txtSelPlace)
end
listPlace1.value = txtSelPlace
else --no place Selected
txtSelPlace = ""
end
--populate addresses
listAddress.REMOVEITEM = "ALL"
PopulateList(listAddress,AddressesForPlace(txtSelPlace, tblAddresses))
end
local function Place2LostFocus()
--Place 2 has lost focus so check whether a new place should be created
local placevalue = stringx.strip(listPlace2.value)
if placevalue == txtSelPlace2 then return end
if type(placevalue) == "string" then
txtSelPlace2 = placevalue -- includes an empty string
if tblPlaces[txtSelPlace2] == nil then
--this is a new place or an empty string
PlaceNew(gtxtSelPlace2)
end
listPlace2.value = txtSelPlace2
else --no place Selected
txtSelPlace2 = ""
end
end
local function AddressLostFocus()
--Address has lost focus so check whether a new address should be created
local addressvalue = listAddress.value
if type(addressvalue) == "string" then
txtSelAddress = stringx.strip(addressvalue) -- includes an empty string
if tblAddresses[txtSelPlace..txtSelAddress] == nil and txtSelAddress ~= "" then -- this is a new address
tblAddresses[txtSelPlace..txtSelAddress]={txtSelAddress, txtSelPlace}
listAddress.APPENDITEM = txtSelAddress
end
if txtSelAddress ~= addressvalue then listAddress.value = txtSelAddress end
else --no place Selected
txtSelAddress = ""
end
end
function ClearPlacesandAddress()
txtSelPlace = "" -- selected place
txtSelPlace2 = "" --selected place2
txtSelAddress = "" --selected address
PopulateList(listAddress,AddressesForPlace(txtSelPlace, tblAddresses))
end
listPlace1 = MakeList{editbox = "YES", values=tblPlaces, killfocus = Place1LostFocus, name = "place"}
listPlace2 = MakeList{editbox = "YES", values=tblPlaces, killfocus = Place2LostFocus, tip = "Place 2 will only be used for facts that accept 2 places,\nsuch as Immigration and Emigration.", name = "place2"}
listAddress = MakeList{editbox = "YES", killfocus = AddressLostFocus, values = AddressesForPlace("", tblAddresses), name = "address"}
listPlace2.active = "NO"
return iup.vbox{iup.hbox{MakeLabel{title = "Place"}, listPlace1;},
iup.hbox{MakeLabel{title = "Address"}, listAddress;},
iup.hbox{MakeLabel{title = "Place 2", tip = "Place 2 will only be used for facts that accept 2 places,\n: Immigration and Emigration."}, listPlace2;};}
end
local function AttributeDetails()
local txtAttribute = MakeText{tip = "Attribute will only be used for facts which accept a value.", norm =norm2, name = "attributevalue"}
txtAttribute.active = "NO"
return iup.hbox{MakeLabel{title="Attribute", tip = "Attribute will only be used for facts which accept a value."}, txtAttribute;}
end
local function CauseDetails()
local txtCause = MakeText{tip = "Cause will only be used for a death fact.", norm = norm2, name = "cause"}
txtCause.active = "NO"
return iup.hbox{MakeLabel{title="Cause", tip = "Cause will only be used for a death fact."}, txtCause;}
end
local function NoteDetails()
local txtNote = MakeText{multiline="YES", scrollbar = "VERTICAL", visiblelines = "2", wordwrap = "YES", tip = strNoteTip, name = "factnote"}
if booNoteToggle then
local txtAppendNote = MakeToggle{title="Append note to Auto-create Note if one exists", value=0, name = "appendautonote"}
return iup.vbox{MakeLabel{title = "Note",tip = strNoteTip}, txtNote, txtAppendNote;}
else
return iup.vbox{MakeLabel{title = "Note",tip = strNoteTip},txtNote;}
end
end
boxDetails = iup.vbox{
iup.hbox{DateDetails(), AgeDetails()}, PlaceandAddressDetails(),
iup.hbox{AttributeDetails(), CauseDetails()}, NoteDetails();
}
return MakeExpander(boxDetails, strTitle, expanderstate)
end
local DetailValues = function()
local tblboxDetails = GetDataElements(boxDetails, {}, {}, true) --a table of controls keyed by control name
return GetContainerData(tblboxDetails) -- factdate, age, place, address, place2, attributevalue, cause, factnote and appendautonote all as text
end
local DetailsValid = function()
return booFactDateValid
end
local FactFields = function(pref)
return tblFacts[pref]
end
--expose public methods
return{
Selector = Selector,
ClearSelector = ClearSelector,
FactList = FactList,
Details = Details,
DetailValues = DetailValues,
DetailsValid = DetailsValid,
EnableRestrictedFactFields = EnableRestrictedFactFields,
FactFields = FactFields
}
end
function CallerFacts()
local CheckUIStatus = function()
AdjustButtons()
end
return {CheckUIStatus = CheckUIStatus}
end
---------------------------------------------------------------
--Results And Activity Log classes
---------------------------------------------------------------
--[[Results And Activity Log classes
@Author: Helen Wright
@Version: 1.0
@LastUpdated: 27 October 2019
@V1.0: Initial version.
Classes that manage an ongoing Activity Log in an expander; or manage the results display within FH when the plugin exsts.
Multiple instances of the activity log can be created, but only one instance of the Results class (FH limitation)
]]--
function ActivityLog ()
require("iuplua") -- GUI
--also requires Dialog boilerplate
--public methods and associated private state variables
local Log = nil --control for activity log
local Update = function(strupdate)
Log.append = strupdate
end
local Make = function (s,t)
local state = s or "OPEN"
local title = t or "Activity Log"
Log = MakeText{multiline="YES", scrollbar = "BOTH", readonly="YES"}
return MakeExpander(iup.vbox{Log;}, title, state)
end
--expose public methods
return{Make=Make, Update = Update}
end
function Results (intTableCount)
require("iuplua") -- GUI
--also requires fh API and Dialog boilerplate
--public methods and associated private state variables
local iRes = 0 -- index used to track results
local strTitle = ""
local tblResults = {} --table of results tables
local tblVisibility = {}
local tblResultHeadings = {}
local tblResultType = {}
for i = 1, intTableCount do
tblResults[i] = {}
end
local Update = function(tblNewResults)
iRes = iRes + 1
for i, v in ipairs(tblNewResults) do
tblResults[i][iRes] = v
end
end
local Title = function(str)
strTitle = str
end
local Types = function(types)
tblResultType = tablex.copy(types)
end
local Headings = function(headings)
tblResultHeadings = tablex.copy(headings)
end
local Visibility = function(visibility)
tblVisibility =tablex.copy(visibility)
end
local Display = function()
if iRes > 0 then -- there are results to display
fhOutputResultSetTitles(strTitle)
for i, _ in ipairs(tblResults) do
local strV = utils.choose(tblVisibility[i]==true, "show", "hide")
fhOutputResultSetColumn(tblResultHeadings[i], tblResultType[i], tblResults[i], iRes, 80 ,"align_left",i, true , "default", strV)
end
end
fhUpdateDisplay()
end
--expose public methods
return{Title = Title, Headings = Headings, Visibility = Visibility, Types = Types, Update = Update, Display = Display}
end
--------------------------------------------------------------
--MAIN DIALOG ACTIONS
--------------------------------------------------------------
function MakeMainDialog()
local function FactsCreate()
--check for invalid data
if mySource.CitationOK() == false then
Messagebox("Invalid date","Your citation has an invalid entry date so facts cannot be created. Please correct the date highlighted in red and retry",false)
return
elseif myFacts.DetailsValid() == false then
Messagebox("Invalid date","Your fact details have an invalid fact date so facts cannot be created. Please correct the date highlighted in red and retry",false)
return
end
local tblSources = mySource.SourceList() --only the first item will be used (it's a pointer)
local tblCitation = mySource.CitationData() --user specified entrydate, quality, where, from, citationnote
local tblIndividuals = myIndividuals.IndividualList()
local tblFacts = myFacts.FactList()
local tblFactDetails = myFacts.DetailValues() -- user specified factdate, age, place, address, place2, attributevalue, cause, factnote and appendautonote
for _, ptrIndi in pairs(tblIndividuals) do
for _, pref in pairs(tblFacts) do
local tblFactFields = myFacts.FactFields(pref) --get results of parsing the current fact
local tbldet = tablex.copy(tblFactDetails) --make a copy of the user specified details to amend based on validity of fields for the current fact
for _, k in ipairs{"factdate", "age", "place", "address", "place2", "attributevalue", "cause", "factnote"} do --iterate the details needed to make the fact
if tblFactFields[k]=="0" then tbldet[k]="" end--clear fields not supported for the current fact
end
if tblFactFields["factnote"]=="1" and tbldet["appendautonote"]=="ON" then --add the autonote to the note (if any)
tbldet["factnote"] = tbldet["factnote"]..utils.choose(tbldet["factnote"] == "", "", "\n")..tblFactFields["autonote"]
end
local ptrFact = myFactTools.Create(tblFactFields["facttag"], ptrIndi, tbldet) --create the fact for the individual
local ptrCitation = mySourceTools.Link(ptrFact, tblSources[1]) --Link the Source
mySourceTools.CitationDetail(ptrCitation, tblCitation) --and populate the citation
myActivity.Update(fhGetDisplayText(tblSources[1]).." | "..fhGetDisplayText(ptrIndi).." | ".. pref)
myResults.Update({tblSources[1]:Clone(), ptrIndi:Clone(), pref, ptrFact:Clone()})
end
end
end
local function FactsClear()
mySource.ClearSelector()
myIndividuals.ClearSelector()
myFacts.ClearSelector()
EnableButtons({btnCreateFacts},"NO")
end
--create buttons
btnCreateFacts = MakeButton{title="Create Facts", callback = FactsCreate, tip = "Create facts using the specified data."}
local btnExitFacts = MakeButton{title="Exit"}
local btnClear = MakeButton{title="Clear", callback= FactsClear, tip = "Clear all selections and data."}
--make dialog
local d = MakeDialog(iup.hbox{
iup.vbox{
mySource.Selector("Use the button to choose a source from within Family Historian.\nYou cannot create facts until you have selected a source.", "HORIZONTAL", "Citation (Optional). Any items specified here will be applied to all facts created."),
iup.hbox{
myIndividuals.Selector("Use the button to choose one or more individuals from within FH.\nYou cannot create facts until you have selected one or more invididuals.","VERTICAL"),
myFacts.Selector("Use the button to choose one or more Facts.\n You cannot create facts until you have made a selection","VERTICAL")
},
myFacts.Details("Fact details (Optional). Any items specified here will be applied to all facts created.", "OPEN", true, "If you specify a note, that will be added to the facts that are created.\n If you don't specify a note, and a fact has an AutoCreate Note defined,\nthe AutoCreate note will be created for that fact.\nYou can also choose to append this note to an AutoCreate note that is defined."),
iup.hbox{iup.fill{}, btnClear, btnCreateFacts, btnExitFacts},
myActivity.Make("CLOSE", "Facts created");
}
},
{title = cstrPluginName})
d.close_cb = function()
d.show_cb = nil --avoid iup bug
return iup.CLOSE
end
DoNormalize() -- normalise all dialogs
EnableButtons({btnCreateFacts},"NO")
return d
end
function AdjustButtons()
EnableButtons({btnCreateFacts},"NO")
local sourcelist = mySource.SourceList()
local indilist = myIndividuals.IndividualList()
local flist = myFacts.FactList()
myFacts.EnableRestrictedFactFields()
if #sourcelist >= 1 and #indilist >= 1 and #flist >= 1 then
EnableButtons({btnCreateFacts},"YES")
end
end
-------------------------------------
--EXECUTE
-------------------------------------
do
SetUTF8IfPossible()
SetTextSize()
myResults = Results(4) --initialise results handling with 4 results tables
myResults.Title("Facts created")
myResults.Headings({"Source", "Individual", "Fact", "Fact link"})
myResults.Types({"item", "item", "text", "item"})
myResults.Visibility({true,true,true,true})
myActivity = ActivityLog()
mySource = Source(1, false, false, true) --create a source object for a single source with a citation
myIndividuals = Individuals(0) --create an individuals object for an unlimited set of individuals
myFacts = Facts(0) --create a facts object for an unlimited set of facts
dlgmain = MakeMainDialog()
dlgmain:show()
mySource.CitationState("CLOSE")
iup.MainLoop()
DestroyAllDialogs()
myResults.Display()
end
--DONE
--[[
@Title: Multifact
@Author: Helen Wright
@Version: 1.9.1
@LastUpdated: 23 December 2019
@Description: Create multiple Individual facts for a set of individuals
all linked to a single existing source with optional common data and citation elements
]]--
--[[Licence:
All code within this plugin is released under the Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) licence: https://creativecommons.org/licenses/by-nc-sa/4.0/
except where indicated. Specifically, rights to the encoding functions are reserved to Mike Tate; and rights to the function ProgressBar are reserved to Mike Tate and Jane Taubman
In practice, this means you can steal plunder or reuse any elements of my code (modified if you wish) as long as you credit me by name, link to the licence, and indicate what changes were made. You can't sell any code derived from mine (as if anyone would pay for it!) and you must release it under the same Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) licence: https://creativecommons.org/licenses/by-nc-sa/4.0/
]]--
--[[ChangeLog:
Version 1: Initial version
1.1, 1.2: Fixed initial crop of bugs
1.3 More bug fixes; create Automatic notes if specific note not specified.
1.4 More bug fixes; better dialog size management; option to specify note appended to autonote
1.5 Bug fix (not all places included)
1.6 Better handling of small screens (less than 999 pixels deep. It's usable down to 864 pixels and just about usable down to 800, depending on text scaling, but anything below that won't work.
1.7 Minor layout adjustment
1.8 Changes to boilerplate and further minor layout adjustments
1.9 Minor bug fixes
1.9.1 Bug fix to handle odd fact definitions
]]--
--[[Variable type naming conventions
g prefix for global variables
c for constants
int for integer numbers
n for fractional numbers
str for strings
boo for boolean
tbl for tables
func for functions as parameters
FH API data types
ptr for pointers
dt for date objects
In IUP:
Assume all IUP variables are global
b for "ON"/"OFF" value
tab for tab
dlg for dialogs
norm for normalizer
dlg for dialog
btn for button
txt for text
lab for label
box for hbox or vbox
list for list
Note: where code snippets have been imported these conventions may not be followed
]]--
--------------------------------------------------------------
--EXTERNAL LIBRARIES AND FH VERSION CHECK
--------------------------------------------------------------
do
if fhInitialise(5,0,9) == false then return end-- requires FH 5.0.9 due to use of folder option in fhGetPluginDataFile and the use of Labelled Text
require("iuplua") -- GUI
require("lfs") -- file system access
require ("luacom") --Microsoft's Component Object Model
function fhloadrequire(module,extended)
local function httpRequest(url)
local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
http:Open("GET",url,false)
http:Send()
http:WaitForResponse(30)
return http
end -- local function httpRequest
if not(extended) then extended = module end
local function installmodule(module,filename,size)
local bmodule = false
if not(filename) then
filename = module..'.mod'
bmodule = true
end
local storein = fhGetContextInfo('CI_APP_DATA_FOLDER')..'\\Plugins\\'
-- Check if subdirectory needed
local path = string.match(filename, "(.-)[^/]-[^%.]+$")
if path ~= "" then
path = path:gsub('/','\\')
-- Create sub-directory
lfs.mkdir(storein..path)
end
local attr = lfs.attributes(storein..filename)
if attr and attr.mode == 'file' and attr.size == size then return true end
-- Get file down and install it
local url = "http://www.family-historian.co.uk/lnk/getpluginmodule.php?file="..filename
local isOK, reply = pcall(httpRequest,url)
if not isOK then
fhMessageBox(reply.."\nLoad Require module finds the Internet inaccessible.")
return false
end
local http = reply
local status = http.StatusText
if status == 'OK' then
-- local length = http:GetResponseHeader('Content-Length')
local data = http.ResponseBody
if bmodule then
local modlist = loadstring(http.ResponseBody)
for x,y in pairs(modlist()) do
if type(x) == 'number' and type(y) == 'string' then
x = y -- revert to original 'filename' ipairs modlist
y = 0
end -- otherwise use ['filename']=size pairs modlist
if not(installmodule(module,x,y)) then
break
end
end
else
local function OpenFile(strFileName,strMode)
local fileHandle, strError = io.open(strFileName,strMode)
if not fileHandle then
error("\n Unable to open file in \""..strMode.."\" mode. \n "..
strFileName.." \n "..tostring(strError).." \n")
end
return fileHandle
end -- OpenFile
local function SaveStringToFile(strString,strFileName)
local fileHandle = OpenFile(strFileName,"wb")
fileHandle:write(strString)
assert(fileHandle:close())
end -- SaveStringToFile
SaveStringToFile(data,storein..filename)
end
return true
else
fhMessageBox('An error occurred in Download please try later')
return false
end
end
local function requiref(module)
require(module)
end
local _, err = pcall(requiref,extended)
if err then
if err:match("module '"..extended:gsub("(%W)","%%%1").."' not found") then
local ans = fhMessageBox(
'This plugin requires '..module..' support, please click OK to download and install the module',
'MB_OKCANCEL','MB_ICONEXCLAMATION')
if ans ~= 'OK' then
return false
end
if installmodule(module) then
package.loaded[extended] = nil -- Reset Failed Load
require(extended)
else
return false
end
else
fhMessageBox('Error from require("'..module..'") command:\n'..(err or ''))
return false
end
end
return true
end
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
end
--------------------------------------------------------------
--ENVIRONMENT VARIABLES
--------------------------------------------------------------
local cstrPluginDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local cstrPluginVersion = "1.8"
-- any other constants go here
local myResults = nil
local myActivity = nil
local mySource = nil
local myIndividuals = nil
local myFacts = nil
--------------------------------------------------------------
--UTILITY MODULES AND CLASSES
--------------------------------------------------------------
--[[ Dialog
@Author: Helen Wright
@Version: 1.0
@LastUpdated: 29 October 2019
@Description: Helper functions for IUP dialogs
@V1.0: Initial version.
]]--
do
--Prerequisites:
do
require("iuplua") -- GUI
-- also requires fh API for date handling
end
--useful constants
colorred = "255 0 0" -- used to flag erroneous data
strMin1Char = "/S+" --useful text mask
strMin1Letter = "/l+" --useful text mask
strAlphaNumeric = "[0-9a-zA-Z ()]+" --useful text mask
-- font sizing, normalisation and other layout functions
do
function SetTextSize()
local _,_, height = stringx.partition(iup.GetGlobal("SCREENSIZE"),"x")
if tonumber(height) > 999 then
iup.SetGlobal("DEFAULTFONT", "Arial Unicode MS, 11")
else
iup.SetGlobal("DEFAULTFONT", "Arial Unicode MS, 8") --reduce font size for smaller screens
end
end
function SetUTF8IfPossible()
if fhGetAppVersion() > 5 then -- make interface UTF8; fh API will handle conversion if necessary
iup.SetGlobal("UTF8MODE", "YES")
iup.SetGlobal ("UTF8MODE_FILE", "NO") --use file names in the current locale
fhSetStringEncoding("UTF-8")
end
end
function SetMinimumSize(dlg)
dlg.minsize = iup.NULL
iup.Refresh(dlg)
dlg.minsize = dlg.naturalsize
end
norm = iup.normalizer{} -- normalizer used to layout buttons and labels neatly
norm2 = iup.normalizer{} -- normalizer used to layout lists and text neatly
norm3 = iup.normalizer{} --normalizer used to layout popup dialogs neatly
function DoNormalize()
norm.normalize = "HORIZONTAL"
norm2.normalize = "HORIZONTAL"
norm3.normalize = "HORIZONTAL"
end
end
--dialog handling
do
function MakeDialog(content, options)
local d = iup.dialog{content}
d.title = options.title or ""
if options.expand then d.expand = options.expand end --default is "YES"
if options.resize then d.resize = options.resize end --default is "YES"
if options.size ~= nil then d.size= options.size else d.size = iup.NULL end
if options.show ~= nil then
d.show_cb = function(self, state)
if state == iup.SHOW then SetMinimumSize(self) options.show(state) else return iup.IGNORE end
end
else
d.show_cb = function(self, state)
if state == iup.SHOW then SetMinimumSize(self) else return iup.IGNORE end
end
end
d.margin = options.margin or "3x3"
d.cgap = options.cgap or "1"
d.menubox = options.menubox or "YES"
iup.SetHandle(options.name or d.title, d) --will be used to help identify active window
return d
end
function IdentifyActiveWindow()
--depends on having names associated with all dialogs
local tblD, _ = iup.GetAllDialogs() --get dialog names, which are not the same as their handles
local d = nil
for _, v in ipairs(tblD) do
d = iup.GetHandle(v)
if d.ACTIVEWINDOW == "YES" then
return d
end
end
end
function DestroyAllDialogs()
--depends on having names associated with all dialogs
local tblD, _ = iup.GetAllDialogs() --get dialog names, which are not the same as their handles
local d = nil
for _, v in ipairs(tblD) do
d = iup.GetHandle(v)
d:destroy()
end
end
end
--container handling
do
--local constants used when dealing with containers
local dialogs = dialogs or tablex.index_map({"dialog", "messagedlg", "progesssdlg", "fontdg", "filedlg", "colordlg"})
local containers = containers or tablex.index_map({"frame", "hbox", "vbox", "zbox", "tabs", "radio", "sbox", "cbox", "gridbox", "multibox", "scrollbox", "detachbox", "expander", "detachbox", "split", "backgroundbox", "spinbox"})
local datacontrols = datacontrols or tablex.index_map({"list", "text", "val", "link", "multiline", "toggle" })
local static = static or tablex.index_map({"fill", "normalizer", "button", "label", "menu", "submenu", "item", "separator", "imagergb", "imagergba", "image", "matrix", "cells", "clipboard", "timer", "user", "link"})-- these will be ignored when clearing the dialog
local toohardtohandle = toohardtohandle or tablex.index_map({"tree", "spin", "canvas"}) --only g*d knows
function EnableContainer(ih, tblexcludeih, strstate)
local element = nil
local e = 1 --element index
while ih[e]~= nil do -- loop the elements in the parent
element = ih[e]
if tblexcludeih[element] == nil then
if dialogs[iup.GetClassName(element)] ~= nil or containers[iup.GetClassName(element)] ~= nil then -- this is a container
EnableContainer(element,tblexcludeih, strstate)
elseif datacontrols[iup.GetClassName(element)] ~= nil or iup.GetClassName(element) == "button" then --this is a data control or a button
ih.active = strstate
end
end
e=e+1
end
end
function ClearContainer(ih, tblexcludeih)
local function ClearDataControl(ih)
local cclass = iup.GetClassName(ih)
ih.fgcolor = TXTFGCOLOR
if cclass == "list" then
if ih.editbox=="YES" or iup.multiple=="YES" then
ih.value =""
else
ih.value = "0"
end
elseif cclass == "text" or cclass == "multiline" then
ih.value = ""
elseif cclass == "val" then
ih.value = "0.0"
elseif cclass == "toggle" then
ih.value = "OFF"
end
end
local element = nil
local e = 1 --element index
while ih[e]~= nil do -- loop the elements in the parent
element = ih[e]
if tblexcludeih[element] == nil then
if dialogs[iup.GetClassName(element)] ~= nil or containers[iup.GetClassName(element)] ~= nil then -- this is a container
ClearContainer(element,tblexcludeih)
elseif datacontrols[iup.GetClassName(element)] ~= nil then --this is a data control
ClearDataControl(element)
end
end
e=e+1
end
end
function HideContainer(ih, boostatus)
local strVisible = "NO"
local strFloating = "YES"
if boostatus == false then
strVisible = "YES"
strFloating = "NO"
end
local element = nil
local e = 1 --element index
while ih[e]~= nil do -- loop the elements in the parent
element = ih[e]
if dialogs[iup.GetClassName(element)] ~= nil or containers[iup.GetClassName(element)] ~= nil then -- this is a container
HideContainer(element, booStatus)
else --hide a control
element.visible = strVisible
element.floating = strFloating
iup.Refresh(element)
end
e=e+1
end
end
function GetValueFromControl(ctl)
if type(ctl.value) == "string" or type(ctl.value) == "number" then
return ctl.value
else
return ""
end
end
function GetContainerData(tblCtls)
local is_indexed = (rawget( tblCtls, 1 ) ~= nil)
local tblVals ={}
if not is_indexed then
for k, v in pairs(tblCtls) do
tblVals[k] = GetValueFromControl(v)
end
else
for k,v in ipairs(tblCtls) do
tblVals[k] = GetValueFromControl(v)
end
end
return tblVals
end
function GetDataElements(ih, tblexcludeih, tblElements, keyname)
if not keyname then keyname=false end
local child = iup.GetChild(ih,0)
while child ~= nil do -- loop the elements in the parent and add any data controls to the end of tblElements
if tblexcludeih[child] == nil then
if dialogs[iup.GetClassName(child)] ~= nil or containers[iup.GetClassName(child)] ~= nil then -- this is a container
GetDataElements(child, tblexcludeih, tblElements, keyname)
elseif datacontrols[iup.GetClassName(child)] ~= nil then --this is a data control
if keyname then
tblElements[iup.GetName(child)]=child
else
tblElements[#tblElements+1]=child
end
end
end
child = iup.GetNextChild(ih,child)
end
return tblElements -- a table of data controls
end
end
--message boxes and parameter prompts
do
function Messagebox(strTitle, strMessage, bIsAlert)
local msg = iup.messagedlg{}
msg.TITLE = strTitle
msg.VALUE = strMessage
bIsAlert = bIsAlert or false
if bIsAlert == true then
msg.BUTTONS = "OKCANCEL"
msg.DIALOGTYPE="WARNING"
else
msg.BUTTONS = "OK"
msg.DIALOGTYPE="INFORMATION"
end
msg:popup(iup.CENTERPARENT, iup.CENTERPARENT)--display the dialog
local btnResponse = msg.BUTTONRESPONSE
msg:destroy()
if btnResponse == "1" then return "OK" else return "CANCEL" end
end
function GetText(strPrompt, strDefault, strMask)
local function param_action()
return 1
end
local pstrText = strDefault
local booOK = false
local pstrParam = strPrompt..": %s"..strMask.."\n" --string with mask
booOK, pstrText = iup.GetParam(strPrompt, param_action, pstrParam, pstrText)
pstrText = utils.choose(type(pstrText) == 'string', pstrText, "")
return booOK, utils.choose(booOK == true, pstrText, "")
end
function GetTextandTick(strPrompt, strDefault, strMask, strTickPrompt)
local function param_action()
return 1
end
local booTick = 0
local pstrText = strDefault
local booOK = false
local pstrParam = strPrompt..": %s"..strMask.."\n"..strTickPrompt.." %b\n" --string with mask plus boolean
booOK, pstrText, booTick = iup.GetParam(strPrompt, param_action, pstrParam, pstrText, booTick)
pstrText = utils.choose(type(pstrText) == 'string', pstrText, "")
booTick = utils.choose(booTick == 0, false, true)
return booOK, utils.choose(booOK == true, pstrText, ""), utils.choose(booOK == true, booTick, false)
end
end
--button handling
do
function EnableButtons(tblButtons,strSetting)
-- strSetting can be "YES" or "NO"
for _,v in ipairs(tblButtons) do
v.ACTIVE = strSetting
end
end
function MakeButton(options)
local b=iup.button{}
local callback = options.callback
if callback ~= nil then
if options.close ~= nil then --this is a close button
b=iup.button{action = function(self) if callback() == true then return iup.CLOSE end end}
else
b=iup.button{action = function(self) callback() end}
end
else -- if callback isn't specified this is a cancel button
b=iup.button{action = function(self) return iup.CLOSE end}
end
b.alignment="ALEFT"
b.padding ="10x0"
b.normalizergroup = options.norm or norm
b.title = options.title or ""
if options.tip then b.tip = options.tip end
if options.name then iup.SetHandle(options.name, b) end
return b
end
end
--list handling
do
function PopulateList(l, tblVals)
local is_indexed = (rawget( tblVals, 1 ) ~= nil)
l.REMOVEITEM = "ALL"
if not is_indexed then
local i=1
for k, _ in pairs(tblVals) do
l[tostring(i)]=k
i=i+1
end
else
for i, v in ipairs(tblVals) do
l[tostring(i)]=v
end
end
end
function MultiListSelectionTrue(l)
return l.value:match("%+") ~= nil
end
function MultiListSelectionClear(l)
l.value = string.rep('%-',l.count)
end
function MakeList(options)
local l = iup.list{}
l.editbox = options.editbox or "NO"
l.sort = options.sort or "YES"
l.dropdown=options.dropdown or "YES"
l.multiple=options.multiple or "NO"
if l.dropdown == "YES" then
l.visibleitems = options.visibleitems or "5"
l.expand = "HORIZONTAL"
else
l.visiblelines = options.visiblelines or "9"
if options.visiblecolumns then l.visiblecolumns = options.visiblecolumns end
l.expand = "YES"
end
if options.norm then l.normalizergroup = options.norm end
if options.tip then l.tip = options.tip end
if type(options.values) == "table" then PopulateList(l,options.values) end
--now handle callbacks
if options.action ~= nil then
l.action = function(self, text, item, state) options.action(l, text,item,state) end
end
if options.killfocus ~= nil then
l.killfocus_cb = function(self) options.killfocus() end
end
if options.name then iup.SetHandle(options.name, l) end
return l
end
function GoToInList(str,l)
--find a value within a list and navigate there
local intLength = tonumber(l.COUNT)
if intLength > 0 then
for intPosition = 1, intLength do
if l[tostring(intPosition)]==str then
l.value = intPosition
return intPosition
end
end
end
return 0 --not found
end
function GetSingleValue(list)
return utils.choose(list.value ~=0, list[tostring(list.value)], "")
end
function GetSelectedValues(list)
local tbl = {} --assume nothing selected
local intLength = tonumber(list.COUNT)
if intLength > 0 then
local strSelectionState = list.value -- a sequence of + and -
for i = 1, intLength do
if strSelectionState:sub(i,i) == '\+' then --item is selected
table.insert(tbl, list[tostring(i)]) --insert the list item text in the table
end
end
end
return tbl --an indexed list of strings
end
function SetSelectedValues(list, tblselected) --tbl selected is an indexed list of strings
local tbl = tablex.index_map(tblselected) -- a table keyed on the strings
local strselection = ""
local intLength = tonumber(list.COUNT)
if intLength >0 then
for i = 1, intLength do
strselection = strselection..utils.choose(tbl[list[tostring(i)]], "+", "-")
end
list.value = strselection
end
end
end
--other controls
do
function MakeText(options)
local function CheckTextNotBlank(self)
if self.count == 0 or type(self.value) ~= 'string' then
self.value = options.default or ""
end
end
-- there are no mandatory options
local t= iup.text{wordwrap = options.wordwrap or "YES",
append = options.append or "YES", scrollbar = options.scrollbar or "NO",
multiline = options.multiline or "NO",
visiblelines = options.visiblelines or "2", readonly=options.readonly or "NO",padding ="10x2"}
if options.tip then t.tip = options.tip end
if options.expand then t.expand = options.expand end
if options.norm then t.normalizergroup = options.norm end
if t.multiline == "YES" then
t.expand = options.expand or "YES"
else
t.expand = options.expand or "HORIZONTAL"
end
if options.filter then t.filter = options.filter end
if options.killfocus ~= nil then
t.killfocus_cb = function(self) CheckTextNotBlank(self) options.killfocus() end
else
t.killfocus_cb = function(self) CheckTextNotBlank(self) end
end
if options.mask ~= nil then t.mask = options.mask end
if options.name then iup.SetHandle(options.name, t) end
if options.default then t.value = options.default else t.value = "" end
return t
end
function MakeLabel(options)
local l = iup.label{title = options.title or "", normalizergroup = options.norm or norm, wordwrap = options.wordwrap or "NO"}
if options.tip then l.tip = options.tip end
return l
end
function MakeToggle(options)
local t=iup.toggle{}
local action = options.action
if action ~= nil then
t = iup.toggle{action = function(state) action(state) end}
end
t.alignment="ALEFT"
t.title = options.title or ""
if options.tip then t.tip = options.tip end
if options.name then iup.SetHandle(options.name, t) end
if options.value then t.value = options.value end
return t
end
function MakeExpander(content, title, state)
local e = iup.expander{content}
e.title = title
e.state = state
e.visible = "YES"
return e
end
function MakeGridbox(options)
local gbox = iup.gridbox{}
gbox.expandchildren = options.expandchildren or "HORIZONTAL"
gbox.alignmentlin = options.alignmentlin or "ACENTER"
gbox.orientation = options.orientation or "HORIZONTAL"
gbox.numdiv = options.numdiv or "2"
gbox.gaplin = "10"
gbox.gapcol = "10"
gbox.normalizesize = "YES"
return gbox
end
function MakeDateField(options)
local txtEntryDate = nil
local function ValidateDate()
txtEntryDate.fgcolor = TXTFGCOLOR
local booDateValid = true
local d = txtEntryDate.value
if type(d) == "string" then
_, booDateValid = TestTextDate(d)
end
if not booDateValid then
txtEntryDate.fgcolor = colorred
end
end
txtEntryDate = MakeText{visiblelines = "1", killfocus=ValidateDate, norm = options.norm or norm2, tip = "Enter date. If you type in an invalid date the text will turn red.", norm = options.norm or norm, expand = "NO"}
return iup.hbox{MakeLabel{title=options.title or "", tip = "Enter date. If you type in an invalid date the text will turn red."}, txtEntryDate; alignment = "ACENTER"}
end
end
end
--[[FH Data API
@Author: Helen Wright
@Version: 1.1
@LastUpdated: 29 October 2019
@Description: Helper functions to extend the FH API
]]--
do
--pre-requisites
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
require("iuplua") -- GUI
-- also requires
-- fh API
-- Dialog boilerplate
function PromptForRecords(options)
local tblrec = {}
local tbltxt = {}
local tblptr = {}
local currdlg=IdentifyActiveWindow()
if options.recordcount then
tblptr = fhPromptUserForRecordSel(options.recordtype or "INDI", options.recordcount)
else
tblptr = fhPromptUserForRecordSel(options.recordtype or "INDI")
end
currdlg.bringfront = "YES"
if #tblptr > 0 then
for i, p in ipairs(tblptr) do
tblrec[i]=fhGetRecordId(p)
tbltxt[i]=fhGetDisplayText(p)
end
else
tblrec = {}
tbltxt={}
end
return tblptr, tblrec, tbltxt
end
function GetCurrentRecord(options)
local tblrec = {}
local tbltxt = {}
local tblptr = fhGetCurrentRecordSel(options.recordtype or "INDI")
if #tblptr > 0 then
for i, p in ipairs(tblptr) do
tblrec[i]=fhGetRecordId(p)
tbltxt[i]=fhGetDisplayText(p)
end
end
return tblptr, tblrec, tbltxt
end
function Date()
local currdlg=IdentifyActiveWindow()
local date = fhPromptUserForDate()
currdlg.bringfront = "YES"
return date
end
function TestTextDate(strDateText)
local dt = fhNewDate()
if strDateText == "" then
return dt, true
elseif (stringx.startswith(strDateText,'"') and stringx.endswith(strDateText, '"'))
or (stringx.startswith(strDateText,"'") and stringx.endswith(strDateText, "'")) then
return dt, dt:SetValueAsText(strDateText, true) -- a date phrase
else
return dt, dt:SetValueAsText(strDateText, false) --not a date phrase
end
end
function Places()
local tblPlaces = {}
local ptrPlace = fhNewItemPtr()
ptrPlace:MoveToFirstRecord("_PLAC")
while ptrPlace:IsNotNull() do -- Loop through all Place Records
local strPlace = fhGetValueAsText(fhGetItemPtr(ptrPlace,"~.TEXT"))
tblPlaces[strPlace]=1
ptrPlace:MoveNext()
end
return tblPlaces
end
function Addresses()
local tblAddresses = {}
local ptr = fhNewItemPtr()
local pplace = fhNewItemPtr()
for _, rectype in pairs({"INDI","FAM","REPO","SUBM"}) do
ptr:MoveToFirstRecord(rectype)
while ptr:IsNotNull() do
if fhGetTag(ptr) == "ADDR" then
local strAddress = fhGetValueAsText(ptr)
pplace:MoveToParentItem(ptr) -- i.e. Fact for current address
pplace:MoveTo(pplace,"~.PLAC") -- pplace is place pointer
local strPlace= fhGetValueAsText(pplace)
if strPlace == nil then strPlace = "" end
if tblAddresses[strPlace..strAddress] == nil then --place address combination is unique
tblAddresses[strPlace..strAddress]={strAddress, strPlace}
end
end
ptr:MoveNextSpecial()
end
end
return tblAddresses
end
function AddressesForPlace(place, tblAddress)
local tblresult = {}
for _,a in pairs(tblAddress) do
if a[2]==place then
table.insert(tblresult,a[1])
end
end
return tblresult
end
function CreateNote(ptr, value)
local ptrNew = fhCreateItem("NOTE2",ptr)
fhSetValueAsText(ptrNew,value)
return ptrNew
end
function CreateSharedNote(value)
local ptrNew = fhCreateItem("NOTE")
local Text = fhCreateItem("TEXT",ptrNew)
fhSetValueAsText(Text,value)
return ptrNew
end
function CreateNoteLink(ptrParent,ptrNote)
local ptrWork = fhCreateItem("NOTE", ptrParent)
fhSetValueAsLink(ptrWork, ptrNote)
end
function CreateTagAsText(ptrParent, strTag, strText)
local ptrNew = fhCreateItem(strTag, ptrParent)
fhSetValueAsText(ptrNew,strText)
return ptrNew
end
function SetDate(ptrParent, dtDate)
local ptrDate = fhCreateItem("DATE", ptrParent)
fhSetValueAsDate(ptrDate, dtDate)
return ptrDate
end
function allItems(...)
local iTypeCount = nil
local iPos = 1
local p1 = fhNewItemPtr()
local p2 = fhNewItemPtr()
local tblRecTypes = {}
if arg['n'] == 0 then
-- No parameter do all Record Types
iTypeCount = fhGetRecordTypeCount() -- Get Count of Record types
for i = 1,iTypeCount do
tblRecTypes[i] = fhGetRecordTypeTag(i)
end
else
-- Got Parameters Process them instead
tblRecTypes = arg
iTypeCount = arg['n']
end
p1:MoveToFirstRecord(tblRecTypes[iPos])
return function()
repeat
while p1:IsNotNull() do
p2:MoveTo(p1)
p1:MoveNextSpecial()
if p2:IsNotNull() then
return p2
end
end
-- Loop through Record Types
iPos = iPos + 1
if iPos <= iTypeCount then
p1:MoveToFirstRecord(tblRecTypes[iPos])
end
until iPos > iTypeCount
end
end
function records(type)
local pi = fhNewItemPtr()
local p2 = fhNewItemPtr()
pi:MoveToFirstRecord(type)
return function ()
p2:MoveTo(pi)
pi:MoveNext()
if p2:IsNotNull() then return p2 end
end
end
function CopyBranch(ptrSource,ptrTarget)
local ptrNew = fhCreateItem(fhGetTag(ptrSource),ptrTarget,true)
fhSetValue_Copy(ptrNew,ptrSource)
CopyChildren(ptrSource,ptrNew)
end -- function CopyBranch
function CopyChildren(ptrSource,ptrTarget)
local ptrFrom = fhNewItemPtr()
ptrFrom = ptrSource:Clone()
ptrFrom:MoveToFirstChildItem(ptrFrom)
while ptrFrom:IsNotNull() do
CopyBranch(ptrFrom,ptrTarget)
ptrFrom:MoveNext()
end
end -- function CopyChildren
end
--[[Unicode conversion
@Author: Mike Tate
@Description: Convert betweeen UTF16 and UF8/ANSI
]]
do
--Prerequisites:
-- fh API
-- fh version > 5
function StrANSI_UTF8(strText)
return fhConvertANSItoUTF8(strText) --requires FH version >= 5
end -- function StrANSI_UTF8
function StrUTF8_ANSI(strText)
return fhConvertUTF8toANSI(strText) --requires FH version >= 5
end
-- UTF8 <=> UTF16
local intTop10 = 0
function StrUtf16toUtf8(strChar1,strChar2) -- Convert a UTF-16 word or pair to UTF-8 bytes --
local intUtf16 = string.byte(strChar2) * 0x100 + string.byte(strChar1)
if intUtf16 < 0x80 then -- U+0000 to U+007F (ASCII)
return string.char(intUtf16)
end
if intUtf16 < 0x800 then -- U+0080 to U+07FF
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16
return string.char( intByte2 + 0xC0, intByte1 + 0x80 )
end
if intUtf16 < 0xD800 -- U+0800 to U+FFFF
or intUtf16 > 0xDFFF then
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte3 = intUtf16
return string.char( intByte3 + 0xE0, intByte2 + 0x80, intByte1 + 0x80 )
end
if intUtf16 < 0xDC00 then -- U+10000 to U+10FFFF High 16-bit Surrogate Supplementary Planes -- V2.6
intTop10 = ( intUtf16 - 0xD800 ) * 0x400 + 0x10000
return ""
end
intUtf16 = intUtf16 - 0xDC00 + intTop10 -- U+10000 to U+10FFFF Low 16-bit Surrogate Supplementary Planes -- V2.6
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte3 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte4 = intUtf16
return string.char( intByte4 + 0xF0, intByte3 + 0x80, intByte2 + 0x80, intByte1 + 0x80 )
end -- function StrUtf16toUtf8
function StrUTF16_UTF8(strText) --Encode UTF16 words into UTF8 bytes
return ( (strText or ""):gsub("(.)(.)",StrUtf16toUtf8) )
end -- function StrUTF16_UTF8
local tblByte = {}
local tblLead = { 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }
function StrUtf8toUtf16(strChar) -- Convert UTF-8 bytes to a UTF-16 word or pair
-- Convert any UTF-8 multibytes to UTF-16 --
local function strUtf8()
if #tblByte > 0 then
local intUtf16 = 0
for intIndex, intByte in ipairs (tblByte) do -- Convert UTF-8 bytes to UNICODE U+0080 to U+10FFFF
if intIndex == 1 then
intUtf16 = intByte - tblLead[#tblByte]
else
intUtf16 = intUtf16 * 0x40 + intByte - 0x80
end
end
if intUtf16 > 0xFFFF then -- U+10000 to U+10FFFF Supplementary Planes -- V2.6
tblByte = {}
intUtf16 = intUtf16 - 0x10000
local intLow10 = 0xDC00 + ( intUtf16 % 0x400 ) -- Low 16-bit Surrogate
local intTop10 = 0xD800 + math.floor( intUtf16 / 0x400 ) -- High 16-bit Surrogate
local intChar1 = intTop10 % 0x100
local intChar2 = math.floor( intTop10 / 0x100 )
local intChar3 = intLow10 % 0x100
local intChar4 = math.floor( intLow10 / 0x100 )
return string.char(intChar1,intChar2,intChar3,intChar4) -- Surrogate 16-bit Pair
end
if intUtf16 < 0xD800 -- U+0080 to U+FFFF (except U+D800 to U+DFFF) -- V2.6
or intUtf16 > 0xDFFF then -- Basic Multilingual Plane
tblByte = {}
local intChar1 = intUtf16 % 0x100
local intChar2 = math.floor( intUtf16 / 0x100 )
return string.char(intChar1,intChar2) -- BPL 16-bit
end
local strUtf8 = "" -- U+D800 to U+DFFF Reserved Code Points -- V2.6
for _, intByte in ipairs (tblByte) do
strUtf8 = strUtf8..string.format("%.2X ",intByte)
end
local strUtf16 = string.format("%.4X ",intUtf16)
fhMessageBox("\n UTF-16 Reserved Code Point U+D800 to U+DFFF \n UTF-16 = "..strUtf16.." UTF-8 = "..strUtf8.."\n Character will be replaced by a question mark. \n")
tblByte = {}
return "?\0"
end
return ""
end -- local function strUtf8
local intUtf8 = string.byte(strChar)
if intUtf8 < 0x80 then -- U+0000 to U+007F (ASCII)
return strUtf8()..strChar.."\0" -- Previous UTF-8 multibytes + current ASCII char
end
if intUtf8 >= 0xC0 then -- Next UTF-8 multibyte start
local strUtf16 = strUtf8()
table.insert(tblByte,intUtf8)
return strUtf16 -- Previous UTF-8 multibytes
end
table.insert(tblByte,intUtf8)
return ""
end -- function StrUtf8toUtf16
function StrUTF8_UTF16(strText) -- Encode UTF-8 bytes into UTF-16 words
tblByte = {} -- (0xFF) flushes last UTF-8 character
return ((strText or "")..string.char(0xFF)):gsub("(.)",StrUtf8toUtf16)
end
end
--[[File handling
@Author: Helen Wright
@Version: 1.0
@LastUpdated: 29 October 2019
@Description: Helper functions for File handling
@V1.0: Initial version.
]]
do
--prerequisites
require ("luacom") --Microsoft's Component Object Model
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
-- also requires
-- Unicode boilerplate
function FileExists(strFile)
return path.isfile(strFile)
end
function FileDownload(strURL, strFile)
-- retrieve the content of a URL
local function httpRequest(strURL)
local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
http:Open("GET",strURL,false)
http:Send()
http:WaitForResponse(30)
return http.Responsebody
end
local isOK, body = pcall(httpRequest, strURL)
if isOK then
if body ~= nil then
-- save the content to a file (assume it's binary)
local f = assert(io.open(strFile, "wb")) -- open in "binary" mode -- throws an error if the open fails
if f ~= nil then --open succeeded
f:write(body)
f:close()
end
return true
end
end
return false,'An error occurred in Download. Please try later'
end
function WriteFile(strContents, strTarget)
local f = assert(io.open(strTarget, "wb")) -- open for writing in "binary" mode -- throws an error if the open fails
if f ~= nil then --open succeeded
f:write(strContents)
f:close()
end
end
function ReadFile(strSource)
local strContent = ""
local f = assert(io.open(strSource, "rb")) -- open for reading in "binary" mode -- throws an error if the open fails
if f ~= nil then --open succeeded
strContent = f:read("*all")
f:close()
end
return strContent
end
function CopyFile(old, new)
WriteFile(ReadFile(old), new) --doing it this way preserve file name case
end
local bomUtf16= string.char(0xFF,0xFE) -- "ÿþ"
function WriteUTF16File(strUTF8, strTarget)
--strUTF8 is a UTF8 or ANSI string
WriteFile(bomUtf16 .. StrUTF8_UTF16(strUTF8:gsub("\n","\r\n") ), strTarget)
end
function ReadUTF16File(strSource)
local strUTF8 = ReadFile(strSource)
return StrUTF16_UTF8(string.gsub(string.gsub(strUTF8,bomUtf16,""), "\r\n","\n")) --remove BOM, rationalise newlines and convert to UTF8
end
function DeleteFile(strFile)
os.remove(strFile)
end
function RenameFile(oldname,newname)
os.rename(oldname, newname)
end
--Chillcode code snippets for loading/saving tables to a file -- used for storage of template definitions
--[[
Save Table to File
Load Table from File
v 1.0
Lua 5.2 compatible
Only Saves Tables, Numbers and Strings
Insides Table References are saved
Does not save Userdata, Metatables, Functions and indices of these
----------------------------------------------------
table.save( table , filename )
on failure: returns an error msg
----------------------------------------------------
table.load( filename or stringtable )
Loads a table that has been saved via the table.save function
on success: returns a previously saved table
on failure: returns as second argument an error msg
----------------------------------------------------
Licensed under the same terms as Lua itself.
]]--
-- declare local variables
--// exportstring( string )
--// returns a "Lua" portable version of the string
local function exportstring( s )
return string.format("%q", s)
end
--// The Save Function
function table.save(tbl,filename )
local charS,charE = " ","\n"
local file,err = io.open( filename, "wb" )
if err then return err end
-- initiate variables for save procedure
local tables,lookup = { tbl },{ [tbl] = 1 }
file:write( "return {"..charE )
for idx,t in ipairs( tables ) do
file:write( "-- Table: {"..idx.."}"..charE )
file:write( "{"..charE )
local thandled = {}
for i,v in ipairs( t ) do
thandled[i] = true
local stype = type( v )
-- only handle value
if stype == "table" then
if not lookup[v] then
table.insert( tables, v )
lookup[v] = #tables
end
file:write( charS.."{"..lookup[v].."},"..charE )
elseif stype == "string" then
file:write( charS..exportstring( v )..","..charE )
elseif stype == "number" then
file:write( charS..tostring( v )..","..charE )
end
end
for i,v in pairs( t ) do
-- escape handled values
if (not thandled[i]) then
local str = ""
local stype = type( i )
-- handle index
if stype == "table" then
if not lookup[i] then
table.insert( tables,i )
lookup[i] = #tables
end
str = charS.."[{"..lookup[i].."}]="
elseif stype == "string" then
str = charS.."["..exportstring( i ).."]="
elseif stype == "number" then
str = charS.."["..tostring( i ).."]="
end
if str ~= "" then
stype = type( v )
-- handle value
if stype == "table" then
if not lookup[v] then
table.insert( tables,v )
lookup[v] = #tables
end
file:write( str.."{"..lookup[v].."},"..charE )
elseif stype == "string" then
file:write( str..exportstring( v )..","..charE )
elseif stype == "number" then
file:write( str..tostring( v )..","..charE )
end
end
end
end
file:write( "},"..charE )
end
file:write( "}" )
file:close()
end
--// The Load Function
function table.load( sfile )
local ftables,err = loadfile( sfile )
if err then return _,err end
local tables = ftables()
if tables then
for idx = 1,#tables do
local tolinki = {}
for i,v in pairs( tables[idx] ) do
if type( v ) == "table" then
tables[idx][i] = tables[v[1]]
end
if type( i ) == "table" and tables[i[1]] then
table.insert( tolinki,{ i,tables[i[1]] } )
end
end
-- link indices
for _,v in ipairs( tolinki ) do
tables[idx][v[2]],tables[idx][v[1]] = tables[idx][v[1]],nil
end
end
return tables[1]
else
iup.Message("Table load fail","No data returned")
end
end
end
--[[Progress Bar
@Title: Progress Bar (drop in)
@Author: Jane Taubman / Mike Tate
@LastUpdated: January 2013
@Description: Allows easy adding of a Progress Bar to any long running Plugin
]]
do
--prerequisites
require("iuplua") -- GUI
function ProgressBar(tblGauge)
local tblGauge = tblGauge or {} -- Optional table of external parameters
local strFont = tblGauge.Font or nil -- Font dialogue default is current font
local strButton = tblGauge.Button or "255 0 0" -- Button colour default is red
local strBehind = tblGauge.Behind or "255 255 255" -- Background colour default is white
local intShowX = tblGauge.ShowX or iup.CENTER -- Show window default position is central
local intShowY = tblGauge.ShowY or iup.CENTER
local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
local lblText, barGauge, lblDelta, btnStop, dlgGauge
local function doFocus() -- Bring the Progress Bar window into Focus
dlgGauge.bringfront="YES" -- If used too often, inhibits other windows scroll bars, etc
end -- local function doFocus
local function doUpdate() -- Update the Progress Gauge and the Delta % with clock
barGauge.value = intVal
lblDelta.title = string.format("%4d %% %s ",intPercent,strClock)
end -- local function doUpdate
local function doReset() -- Reset all dialogue variables and Update display
intVal = 0 -- Current value of Progress Bar
intPercent = 0.01 -- Percentage of progress
intStart = os.time() -- Start time of progress
intDelta = 0 -- Delta time of progress
intScale = math.ceil( intMax / 1000 ) -- Scale of percentage per second (this guess is corrected in Step function)
strClock = "00 : 00 : 00" -- Clock delta time display
isBarStop = false -- Stop button pressed signal
doUpdate()
doFocus()
end -- local function doReset
local tblProgressBar = {
Start = function(strTitle,intMaximum) -- Create & start Progress Bar window
if not dlgGauge then
strTitle = strTitle or "" -- Dialogue and button title
intMax = intMaximum or 100 -- Maximun range of Progress Bar, default is 100
local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30" -- Adjust Stop button size to Title
lblText = iup.label { title=" ", expand="YES", alignment="ACENTER", tip="Progress Message" }
barGauge = iup.progressbar { rastersize="400x30", value=0, max=intMax, tip="Progress Bar" }
lblDelta = iup.label { title=" ", expand="YES", alignment="ACENTER", tip="Percentage and Elapsed Time" }
btnStop = iup.button { title=" Stop "..strTitle, rastersize=strSize, fgcolor=strButton, tip="Stop Progress Button", action=function() isBarStop = true end } -- Signal Stop button pressed return iup.CLOSE -- Often caused main GUI to close !!!
dlgGauge = iup.dialog { title=strTitle.." Progress ", font=strFont, dialogframe="YES", background=strBehind, -- Remove Windows minimize/maximize menu
iup.vbox{ alignment="ACENTER", gap="10", margin="10x10",
lblText,
barGauge,
lblDelta,
btnStop,
},
move_cb = function(self,x,y) tblGauge.ShowX = x tblGauge.ShowY = y end,
close_cb = btnStop.action, -- Windows Close button = Stop button
}
dlgGauge:showxy(intShowX,intShowY) -- Show the Progress Bar window
doReset() -- Reset the Progress Bar display
end
end,
Message = function(strText) -- Show the Progress Bar message
if dlgGauge then lblText.title = strText end
end,
Step = function(intStep) -- Step the Progress Bar forward
if dlgGauge then
intVal = intVal + ( intStep or 1 ) -- Default step is 1
local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
if intPercent ~= intNew then -- Update progress once per percent or per second, whichever is smaller
intPercent = math.max( 0.1, intNew ) -- Ensure percentage is greater than zero
if intVal > intMax then intVal = intMax intPercent = 100 end -- Ensure values do not exceed maximum
intNew = os.difftime(os.time(),intStart)
if intDelta < intNew then -- Update clock of elapsed time
intDelta = intNew
intScale = math.ceil( intDelta / intPercent ) -- Scale of seconds per percentage step
local intHour = math.floor( intDelta / 3600 )
local intMins = math.floor( intDelta / 60 - intHour * 60 )
local intSecs = intDelta - intMins * 60 - intHour * 3600
strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
end
doUpdate() -- Update the Progress Bar display
end
iup.LoopStep()
end
end,
Focus = function()
if dlgGauge then doFocus() end -- Bring the Progress Bar window to front
end,
Reset = function() -- Reset the Progress Bar display
if dlgGauge then doReset() end
end,
Stop = function() -- Check if Stop button pressed
iup.LoopStep()
return isBarStop
end,
Close = function() -- Close the Progress Bar window
isBarStop = false
if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
end,
} -- end newProgressBar
return tblProgressBar
end -- function ProgressBar
end
---------------------------------------------------------------
--DATA CLASSES
---------------------------------------------------------------
--[[Various source related classes
@Author: Helen Wright
@V1.1: Initial version.
@LastUpdated: 5 November 2019
A UI class that implement:
1. A source selector optionally with a source definition control, with options to edit the selected source or create a new source
2. A citation definition control
Multiple instances of the class can be created by a plugin.
2. a Class that implements a number of source-related utility functions
]]--
function SourceTools ()
--a class containing a number of utility routines for dealing with Sources
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
utils = require("pl.utils")
--also requires progress boilerplate and fh API
local Link = function(ptrParent,ptrSour)
local ptrWork = fhCreateItem("SOUR", ptrParent)
fhSetValueAsLink(ptrWork, ptrSour)
return ptrWork
end
local CitationDetail = function(ptrParent, values) --values: entrydate, quality, where, from, citationnote
local ptrData = nil
if not types.is_empty(values.where) then
local ptrP = fhCreateItem('PAGE',ptrParent,true)
fhSetValueAsText(ptrP, values.where)
end
if not types.is_empty(values.entrydate) then
if ptrData == nil then ptrData = fhCreateItem('DATA', ptrParent, true) end
local ptrD = fhCreateItem('DATE',ptrData,true)
fhSetValueAsText(ptrD, values.entrydate)
end
if not types.is_empty(values.from) then
if ptrData == nil then ptrData = fhCreateItem('DATA', ptrParent, true) end
local ptrT = fhCreateItem('TEXT',ptrData, true)
fhSetValueAsText(ptrT, values.from)
end
if not types.is_empty(values.quality) then
local ptrQ = fhCreateItem('QUAY',ptrParent,true)
fhSetValueAsText(ptrQ,values.quality)
end
if not types.is_empty(values.citationnote) then
local ptrN = fhCreateItem('NOTE2',ptrParent,true)
fhSetValueAsText(ptrN,values.citationnote)
end
end
return{Link = Link, CitationDetail = CitationDetail}
end
local mySourceTools = SourceTools()
function Source (imax, booE, booN, booC) --optional parameters to allow editing or creation of sources and citations
--prerquisities: Penlight libraries, DH Data api
--TODO: Include making new sources and displaying/editing source fields
--private state variables
local booEdit = booE or false --is editing source fields allowed
local booNew = booN or false --is creating new sources allowed
local imaxSource = imax or 1 --how many sources can be selected at one (0 = unlimited)
local booCitation = booC or false --is a citation container required
if booCitation then imaxSource = 1 end --only one source allowed if a citation is required
local caller = CallerSources()
local txtSources = nil --text field to display selected sources
local tblSources = {} --table of selected source pointers
local btnSources = nil --button to select a source
local boxSource = nil
local boxCitation = nil --will be used to hold citation details if required
local booEntryDateValid = true
local tblAssessment = {"Unreliable", "Questionable", "Secondary Evidence", "Primary Evidence", "" } --Source assessment text values
--public methods
local function Populate(tbltxt)
txtSources.VALUE = ""
if #tbltxt > 0 then txtSources.value = table.concat(tbltxt,"\n") end
end
local function ClearCitation()
if boxCitation then
ClearContainer(boxCitation, {})
booEntryDateValid = true
end
end
local Selector = function (strSourceTip, strOrientation, strCitationTip) --returns an iup control
local function Initialise()
local tbltxt = {}
tblSources, _, tbltxt = GetCurrentRecord {recordtype="SOUR"}
Populate(tbltxt)
end
local function MakeCitation()
local function DateDetails()
local dtEntryDate = nil --result of entry date operations
local txtEntryDate = nil
local function DateChoose()
dtEntryDate = Date()
if dtEntryDate ~= nil then
txtEntryDate.value = dtEntryDate:GetDisplayText()
else
txtEntryDate.value= ""
end
booEntryDateValid = true
txtEntryDate.fgcolor = TXTFGCOLOR
end
local function ValidateDate()
txtEntryDate.fgcolor = TXTFGCOLOR
booEntryDateValid = true
local d = txtEntryDate.value
if type(d) == "string" then
dtEntryDate, booEntryDateValid = TestTextDate(d)
end
if not booEntryDateValid then
txtEntryDate.fgcolor = colorred
end
end
local btnGetEntryDate = MakeButton{title="Date...", callback=DateChoose, tip = "Use the button to call Family Historian's Date Entry Assistant, or type directly into the Date box. \n If you type in an invalid date the text will turn red."}
txtEntryDate = MakeText{visiblelines = "1", killfocus=ValidateDate, norm = norm2, tip = "Use the button to call Family Historian's Date Entry Assistant, or type directly into the Date box. \n If you type in an invalid date the text will turn red.", name = "entrydate"}
return iup.hbox{btnGetEntryDate, txtEntryDate; alignment = "ACENTER"}
--txtEntryDate.value will be returned as part of the Citation Data
end
local function QualityDetails()
local listQuality = nil
local function CheckQualityValue(self, text, item, state)
if item == 5 and state == 1 then listQuality.value = "0" end
end
listQuality = MakeList{sort = "NO", action = CheckQualityValue, values = tblAssessment, norm = norm2, name = "quality"}
return iup.hbox{MakeLabel{title = "Assessment"},listQuality; alignment = "ACENTER"}
--listquality.value will be returned as part of the Citation Data
end
local txtWhere = MakeText{visiblelines="1", name = "where"}
local txtFrom = MakeText{scrollbar="VERTICAL", multiline="YES", name = "from"}
local txtCitationNote = MakeText{scrollbar="VERTICAL", multiline="YES", name = "citationnote"}
local box = iup.vbox{
iup.hbox{DateDetails(),QualityDetails()},
iup.vbox{MakeLabel{title = "Where in Source"}, txtWhere},
iup.vbox{MakeLabel{title = "Text from Source"}, txtFrom},
iup.vbox{MakeLabel{title = "Citation Note"}, txtCitationNote};
}
return MakeExpander(box,
strCitationTip or "Citation (Optional).",
"OPEN")
end
local function Choose()
local PreviousSource = tblSources[1] --will only be used if booCitation is true
local tbltxt = {}
if imaxSource == 0 then
tblSources, _, tbltxt = PromptForRecords {recordtype="SOUR"}
else
tblSources, _, tbltxt = PromptForRecords {recordtype="SOUR", recordcount = imaxSource}
end
Populate(tbltxt)
if booCitation and tblSources[1] ~= PreviousSource then
ClearCitation()
end
caller.CheckUIStatus()
end
strSourceTip = strSourceTip or "Use the Source button to choose sources(s) from within Family Historian."
strOrientation = strOrientation or "HORIZONTAL"
txtSources = MakeText{readonly = "YES", expand = "HORIZONTAL", norm = norm2, tip = strSourceTip, multiline = utils.choose(imaxSource ~= 1, "YES", "NO")}
Initialise()
btnSources = MakeButton{title="Source...", callback=Choose, tip = strSourceTip}
if strOrientation == "HORIZONTAL" then
boxSource = iup.hbox{btnSources, txtSources; alignment = "ACENTER"}
else
boxSource = iup.vbox{btnSources, txtSources; alignment = "ACENTER"}
end
if booCitation then
boxCitation = MakeCitation()
return iup.vbox{boxSource, boxCitation}
else
return boxSource
end
end
local SourceList = function()-- returns a table of source pointers
return tblSources
end
local ClearSelector = function()
tblSources = {}
txtSources.value = ""
if booCitation then ClearCitation() end
end
local DisableSelector = function (boo)
local strStatus = utils.choose(boo, "NO", "YES")
btnSources.active = strStatus
txtSources.active = strStatus
if boo then ClearSelector() end
end
local CitationData = function() --returns a table of Citation Data
local tblCitationElements = GetDataElements(boxCitation,{},{},true)
local tbldata = GetContainerData(tblCitationElements) --entrydate, quality, where, from, citationnote
if not types.is_empty(tbldata.quality) then tbldata.quality = tblAssessment[tonumber(tbldata.quality)] end
return tbldata
end
local CitationState = function(state)
boxCitation.state = state
end
local CitationOK = function()
return booEntryDateValid
end
--expose public methods
return{
Selector = Selector,
SourceList = SourceList,
ClearSelector = ClearSelector,
DisableSelector = DisableSelector,
CitationData = CitationData,
CitationState = CitationState,
CitationOK = CitationOK
}
end
function CallerSources ()
local CheckUIStatus = function()
AdjustButtons()
end
return {CheckUIStatus = CheckUIStatus}
end
--[[Individuals class
@Author: Helen Wright
@V1.0: Initial version.
@LastUpdated: 27 October 2019
A class that implements a field to prompt for one or more individuals
Multiple instances of the Individuals class can be created by a plugin
]]--
function Individuals (iMax)
--local constants go here
--private state variables
local tblIndividuals = {}
local imaxIndividuals = iMax or 0 --0 for unlimited
local txtIndividuals = nil
local btnIndividuals = nil
local caller = nil --customised class in caller plugin
--private methods
local function Populate(tbltxt)
txtIndividuals.VALUE = ""
if #tbltxt > 0 then txtIndividuals.value = table.concat(tbltxt,"\n") end
end
--public methods
local Selector = function (strIndividualTip, strOrientation)
local function Initialise()
local tbltxt = {}
tblIndividuals, _, tbltxt = GetCurrentRecord {recordtype="INDI"}
Populate(tbltxt)
end
local function Choose()
local tbltxt = {}
if imaxIndividuals == 0 then
tblIndividuals, _, tbltxt = PromptForRecords {recordtype="INDI"}
else
tblIndividuals, _, tbltxt = PromptForRecords {recordtype="INDI", recordcount = imaxIndividuals}
end
Populate(tbltxt)
caller.CheckUIStatus()
end
local strTip = strIndividualTip or "Use the button to choose individual(s) from within Family Historian."
local Orientation = strOrientation or "HORIZONTAL"
txtIndividuals = MakeText{readonly = "YES", expand = "YES", norm = norm2, tip = strTip, multiline = utils.choose(imaxIndividuals ~= 1, "YES", "NO")}
Initialise()
btnIndividuals = MakeButton{title="Individual(s)...", callback=Choose, tip = strTip}
if Orientation == "HORIZONTAL" then
return iup.hbox{btnIndividuals, txtIndividuals; alignment = "ALEFT"}
else
return iup.vbox{btnIndividuals, txtIndividuals; alignment = "ALEFT"}
end
end
local IndividualList= function () --returns a table of Individual pointers
return tblIndividuals
end
local ClearSelector = function()
tblIndividuals = {}
txtIndividuals.value = ""
end
local DisableSelector = function (boo)
local strStatus = utils.choose(boo, "NO", "YES")
btnIndividuals.active = strStatus
txtIndividuals.active = strStatus
if boo then ClearSelector() end
end
local Load = function(tblptr)
tblIndividuals = tablex.copy(tblptr)
local tbltxt = {}
for i, p in ipairs(tblptr) do
tbltxt[i]=fhGetDisplayText(p)
end
Populate(tbltxt)
end
caller = CallerIndividuals(iMax)
--expose public methods
return{
Selector = Selector,
IndividualList = IndividualList,
ClearSelector = ClearSelector,
DisableSelector = DisableSelector,
Load = Load
}
end
function CallerIndividuals(iMax)
local CheckUIStatus = function()
AdjustButtons()
end
return {CheckUIStatus = CheckUIStatus}
end
--[[Various fact-related classes
@Author: Helen Wright
@V1.0: Initial version.
@LastUpdated: 6 November 2019
]]--
function FactTools ()
--a class containing a number of utility routines for dealing with Facts
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
utils = require("pl.utils")
--also requires progress boilerplate and fh API
local Exists = function(strname, booattr, booIndi)
booIndi = booIndi or true
booattr = booattr or false
local strTag, _ = fhGetFactTag(strname,
utils.choose(booattr, "Attribute", "Event"), utils.choose(booIndi, "INDI", "FAM"), false)
return strTag ~= "", strTag
end
local Create = function(strFactTag, ptr, values)
--values should be a table containing text items (or nil or ""):
--date, age, place, address, place2, attribute, cause and note
--assumes that the calling function has done validation
local ptrFact = fhCreateItem(strFactTag, ptr) --ptr can be to an Indi or Fam record
if not types.is_empty(values.factdate) then --add a fact date
local dt = fhNewDate()
local DateOK = dt:SetValueAsText(values.factdate, true)
SetDate(ptrFact, dt)
end
if not types.is_empty(values.age) then --add an age
CreateTagAsText(ptrFact, "AGE", values.age)
end
if not types.is_empty(values.place) then -- add a place
CreateTagAsText(ptrFact, "PLAC", values.place)
end
if not types.is_empty(values.address) then -- add address
CreateTagAsText(ptrFact, "ADDR", values.address)
end
if not types.is_empty(values.place2) then --add place2
CreateTagAsText(ptrFact, "_PLAC", values.place2)
end
if not types.is_empty(values.attributevalue) then
fhSetValueAsText(ptrFact,values.attributevalue)
end
if not types.is_empty(values.cause) then
CreateTagAsText(ptrFact, "CAUS", values.cause)
end
if not types.is_empty(values.factnote) then --add a note
CreateNote(ptrFact, values.factnote)
end
return ptrFact --pointer to the fact
end
local Iterate = function(pi) --iterate over the facts for an individual or family, pointed to by pi
local pf = fhNewItemPtr()
local pf2 = fhNewItemPtr()
pf:MoveToFirstChildItem(pi)
return function ()
while pf:IsNotNull() do
pf2:MoveTo(pf)
pf:MoveNext()
if fhIsFact(pf2) then return pf2 end
end
end
end
return{Exists = Exists, Create = Create, Iterate = Iterate}
end
local myFactTools = FactTools()
function Facts(imax)
local imaxFact = imax or 1 --how many facts can be selected at one (0 = unlimited)
local caller = CallerFacts()
local tblSelectedFacts = {}
local listFacts = nil
local oldfacts = ""
local txtFacts = nil
local dlgFacts = nil
local boxDetails = nil
local booFactDateValid = true
--public methods
local function Parse()
local tblFactsets = {} --Fact sets found
local strfactsetfiletype = ".fhf"
local cstrFactTypeRoot = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Fact Types"
local cstrCustomFactsDir = cstrFactTypeRoot.."\\Custom\\"
local cstrStandardFactsDir = cstrFactTypeRoot.."\\Standard\\"
local cstrFactSetIndex = cstrStandardFactsDir.."GroupIndex.fhdata"
local tblFacts = {}
local function ParseFile(strFacts)
--returns individual facts that are not hidden; custom facts override standard ones
local function FactIsUnique(fctname)
return tblFacts[fctname]==nil
end
local function ParseAutonote(ident)
local noteparse="%[Text%-"..utils.escape(ident).."%-Auto Note%]%c+(Count=%d+%c+.-)" --escape magic characters in ident string
local strParsed = strFacts:match(noteparse.."%c+%[")
or strFacts:match(noteparse.."$")
if strParsed ~= nil then
local strlines, content = strParsed:match("Count=(%d+)%c+(.*)")
if strlines ~= "0" then
local strAutonote = content:gsub("Line%d+=[n0];","") --remove preamble from all lines
strAutonote = strAutonote:gsub(";(%c+)","%1") --remove final ; at end of lines
return strAutonote:gsub(";$","") --and lose the very last line end
end
end
return "" --Autonote not found or contains no lines
end
local foundCount=0 --to hold the running count of facts in the file
local factCount=tonumber(strFacts:match("Count=(%d+)%c")) -- find the first instance of a Count in the file and get the total count of facts in the file from it
local strParseFact = "%[(FCT%-[_%u%-]+%-([IF])([EA]))%]%c+Name=(.-)%c.-Label=(.-)%c(.-)\r\n{END}%c" --order of these elements in the fact file seems standard
local strFacts = strFacts:gsub("(%c+%[FCT)","\r\n{END}%1").."\r\n{END}"
for ident, recordtype, facttype, name, label, strdetail in strFacts:gmatch(strParseFact) do
local hidden = strdetail:match("Hidden=(%a)%c") or "N"
if recordtype == "I" and hidden == "N" then --this is an individual fact which isn't hidden so process it
if label ~= name then pref=label else pref=name end
if FactIsUnique(pref) then -- need to check that it is unique
--extract details
local facttag = fhGetFactTag(name, utils.choose(facttype=="A","Attribute","Event"), "INDI", false)
local abbr=strdetail:match("Abbr=(.-)%c") or "" --may be blank
local date=strdetail:match("Field Date=(%d)") or "1"
local age=strdetail:match("Field Age=(%d)") or "1"
local place=strdetail:match("Field Place=(%d)") or "1"
local address=strdetail:match("Field Address=(%d)") or "1"
local note=strdetail:match("Field Note=(%d)") or "1"
--need to sanitise ident which could have a bunch of extraneous material tacked on the front and use it to get the autonote
-- local ident=sident:match(".*%[(FCT%-.-%-)$")..recordtype..facttype
local autonote=ParseAutonote(ident)
tblFacts[pref] = {}
tblFacts[pref]["ident"] = ident
tblFacts[pref]["facttype"] = utils.choose(facttype=="A","attribute","event")
tblFacts[pref]["facttag"] = facttag
tblFacts[pref]["factname"] = name
tblFacts[pref]["factlabel"] = label
tblFacts[pref]["factabbr"] = abbr
tblFacts[pref]["factdate"] = date
tblFacts[pref]["age"] = age
tblFacts[pref]["place"] = place
tblFacts[pref]["place2"] = utils.choose(facttag == "IMMI" or facttag == "EMIG", "1", "0")
tblFacts[pref]["cause"] = utils.choose(facttag == "DEAT", "1", "0")
tblFacts[pref]["attributevalue"] = utils.choose(facttype=="A","1","0")
tblFacts[pref]["address"] = address
tblFacts[pref]["factnote"] = note
tblFacts[pref]["autonote"] = autonote
--add to the Facts table (keyed on pref)
-- end
end
foundCount = foundCount+1
if foundCount == factCount then return end --decide whether to break: have all facts been found (avoids unnecessary searching of file tail)?
end
end
end
local function GetFactSize(tblFactsets)
local intSize = 0
for _, filename in ipairs(tblFactsets) do -- Get facts sets file sizes
local attr = lfs.attributes(utils.choose(filename=="Standard",cstrStandardFactsDir, cstrCustomFactsDir)..filename..strfactsetfiletype)
intSize = intSize + attr.size
end
return intSize
end
-- get list of files to process
local strFactsSetList = ReadUTF16File(cstrFactSetIndex)
for fs,fileind in strFactsSetList:gmatch("([%w_ ]+)=(%d+)") do
tblFactsets[tonumber(fileind)]=fs
end
local progbar = ProgressBar()
if GetFactSize(tblFactsets) > 400000 then -- start progressbar
progbar.Start("Loading Fact Sets",#tblFactsets)
end
for _, filename in ipairs(tblFactsets) do --process facts sets in ascending fileind order to ensure older definitions don't overwrite new
progbar.Message("Loading "..filename)
progbar.Step(1)
local strFactsSet = ReadUTF16File(utils.choose(filename=="Standard",cstrStandardFactsDir, cstrCustomFactsDir)..filename..strfactsetfiletype)
ParseFile(strFactsSet)
end
progbar.Close()
--tblFacts now holds the list of facts and their associated parameters
return tblFacts
end
local tblFacts = Parse()
local function RestrictedFields()
if boxDetails ~= nil then
local tblboxDetails = GetDataElements(boxDetails, {}, {})
tblboxDetails[5].ACTIVE = "NO" -- Place 2
tblboxDetails[6].ACTIVE = "NO" -- Attribute
tblboxDetails[7].ACTIVE = "NO" -- Cause
local tblSelFacts = utils.choose (imax ~= 1, tblSelectedFacts, {listFacts[1]})
for _, pref in pairs(tblSelFacts) do
local tblDetails = tblFacts[pref]
if tblDetails["place2"]== "1" then tblboxDetails[5].ACTIVE = "YES" end
if tblDetails["facttype"] == "attribute" then tblboxDetails[6].ACTIVE = "YES" end
if tblDetails["cause"] == "1" then tblboxDetails[7].ACTIVE = "YES" end
end
end
end
local EnableRestrictedFactFields = function()
RestrictedFields()
end
local Selector = function (strT, strO)
--returns an iup control which may be:
-- a dropdown list if imax == 1
-- a text box plus button plus popup behind it if imax ~= 1
strOrientation = strO or "HORIZONTAL"
local function MakeBoxChooseFacts()
local function FactsConfirm()
txtFacts.value = "" -- empty existing fact selections if anything
tblSelectedFacts = {}
--populate txtFacts and tblSelectedTasks from listFacts
local strSelectionState = listFacts.value -- a sequence of + and -
for i = 1, #strSelectionState do
if strSelectionState:sub(i,i) == "\+" then --Template is selected
table.insert(tblSelectedFacts, listFacts[i])
txtFacts.APPEND = listFacts[i]
end
end
return true
end
local function FactsClear()
listFacts.value = ""
end
local function FactsRestore()
listFacts.value = oldfacts
return FactsConfirm()
end
local function FactsShow(state)
oldfacts = listFacts.value
end
local btnChooseFacts = MakeButton{title="Confirm Facts", callback=FactsConfirm, close="YES", tip = "Confirm that you have selected all the relevant facts."}
local btnCancelChoice = MakeButton{title="Cancel", tip = "Discard changes and exit.", callback = FactsRestore, close = "YES"}
local btnClear = MakeButton{title="Clear", callback=FactsClear, tip = "Clear fact selection."}
listFacts = MakeList{dropdown="NO", visiblelines="20", multiple="YES", values = tblFacts}
d = MakeDialog(
iup.vbox{listFacts,
iup.hbox{iup.fill{}, btnClear, btnChooseFacts, btnCancelChoice};expandchildren = "HORIZONTAL"
},
{title = "Choose Facts", menubox="NO", show = FactsShow })
d.close_cb = function()
d.show_cb = nil --avoid iup bug
return iup.CLOSE
end
return d
end
local function FactsChoose()
--pop up a multiselect combolist
dlgFacts:popup(iup.CENTERPARENT, iup.CENTERPARENT) --display the fact choice dialog
caller.CheckUIStatus()
end
if imaxFact == 1 then
--TODO: Not tested -- single fact selector
local function Updated(l, text,item,state)
caller.CheckUIStatus()
end
local strTip = strT or "Use the dropdown to choose a fact."
local lblChooseFacts = MakeLabel{title = "Choose fact", tip = strTip}
listFacts = MakeList{tip = strTip, values = tblFacts, action = Updated}
return utils.choose(strOrientation == "HORIZONTAL",
iup.hbox{lblChooseFacts, lstFacts},
iup.vbox{lblChooseFacts, lstFacts}
)
else
dlgFacts = MakeBoxChooseFacts()
local strTip = strT or "Use the button to choose one or more facts."
local btnChooseFacts = MakeButton{title="Fact(s)...", callback=FactsChoose, tip = strTip}
txtFacts=MakeText{multiline="YES", readonly = "YES", expand = "YES", norm = norm2, tip = strTip}
return utils.choose(strOrientation == "HORIZONTAL",
iup.hbox{btnChooseFacts, txtFacts},
iup.vbox{btnChooseFacts, txtFacts}
)
end
end
local ClearSelector = function()
if imaxFact == 1 then
listFacts.value = 0
else
listFacts.value = ""
txtFacts.value = ""
tblSelectedFacts = {}
oldfacts = {}
end
if boxDetails then
ClearContainer(boxDetails,{})
RestrictedFields()
end
end
local FactList = function()-- returns a table of facts
return utils.choose (imax ~= 1, tblSelectedFacts, {listFacts[1]}) --return a table of fact names
end
local Details = function(strTitle, expanderstate, booNoteToggle, strNoteTip)
local function DateDetails()
local dtFactDate = nil --result of entry date operations
local txtFactDate = nil
local function DateChoose()
dtFactDate = Date()
if dtFactDate ~= nil then
txtFactDate.value = dtFactDate:GetDisplayText()
else
txtFactDate.value= ""
end
booFactDateValid = true
txtFactDate.fgcolor = TXTFGCOLOR
end
local function ValidateDate()
txtFactDate.fgcolor = TXTFGCOLOR
booFactDateValid = true
local d = txtFactDate.value
if type(d) == "string" then
dtFactDate, booFactDateValid = TestTextDate(d)
end
if not booFactDateValid then
txtFactDate.fgcolor = colorred
end
end
local btnGetFactDate = MakeButton{title="Date...", callback=DateChoose, tip = "Use the button to call Family Historian's Date Entry Assistant, or type directly into the Date box.\nIf you type in an invalid date the text will turn red and you will not be able to use the fact details."}
txtFactDate = MakeText{visiblelines = "1", killfocus=ValidateDate, norm = norm2, tip = "Use the button to call Family Historian's Date Entry Assistant, or type directly into the Date box.\nIf you type in an invalid date the text will turn red and you will not be able to use the fact details.", name = "factdate"}
return iup.hbox{btnGetFactDate, txtFactDate; alignment = "ACENTER"}
--txtEntryDate.value will be returned as part of the Citation Data
end
local function AgeDetails()
local dlgage = nil
local txtage = nil
local function MakeBoxChooseAge()
local child, infant, stillborn, agebelow, younger, older = nil
local txtyears, txtmonths, txtdays = nil
local function AgeConfirm()
local strAge = ""
if child.value == "ON" then txtage.value = "Child" return end
if infant.value == "ON" then txtage.value = "Infant" return end
if stillborn.value == "ON" then txtage.value = "Stillborn" return end
local y = txtyears.VALUE
local m = txtmonths.VALUE
local d = txtdays.VALUE
if type(y) == "string" and (y ~= "" and y ~= "0") then
strAge = strAge..y.." yr"
if tonumber(y) > 1 then strAge = strAge.."s" end
strAge = strAge.." "
end
if type(m) == "string" and (m ~= "" and m ~= "0") then
strAge = strAge..m.." mn"
if tonumber(m) > 1 then strAge = strAge.."s" end
strAge = strAge.." "
end
if type(d) == "string" and (d ~= "" and d ~= "0") then
strAge = strAge..d.." dy"
if tonumber(d) > 1 then strAge = strAge.."s" end
end
strAge = stringx.strip(strAge)
if strAge ~= "" then
if younger.value == "ON" then
strAge = "< "..strAge
elseif older.value == "ON" then
strAge = "> "..strAge
end
end
txtage.value = strAge
return true
end
local btnChooseAge = MakeButton{title="Confirm Age", callback=AgeConfirm, close="YES"}
local btnCancelAge = MakeButton{title="Cancel"}
txtyears = MakeText{visiblelines="1", filter="NUMBER", norm=norm}
txtmonths = MakeText{visiblelines="1", filter="NUMBER", norm=norm}
txtdays = MakeText{visiblelines="1", filter="NUMBER", norm=norm}
local boxYears= iup.hbox{MakeLabel{title = "Years"}, txtyears;}
local boxMonths = iup.hbox{MakeLabel{title = "Months"}, txtmonths;}
local boxDays = iup.hbox{MakeLabel{title = "Days"}, txtdays;}
local agespec = iup.vbox{boxYears, boxMonths, boxDays;}
agespec.active = "YES"
txtyears.value = ""
txtmonths.value = ""
txtdays.value = ""
local function ToggleAction1(state)
if state == 1 then agespec.active = "YES" else agespec.active = "NO" end
end
local function ToggleAction2(state)
if state == 1 then agespec.active = "NO" else agespec.active = "YES" end
end
child = MakeToggle{title="Child", action = ToggleAction1}
infant = MakeToggle{title="Infant", action = ToggleAction1}
stillborn = MakeToggle{title="Stillborn", action = ToggleAction1}
agebelow = MakeToggle{title="Age given below", action = ToggleAction2}
younger = MakeToggle{title="Younger than age given below", action = ToggleAction2}
older = MakeToggle{title="Older than age given below", action = ToggleAction2}
return MakeDialog(
iup.vbox{
iup.radio{iup.vbox{child,infant,stillborn,agebelow,younger,older}; value=agebelow},
agespec,
iup.hbox{iup.fill{}, btnChooseAge, btnCancelAge};
},
{title = "Enter Age", menubox="NO", resize="NO"})
end
local function AgeChoose()
dlgage:popup(iup.CENTERPARENT, iup.CENTERPARENT)
end
dlgage = MakeBoxChooseAge()
local btnSetAge = MakeButton{title="Age...", callback=AgeChoose, tip = "Use the button to open an Age Entry Assistant, or type directly into the age box.\nYou will not be able to enter an invalid age."}
txtage = MakeText{visiblelines = "1", norm = norm2, tip = "Use the button to open an Age Entry Assistant, or type directly into the age box.\nYou will not be able to enter an invalid age.", name = "age"}
txtage.mask = "^(Child|Infant|Stillborn|([<>]?((/d+)|(/d+y)|(/d+y/s/d+m)|(/d+y/s/d+m/s/d+d)|(/d+y/s/d+d)|(/d+m/s/d+d)|(/d+d))))"
return iup.hbox{btnSetAge, txtage; alignment = "ACENTER"}
end
local function PlaceandAddressDetails()
local txtSelPlace = "" -- selected place
local txtSelPlace2 = "" --selected place2
local txtSelAddress = "" --selected address
local listPlace1 = nil
local listPlace2 = nil
local listAddress = nil
local tblPlaces = Places() --place lookup data
local tblAddresses = Addresses() -- address lookup data
local function PlaceNew(p)
if p ~= "" then --ignore empty strings
tblPlaces[p] = 1
listPlace1.APPENDITEM = p
listPlace2.APPENDITEM = p
listAddress.value = ""
end
end
local function Place1LostFocus()
--Place has lost focus so populate the address list if the place has changed
local placevalue = stringx.strip(listPlace1.value)
if placevalue == txtSelPlace then return end --place hasn't changed
if type(placevalue) == "string" then
txtSelPlace = placevalue -- includes an empty string
if tblPlaces[txtSelPlace] == nil then
--this is a new place or an empty string
PlaceNew(txtSelPlace)
end
listPlace1.value = txtSelPlace
else --no place Selected
txtSelPlace = ""
end
--populate addresses
listAddress.REMOVEITEM = "ALL"
PopulateList(listAddress,AddressesForPlace(txtSelPlace, tblAddresses))
end
local function Place2LostFocus()
--Place 2 has lost focus so check whether a new place should be created
local placevalue = stringx.strip(listPlace2.value)
if placevalue == txtSelPlace2 then return end
if type(placevalue) == "string" then
txtSelPlace2 = placevalue -- includes an empty string
if tblPlaces[txtSelPlace2] == nil then
--this is a new place or an empty string
PlaceNew(gtxtSelPlace2)
end
listPlace2.value = txtSelPlace2
else --no place Selected
txtSelPlace2 = ""
end
end
local function AddressLostFocus()
--Address has lost focus so check whether a new address should be created
local addressvalue = listAddress.value
if type(addressvalue) == "string" then
txtSelAddress = stringx.strip(addressvalue) -- includes an empty string
if tblAddresses[txtSelPlace..txtSelAddress] == nil and txtSelAddress ~= "" then -- this is a new address
tblAddresses[txtSelPlace..txtSelAddress]={txtSelAddress, txtSelPlace}
listAddress.APPENDITEM = txtSelAddress
end
if txtSelAddress ~= addressvalue then listAddress.value = txtSelAddress end
else --no place Selected
txtSelAddress = ""
end
end
function ClearPlacesandAddress()
txtSelPlace = "" -- selected place
txtSelPlace2 = "" --selected place2
txtSelAddress = "" --selected address
PopulateList(listAddress,AddressesForPlace(txtSelPlace, tblAddresses))
end
listPlace1 = MakeList{editbox = "YES", values=tblPlaces, killfocus = Place1LostFocus, name = "place"}
listPlace2 = MakeList{editbox = "YES", values=tblPlaces, killfocus = Place2LostFocus, tip = "Place 2 will only be used for facts that accept 2 places,\nsuch as Immigration and Emigration.", name = "place2"}
listAddress = MakeList{editbox = "YES", killfocus = AddressLostFocus, values = AddressesForPlace("", tblAddresses), name = "address"}
listPlace2.active = "NO"
return iup.vbox{iup.hbox{MakeLabel{title = "Place"}, listPlace1;},
iup.hbox{MakeLabel{title = "Address"}, listAddress;},
iup.hbox{MakeLabel{title = "Place 2", tip = "Place 2 will only be used for facts that accept 2 places,\n: Immigration and Emigration."}, listPlace2;};}
end
local function AttributeDetails()
local txtAttribute = MakeText{tip = "Attribute will only be used for facts which accept a value.", norm =norm2, name = "attributevalue"}
txtAttribute.active = "NO"
return iup.hbox{MakeLabel{title="Attribute", tip = "Attribute will only be used for facts which accept a value."}, txtAttribute;}
end
local function CauseDetails()
local txtCause = MakeText{tip = "Cause will only be used for a death fact.", norm = norm2, name = "cause"}
txtCause.active = "NO"
return iup.hbox{MakeLabel{title="Cause", tip = "Cause will only be used for a death fact."}, txtCause;}
end
local function NoteDetails()
local txtNote = MakeText{multiline="YES", scrollbar = "VERTICAL", visiblelines = "2", wordwrap = "YES", tip = strNoteTip, name = "factnote"}
if booNoteToggle then
local txtAppendNote = MakeToggle{title="Append note to Auto-create Note if one exists", value=0, name = "appendautonote"}
return iup.vbox{MakeLabel{title = "Note",tip = strNoteTip}, txtNote, txtAppendNote;}
else
return iup.vbox{MakeLabel{title = "Note",tip = strNoteTip},txtNote;}
end
end
boxDetails = iup.vbox{
iup.hbox{DateDetails(), AgeDetails()}, PlaceandAddressDetails(),
iup.hbox{AttributeDetails(), CauseDetails()}, NoteDetails();
}
return MakeExpander(boxDetails, strTitle, expanderstate)
end
local DetailValues = function()
local tblboxDetails = GetDataElements(boxDetails, {}, {}, true) --a table of controls keyed by control name
return GetContainerData(tblboxDetails) -- factdate, age, place, address, place2, attributevalue, cause, factnote and appendautonote all as text
end
local DetailsValid = function()
return booFactDateValid
end
local FactFields = function(pref)
return tblFacts[pref]
end
--expose public methods
return{
Selector = Selector,
ClearSelector = ClearSelector,
FactList = FactList,
Details = Details,
DetailValues = DetailValues,
DetailsValid = DetailsValid,
EnableRestrictedFactFields = EnableRestrictedFactFields,
FactFields = FactFields
}
end
function CallerFacts()
local CheckUIStatus = function()
AdjustButtons()
end
return {CheckUIStatus = CheckUIStatus}
end
---------------------------------------------------------------
--Results And Activity Log classes
---------------------------------------------------------------
--[[Results And Activity Log classes
@Author: Helen Wright
@Version: 1.0
@LastUpdated: 27 October 2019
@V1.0: Initial version.
Classes that manage an ongoing Activity Log in an expander; or manage the results display within FH when the plugin exsts.
Multiple instances of the activity log can be created, but only one instance of the Results class (FH limitation)
]]--
function ActivityLog ()
require("iuplua") -- GUI
--also requires Dialog boilerplate
--public methods and associated private state variables
local Log = nil --control for activity log
local Update = function(strupdate)
Log.append = strupdate
end
local Make = function (s,t)
local state = s or "OPEN"
local title = t or "Activity Log"
Log = MakeText{multiline="YES", scrollbar = "BOTH", readonly="YES"}
return MakeExpander(iup.vbox{Log;}, title, state)
end
--expose public methods
return{Make=Make, Update = Update}
end
function Results (intTableCount)
require("iuplua") -- GUI
--also requires fh API and Dialog boilerplate
--public methods and associated private state variables
local iRes = 0 -- index used to track results
local strTitle = ""
local tblResults = {} --table of results tables
local tblVisibility = {}
local tblResultHeadings = {}
local tblResultType = {}
for i = 1, intTableCount do
tblResults[i] = {}
end
local Update = function(tblNewResults)
iRes = iRes + 1
for i, v in ipairs(tblNewResults) do
tblResults[i][iRes] = v
end
end
local Title = function(str)
strTitle = str
end
local Types = function(types)
tblResultType = tablex.copy(types)
end
local Headings = function(headings)
tblResultHeadings = tablex.copy(headings)
end
local Visibility = function(visibility)
tblVisibility =tablex.copy(visibility)
end
local Display = function()
if iRes > 0 then -- there are results to display
fhOutputResultSetTitles(strTitle)
for i, _ in ipairs(tblResults) do
local strV = utils.choose(tblVisibility[i]==true, "show", "hide")
fhOutputResultSetColumn(tblResultHeadings[i], tblResultType[i], tblResults[i], iRes, 80 ,"align_left",i, true , "default", strV)
end
end
fhUpdateDisplay()
end
--expose public methods
return{Title = Title, Headings = Headings, Visibility = Visibility, Types = Types, Update = Update, Display = Display}
end
--------------------------------------------------------------
--MAIN DIALOG ACTIONS
--------------------------------------------------------------
function MakeMainDialog()
local function FactsCreate()
--check for invalid data
if mySource.CitationOK() == false then
Messagebox("Invalid date","Your citation has an invalid entry date so facts cannot be created. Please correct the date highlighted in red and retry",false)
return
elseif myFacts.DetailsValid() == false then
Messagebox("Invalid date","Your fact details have an invalid fact date so facts cannot be created. Please correct the date highlighted in red and retry",false)
return
end
local tblSources = mySource.SourceList() --only the first item will be used (it's a pointer)
local tblCitation = mySource.CitationData() --user specified entrydate, quality, where, from, citationnote
local tblIndividuals = myIndividuals.IndividualList()
local tblFacts = myFacts.FactList()
local tblFactDetails = myFacts.DetailValues() -- user specified factdate, age, place, address, place2, attributevalue, cause, factnote and appendautonote
for _, ptrIndi in pairs(tblIndividuals) do
for _, pref in pairs(tblFacts) do
local tblFactFields = myFacts.FactFields(pref) --get results of parsing the current fact
local tbldet = tablex.copy(tblFactDetails) --make a copy of the user specified details to amend based on validity of fields for the current fact
for _, k in ipairs{"factdate", "age", "place", "address", "place2", "attributevalue", "cause", "factnote"} do --iterate the details needed to make the fact
if tblFactFields[k]=="0" then tbldet[k]="" end--clear fields not supported for the current fact
end
if tblFactFields["factnote"]=="1" and tbldet["appendautonote"]=="ON" then --add the autonote to the note (if any)
tbldet["factnote"] = tbldet["factnote"]..utils.choose(tbldet["factnote"] == "", "", "\n")..tblFactFields["autonote"]
end
local ptrFact = myFactTools.Create(tblFactFields["facttag"], ptrIndi, tbldet) --create the fact for the individual
local ptrCitation = mySourceTools.Link(ptrFact, tblSources[1]) --Link the Source
mySourceTools.CitationDetail(ptrCitation, tblCitation) --and populate the citation
myActivity.Update(fhGetDisplayText(tblSources[1]).." | "..fhGetDisplayText(ptrIndi).." | ".. pref)
myResults.Update({tblSources[1]:Clone(), ptrIndi:Clone(), pref, ptrFact:Clone()})
end
end
end
local function FactsClear()
mySource.ClearSelector()
myIndividuals.ClearSelector()
myFacts.ClearSelector()
EnableButtons({btnCreateFacts},"NO")
end
--create buttons
btnCreateFacts = MakeButton{title="Create Facts", callback = FactsCreate, tip = "Create facts using the specified data."}
local btnExitFacts = MakeButton{title="Exit"}
local btnClear = MakeButton{title="Clear", callback= FactsClear, tip = "Clear all selections and data."}
--make dialog
local d = MakeDialog(iup.hbox{
iup.vbox{
mySource.Selector("Use the button to choose a source from within Family Historian.\nYou cannot create facts until you have selected a source.", "HORIZONTAL", "Citation (Optional). Any items specified here will be applied to all facts created."),
iup.hbox{
myIndividuals.Selector("Use the button to choose one or more individuals from within FH.\nYou cannot create facts until you have selected one or more invididuals.","VERTICAL"),
myFacts.Selector("Use the button to choose one or more Facts.\n You cannot create facts until you have made a selection","VERTICAL")
},
myFacts.Details("Fact details (Optional). Any items specified here will be applied to all facts created.", "OPEN", true, "If you specify a note, that will be added to the facts that are created.\n If you don't specify a note, and a fact has an AutoCreate Note defined,\nthe AutoCreate note will be created for that fact.\nYou can also choose to append this note to an AutoCreate note that is defined."),
iup.hbox{iup.fill{}, btnClear, btnCreateFacts, btnExitFacts},
myActivity.Make("CLOSE", "Facts created");
}
},
{title = cstrPluginName})
d.close_cb = function()
d.show_cb = nil --avoid iup bug
return iup.CLOSE
end
DoNormalize() -- normalise all dialogs
EnableButtons({btnCreateFacts},"NO")
return d
end
function AdjustButtons()
EnableButtons({btnCreateFacts},"NO")
local sourcelist = mySource.SourceList()
local indilist = myIndividuals.IndividualList()
local flist = myFacts.FactList()
myFacts.EnableRestrictedFactFields()
if #sourcelist >= 1 and #indilist >= 1 and #flist >= 1 then
EnableButtons({btnCreateFacts},"YES")
end
end
-------------------------------------
--EXECUTE
-------------------------------------
do
SetUTF8IfPossible()
SetTextSize()
myResults = Results(4) --initialise results handling with 4 results tables
myResults.Title("Facts created")
myResults.Headings({"Source", "Individual", "Fact", "Fact link"})
myResults.Types({"item", "item", "text", "item"})
myResults.Visibility({true,true,true,true})
myActivity = ActivityLog()
mySource = Source(1, false, false, true) --create a source object for a single source with a citation
myIndividuals = Individuals(0) --create an individuals object for an unlimited set of individuals
myFacts = Facts(0) --create a facts object for an unlimited set of facts
dlgmain = MakeMainDialog()
dlgmain:show()
mySource.CitationState("CLOSE")
iup.MainLoop()
DestroyAllDialogs()
myResults.Display()
end
--DONE Source:Multifact-3.fh_lua