Research Planner.fh_lua--[[
@Title: Research Planner
@Author: Helen Wright
@Version: 2.0.1
@LastUpdated: 6 December 2019
@Description: Support creation of research tasks using a custom attribute or shared notes; optionally allows templates to support the creation of tasks in bulk
]]--
--[[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
Version 2: Enhanced functionality, including config options, ability use shared Notes or Facts to record tasks, and ability to create one-off tasks (not using templates).
Version 2.0.1: Modified help file extraction method to work for Wine users
]]--
--[[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 cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local cstrPluginVersion = "2.0"
local cstrFactName = "Task"
local cstrFactTag = ""
local myHelp = nil
local myConfig = nil
local myTemplates = nil
local indi = nil
local myResults = nil
local myActivity = nil
local myReports = nil
local strSep = ": "
local strRule = string.rep('_', 20).."\n"
--------------------------------------------------------------
--UTILITY CODE AND OBJECTS
--------------------------------------------------------------
--[[ 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.normalizergroup = options.norm or norm
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
--[[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
--[[Routine to extract zip files]]--
do
if not fhloadrequire("zip") then return end
function ExtractZip(zipPath, zipFilename, destinationPath)
local zfile = assert(zip.open(zipPath.."\\"..zipFilename),"Failed to open zip file "..zipPath.."\\"..zipFilename) --open the zip file for processing
local function CopyFile(file)
local currFile, err = zfile:open(file.filename) --open a file within the zipfile
local currFileContents = currFile:read("*a") -- read entire contents of current file
local hBinaryOutput = io.open(destinationPath .."\\".. file.filename, "wb") --open an outputfile
-- write current file inside zip to a file outside zip
if hBinaryOutput then
hBinaryOutput:write(currFileContents) --write the new file as a copy of the file within the zipfile
hBinaryOutput:close() --close the new file
end
currFile:close() --close the file within the zipfile
end
-- iterate through each file inside the zip file
for file in zfile:files() do
local newdir = path.dirname(file.filename)
if not path.exists(destinationPath.."\\"..newdir) then
local destPath = destinationPath
for nextdir in newdir:gmatch("[^/]+") do
destPath = destPath.."\\"..nextdir
lfs.mkdir(destPath)
end
end
if path.basename(file.filename) ~= "" then CopyFile(file) end
end
zfile:close()
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
function FactSet (factsetname)
--[[A class to create a single factset -- can be invoked many times (once per fact set)]]--
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
--Prerequisites: FH API, Files boilerplate
--initialise location variables
local FactSetDirectory = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Fact Types\\Custom\\"
local strfactsetfiletype = ".fhf"
local cstrSaveLocation = fhGetPluginDataFileName("LOCAL_MACHINE",true).."\\"..factsetname..strfactsetfiletype
local cstrInstallLocation = FactSetDirectory..factsetname..strfactsetfiletype
--initialise Factset
local FactCount = 0
local FactDef = {}
local tblFactIDs = {}
--TODO: Include handling of witness roles (not currently supported
--[[Tasks have a Name, Label (defaults to Name), Abbreviation (defaults to ""),
a FactType (Attribute or Event), a RecordType (Individual or Family),
a Sentence template, default "{individual} experienced {label} {date} {place}"
a Time frame (None, Pre-Birth, Birth, Shortly After Birth, Life - the default, Marriage, Post-Marriage, Death, Post-Death)
optional Fields Date Age Place Address Note (all default to 1)
Can be on the Fast-Add Menu (default N)
Can be Hidden (default N)
Can Use an Overide template string for fact tabs Listings (default N)
Can use an Override template string for Record window Listings (default N)
Can have an Auto-Note string (default blank)
]]--
local AddFact = function(Name, factoptions)
--set contents according to options or defaults
local Label = factoptions.Label or Name
local Abbr = factoptions.Abbreviation or Name
local Record = factoptions.Record or "I"
local Type = factoptions.Type or "E"
local Sentence = factoptions.Sentence or "{individual} experienced {label} {date} {place}"
local Timeframe = string.upper(factoptions.Timeframe or "Life")
local Date = factoptions.Date or "1"
local Age = factoptions.Age or "1"
local Place = factoptions.Place or "1"
local Address = factoptions.Address or "1"
local Note = factoptions.Note or 1
local FastAdd = factoptions.FastAdd or "N"
local Hidden = factoptions.Hidden or "N"
local OverrideFactsTab = factoptions.OverrideFactsTab or ""
local OverrideRecordWindow = factoptions.OverrideRecordWindow or ""
local AutoNote = factoptions.AutoNote or ""
FactCount = FactCount + 1
local FactID = utils.choose(Type == Event, "EVEN-", "_ATTR-")..stringx.replace(string.upper(Name)," ","-")..[[-]]..Record..Type
tblFactIDs[FactCount] = [[Item]]..FactCount..[[=]]..FactID
local FactLines = {}
table.insert(FactLines, "[FCT-"..FactID.."]")
table.insert(FactLines, "Name="..Name)
table.insert(FactLines, "Template="..Sentence)
table.insert(FactLines, "Event Tab="..OverrideFactsTab)
table.insert(FactLines, "Rec Win="..OverrideRecordWindow)
table.insert(FactLines, "Label="..Label)
table.insert(FactLines, "Abbr="..Abbr)
table.insert(FactLines, "Timeframe=".. Timeframe)
table.insert(FactLines, "Field Date="..Date)
table.insert(FactLines, "Field Age="..Age)
table.insert(FactLines, "Field Place="..Place)
table.insert(FactLines, "Field Address="..Address)
table.insert(FactLines, "Field Note="..Note)
table.insert(FactLines, "Fast-Add Menu="..FastAdd)
table.insert(FactLines, "Hidden="..Hidden)
table.insert(FactLines, "[Text-FCT-"..FactID.."-Auto Note]")
--now create the autonote lines, if any
if stringx.strip(AutoNote) == "" then
table.insert(FactLines, "Count=0")
else
local tblAN = stringx.splitlines(AutoNote)
local ANcount = #tblAN
table.insert(FactLines, "Count="..ANcount)
for i = 1, ANcount do
if i == ANcount then
table.insert(FactLines, "Line"..i.."=0;"..tblAN[i]..";") --last line in Autonote
else
table.insert(FactLines, "Line"..i.."=n;"..tblAN[i]..";")
end
end
end
table.insert(FactLines, "[FCT-"..FactID.."-ROLE]")
table.insert(FactLines, "Roles=0") --Witnesses not currently supported
FactDef[FactCount]=table.concat(FactLines, "\n") --Add the fact definition to the table of fact definitions
end
local Save = function() --this will overwrite any existing fact set with the same name
--first create the factset file contents
local strFactSetPreamble = [[
[.index]
Ver1=4
Ver2=0
]]
local strFileContents =
strFactSetPreamble.."Count="..#tblFactIDs.."\n"..
table.concat(tblFactIDs, "\n").."\n"..
table.concat(FactDef, "\n").."\n"
--now write it as a UTF16 file
WriteUTF16File(strFileContents, cstrSaveLocation)
end
local Install = function()
if FileExists(cstrSaveLocation) then --copy the file to the installation location
CopyFile(cstrSaveLocation, cstrInstallLocation) --doing it this way preserve file name case
end
end
local Download = function(downloadlocation)
--TODO: NOT TESTED --should be used for factsets with a fixed content
FileDownload(downloadlocation, cstrInstallLocation)
end
local Exists = function()
return FileExists(cstrInstallLocation)
end
return{AddFact = AddFact, Save = Save, Install = Install, Download = Download, Exists = Exists}
end
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()
--[[HTML Help class
A class that provides support for a html help file
Only a single optional instance of the Help class is supported per plugin. Best choice if context sensitive help is required but uses more disk space usually
@Author: Helen Wright
@V1.0: Initial version.
@LastUpdated: 8 November 2019
]]--
function HTMLHelp(strV)
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
require "iupluaweb"
-- Also requires File handling boilerplate, Zipfile handling boilerplate
local strVersion = strV or "1.0"
local cstrPluginDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local HelpFileDirectory = cstrPluginDir.."\\"..cstrPluginName.." Help "..strVersion
local HelpBrowserWindow = nil
local HelpBrowserControl = nil
local DialogTitle = cstrPluginName.." Help "..strVersion
local function GetHelpFile()
local downloadname = cstrPluginName.." Help "..strVersion
local downloadlocation = "http://www.fhug.org.uk/colevalleygirl/"..downloadname..".zip"
local booStatus,_ = FileDownload(downloadlocation, cstrPluginDir.."\\"..downloadname..".zip") --download the zipped help file to the plugin data directory
if booStatus == false then
Messagebox("Download failed", "Failed to download the up-to-date Help file. The plugin will try again next time you run it, or (recommended if this error persists) you can download it yourself from "..downloadlocation.." and extract its contents to directory "..cstrPluginDir)
else
Messagebox("About to extract Help files", "About to extract Help files which may take some time; please wait")
ExtractZip(cstrPluginDir, downloadname..".zip", cstrPluginDir)
DeleteFile(cstrPluginDir.."\\"..downloadname..".zip")
end
end
local function MakeHelpBrowser()
local d = nil
if HelpBrowserControl == nil then
HelpBrowserControl = iup.webbrowser{}
end
if HelpBrowserWindow == nil then
HelpBrowserWindow = MakeDialog(HelpBrowserControl,{title=DialogTitle})
HelpBrowserWindow.size = "HALFxHALF"
HelpBrowserWindow.close_cb = function()
HelpBrowserWindow:hide()
return iup.IGNORE
end
end
HelpBrowserWindow:show()
end
if path.isdir(HelpFileDirectory) then
--Up to date help file exists
else --help file must be downloaded
local tblOldHelpFiles = dir.getdirectories(cstrPluginDir)
for _, v in ipairs(tblOldHelpFiles) do
if stringx.beginswith(v, HelpFileDirectoryBase) then
dir.rmtree(v)
end
end
tblOldHelpFiles = dir.getfiles(cstrPluginDir, "*.chm")
for _, v in ipairs(tblOldHelpFiles) do
DeleteFile(v) --Delete all old help files
end
GetHelpFile() --Get the new help file
end
local Button = function(optnorm, topicpath)
--Topic path is the name within the help file of the topic required -- no changes to capitalisation or anything else
local function ShowHelpFile()
if path.isdir(HelpFileDirectory) == true then
topicpath = topicpath or "index"
MakeHelpBrowser()
HelpBrowserControl.value = HelpFileDirectory.."\\"..topicpath..".html"
end
end
optnorm = optnorm or norm
return utils.choose(path.isdir(HelpFileDirectory) == true, MakeButton{title="Help", callback=ShowHelpFile, tip = "Show help", norm = optnorm}, nil)
end
return {Button= Button}
end
--[[Config class
A Config class that provides support for a config dialog and storage for the config parameter
Only a single optional instance of the Config class is supported per plugin.
Uses a complementary class within the calling plugin to handle plugin-specific actions.
@Author: Helen Wright
@V1.0: Initial version.
@LastUpdated: 29 October 2019
]]
function Config (strV, HelpClass)
--booH true if a help file is to be used; booO true if config data required; strV identifies the version of help file and/or config file to be used.
--Prerequisites:
do
require("iuplua") -- GUI
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
--also requires:
-- fh API
-- File handling boilerplate
-- Dialog boilerplate
end
options = {} --make the options table global because it will be widely used
--local constants
local strVersion = strV or "1.0"
local cstrPluginDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local ConfigFile = cstrPluginDir.."\\"..cstrPluginName..".cfg"
--private state variables
local ctlContainer = nil
local tblControls = {}
local tblDefaults = {}
local caller = nil
local dlgoptions = nil
--private methods
local SaveConfig = function()
options.ConfigVersion = strVersion
local strConfig = "#"..cstrPluginName.." configuration "
for key, val in pairs(options) do
strConfig = strConfig.."\n"..key.."="..tostring(val)
end
WriteFile(strConfig, ConfigFile) -- write new config file
end
local DisplayConfig = function()
for key, ctl in pairs(tblControls) do --update the control values
ctl.value = options[key]
end
end
local SetConfigToDefault = function()
for key, val in pairs(tblDefaults) do
options[key]=val
end
DisplayConfig()
caller.ActionConfig()
SaveConfig()
end
local MakePromptDialog = function()
local function NewConfig()
for key, ctl in pairs(tblControls) do --read the config from the controls and update tblConfig
options[key]=ctl.value
end
caller.ActionConfig()
SaveConfig()
return true
end
local function CancelConfig()
DisplayConfig() --reset config controls to last saved values which will already have been saved and actioned
caller.ActionConfig()
return true
end
local btnSaveConfig = MakeButton{title="Confirm", tip = "Confirm options", callback = NewConfig, close = "YES", norm = norm3}
local btnResetConfig = MakeButton{title="Reset", tip = "Reset to default options", callback = SetConfigToDefault, norm = norm3}
local btnCancelConfig = MakeButton{title = "Cancel", tip = "Discard changes and exit", callback = CancelConfig, close = "YES", norm = norm3}
local btnHelp = utils.choose(HelpClass == nil, nil, HelpClass.Button(norm3, "options"))
local btnBox = iup.vbox{btnHelp, btnSaveConfig, btnResetConfig, btnCancelConfig}
dlgoptions = MakeDialog(iup.hbox{ctlContainer, btnBox},
{title="Options", expand = "NO", resize = "NO", menubox="NO"})
DoNormalize()
end
local PromptConfig = function()
dlgoptions:popup(iup.CENTERPARENT, iup.CENTERPARENT)
end
--public methods
local Initialise = function()
ctlContainer = caller.GetContainer()
tblControls = GetDataElements(ctlContainer, {}, {}, true)
tblDefaults = caller.GetDefaults()
MakePromptDialog()
if options.ConfigVersion ~= strVersion then --the config file doesn't exist or isn't up-to-date
--supplement what options exist if any with default values
for key, val in pairs(tblDefaults) do
if not options[key] then options[key] = val end
end
DisplayConfig()
caller.ActionConfig()
PromptConfig() --get user preferences and action them and save them
else
DisplayConfig()
caller.ActionConfig()
end
SaveConfig()
end
local OptionsButton = function ()
return MakeButton{title = "Options", tip = "Select plugin options", callback = PromptConfig}
end
caller = CallerConfig()
if FileExists(ConfigFile) then options, _ = config.read(ConfigFile, {convert_numbers = false}) end
--expose public methods
return{OptionsButton = OptionsButton, Initialise = Initialise}
end
function CallerConfig ()
local tblD = {} -- default config values
tblD.UseAttribute= "ON"
tblD.UseNotes = "OFF"
tblD.UseGroup = "ON"
tblD.GroupLabel = "Group"
tblD.UseTag = "ON"
tblD.PrefixTag = "#" --not a config option
tblD.TagLabel = "RP"
tblD.UseDueDate = "OFF"
tblD.DueDateLabel = "Due" --not a config option
tblD.UseUpdateDate = "OFF"
tblD.UpdateDateLabel = "Updated" --not a config option
tblD.UseFactDate = "OFF"
tblD.FactDateLabel = "Fact date" --not a config option
tblD.UseField1 = "ON"
tblD.Field1Label = "Title"
tblD.UseField2 = "ON"
tblD.Field2Label="Status"
tblD.UseField3 = "ON"
tblD.Field3Label="Priority"
tblD.UseField4 = "ON"
tblD.Field4Label="Objective"
tblD.UseField5 = "ON"
tblD.Field5Label="Notes"
--config field local variables put here for scope reasons
local UseAttribute, UseNotes,
UseGroup, GroupLabel,
UseTag, PrefixTag, TagLabel,
UseDueDate, DueDateLabel,
UseUpdateDate, UpdateDateLabel,
UseFactDate, FactDateLabel,
UseField1, Field1Label,
UseField2, Field2Label,
UseField3, Field3Label,
UseField4, Field4Label,
UseField5, Field5Label,
hboxReload
= nil --config container fields put here for scope
local GetContainer = function()
--returns a container of config controls with Names equivalent to the config keys
--make a gridbox, 2 columns wide
local gboxOptions1 = MakeGridbox{}
--make mechanism radio
UseNotes = MakeToggle{title="Use Notes", name = "UseNotes", tip = "Create research tasks using shared notes"}
UseAttribute = MakeToggle{title="Use Facts*", name = "UseAttribute", tip = "Create research tasks using a custom 'Task' attribute"}
gboxOptions1:append(iup.radio{iup.vbox{UseAttribute, UseNotes}})
--make Date Options
UseDueDate = MakeToggle{title="Due Date", name = "UseDueDate", tip = "Include a field for due date in the task details"}
UseUpdateDate = MakeToggle{title="Update Date", name = "UseUpdateDate", tip = "Include a field for updated date in the task details"}
gboxOptions1:append(iup.vbox{UseDueDate, UseUpdateDate})
--make Tag and Group Options
UseTag = MakeToggle{title="Research Tag", name = "UseTag", tip = "Include a research tag in the task details"}
TagLabel = MakeText{name = "TagLabel", tip = "String to use as a research tag. Will be prefixed with "..tblD.PrefixTag..". If no tag is specified it will default to "..tblD.TagLabel, mask = strMin1Letter, default = tblD.TagLabel}
function UseTag:action(state)
TagLabel.active = utils.choose(state==1, "YES", "NO")
end
UseGroup = MakeToggle{title="Group", name = "UseGroup", tip = "Include a group field in the task details (specified when you create the task)"}
GroupLabel = MakeText{name = "GroupLabel", tip = "Word to identify the group field in the task details. If no value is specified it will default to "..tblD.GroupLabel, mask = strMin1Char, default = tblD.GroupLabel}
function UseGroup:action(state)
GroupLabel.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseTag)
gboxOptions1:append(TagLabel)
gboxOptions1:append(UseGroup)
gboxOptions1:append(GroupLabel)
UseField1 = MakeToggle{title="Field1", name = "UseField1", tip = "Include a single line Field1 in the task details -- if using Attributes if will be treated as the Attribute value." }
Field1Label = MakeText{name = "Field1Label", tip = "Word to identify Field1 in the task details. If no value is specified it will default to "..tblD.Field1Label, mask = strMin1Char, default = tblD.Field1Label}
function UseField1:action(state)
Field1Label.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseField1)
gboxOptions1:append(Field1Label)
UseField2= MakeToggle{title="Field2", name = "UseField2", tip = "Include a single line Field2 in the task details"}
Field2Label = MakeText{name = "Field2Label", tip = "Word to identify Field2 in the task details. If no value is specified it will default to "..tblD.Field2Label, mask = strMin1Char, default = tblD.Field2Label}
function UseField2:action(state)
Field2Label.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseField2)
gboxOptions1:append(Field2Label)
UseField3 = MakeToggle{title="Field3", name = "UseField3", tip = "Include a single line Field3 in the task details"}
Field3Label = MakeText{name = "Field3Label", tip = "Word to identify Field3 in the task details. If no value is specified it will default to "..tblD.Field3Label, mask = strMin1Char, default = tblD.Field3Label}
function UseField3:action(state)
Field3Label.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseField3)
gboxOptions1:append(Field3Label)
UseField4= MakeToggle{title="Field4", name = "UseField4", tip = "Include a multiline Field4 in the task details"}
Field4Label = MakeText{name = "Field4Label", tip = "Word to identify Field4 in the task details. If no value is specified it will default to "..tblD.Field4Label, mask = strMin1Char, default = tblD.Field4Label}
function UseField4:action(state)
Field4Label.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseField4)
gboxOptions1:append(Field4Label)
UseField5 = MakeToggle{title="Field5", name = "UseField5", tip = "Include a multiline Field5 in the task details"}
Field5Label = MakeText{name = "Field5Label", tip = "Word to identify Field5 in the task details. If no value is specified it will default to "..tblD.Field5Label, mask = strMin1Char, default = tblD.Field5Label}
function UseField5:action(state)
Field5Label.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseField5)
gboxOptions1:append(Field5Label)
UseFactDate = MakeToggle{title="Fact Date", name = "UseFactDate", tip = "Include a date field in the task details"}
gboxOptions1:append(UseFactDate)
--make attribute warning
hboxReload = iup.hbox{MakeLabel{title = "* If using Facts, you will need to restart FH to\ncreate tasks with the specified configuration\ndirectly within Family Historian", norm = ""}}
function UseNotes:action(state) --disable/enable various fields if Notes is being used
UseFactDate.active = utils.choose(state==1, "NO", "YES") --Notes don't have fact dates
UseFactDate.value = utils.choose(state==1, "OFF", UseFactDate.value)
UseTag.active = utils.choose(state==1, "NO", "YES") --force it to be inactive if Notes are being used
TagLabel.active = utils.choose(state == 1, "YES", TagLabel.active)
end
return iup.vbox{gboxOptions1, hboxReload}
end
local GetDefaults = function()
return tblD
end
local ActionConfig = function()
--adjusts the main plugin UI< state etc. (including the config dialog) according to the current options
local function AdjustConfigDialog()
GroupLabel.active = utils.choose(options.UseGroup == "ON", "YES", "NO")
Field1Label.active = utils.choose(options.UseField1 == "ON", "YES", "NO")
Field2Label.active = utils.choose(options.UseField2 == "ON", "YES", "NO")
Field3Label.active = utils.choose(options.UseField3 == "ON", "YES", "NO")
Field4Label.active = utils.choose(options.UseField4 == "ON", "YES", "NO")
Field5Label.active = utils.choose(options.UseField5 == "ON", "YES", "NO")
UseFactDate.active = utils.choose(options.UseNotes == "ON", "NO", "YES") --Notes don't have fact dates
UseTag.active = utils.choose(options.UseNotes == "ON", "NO", "YES") --force it to be inactive if Notes are being used
TagLabel.active = utils.choose(options.UseNotes == "ON", "YES", TagLabel.active)
end
local function AdjustTemplateTab()
if dlgmain ~= nil then --Templates tab exists
for i = 1,5 do
HideContainer(boxTemplateEdit[i], options["UseField"..i] == "OFF") --hide the container if it is no in use
boxTemplateEdit[i][1].title = options["Field"..i.."Label"] --modify the fild label
end
end
end
local function AdjustTasksTab()
if dlgmain ~= nil then --Tasks tab exists
HideContainer(boxGroupData, options.UseGroup == "OFF")
boxGroupData[1].title = options.GroupLabel
for i = 1,5 do
HideContainer(boxTaskEdit[i], options["UseField"..i] == "OFF") --hide the container if it is no in use
boxTaskEdit[i][1].title = options["Field"..i.."Label"] --modify the fild label
end
HideContainer(boxTFactDate, options.UseFactDate == "OFF")
HideContainer(boxTDueDate, options.UseDueDate == "OFF")
end
end
local function MakeFactDefinition()
local myFactSet = FactSet(cstrPluginName)
local factsentence = [[{label}: <{value}> ]]..
utils.choose(options.UseField2 =="OFF", "",
[[{=CombineText( "[]]..options.Field2Label..[[: ", GetLabelledText( %FACT.NOTE2%, "]]..options.Field2Label..[[: " ), "]", "" )}]])..
utils.choose(options.UseField3 =="OFF", "",
[[{=CombineText( "[]]..options.Field3Label..[[: ", GetLabelledText( %FACT.NOTE2%, "]]..options.Field3Label..[[: " ), "]", "" )}]])
local Anote = MakeNoteText{
GroupValue = "",
DueDateValue = "",
UpdateDateValue = "",
FactDateValue = "",
IncludeIndividual = false,
Individual = nil,
Tag = options.TagLabel,
UseField1 = options.UseField1,
Field1Value = "",
Field1Value = "",
Field2Value = "",
Field3Value = "",
Field4Value = "",
Field5Value = "",
Source = nil
}
local DateValue = utils.choose(options.UseFactDate =="OFF", 0, 1)
local factparms = {Record="I", Type="A",
Sentence = factsentence, Timeframe = "Post-Death", Date = DateValue, Age = 0,
Place = 0 ,Address = 0, Note = 1, FastAdd = "Y", Hidden = "N",
OverrideFactsTab = factsentence,
OverrideRecordWindow = factsentence,
AutoNote = Anote}
myFactSet.AddFact(cstrFactName,factparms)
myFactSet.Save()
--Check if Fact exists
local booFactExists = false
local booFactSetExists = false
booFactSetExists = myFactSet.Exists()
if cstrFactTag ~= "" and not booFactSetExists then
--the user has created their own fact so do nothing further
else
myFactSet.Install()
booFactExists, cstrFactTag = myFactTools.Exists(cstrFactName, true, true)
end
end
local function MakeQuery()
function Query (strqtype, strtitle, strdescription, booReadOnly, strOrientation)
--prerequisites: File handling boilerplate; Penlight libraries
local cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local cstrPluginDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local QueryDirectory = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Queries\\Custom\\"
local strqueryfiletype = ".fhq"
local strQueryTitle = strtitle
local QueryTypes = {}
QueryTypes.Individual = "INDI"
QueryTypes.Family="FAM"
QueryTypes.Note = "NOTE"
QueryTypes.Source = "SOURCE"
QueryTypes.Repository = "REPO"
QueryTypes.Submitter = "SUBM"
QueryTypes.Submission = "SUBN"
QueryTypes.Media = "OBJE"
QueryTypes.Fact = "FACT"
QueryTypes.PLace = "PLAC"
local RowType = {}
RowType["Add if"] = "ADD,IF"
RowType["Add unless"] = "ADD,UNLESS"
RowType["Exclude if"] = "EXC,IF"
RowType["Exclude unless"] = "EXC,UNLESS"
local Comparators = {}
Comparators["matches"] = "="
Comparators["comes after"] = "<"
Comparators["comes before"] = ">"
Comparators["begins with"]= "begins"
Comparators["ends with"] = "ends"
Comparators["contains"] = "contains"
Comparators["is null"] = "null"
Comparators["is true"] = "true"
local RelTypes = {}
RelTypes["Ancestor"] = "ANC"
RelTypes["Descendant"] = "DESC"
RelTypes["Spouse"] = "SPOUSE"
--construct preamble
local strPreamble = [[
[Family Historian Query]
VERSION=3.0
TYPE=QQQQ
DESC=DDDD
.
TITLE="YYYY"
SUBTITLE="%#x"
ORIENTATION="OOOO"
]]
strPreamble = stringx.replace(strPreamble, "DDDD", strdescription)
strPreamble = stringx.replace(strPreamble, "YYYY", strtitle)
if booReadOnly then
strPreamble = stringx.replace(strPreamble, "TYPE=QQQQ", "TYPE=QQQQ\nATTR=READ_ONLY")
end
strPreamble = stringx.replace(strPreamble, "QQQQ", QueryTypes[strqtype])
strPreamble = stringx.replace(strPreamble, "OOOO", string.upper(strOrientation))
local tblcolumns = {}
--[[column format is
TAG = "header", "datareference", "type", "sort"
header is a string
datareference is a string (exactly as taken from the expression field within a query)
optional type can be "HIDDEN" "BUDDY" or nil -- cannot be omitted
optional sort can be "ASC" "DESC" or nil -- cannot be omitted
]]--
local AddColumn = function(sheader,sdatareference, stype, ssort)
local str = [[TAG="]]..sheader..[[","]]..sdatareference..[["]]
if stype then str = str..[[,"]]..stype..[["]] end
if ssort then
str = str..utils.choose(stype,[[,]]..ssort, [[,,]]..ssort)
end
table.insert(tblcolumns,str)
end
local tblprompts = {}
--[[Prompts are not an fH concept, but it can be useful to set a prompt and default for a field within a query.
The Query class will create prompts before filters, so that the filters can refer to the values returned by prompts
Prompt parameters are is:
default is the default value -- use * for all and * for none and "" for no default
Label is the string to use (followed by a prefix determined by the default) in the prompt and later filters
]]--
local AddPrompt = function(default, label)
local valuepromptsuffix =""
if default == "*" then
valuepromptsuffix = " (* for all)"
elseif default == "-" then
valuepromptsuffix = " (- for none)"
elseif default ~= "" then
valuepromptsuffix =" ("..default..")"
end
local s = [[FILTER=GEN,EXC,IF,Y,"]]..label..valuepromptsuffix..[[","=Text(%INDI.NOTE[1000]%)",,"=",TEXT,"]]..default..[["]]
table.insert(tblprompts, s)
end
local tblfilters = {}
--TODO: Not fully TESTED
local AddGenFilter = function(rowtype, booparm, strparmlabel, expression, boomatchcase, comparator, valuetype, value)
local rowtext = RowType[rowtype]..","
local parametertext = utils.choose(booparm, "Y,", "N,")
local labeltext = utils.choose(booparm, [["]]..strparmlabel..[[",]],[["",]])
local expressiontext = [["]]..expression..[[",]]
local comparatortext = [["]]..Comparators[comparator]..[[",]]
local matchtext = utils.choose(boomatchcase, "MC", "")..[[,]]
local strRow = "FILTER=GEN,"..rowtext..parametertext..labeltext..expressiontext..matchtext..comparatortext
if valuetype ~= nil then strRow = strRow..string.upper(valuetype)..[[,]] end
if value ~= nil then strRow = strRow..utils.choose(valuetype=="NUMBER", value, [["]]..value..[["]]) end
strRow = stringx.rstrip(strRow,",") --remove any trailing commas
table.insert(tblfilters, strRow)
end
local AddRelFilter = function(rowtype, indichoice, strparmlabel, relationship, booincludeoriginal, booincludespouses, maxgen)
--TODO: NOT AT ALL TESTED
local rowtext = RowType[rowtype]..","
booparm = indichoice == "Individual"
local parametertext = utils.choose(booparm, "Y,", "N,")
local labeltext = utils.choose(booparm, [["]]..strparmlabel..[[",]],[["",]])
local relationshiptext = [["]]..RelTypes[relationship]..[[",]]
local originaltext = utils.choose(booincludeoriginal, "ORG", "")..[[,]]
local spousetext = utils.choose(booincludespouses, "SP", "")..[[,]]
local strRow = "FILTER=REL,"..rowtext..parametertext..labeltext..relationshiptext..originaltext..spousetext
if maxgen ~= nil then local strRow=strRow..maxgen end
strRow = stingx.rstrip(strRow,",") --remove any trailing commas
table.insert(tblfilters, strRow)
end
local AddListFilter = function(rowtype, list)
--TODO: NOT AT ALL TESTED
local rowtext = RowType[rowtype]..","
local parametertext = "N,"
local labeltext = ","
local listtext = [["]]..list..[["]]
local strRow = "FILTER=LST,"..rowtext..parametertext..labeltext..listtext
table.insert(tblfilters, strRow)
end
local Install = function()
local sQ = strPreamble.."\n"..table.concat(tblcolumns,"\n").."\n"..table.concat(tblprompts,"\n").."\n"..table.concat(tblfilters,"\n") --UTF8
WriteUTF16File(sQ, QueryDirectory..strQueryTitle..strqueryfiletype)
end
local Download = function(downloadlocation, strQueryTitle)
--TODO: NOT TESTED
FileDownload(downloadlocation, QueryDirectory..strQueryTitle..strqueryfiletype)
end
return {AddColumn = AddColumn, AddPrompt = AddPrompt, AddGenFilter = AddGenFilter, AddRelFilter= AddRelFilter, AddListFilter = AddListFilter, Install = Install, Download = Download}
end
local strQueryType = utils.choose(options.UseNotes == "ON", "Note", "Fact")
local strTextField = utils.choose(options.UseNotes == "ON", "%NOTE.TEXT%", "%FACT.NOTE2%")
local myQuery = Query(
strQueryType,
"Research Tasks ("..strQueryType.."s)",
"All Research Tasks ("..strQueryType.."s) optionally filtered by various criteria",
true, "LANDSCAPE"
)
do --make columns
if strQueryType == "Note" then --identify task
myQuery.AddColumn("Individual",
[[=Record(TextToNumber(GetLabelledText(%NOTE.TEXT%,""Individual: "")),""I"")]],nil,"ASC")
myQuery.AddColumn("Note", "%NOTE%", nil, "ASC")
else
myQuery.AddColumn("Individual","=FactOwner(%FACT%,1,MALES_FIRST)",nil,"ASC")
myQuery.AddColumn("Task", "FACT",nil,"ASC")
end
for _, f in ipairs{"Group", "Tag", "FactDate", "DueDate", "UpdateDate", "Field1", "Field2", "Field3", "Field4"} do
if options["Use"..f] == "ON" then
if f == "FactDate" then
myQuery.AddColumn("Date","FACT.DATE")
elseif f == "UpdateDate" then
myQuery.AddColumn("Updated",
utils.choose(strQueryType=="Note",
[[=LastUpdated()]],
[[=GetLabelledText(%FACT.NOTE2%,""Updated: "")]])
)
elseif f == "Tag" then
myQuery.AddColumn("Tag",[[=Text(""]]..options.PrefixTag..[["".GetLabelledText(]]..strTextField..[[,""]]..options.PrefixTag..[[""))]])
else
if strQueryType == "Note" or f ~= "Field1" then
myQuery.AddColumn(options[f.."Label"],
[[=GetLabelledText(]]..strTextField..[[,""]]..options[f.."Label"]..[[: "")]])
end
end
end
end
myQuery.AddColumn("Source",string.upper(strQueryType)..".SOUR>")
myQuery.AddColumn("Repository", string.upper(strQueryType)..".SOUR>REPO>")
end
do --make defaults/prompts
local tblDefaults = {}
for _,f in ipairs{"Group","Tag", "Field2", "Field3"} do
if options["Use"..f] == "ON" then
local strLabel = utils.escape(utils.choose(f == "Tag", "Tag", options[f.."Label"]))
myQuery.AddPrompt("*", strLabel)
end
end
for _, f in ipairs{"Task", "Source", "Repository"} do
myQuery.AddPrompt("-", f.." words")
end
end
do --make filters
for _,f in ipairs{"Tag","Group", "Field2", "Field3"} do
if options["Use"..f] == "ON" then
local expression ='=IsTrue((Text([""LLLL (* for all)""]) = ""*"") or (GetLabelledText(%NOTE.TEXT%,""AAAA"") = [""LLLL (* for all)""]))'
expression = stringx.replace(expression, "LLLL", utils.escape(utils.choose(f == "Tag", "Tag", options[f.."Label"])))
expression = stringx.replace(expression, "AAAA", utils.escape(utils.choose(f == "Tag", options.PrefixTag, options[f.."Label"]..": ")))
myQuery.AddGenFilter("Exclude unless", false, "", expression, false, "is true")
end
end
myQuery.AddGenFilter("Exclude unless", false, "", '=IsTrue((Text([""Task words (- for none)""]) = ""-"") or ContainsText(%'..string.upper(strQueryType)..'%,[""Task words (- for none)""],STD))', false, "is true")
myQuery.AddGenFilter("Exclude unless", false, "", '=IsTrue((Text([""Source words (- for none)""])= ""-"") or ContainsText(%'..string.upper(strQueryType)..'.SOUR>%,[""Source words (- for none)""],STD))', false, "is true")
myQuery.AddGenFilter("Exclude unless", false, "", '=IsTrue((Text([""Repository words (- for none)""]) = ""-"") or ContainsText(%'..string.upper(strQueryType)..'.SOUR>REPO>%,[""Repository words (- for none)""],STD))', false, "is true")
if strQueryType == "Fact" then
myQuery.AddGenFilter("Exclude unless", false, "", "=FactLabel(%FACT%)", false, "matches", "TEXT", "Task") --filter on Task facts only
end
end
myQuery.Install()
end
local function AdjustToolsTab()
HideContainer(htoolsFactsBox, options.UseAttribute == "OFF")
HideContainer(htoolsNotesBox, options.UseNotes == "OFF")
end
local function AdjustResults()
--set Results parameters to defaults for creating tasks -- if any of the options on tabTools are used, they will override this
myResults.Types({"item","item","text","text","text","text","text","text","text","text","text","item","item"})
myResults.Headings({"Individual", "Task",
"Tag", options.GroupLabel, options.FactDateLabel,
options.DueDateLabel, options.UpdateDateLabel,
options.Field1Label, options.Field2Label,
options.Field3Label,options.Field4Label,
"Source", "Repository"})
myResults.Visibility({true, true,
options.UseTag == "ON", options.UseGroup == "ON",
options.UseFactDate == "ON", options.UseDueDate =="ON", options.UseUpdateDate == "ON",
options.UseField1 =="ON" and options.UseNotes == "ON", options.UseField2 == "ON",
options.UseField3 == "ON", options.UseField4 == "ON",
true, true})
end
AdjustConfigDialog()
AdjustTemplateTab()
AdjustTasksTab()
_, cstrFactTag = myFactTools.Exists(cstrFactName, true, true)
if options.UseAttribute == "ON" then MakeFactDefinition() end
MakeQuery()
AdjustToolsTab()
AdjustResults()
end
return{
GetContainer = GetContainer,
GetDefaults = GetDefaults,
ActionConfig = ActionConfig
}
end
--[[Template handling class
A Templates class that implements a set ot text-based template deinitions. The contents of the templates are determined by the calling plugin, which interacts with the template class via defined methods and makes plug-in specific features (i.e. non text-fields) available via an associated PluginTemplates class.
Multiple instances of the Templates class can be created by a plugin, as long as the plugin specifies a separate subdirectory to hold templates other than the first set (which are held in the main plugin directory.
Templates can be defined to be Global (available to all fH projects), or Project (available to a single project only). Project templates will typically be required when a template includes project-specific data such as a source identifier.
Use as follows
MyTemplate = Template(initvalue) to instantiate
MyTemplate.DoSomething(parms) to call a method
TODO: (future enhancement): the ability to use templates that include tokens (text substitions when the template is used, with default values)
@Author: Helen Wright
@V1.0: Initial version.
@LastUpdated: 27 October 2019
]]--
function Templates (booAllowProjectTemplates, subdir, booTokens)
do --prerequisites
require("iuplua")
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
-- fH API boilerplate
-- Dialog boilerplate
-- File handling boilerplate
end
local caller = CallerTemplates(subdir) --initialise caller class
--variables and functions for handling global and project templates
local booProjectTemplatesAllowed = booAllowProjectTemplates
local strGlobalTemplateDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local strProjectTemplateDir = utils.choose(booProjectTemplatesAllowed, fhGetPluginDataFileName("CURRENT_PROJECT", true), nil)
if subdir then
strGlobalTemplateDir = path.join(strGlobalTemplateDir, subdir)
if not path.isdir(strGlobalTemplateDir) then
path.makepath(strGlobalTemplateDir)
end
if booProjectTemplatesAllowed then
strProjectTemplateDir = path.join(strProjectTemplateDir, subdir)
if not path.isdir(strProjectTemplateDir) then
path.makepath(strProjectTemplateDir)
end
end
end
local function TemplatePath(boop) --returns directory
return utils.choose(boop,strProjectTemplateDir,strGlobalTemplateDir)
end
--data and function for Template details
local tblTemplates = {} --keyed by Template Name which is unique; values are Template Directory and boolean 'IsProjectTemplate'
local tblSelectors = {} --list of selectors that have been created using tblTemplates
local GetTemplates = function()
local tcount = 1
local tname = ""
tblTemplates = {}
for _, t in ipairs(dir.getfiles(strGlobalTemplateDir, "*.dat")) do
tname = path.basename(path.splitext(t)) --isolate the file name which will be the template name
tblTemplates[tname] = {strGlobalTemplateDir, false}
tcount = tcount+1
end
if booProjectTemplatesAllowed then
for _, t in ipairs(dir.getfiles(strProjectTemplateDir, "*.dat")) do
tname = path.basename(path.splitext(t)) --isolate the file name which will be the template name
tblTemplates[tname] = {strProjectTemplateDir, true}
tcount = tcount+1
end
end
end
local function RepopulateSelectors()
for _, l in ipairs(tblSelectors) do
--save current selection
if l.MULTIPLE == "YES" then
local ltbl = GetSelectedValues(l)
PopulateList(l, tblTemplates)
SetSelectedValues(l, ltbl)
else
local lvalue = GetSingleValue(l)
PopulateList(l, tblTemplates)
GoToInList(lvalue, l)
end
end
end
GetTemplates() -- populate the template details when class is instantiated
local PromptTemplateName = function(prompt, default) --returns string
local booOK, strName = GetText(prompt, default, strAlphaNumeric)
return utils.choose(booOK, stringx.strip(strName), "")
end
local PromptTemplateNameAndType = function (prompt, default) --returns boolean, string
local booOK, strName, booProject = GetTextandTick(prompt, default, strAlphaNumeric, "Project template?")
return booProject, utils.choose(booOK, stringx.strip(strName), "")
end
local function PromptName() --returns name and project indicator
local boop = false
local name = ""
if booProjectTemplatesAllowed then
boop, name = PromptTemplateNameAndType("Enter new name", "")
else -- get target name
name = PromptTemplateName("Enter new name", "")
end
return boop, name
end
local function SaveTemplate(values, name, directory)
table.save(values, path.join(directory, name.."\.dat")) --save the file
end
local function ReadTemplate(name) --returns table of values
return table.load(path.join(tblTemplates[name][1], name.."\.dat"))
end
local function AddTemplate(values, name, booproject)
SaveTemplate(values, name, TemplatePath(booproject))
GetTemplates() --repopulate the template data
RepopulateSelectors()
end
local function DeleteTemplate(name)
DeleteFile(path.join(tblTemplates[name][1], name.."\.dat"))
GetTemplates() --repopulate the template data
RepopulateSelectors()
end
local function RenameTemplate(oldname, newname)
--rename the file
local oldpath = path.join(tblTemplates[oldname][1], oldname.."\.dat")
local newpath = path.join(tblTemplates[oldname][1], newname.."\.dat")
RenameFile(oldpath, newpath)
GetTemplates() --repopulate the template data
RepopulateSelectors()
end
local TemplatesExist = function() --returns boolean
return tablex.size(tblTemplates) > 0
end
--data and functions for Template container handling
local function TemplateGetFromContainer(container) --returns table of values
local tblvalues = GetContainerData(GetDataElements(container, {},{}, false))
return caller.Get(tblvalues, container)
end
local function TemplateLoadToContainer(container, tblvalues)
for k, v in ipairs(GetDataElements(container, {},{}, false)) do
v.VALUE = tblvalues[k]
end
caller.Load(tblvalues, container)
EnableContainer(container, {}, "YES")
end
local function ContainerClear(container)
ClearContainer(container, {})
caller.Clear(container)
end
--data for Template selection controls
--methods for selecting a single template
local btnUseNew =nil
local btnUseUpdate = nil
local UseContainer = nil --iup container for the use control
local ListUse = nil
local UseSelector = function(container) --returns list
UseContainer = container
local function Use(ctl, name) --load template into UseContainer
local values = ReadTemplate(name)
local booProject = tblTemplates[name][2]
TemplateLoadToContainer(UseContainer, values)
btnUseNew.active = "ON"
btnUseUpdate.active = "ON"
caller.Display(booProject, UseContainer)
caller.CheckUIStatus()
end
local function New()
local booProject, strNewName = PromptName()
if strNewName ~= "" then --not cancelled
if tblTemplates[strNewName] then --this is a duplicate name
Messagebox("New template failed","A template called "..strNewName.." already exists", false)
return
end
local booOK, strError = caller.OK(booProject, UseContainer)
if booOK then
AddTemplate(TemplateGetFromContainer(UseContainer), strNewName, booProject)
GoToInList(strNewName, ListUse)
btnUseNew.active = "ON"
btnUseUpdate.active = "ON"
caller.Display(booProject, UseContainer)
caller.CheckUIStatus()
Messagebox("New template succeeded","Template "..strNewName.." created",false)
else
Messagebox("Save failed","Cannot save "..strNewName.." - "..strError, true)
end
end
end
local function Update()
local strname = GetSingleValue(ListUse)
local booOK, strError = caller.OK(tblTemplates[strname][2],UseContainer)
if booOK then
SaveTemplate(TemplateGetFromContainer(UseContainer), strname, tblTemplates[strname][1])
else
Messagebox("Save failed","Cannot save "..strname.." - "..strError, true)
end
end
ListUse = MakeList{values = tblTemplates, action = Use,
tip = "Choose a template"}
table.insert(tblSelectors, ListUse)
btnUseNew = MakeButton{title = "New", callback = New, tip = "Create a new template from the contents"}
btnUseUpdate = MakeButton{title = "Update", callback = Update, tip = "Update the template from the contents"}
btnUseUpdate.active = "OFF"
btnUseNew.active = "ON"
return iup.vbox{iup.hbox{MakeLabel{title = "Template", tip = cstrTemplateTip}, ListUse}, iup.hbox{iup.fill{}, btnUseNew, btnUseUpdate};}
end
local ClearUseSelector = function()
ContainerClear(UseContainer)
ListUse.value = 0
btnUseNew.active = "ON"
btnUseUpdate.active = "OFF"
end
local GetUseName = function()
return GetSingleValue(ListUse)
end
local GetUseValues = function()
return TemplateGetFromContainer(UseContainer)
end
--methods for selecting templates in bulk
local listBulk = nil
local BulkSelector= function() --returns list
local function Action()
caller.CheckUIStatus()
end
listBulk = MakeList{dropdown="NO", visiblelines = "20", multiple = "YES",
values = tblTemplates, action = Action, tip = "Choose one or more templates"}
table.insert(tblSelectors, listBulk)
return listBulk
end
local ClearBulkSelector = function()
MultiListSelectionClear(listBulk)
caller.CheckUIStatus()
end
local BulkTemplates = function() --returns successive tables of values
--iterator through selected templates
local t = GetSelectedValues(listBulk)
local contents = ""
local k, v = next(t, nil) --v is a name
return function ()
if k then
contents = ReadTemplate(v)
k, v =next(t, k)
return contents
end
end
end
local BulkTemplatesAreSelected = function() --returns boolean
return MultiListSelectionTrue(listBulk)
end
--methods and controls to manipulate template definitions
local EditContainer = nil -- iup container for the edit control
local tblLastSaved = nil
local strLastEdited = ""
local tblbtnTemplates = {}
local listEdit = nil
local function Save()
local booOK, strError = caller.OK(tblTemplates[strLastEdited][2],EditContainer)
if booOK then
tblLastSaved = TemplateGetFromContainer(EditContainer)
SaveTemplate(tblLastSaved, strLastEdited, tblTemplates[strLastEdited][1])
else
Messagebox("Save failed","Cannot save "..strLastEdited.." - "..strError, true)
end
end
local function CheckEditChanges() --returns boolean
local values = TemplateGetFromContainer(EditContainer)
if tablex.compare(values, tblLastSaved, "==") then return true end --no changes since last save
if Messagebox("Unsaved template changes","You have template changes to save. Press OK to Save those changes and Continue, or Cancel to Discard them and Continue", true) == "OK" then
local booOK, strError = caller.OK(EditContainer)
if not booOK then --cannot save so will not continue
Messagebox("Save failed","Cannot save "..strLastEdited.." - "..strError.." so will not continue with selected operation", true)
return false--abort
end
Save() --save changes
else
TemplateLoadToContainer(EditContainer, tblLastSaved) --restore last saved value
end
return true --continue
end
local EditSelector = function(container) --returns vbox
local cstrProjectTemplate = "Project template"
local cstrGlobalTemplate = "Global template"
local labProjectTemplate = MakeLabel{title="", norm = norm2}
local ctlProjectTemplateContainer = iup.hbox{labProjectTemplate}
local function SetTemplateLabel(boop)
labProjectTemplate.title = utils.choose(boop, cstrProjectTemplate, cstrGlobalTemplate)
end
local function Edit(ctl, name)
if name == strLastEdited then return end --nothing changed
if CheckEditChanges() then
local values = ReadTemplate(name)
local booProject = tblTemplates[name][2]
TemplateLoadToContainer(EditContainer, values)
SetTemplateLabel(booProject)
EnableButtons(tblbtnTemplates, "YES")
caller.Display(booProject, EditContainer)
strLastEdited = name
tblLastSaved = values
else
GoToInList(strLastEdited, listEdit) -- return to the previous selection
end
end
local function New()
if CheckEditChanges() then
local booProject, strNewName = PromptName()
if strNewName ~= "" then --not cancelled
if tblTemplates[strNewName] then --this is a duplicate name
Messagebox("New template failed","A template called "..strNewName.." already exists", false)
return
end
ContainerClear(EditContainer)
tblLastSaved = TemplateGetFromContainer(EditContainer)
AddTemplate(tblLastSaved, strNewName, booProject)
GoToInList(strNewName, listEdit)
SetTemplateLabel(booProject)
EnableButtons(tblbtnTemplates, "YES")
EnableContainer(EditContainer, {}, "YES")
caller.Display(booProject, EditContainer)
caller.CheckUIStatus()
Messagebox("Create succeeded","Template "..strNewName.." created",false)
strLastEdited = strNewName
end
end
end
local function Copy()
local strOldName = GetSingleValue(listEdit)
if CheckEditChanges() then
local booProject, strNewName = PromptName()
if strNewName ~= "" then --not cancelled
if tblTemplates[strNewName] then --this is a duplicate name
Messagebox("Copy template failed","A template called "..strNewName.." already exists", false)
else
AddTemplate(tblLastSaved, strNewName, booProject)
GoToInList(strNewName, listEdit)
Messagebox("Copy template succeeded", strOldName.." copied to "..strNewName, false)
strLastEdited = strNewName
end
end
end
end
local function Rename()
local strNewName=PromptTemplateName("Enter new name","") --can't change type of an existing template
local strOldName = GetSingleValue(listEdit)
if strNewName ~= "" then --not cancelled
if tblTemplates[strNewName] then --this is a duplicate name
Messagebox("Rename template failed","A template called "..strNewName.." already exists", false)
else
RenameTemplate(strOldName, strNewName)
GoToInList(strNewName, listEdit)
Messagebox("Rename template succeeded", strOldName.." renamed to "..strNewName, false)
strLastEdited = strNewName
end
end
end
local function Delete()
local name = GetSingleValue(listEdit)
if Messagebox("Confirm template deletion","Confirm deletion of "..name, true) == "OK" then
DeleteTemplate(name)
ContainerClear(EditContainer)
EnableContainer(EditContainer, {}, "NO") --no selected template
SetTemplateLabel(false) --default is not a project
caller.CheckUIStatus()
Messagebox("Delete template succeeded","Template "..name.." deleted",false)
strLastEdited = ""
tblLastSaved = TemplateGetFromContainer(EditContainer)
end
end
local function MakeButtons() --returns hbox
local btnCopy = MakeButton{title="Copy", callback = Copy, tip = "Copy selected template"}
local btnRename = MakeButton{title="Rename", callback = Rename, tip = "Rename selected template"}
local btnDelete = MakeButton{title="Delete", callback = Delete, tip = "Delete selected template"}
local btnSave = MakeButton{title="Save", callback = Save, tip = "Save changes to selected template"}
tblbtnTemplates = {btnCopy, btnSave, btnRename, btnDelete}
EnableButtons(tblbtnTemplates, "NO") -- can't do any of this until a template is selected
return iup.hbox{iup.fill{}, btnCopy, btnSave, btnRename, btnDelete, iup.fill{}}
end
local cstrTemplateTip = "Choose a template to modify or view"
EditContainer = container
tblLastSaved = TemplateGetFromContainer(EditContainer) --empty values
EnableContainer(EditContainer, {}, "NO")
listEdit = MakeList{values=tblTemplates, action = Edit, tip = cstrTemplateTip}
table.insert(tblSelectors, listEdit)
local vbox = iup.vbox{
iup.hbox{MakeLabel{title = "Template", tip = cstrTemplateTip},
listEdit,
MakeButton{title="New", callback = New, tip = "Create new template"};
alignment = "ACENTER"},
ctlProjectTemplateContainer,
MakeButtons()
}
SetTemplateLabel(false) --default is not a project
if booProjectTemplatesAllowed == false then
HideContainer(ctlProjectTemplateContainer)
end
return vbox
end
local function CheckForUnsavedEditChanges()
if CheckEditChanges() then caller.CheckUIStatus() return true end
return false
end
local function ClearEditSelector()
ContainerClear(EditContainer)
tblLastSaved = TemplateGetFromContainer(EditContainer)
strLastEdited = ""
listEdit.value = 0
EnableButtons(tblbtnTemplates, "NO")
caller.CheckUIStatus()
end
--expose public methods
return{TemplatesExist = TemplatesExist,
UseSelector = UseSelector,
ClearUseSelector = ClearUseSelector,
GetUseName = GetUseName,
GetUseValues = GetUseValues,
BulkSelector = BulkSelector,
BulkTemplates = BulkTemplates,
BulkTemplatesAreSelected = BulkTemplatesAreSelected,
ClearBulkSelector = ClearBulkSelector,
EditSelector = EditSelector,
ClearEditSelector = ClearEditSelector,
CheckForUnsavedEditChanges = CheckForUnsavedEditChanges}
end
function CallerTemplates(subdir)
local source = nil
local function IdentifySource(container)
return utils.choose(container == boxTemplateEdit, TemplateSource, TaskSource)
end
--public methods
local Display = function(boop, container)
source = IdentifySource(container)
source.DisableSelector(not boop and container ~= boxTaskEdit)
end
local Load = function(tblvalues, container)
--handle the source as the last item in the template
local source = IdentifySource(container)
if tblvalues[#tblvalues] ~= "" then
local ptr = fhNewItemPtr()
ptr:MoveToRecordById('SOUR', tblvalues[#tblvalues])
source.Load({ptr})
else
source.ClearSelector()
end
end
local Get = function(tblvalues, container) --returns a set of values suitable to save as a template
local source = IdentifySource(container)
--now handle the source as the last item in the template
local tbls = source.SourceList()
if #tbls > 0 then
tblvalues[#tblvalues] = fhGetRecordId(tbls[1])
else
tblvalues[#tblvalues] = ""
end
return tblvalues
end
local Clear = function(container)
local source = IdentifySource(container)
source.ClearSelector()
EnableContainer(container, {}, utils.choose(container == boxTemplateEdit, "NO", "YES"))
end
local OK = function(boop, container) --returns boolean and optional error message
local source = IdentifySource(container)
local slist = source.SourceList()
local booOK = boop == true or #slist == 0
local strError = utils.choose(booOK, "", "A global template cannot specify a source")
return booOK, strError
end
local InitialiseTemplate = function(boop, container)
return --do nothing
end
local CheckUIStatus = function()
AdjustButtons()
end
return{
Display = Display,
Load = Load,
Get = Get,
Clear = Clear,
OK = OK,
InitialiseTemplate = InitialiseTemplate,
CheckUIStatus = CheckUIStatus,
}
end
--[[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()
return --no need to do anything
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 = "HORIZONTAL", norm = norm2, tip = strTip, multiline = utils.choose(imaxIndividuals ~= 1, "YES", "NO")}
Initialise()
btnIndividuals = MakeButton{title="Individual...", callback=Choose, tip = strTip}
if Orientation == "HORIZONTAL" then
return iup.hbox{btnIndividuals, txtIndividuals; alignment = "ACENTER"}
else
return iup.vbox{btnIndividuals, txtIndividuals; alignment = "ACENTER"}
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
--[[Results And Activity Log classes
@Author: Helen Wright
@Version: 1.0
@LastUpdated: 18 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
--[[Search class
A class that implements a Generic TextSearch object
]]--
function Search (strPattern, booPlain, booWhole, booSensitive)
--TODO: Fully test Search class
--TODO: Generalise to take case sensitivity into account
--pre-requisites
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
local function isWordFoundInString(w,s)
return select(2,s:gsub('^' .. w .. '%W+','')) +
select(2,s:gsub('%W+' .. w .. '$','')) +
select(2,s:gsub('^' .. w .. '$','')) +
select(2,s:gsub('%W+' .. w .. '%W+','')) > 0
end
--private state variables
local plain = booPlain or true
local whole = booWhole or true
local spattern = strPattern
if plain then spattern = utils.escape(spattern) end --escape magic characters
local Found = function (strSearch)
local ssearch = strSearch
if whole then
return isWordFoundInString(spattern, ssearch)
else
return string.find(spattern, ssearch) ~= nil
end
end
--expose public methods
return{Found = Found}
end
function Report ()
--class to create reports
--prerequisites: File handling boilerplate; Penlight libraries
local cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local cstrPluginDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local ReportDirectory = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Reports\\Custom\\"
local strreportfiletype = ".fhr"
local Download = function(strReportTitle)
local downloadlocation = "http://www.fhug.org.uk/colevalleygirl/"..strReportTitle..strreportfiletype
FileDownload(downloadlocation, ReportDirectory..strReportTitle..strreportfiletype)
end
return {Download = Download}
end
myReports = Report()
--------------------------------------------------------------
--MAIN DIALOG ACTIONS
--------------------------------------------------------------
function AdjustButtons()
local tblIndividuals = indi.IndividualList()
local booBulkOK = myTemplates.TemplatesExist() and myTemplates.BulkTemplatesAreSelected() and #tblIndividuals > 0
btnBulkMake.active = utils.choose(booBulkOK, "YES", "NO")
local booTextOK = #tblIndividuals > 0
btnTaskMake.active = utils.choose(booTextOK, "YES", "NO") --can only make a task from text when an individual is selected
end
--[[taskoptions components; there are no defaults
These are used in both MakeNoteText and MakeTask
GroupValue
DueDateValue
UpdateDateValue
FactDateValue
IncludeIndividual = true|false
Individual = ptr
Tag
UseField1
Field1Value
Field2Value
Field3Value
Field4Value
Field5Value
Source = ptr
]]--
function MakeNoteHeader(note, taskoptions)
local strNote = note.."\[\[\n" --privacy markers
if taskoptions.UseField1 == "ON" then --set first line of note as title
strNote = strNote..options.Field1Label..strSep..taskoptions.Field1Value.."\n"..strRule.."\n"
end
if taskoptions.IncludeIndividual then
local txtIndi = fhGetDisplayText(taskoptions.Individual)
local intIndi = fhGetRecordId(taskoptions.Individual)
strNote = strNote.."Individual: "..tostring(intIndi).." "..txtIndi.."\n"..strRule.."\n"--add the identity of the individual
end
return strNote
end
function MakeNoteTail(note)
return note.."\n\]\]"
end
function AddTagTextandGroup(note, booTag, tag, booGroup, group)
if booTag and tag then
note = note..options.PrefixTag..tag.."\n"
end
if booGroup then
note = note..options.GroupLabel..strSep..group.."\n"
end
if booTag or booGroup then
note = note..strRule.."\n"
end
return note
end
function MakeNoteText(taskoptions)
local strNote = ""
local booRuleNeeded = false
local function InsertRuleIfNeeded()
if booRuleNeeded == true then
strNote = strNote..strRule.."\n"
booRuleNeeded = false
end
end
if options.UseNotes == "ON" then strNote = MakeNoteHeader(strNote, taskoptions) end
strNote = AddTagTextandGroup(strNote, options.UseTag =="ON", taskoptions.Tag, options.UseGroup == "ON", taskoptions.GroupValue)
if options.UseDueDate == "ON" then
strNote = strNote..options.DueDateLabel..strSep..taskoptions.DueDateValue .."\n"
booRuleNeeded = true
end
if options.UseUpdateDate == "ON" and options.UseAttribute == "ON" then
strNote = strNote..options.UpdateDateLabel..strSep..taskoptions.UpdateDateValue .."\n"
booRuleNeeded = true
end
InsertRuleIfNeeded()
for i = 2,3 do
if options["UseField"..tostring(i)] == "ON" then
strNote = strNote..options["Field"..tostring(i).."Label"]..strSep..taskoptions["Field"..tostring(i).."Value"].."\n"
booRuleNeeded = true
end
end
for i = 4,5 do
if options["UseField"..tostring(i)] == "ON" then
InsertRuleIfNeeded()
strNote = strNote..options["Field"..tostring(i).."Label"]..strSep..taskoptions["Field"..tostring(i).."Value"].."\n"
booRuleNeeded = true
end
end
if options.UseNotes == "ON" then strNote = MakeNoteTail(strNote) end
return strNote
end
function MakeTask(strNote, taskoptions)
local ptrTask = nil --will hold a pointer to the new task item
if options.UseAttribute == "ON" then
local values = {}
values.attributevalue = utils.choose(options.UseField1 == "ON", taskoptions.Field1Value, "")
if options.UseFactDate == "ON" then
values.factdate = taskoptions.FactDateValue
end
values.factnote = strNote
ptrTask = myFactTools.Create(cstrFactTag, taskoptions.Individual, values) --create the task fact linked to the individual with fact date and note if appropriate
else --using Notes
ptrTask = CreateSharedNote(strNote) --create the note
CreateNoteLink(taskoptions.Individual, ptrTask) --link the note to the individuals
end
local ptrS = taskoptions.Source
local ptrR = nil
if ptrS:IsNotNull() then
mySourceTools.Link(ptrTask,ptrS) --add source link if specified
if fhGetItemPtr(ptrS,'~.REPO>'):IsNotNull() then
ptrR = fhGetItemPtr(ptrS,'~.REPO>')
end
end
local txtIndi = fhGetDisplayText(taskoptions.Individual)
local strLogText = txtIndi.." | ".. taskoptions.Field1Value
if options.UseGroup == "ON" then
if taskoptions.GroupValue ~= "" then
strLogText = strLogText .." | "..options.GroupLabel..": "..taskoptions.GroupValue
end
end
myActivity.Update(strLogText)
local resultstable = {taskoptions.Individual, ptrTask, options.PrefixTag..options.TagLabel, taskoptions.GroupValue,
taskoptions.FactDateValue, taskoptions.DueDateValue, taskoptions.UpdateDateValue,
taskoptions.Field1Value, taskoptions.Field2Value, taskoptions.Field3Value, taskoptions.Field4Value,
ptrS, ptrR}
myResults.Update(resultstable)
myResults.Title("Tasks created")
EnableContainer(tabTools,{},"NO")--disable tools because results tables are being used for task creation
end
function MakeMainDialog()
local function MakeBulkTasks()
local tblTemplateValues = {}
for tblTemplateValues in myTemplates.BulkTemplates() do --returns a table of values 1-6, 6 being the source
local taskoptions = {
DueDateValue = "",
UpdateDateValue = os.date("%x"),
FactDateValue = os.date("%x"),
GroupValue = boxGroupData[2].value,
Field1Value = tblTemplateValues[1],
Field2Value = tblTemplateValues[2],
Field3Value = tblTemplateValues[3],
Field4Value = tblTemplateValues[4],
Field5Value = tblTemplateValues[5],
UseField1 = options.UseField1,
Tag = options.TagLabel
}
for k, v in pairs(taskoptions) do
taskoptions[k] = utils.choose(type(v) == 'string', v, "")
end
taskoptions.IncludeIndividual = true
local tblindi = indi.IndividualList()
taskoptions.Individual = tblindi[1]
local sptr = fhNewItemPtr()
local ints = tonumber(tblTemplateValues[6]) --SourceID
if type(ints) == 'number' then
sptr:MoveToRecordById('SOUR', ints)
else
sptr:SetNull()
end
taskoptions.Source = sptr
local strTaskNote = MakeNoteText(taskoptions)
MakeTask(strTaskNote, taskoptions)
end
end
local function MakeTaskFromText()
local tblTemplateValues = myTemplates.GetUseValues() --returns a table of values 1-6, 6 being the source
local taskoptions = {
DueDateValue = boxTDueDate[2].value,
UpdateDateValue = os.date("%x"),
FactDateValue = boxTFactDate[2].value,
GroupValue = boxGroupData[2].value,
Field1Value = tblTemplateValues[1],
Field2Value = tblTemplateValues[2],
Field3Value = tblTemplateValues[3],
Field4Value = tblTemplateValues[4],
Field5Value = tblTemplateValues[5],
UseField1 = options.UseField1,
Tag = options.TagLabel
}
for k, v in pairs(taskoptions) do
taskoptions[k] = utils.choose(type(v) == 'string', v, "")
end
taskoptions.IncludeIndividual = true
local tblindi = indi.IndividualList()
taskoptions.Individual = tblindi[1]
local sptr = fhNewItemPtr()
local ints = tonumber(tblTemplateValues[6]) --SourceID
if type(ints) == 'number' then
sptr:MoveToRecordById('SOUR', ints)
else
sptr:SetNull()
end
taskoptions.Source = sptr
local strTaskNote = MakeNoteText(taskoptions)
MakeTask(strTaskNote, taskoptions)
end
local function FindTag()
--TODO: Possibly search for multiple tags or all tags
local TagSearch = nil
--get search text
local booOK, strTag = GetText("Specify tag, which will be prefixed with "..options.PrefixTag, options.TagLabel, strMin1Letter)
if not booOK then
return false
else
strTag = options.PrefixTag..strTag
TagSearch = Search(strTag, true, true)
end
--identify data classes that will be searched
local tblClass = {text=1,longtext=1,name=0,place=0,wordlist=1, word = 1}
for strType, v in pairs(tblClass) do
tblClass[strType] = (v==1) --make values boolean
end
--Process all items in data
local booFound = false
for ptrItem in allItems() do
local strDataClass = fhGetDataClass(ptrItem)
if fhGetValueType(ptrItem) == 'text' and tblClass[strDataClass] == true then
local strPtrText = fhGetValueAsText(ptrItem)
if TagSearch.Found(strPtrText) then -- add to results
booFound = true
local strRecordType = fhGetTag(ptrItem)
--get the parent item
local ptrParent =fhNewItemPtr()
ptrParent:MoveToParentItem(ptrItem)
--get a parent item date if one exists
local strDate = ""
local dtptr = fhGetItemPtr(ptrParent,'~.DATE')
if dtptr:IsNotNull() then
strDate = fhGetValueAsDate(dtptr):GetValueAsText()
end
--get an updated date if possible
local strUpdated = fhGetLabelledText(ptrItem, options.UpdateDateLabel..strSep) --look for labelled text first
if strUpdated == "" and fhGetDataClass(ptrParent) == "record" then
local dtupdated = fhCallBuiltInFunction ("LastUpdated", ptrParent) --look for a record updated date, returns a datept
if dtupdated:IsNull() == false then
local d = fhNewDate()
d:SetSimpleDate(dtupdated)
strUpdated = d:GetDisplayText("ABBREV")
end
end
--get the parent source and corresponding repository if they exist
local ptrS = nil
local ptrR = nil
ptrS = fhGetItemPtr(ptrParent, '~.SOUR>')
if ptrS:IsNull() then
ptrS = nil
else
ptrR = fhGetItemPtr(ptrS,'~.REPO>')
if ptrR:IsNull() then
ptrR = nil
end
end
myResults.Update({ptrParent:Clone(),ptrItem:Clone(),
strTag, fhGetLabelledText(ptrItem,options.GroupLabel..strSep),
strDate, fhGetLabelledText(ptrItem,options.DueDateLabel..strSep), strUpdated,
fhGetLabelledText(ptrItem,options.Field1Label..strSep),
fhGetLabelledText(ptrItem,options.Field2Label..strSep),
fhGetLabelledText(ptrItem,options.Field3Label..strSep),
fhGetLabelledText(ptrItem,options.Field4Label..strSep),
ptrS, ptrR})
end
end
end
if booFound == false then
Messagebox("No results found", "No results found")
else
--results will be displayed on exit
myResults.Title("Tagged items")
myResults.Headings({"Parent", "Item",
"Tag", options.GroupLabel, options.FactDateLabel,
options.DueDateLabel, options.UpdateDateLabel,
options.Field1Label, options.Field2Label,
options.Field3Label,options.Field4Label,
"Source", "Repository"})
myResults.Visibility({true, true,
true, options.UseGroup == "ON",
options.UseFactDate == "ON", options.UseDueDate =="ON", options.UseUpdateDate == "ON",
options.UseField1 =="ON" and options.UseNotes == "ON", options.UseField2 == "ON",
options.UseField3 == "ON", options.UseField4 == "ON",
true, true})
end
return booFound
end
local function ConvertFacts()
local function MakeNewNote(pi,pf,strTag)
local ptrt = fhNewItemPtr()
ptrt:MoveTo(pf, "~.NOTE2")
local strText = fhGetValueAsText(ptrt)
local strNote = ""
local taskoptions = {
IncludeIndividual = true,
Individual = pi,
}
--set UseField1 and Field1Value
taskoptions.Field1Value = fhGetValueAsText(pf)
taskoptions.UseField1 = utils.choose(taskoptions.Field1Value == "", "OFF", "ON")
strNote = MakeNoteHeader(strNote, taskoptions)
--set tag and group if in use
local strGroup = fhGetLabelledText(ptrt, options.GroupLabel..strSep)
local existingTag = fhGetLabelledText(ptrt, options.PrefixTag)
local usetag = utils.choose(existingTag == "", strTag, existingTag)
strNote = AddTagTextandGroup(strNote, true, utils.choose(existingTag == "", strTag, existingTag), options.UseGroup == "ON", strGroup)
if existingTag ~= "" then
strText = stringx.replace(strText, options.PrefixTag..existingTag, "")
end--remove tag from the text
if options.UseGroup == "ON" and strGroup ~= "" then
strText = stringx.replace(strText, options.GroupLabel..strSep..strGroup, "") --remove group
end
strText = stringx.replace(strText, options.GroupLabel..strSep, "") --remove group label
--concertt old separators to new, and then remove double separators
strText = stringx.replace(strText, "====================", strRule)
strText = stringx.replace(strText, strRule.."%s."..strRule, strRule)
--remove leading and trailing whitespace
strText = stringx.strip(strText)
--add anything left to the note without changing it; worst case, the user gets some extraneous separators but doesn't lose any data
strNote = strNote..strText
strNote = MakeNoteTail(strNote)
return strNote
end
local booconversionsdone = false
local booOK, strDefaultTag, boodeletefacts = GetTextandTick("Specify tag to use if one isn't found", options.TagLabel, strMin1Letter, "Delete facts after conversion?")
if not booOK then return false end --abort
for pi in records('INDI') do --iterate all individuals
for pf in myFactTools.Iterate(pi) do --iterate all facts
if fhGetTag(pf) == cstrFactTag then --test if Fact is a Task
booconversionsdone = true
local notetext = MakeNewNote(pi, pf, strDefaultTag)
ptrTask = CreateSharedNote(notetext) --create the note
CreateNoteLink(pi, ptrTask) --link the note to the individuals
CopyChildren(pf, ptrTask) --copy the sources
if boodeletefacts then
fhDeleteItem(pf)
end
local ptrt = fhNewItemPtr()
ptrt:MoveTo(ptrTask, "~.TEXT")
local strText = fhGetValueAsText(ptrt)
local resultstable = {pi:Clone(), ptrTask:Clone(), fhGetLabelledText(ptrt, options.PrefixTag),
fhGetLabelledText(ptrt, options.GroupLabel..strSep),
"", "", "", fhGetLabelledText(ptrt, options.Field1Label..strSep), "", "", "", pf:Clone(), nil}
myResults.Update(resultstable)
end
end
end
if booconversionsdone then
myResults.Title("Converted facts")
myResults.Headings({"Individual", "Task",
"Tag", options.GroupLabel, options.FactDateLabel,
options.DueDateLabel, options.UpdateDateLabel,
options.Field1Label, options.Field2Label,
options.Field3Label,options.Field4Label,
"Source fact", ""})
myResults.Visibility({true, true, true, options.UseGroup == "ON",
false, false, false, true, false, false, false, utils.choose(boodeletefacts, false, true), false})
Messagebox("Things to delete?", [[Once you've converted all your Task facts to notes, you can safely delete the query "Research Tasks (Facts)", the "Research Planner" Fact Set, and any queries you downloaded associated with version 1 of the Research PLanner plugin.]])
return true
else
Messagebox("No facts found to convert", "No facts found to convert")
return false
end
end
local function ConvertNotes()
local function MakeNewNote(ptrtext)
local strText = fhGetValueAsText(ptrtext)
--remove privacy markers
strText = stringx.replace(strText,"[[\n","")
strText = stringx.replace(strText,"\n]]","")
--Get field1 if it exists and remove it from the note
local f1value = fhGetLabelledText(ptrtext, options.Field1Label..strSep)
if f1value ~= "" then
strText = stringx.replace(strText, options.Field1Label..strSep..f1value.."\n"..strRule,"")
end
--get the individual
local strIndividual = fhGetLabelledText(ptrtext, "Individual: ")
-- local iIndividual = TextToNumber(strIndividual)
local indi = fhNewItemPtr()
if strIndividual ~= "" then
-- -- indi:MoveToRecordById("INDI", tonumber(strIndividual))
indi:MoveToRecordById("INDI", fhCallBuiltInFunction("TextToNumber", strIndividual))
strText = stringx.replace(strText, "Individual: "..strIndividual.."\n"..strRule, "") --remove the individual identifiers
end
--convert old separators to new, and then remove double separators
strText = stringx.replace(strText, "====================", strRule)
strText = stringx.replace(strText, strRule.."%s."..strRule, strRule)
strText = stringx.strip(strText)
return strText, indi, f1value
end
local booconversionsdone = false
local booOK, strTag, boodeletenotes = GetTextandTick("Specify tag to search for", options.TagLabel, strMin1Letter, "Delete notes after conversion?")
if not booOK then return false end --abort
for pn in records('NOTE') do --iterate all Notes
local ptrt = fhNewItemPtr()
ptrt:MoveTo(pn, "~.TEXT")
local thistag = fhGetLabelledText(ptrt, options.PrefixTag)
if thistag == strTag then
booconversionsdone = true
local notetext, pi, f1value = MakeNewNote(ptrt) --get the note individual and field1 value
if pi ~= nil then --can't make a fact if the individual isn't identified
ptrTask = myFactTools.Create(cstrFactTag, pi, f1value) --create the task fact linked to the individual
if notetext ~= "" then CreateNote(ptrTask, notetext) end --add the note if it isn't blank
if options.UseFactDate == "ON" then
local dt = fhNewDate()
local DateOK = dt:SetValueAsText(os.date("%x"), true) --defaults to today
SetDate(ptrTask, dt)
end
CopyChildren(pn, ptrTask) --copy the sources
if boodeletenotes then
fhDeleteItem(pn)
end
local ptrt = fhNewItemPtr()
ptrt:MoveTo(ptrTask, "~.TEXT")
local strText = fhGetValueAsText(ptrt)
local resultstable = {pi:Clone(), ptrTask:Clone(), strTag,
fhGetLabelledText(ptrt, options.GroupLabel..strSep),
"", "", "", fhGetLabelledText(ptrt, options.Field1Label..strSep), "", "", "", pn:Clone(), nil}
myResults.Update(resultstable)
end
end
end
if booconversionsdone then
myResults.Title("Converted notes")
myResults.Headings({"Individual", "Task",
"Tag", options.GroupLabel, options.FactDateLabel,
options.DueDateLabel, options.UpdateDateLabel,
options.Field1Label, options.Field2Label,
options.Field3Label,options.Field4Label,
"Original note", ""})
myResults.Visibility({true, true, true, options.UseGroup == "ON",
false, false, false, false, false, false, false, utils.choose(boodeletefacts, false, true), false})
Messagebox("Things to delete?", [[Once you've converted all your Task notes, you can safely delete the query "Research Tasks (Notes)"]])
return true
else
Messagebox("No notes found to convert", "No notes found to convert")
return false
end
end
local function FactsReport()
myReports.Download("Tasks - Individual (Facts)")
Messagebox("Report installed", "Report installed")
end
local function NotesReport()
myReports.Download("Tasks - Individual (Notes)")
Messagebox("Report installed", "Report installed")
end
local function MakeTemplateBox(source)
--The template box contains 6 containers each with 2 items in them (either a button or a label, and a text field)
--the elements can be accessed using mybox[n] for the containers or mybox[n][m] for the elements
local txtField1 = MakeText{expand="HORIZONTAL", wordwrap="NO"}
local labField1= MakeLabel{title=options.Field1Label}
local hboxField1 = iup.hbox{labField1, txtField1}
local txtField2 = MakeText{expand="HORIZONTAL", wordwrap="NO"}
local labField2 = MakeLabel{title=options.Field2Label}
local hboxField2 = iup.hbox{labField2, txtField2}
local txtField3 = MakeText{expand="HORIZONTAL", wordwrap="NO"}
local labField3 = MakeLabel{title=options.Field3Label}
local hboxField3 = iup.hbox{labField3, txtField3}
local txtField4 = MakeText{wordwrap="YES", multiline="YES", scrollbar = "VERTICAL", visiblelines = "2"}
local labField4 = MakeLabel{title=options.Field4Label}
local vboxField4 = iup.vbox{labField4, txtField4}
local txtField5 = MakeText{wordwrap="YES", multiline="YES", scrollbar = "VERTICAL", visiblelines = "2"}
local labField5 = MakeLabel{title=options.Field5Label}
local vboxField5 = iup.vbox{labField5, txtField5}
return iup.vbox{
hboxField1, hboxField2, hboxField3, vboxField4, vboxField5, source.Selector("Use the Source button to choose a source from within Family Historian.","HORIZONTAL");
}
end
local function MakeTabTasks(strTabTitle)
local UseSelector = nil
boxTDueDate = nil
boxTFactDate = nil
local function TabMakeText()
local function ClearTabMakeText()
myTemplates.ClearUseSelector(UseSelector) --clears Template selector and container
boxTDueDate[2].value = ""
boxTDueDate[2].value = ""
end
--define buttons
btnTaskMake = MakeButton{title="Make", callback = MakeTaskFromText, tip = "Make task from specified details"}
local btnClear = MakeButton{title="Clear", callback = ClearTabMakeText, tip = "Clear task details"}
local btns = iup.hbox{iup.fill{}, btnClear, btnTaskMake}
btnTaskMake.active = "YES"
--define template fields
TaskSource = Source(1)
boxTaskEdit = MakeTemplateBox(TaskSource)
--define date fields
boxTDueDate = MakeDateField{title=options.DueDateLabel}
boxTFactDate = MakeDateField{title=options.FactDateLabel}
UseSelector = myTemplates.UseSelector(boxTaskEdit)
return iup.vbox{UseSelector,
UseSelector, boxTaskEdit, iup.vbox{boxTDueDate, boxTFactDate}, btns;
tabtitle = "From Text"}
end
local function TabMakeBulk()
--define buttons
btnBulkMake = MakeButton{title="Make", callback = MakeBulkTasks, tip = "Make tasks from selected templates"}
local btnClear = MakeButton{title="Clear", callback = myTemplates.ClearBulkSelector, tip = "Clear selected templates"}
local btns = iup.hbox{iup.fill{}, btnClear, btnBulkMake}
btnBulkMake.active = "NO"
local boxBulkMake = iup.vbox{
myTemplates.BulkSelector(),
btns;
} --define container
return iup.vbox{boxBulkMake; tabtitle = "In Bulk"}
end
--define tab-wide buttons and fields
local btns = iup.hbox{iup.fill{}, myHelp.Button(norm, "creating_and_editing_tasks" ), myConfig.OptionsButton(), MakeButton{title="Exit"}}
--Project field
local labGroup = MakeLabel{title = options.GroupLabel, tip = "Grouping to use when creating tasks"}
local txtGroup = MakeText{tip = "Grouping to use when creating tasks", default = ""}
boxGroupData = iup.hbox{labGroup, txtGroup}
return iup.vbox{
indi.Selector("Use the button to choose an individual from within Family Historian.\nYou cannot create tasks until you have selected an individual."),
boxGroupData,
iup.tabs{TabMakeText(), TabMakeBulk()},
btns, myActivity.Make("OPEN", "Task history"); tabtitle = strTabTitle}
end
local function MakeTabTemplates(strTabTitle)
TemplateSource = Source(1)
boxTemplateEdit = MakeTemplateBox(TemplateSource)
local btnExit = MakeButton{title="Exit", callback = myTemplates.CheckForUnsavedEditChanges, close = "YES"}
local btns = iup.hbox{iup.fill{}, myHelp.Button(norm, "managing_task_templates"), myConfig.OptionsButton(), btnExit}
return iup.vbox{myTemplates.EditSelector(boxTemplateEdit), iup.frame{boxTemplateEdit}, btns; tabtitle = strTabTitle}
end
local function MakeTabTools(strTabTitle)
local btns = iup.hbox{iup.fill{}, myHelp.Button(norm,"miscellaneous_tools" ), myConfig.OptionsButton(), MakeButton{title="Exit"}}
hFindBox = iup.hbox{
MakeButton{title = "Find Items", callback = FindTag, close = "YES", tip = "Find items with a specified tag"}
}
htoolsFactsBox = iup.hbox{
MakeButton{title = "Convert Notes", callback = ConvertNotes, close = "YES", tip = "Convert any Note records with a specified tag to Task facts"},
MakeButton{title = "Download Report", callback = FactsReport, close = "YES", tip = "Download a Report for Tasks using Facts"}
} --tools available when facts are in use
htoolsNotesBox = iup.hbox{
MakeButton{title = "Convert Facts", callback = ConvertFacts, close = "YES", tip = "Convert any Task facts to Note records with a specified tag"},
MakeButton{title = "Download Report", callback = NotesReport, close = "YES", tip = "Download a Report for Tasks using Notes"}
} --tools available when notes are in use
return iup.vbox{hFindBox, htoolsFactsBox, htoolsNotesBox, btns;
tabtitle = "Tools"}
end
local strtabTasksTitle = "Make tasks"
local strtabTemplatesTitle = "Manage templates"
local strtabToolsTitle = "Tools"
local tabTasks = MakeTabTasks(strtabTasksTitle)
local tabTemplates = MakeTabTemplates(strtabTemplatesTitle)
tabTools = MakeTabTools(strtabToolsTitle)
local MainTabs = iup.tabs{tabTasks, tabTemplates, tabTools}
MainTabs.tabchange_cb = function(self, newTab, oldTab)
if oldTab.tabTitle == strtabTemplatesTitle and myTemplates.CheckForUnsavedEditChanges() == false then
tabs.value = tabTemplates --revert tab
end
end
local d = MakeDialog(MainTabs, {title = cstrPluginName})
d.close_cb = function()
if myTemplates.CheckForUnsavedEditChanges() then
d.show_cb = nil --avoid iup bug
return iup.CLOSE
else
return iup.IGNORE
end
end
DoNormalize()
return d
end
-------------------------------------
--EXECUTE
-------------------------------------
do
SetUTF8IfPossible()
SetTextSize()
myHelp = HTMLHelp(cstrPluginVersion) --create Help object to display Help with a browser interface
myConfig = Config(cstrPluginVersion, myHelp ) --create config object to make all methods available
myTemplates = Templates(true) --create template object to make all methods available -- allow project templates; do not use a subdirectory and don't use tokens
myResults = Results(13) --initialise results handling with 13 results tables; remaining details will depend on user actions
myActivity = ActivityLog()
indi = Individuals(1) --create an object of the Individuals class that allows a single individual
dlgmain = MakeMainDialog()
myConfig.Initialise() --initialise the configuration
dlgmain:show()
iup.MainLoop()
DestroyAllDialogs()
myResults.Display()
end
--DONE
--[[
@Title: Research Planner
@Author: Helen Wright
@Version: 2.0.1
@LastUpdated: 6 December 2019
@Description: Support creation of research tasks using a custom attribute or shared notes; optionally allows templates to support the creation of tasks in bulk
]]--
--[[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
Version 2: Enhanced functionality, including config options, ability use shared Notes or Facts to record tasks, and ability to create one-off tasks (not using templates).
Version 2.0.1: Modified help file extraction method to work for Wine users
]]--
--[[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 cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local cstrPluginVersion = "2.0"
local cstrFactName = "Task"
local cstrFactTag = ""
local myHelp = nil
local myConfig = nil
local myTemplates = nil
local indi = nil
local myResults = nil
local myActivity = nil
local myReports = nil
local strSep = ": "
local strRule = string.rep('_', 20).."\n"
--------------------------------------------------------------
--UTILITY CODE AND OBJECTS
--------------------------------------------------------------
--[[ 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.normalizergroup = options.norm or norm
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
--[[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
--[[Routine to extract zip files]]--
do
if not fhloadrequire("zip") then return end
function ExtractZip(zipPath, zipFilename, destinationPath)
local zfile = assert(zip.open(zipPath.."\\"..zipFilename),"Failed to open zip file "..zipPath.."\\"..zipFilename) --open the zip file for processing
local function CopyFile(file)
local currFile, err = zfile:open(file.filename) --open a file within the zipfile
local currFileContents = currFile:read("*a") -- read entire contents of current file
local hBinaryOutput = io.open(destinationPath .."\\".. file.filename, "wb") --open an outputfile
-- write current file inside zip to a file outside zip
if hBinaryOutput then
hBinaryOutput:write(currFileContents) --write the new file as a copy of the file within the zipfile
hBinaryOutput:close() --close the new file
end
currFile:close() --close the file within the zipfile
end
-- iterate through each file inside the zip file
for file in zfile:files() do
local newdir = path.dirname(file.filename)
if not path.exists(destinationPath.."\\"..newdir) then
local destPath = destinationPath
for nextdir in newdir:gmatch("[^/]+") do
destPath = destPath.."\\"..nextdir
lfs.mkdir(destPath)
end
end
if path.basename(file.filename) ~= "" then CopyFile(file) end
end
zfile:close()
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
function FactSet (factsetname)
--[[A class to create a single factset -- can be invoked many times (once per fact set)]]--
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
--Prerequisites: FH API, Files boilerplate
--initialise location variables
local FactSetDirectory = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Fact Types\\Custom\\"
local strfactsetfiletype = ".fhf"
local cstrSaveLocation = fhGetPluginDataFileName("LOCAL_MACHINE",true).."\\"..factsetname..strfactsetfiletype
local cstrInstallLocation = FactSetDirectory..factsetname..strfactsetfiletype
--initialise Factset
local FactCount = 0
local FactDef = {}
local tblFactIDs = {}
--TODO: Include handling of witness roles (not currently supported
--[[Tasks have a Name, Label (defaults to Name), Abbreviation (defaults to ""),
a FactType (Attribute or Event), a RecordType (Individual or Family),
a Sentence template, default "{individual} experienced {label} {date} {place}"
a Time frame (None, Pre-Birth, Birth, Shortly After Birth, Life - the default, Marriage, Post-Marriage, Death, Post-Death)
optional Fields Date Age Place Address Note (all default to 1)
Can be on the Fast-Add Menu (default N)
Can be Hidden (default N)
Can Use an Overide template string for fact tabs Listings (default N)
Can use an Override template string for Record window Listings (default N)
Can have an Auto-Note string (default blank)
]]--
local AddFact = function(Name, factoptions)
--set contents according to options or defaults
local Label = factoptions.Label or Name
local Abbr = factoptions.Abbreviation or Name
local Record = factoptions.Record or "I"
local Type = factoptions.Type or "E"
local Sentence = factoptions.Sentence or "{individual} experienced {label} {date} {place}"
local Timeframe = string.upper(factoptions.Timeframe or "Life")
local Date = factoptions.Date or "1"
local Age = factoptions.Age or "1"
local Place = factoptions.Place or "1"
local Address = factoptions.Address or "1"
local Note = factoptions.Note or 1
local FastAdd = factoptions.FastAdd or "N"
local Hidden = factoptions.Hidden or "N"
local OverrideFactsTab = factoptions.OverrideFactsTab or ""
local OverrideRecordWindow = factoptions.OverrideRecordWindow or ""
local AutoNote = factoptions.AutoNote or ""
FactCount = FactCount + 1
local FactID = utils.choose(Type == Event, "EVEN-", "_ATTR-")..stringx.replace(string.upper(Name)," ","-")..[[-]]..Record..Type
tblFactIDs[FactCount] = [[Item]]..FactCount..[[=]]..FactID
local FactLines = {}
table.insert(FactLines, "[FCT-"..FactID.."]")
table.insert(FactLines, "Name="..Name)
table.insert(FactLines, "Template="..Sentence)
table.insert(FactLines, "Event Tab="..OverrideFactsTab)
table.insert(FactLines, "Rec Win="..OverrideRecordWindow)
table.insert(FactLines, "Label="..Label)
table.insert(FactLines, "Abbr="..Abbr)
table.insert(FactLines, "Timeframe=".. Timeframe)
table.insert(FactLines, "Field Date="..Date)
table.insert(FactLines, "Field Age="..Age)
table.insert(FactLines, "Field Place="..Place)
table.insert(FactLines, "Field Address="..Address)
table.insert(FactLines, "Field Note="..Note)
table.insert(FactLines, "Fast-Add Menu="..FastAdd)
table.insert(FactLines, "Hidden="..Hidden)
table.insert(FactLines, "[Text-FCT-"..FactID.."-Auto Note]")
--now create the autonote lines, if any
if stringx.strip(AutoNote) == "" then
table.insert(FactLines, "Count=0")
else
local tblAN = stringx.splitlines(AutoNote)
local ANcount = #tblAN
table.insert(FactLines, "Count="..ANcount)
for i = 1, ANcount do
if i == ANcount then
table.insert(FactLines, "Line"..i.."=0;"..tblAN[i]..";") --last line in Autonote
else
table.insert(FactLines, "Line"..i.."=n;"..tblAN[i]..";")
end
end
end
table.insert(FactLines, "[FCT-"..FactID.."-ROLE]")
table.insert(FactLines, "Roles=0") --Witnesses not currently supported
FactDef[FactCount]=table.concat(FactLines, "\n") --Add the fact definition to the table of fact definitions
end
local Save = function() --this will overwrite any existing fact set with the same name
--first create the factset file contents
local strFactSetPreamble = [[
[.index]
Ver1=4
Ver2=0
]]
local strFileContents =
strFactSetPreamble.."Count="..#tblFactIDs.."\n"..
table.concat(tblFactIDs, "\n").."\n"..
table.concat(FactDef, "\n").."\n"
--now write it as a UTF16 file
WriteUTF16File(strFileContents, cstrSaveLocation)
end
local Install = function()
if FileExists(cstrSaveLocation) then --copy the file to the installation location
CopyFile(cstrSaveLocation, cstrInstallLocation) --doing it this way preserve file name case
end
end
local Download = function(downloadlocation)
--TODO: NOT TESTED --should be used for factsets with a fixed content
FileDownload(downloadlocation, cstrInstallLocation)
end
local Exists = function()
return FileExists(cstrInstallLocation)
end
return{AddFact = AddFact, Save = Save, Install = Install, Download = Download, Exists = Exists}
end
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()
--[[HTML Help class
A class that provides support for a html help file
Only a single optional instance of the Help class is supported per plugin. Best choice if context sensitive help is required but uses more disk space usually
@Author: Helen Wright
@V1.0: Initial version.
@LastUpdated: 8 November 2019
]]--
function HTMLHelp(strV)
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
require "iupluaweb"
-- Also requires File handling boilerplate, Zipfile handling boilerplate
local strVersion = strV or "1.0"
local cstrPluginDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local HelpFileDirectory = cstrPluginDir.."\\"..cstrPluginName.." Help "..strVersion
local HelpBrowserWindow = nil
local HelpBrowserControl = nil
local DialogTitle = cstrPluginName.." Help "..strVersion
local function GetHelpFile()
local downloadname = cstrPluginName.." Help "..strVersion
local downloadlocation = "http://www.fhug.org.uk/colevalleygirl/"..downloadname..".zip"
local booStatus,_ = FileDownload(downloadlocation, cstrPluginDir.."\\"..downloadname..".zip") --download the zipped help file to the plugin data directory
if booStatus == false then
Messagebox("Download failed", "Failed to download the up-to-date Help file. The plugin will try again next time you run it, or (recommended if this error persists) you can download it yourself from "..downloadlocation.." and extract its contents to directory "..cstrPluginDir)
else
Messagebox("About to extract Help files", "About to extract Help files which may take some time; please wait")
ExtractZip(cstrPluginDir, downloadname..".zip", cstrPluginDir)
DeleteFile(cstrPluginDir.."\\"..downloadname..".zip")
end
end
local function MakeHelpBrowser()
local d = nil
if HelpBrowserControl == nil then
HelpBrowserControl = iup.webbrowser{}
end
if HelpBrowserWindow == nil then
HelpBrowserWindow = MakeDialog(HelpBrowserControl,{title=DialogTitle})
HelpBrowserWindow.size = "HALFxHALF"
HelpBrowserWindow.close_cb = function()
HelpBrowserWindow:hide()
return iup.IGNORE
end
end
HelpBrowserWindow:show()
end
if path.isdir(HelpFileDirectory) then
--Up to date help file exists
else --help file must be downloaded
local tblOldHelpFiles = dir.getdirectories(cstrPluginDir)
for _, v in ipairs(tblOldHelpFiles) do
if stringx.beginswith(v, HelpFileDirectoryBase) then
dir.rmtree(v)
end
end
tblOldHelpFiles = dir.getfiles(cstrPluginDir, "*.chm")
for _, v in ipairs(tblOldHelpFiles) do
DeleteFile(v) --Delete all old help files
end
GetHelpFile() --Get the new help file
end
local Button = function(optnorm, topicpath)
--Topic path is the name within the help file of the topic required -- no changes to capitalisation or anything else
local function ShowHelpFile()
if path.isdir(HelpFileDirectory) == true then
topicpath = topicpath or "index"
MakeHelpBrowser()
HelpBrowserControl.value = HelpFileDirectory.."\\"..topicpath..".html"
end
end
optnorm = optnorm or norm
return utils.choose(path.isdir(HelpFileDirectory) == true, MakeButton{title="Help", callback=ShowHelpFile, tip = "Show help", norm = optnorm}, nil)
end
return {Button= Button}
end
--[[Config class
A Config class that provides support for a config dialog and storage for the config parameter
Only a single optional instance of the Config class is supported per plugin.
Uses a complementary class within the calling plugin to handle plugin-specific actions.
@Author: Helen Wright
@V1.0: Initial version.
@LastUpdated: 29 October 2019
]]
function Config (strV, HelpClass)
--booH true if a help file is to be used; booO true if config data required; strV identifies the version of help file and/or config file to be used.
--Prerequisites:
do
require("iuplua") -- GUI
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
--also requires:
-- fh API
-- File handling boilerplate
-- Dialog boilerplate
end
options = {} --make the options table global because it will be widely used
--local constants
local strVersion = strV or "1.0"
local cstrPluginDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local ConfigFile = cstrPluginDir.."\\"..cstrPluginName..".cfg"
--private state variables
local ctlContainer = nil
local tblControls = {}
local tblDefaults = {}
local caller = nil
local dlgoptions = nil
--private methods
local SaveConfig = function()
options.ConfigVersion = strVersion
local strConfig = "#"..cstrPluginName.." configuration "
for key, val in pairs(options) do
strConfig = strConfig.."\n"..key.."="..tostring(val)
end
WriteFile(strConfig, ConfigFile) -- write new config file
end
local DisplayConfig = function()
for key, ctl in pairs(tblControls) do --update the control values
ctl.value = options[key]
end
end
local SetConfigToDefault = function()
for key, val in pairs(tblDefaults) do
options[key]=val
end
DisplayConfig()
caller.ActionConfig()
SaveConfig()
end
local MakePromptDialog = function()
local function NewConfig()
for key, ctl in pairs(tblControls) do --read the config from the controls and update tblConfig
options[key]=ctl.value
end
caller.ActionConfig()
SaveConfig()
return true
end
local function CancelConfig()
DisplayConfig() --reset config controls to last saved values which will already have been saved and actioned
caller.ActionConfig()
return true
end
local btnSaveConfig = MakeButton{title="Confirm", tip = "Confirm options", callback = NewConfig, close = "YES", norm = norm3}
local btnResetConfig = MakeButton{title="Reset", tip = "Reset to default options", callback = SetConfigToDefault, norm = norm3}
local btnCancelConfig = MakeButton{title = "Cancel", tip = "Discard changes and exit", callback = CancelConfig, close = "YES", norm = norm3}
local btnHelp = utils.choose(HelpClass == nil, nil, HelpClass.Button(norm3, "options"))
local btnBox = iup.vbox{btnHelp, btnSaveConfig, btnResetConfig, btnCancelConfig}
dlgoptions = MakeDialog(iup.hbox{ctlContainer, btnBox},
{title="Options", expand = "NO", resize = "NO", menubox="NO"})
DoNormalize()
end
local PromptConfig = function()
dlgoptions:popup(iup.CENTERPARENT, iup.CENTERPARENT)
end
--public methods
local Initialise = function()
ctlContainer = caller.GetContainer()
tblControls = GetDataElements(ctlContainer, {}, {}, true)
tblDefaults = caller.GetDefaults()
MakePromptDialog()
if options.ConfigVersion ~= strVersion then --the config file doesn't exist or isn't up-to-date
--supplement what options exist if any with default values
for key, val in pairs(tblDefaults) do
if not options[key] then options[key] = val end
end
DisplayConfig()
caller.ActionConfig()
PromptConfig() --get user preferences and action them and save them
else
DisplayConfig()
caller.ActionConfig()
end
SaveConfig()
end
local OptionsButton = function ()
return MakeButton{title = "Options", tip = "Select plugin options", callback = PromptConfig}
end
caller = CallerConfig()
if FileExists(ConfigFile) then options, _ = config.read(ConfigFile, {convert_numbers = false}) end
--expose public methods
return{OptionsButton = OptionsButton, Initialise = Initialise}
end
function CallerConfig ()
local tblD = {} -- default config values
tblD.UseAttribute= "ON"
tblD.UseNotes = "OFF"
tblD.UseGroup = "ON"
tblD.GroupLabel = "Group"
tblD.UseTag = "ON"
tblD.PrefixTag = "#" --not a config option
tblD.TagLabel = "RP"
tblD.UseDueDate = "OFF"
tblD.DueDateLabel = "Due" --not a config option
tblD.UseUpdateDate = "OFF"
tblD.UpdateDateLabel = "Updated" --not a config option
tblD.UseFactDate = "OFF"
tblD.FactDateLabel = "Fact date" --not a config option
tblD.UseField1 = "ON"
tblD.Field1Label = "Title"
tblD.UseField2 = "ON"
tblD.Field2Label="Status"
tblD.UseField3 = "ON"
tblD.Field3Label="Priority"
tblD.UseField4 = "ON"
tblD.Field4Label="Objective"
tblD.UseField5 = "ON"
tblD.Field5Label="Notes"
--config field local variables put here for scope reasons
local UseAttribute, UseNotes,
UseGroup, GroupLabel,
UseTag, PrefixTag, TagLabel,
UseDueDate, DueDateLabel,
UseUpdateDate, UpdateDateLabel,
UseFactDate, FactDateLabel,
UseField1, Field1Label,
UseField2, Field2Label,
UseField3, Field3Label,
UseField4, Field4Label,
UseField5, Field5Label,
hboxReload
= nil --config container fields put here for scope
local GetContainer = function()
--returns a container of config controls with Names equivalent to the config keys
--make a gridbox, 2 columns wide
local gboxOptions1 = MakeGridbox{}
--make mechanism radio
UseNotes = MakeToggle{title="Use Notes", name = "UseNotes", tip = "Create research tasks using shared notes"}
UseAttribute = MakeToggle{title="Use Facts*", name = "UseAttribute", tip = "Create research tasks using a custom 'Task' attribute"}
gboxOptions1:append(iup.radio{iup.vbox{UseAttribute, UseNotes}})
--make Date Options
UseDueDate = MakeToggle{title="Due Date", name = "UseDueDate", tip = "Include a field for due date in the task details"}
UseUpdateDate = MakeToggle{title="Update Date", name = "UseUpdateDate", tip = "Include a field for updated date in the task details"}
gboxOptions1:append(iup.vbox{UseDueDate, UseUpdateDate})
--make Tag and Group Options
UseTag = MakeToggle{title="Research Tag", name = "UseTag", tip = "Include a research tag in the task details"}
TagLabel = MakeText{name = "TagLabel", tip = "String to use as a research tag. Will be prefixed with "..tblD.PrefixTag..". If no tag is specified it will default to "..tblD.TagLabel, mask = strMin1Letter, default = tblD.TagLabel}
function UseTag:action(state)
TagLabel.active = utils.choose(state==1, "YES", "NO")
end
UseGroup = MakeToggle{title="Group", name = "UseGroup", tip = "Include a group field in the task details (specified when you create the task)"}
GroupLabel = MakeText{name = "GroupLabel", tip = "Word to identify the group field in the task details. If no value is specified it will default to "..tblD.GroupLabel, mask = strMin1Char, default = tblD.GroupLabel}
function UseGroup:action(state)
GroupLabel.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseTag)
gboxOptions1:append(TagLabel)
gboxOptions1:append(UseGroup)
gboxOptions1:append(GroupLabel)
UseField1 = MakeToggle{title="Field1", name = "UseField1", tip = "Include a single line Field1 in the task details -- if using Attributes if will be treated as the Attribute value." }
Field1Label = MakeText{name = "Field1Label", tip = "Word to identify Field1 in the task details. If no value is specified it will default to "..tblD.Field1Label, mask = strMin1Char, default = tblD.Field1Label}
function UseField1:action(state)
Field1Label.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseField1)
gboxOptions1:append(Field1Label)
UseField2= MakeToggle{title="Field2", name = "UseField2", tip = "Include a single line Field2 in the task details"}
Field2Label = MakeText{name = "Field2Label", tip = "Word to identify Field2 in the task details. If no value is specified it will default to "..tblD.Field2Label, mask = strMin1Char, default = tblD.Field2Label}
function UseField2:action(state)
Field2Label.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseField2)
gboxOptions1:append(Field2Label)
UseField3 = MakeToggle{title="Field3", name = "UseField3", tip = "Include a single line Field3 in the task details"}
Field3Label = MakeText{name = "Field3Label", tip = "Word to identify Field3 in the task details. If no value is specified it will default to "..tblD.Field3Label, mask = strMin1Char, default = tblD.Field3Label}
function UseField3:action(state)
Field3Label.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseField3)
gboxOptions1:append(Field3Label)
UseField4= MakeToggle{title="Field4", name = "UseField4", tip = "Include a multiline Field4 in the task details"}
Field4Label = MakeText{name = "Field4Label", tip = "Word to identify Field4 in the task details. If no value is specified it will default to "..tblD.Field4Label, mask = strMin1Char, default = tblD.Field4Label}
function UseField4:action(state)
Field4Label.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseField4)
gboxOptions1:append(Field4Label)
UseField5 = MakeToggle{title="Field5", name = "UseField5", tip = "Include a multiline Field5 in the task details"}
Field5Label = MakeText{name = "Field5Label", tip = "Word to identify Field5 in the task details. If no value is specified it will default to "..tblD.Field5Label, mask = strMin1Char, default = tblD.Field5Label}
function UseField5:action(state)
Field5Label.active = utils.choose(state==1, "YES", "NO")
end
gboxOptions1:append(UseField5)
gboxOptions1:append(Field5Label)
UseFactDate = MakeToggle{title="Fact Date", name = "UseFactDate", tip = "Include a date field in the task details"}
gboxOptions1:append(UseFactDate)
--make attribute warning
hboxReload = iup.hbox{MakeLabel{title = "* If using Facts, you will need to restart FH to\ncreate tasks with the specified configuration\ndirectly within Family Historian", norm = ""}}
function UseNotes:action(state) --disable/enable various fields if Notes is being used
UseFactDate.active = utils.choose(state==1, "NO", "YES") --Notes don't have fact dates
UseFactDate.value = utils.choose(state==1, "OFF", UseFactDate.value)
UseTag.active = utils.choose(state==1, "NO", "YES") --force it to be inactive if Notes are being used
TagLabel.active = utils.choose(state == 1, "YES", TagLabel.active)
end
return iup.vbox{gboxOptions1, hboxReload}
end
local GetDefaults = function()
return tblD
end
local ActionConfig = function()
--adjusts the main plugin UI< state etc. (including the config dialog) according to the current options
local function AdjustConfigDialog()
GroupLabel.active = utils.choose(options.UseGroup == "ON", "YES", "NO")
Field1Label.active = utils.choose(options.UseField1 == "ON", "YES", "NO")
Field2Label.active = utils.choose(options.UseField2 == "ON", "YES", "NO")
Field3Label.active = utils.choose(options.UseField3 == "ON", "YES", "NO")
Field4Label.active = utils.choose(options.UseField4 == "ON", "YES", "NO")
Field5Label.active = utils.choose(options.UseField5 == "ON", "YES", "NO")
UseFactDate.active = utils.choose(options.UseNotes == "ON", "NO", "YES") --Notes don't have fact dates
UseTag.active = utils.choose(options.UseNotes == "ON", "NO", "YES") --force it to be inactive if Notes are being used
TagLabel.active = utils.choose(options.UseNotes == "ON", "YES", TagLabel.active)
end
local function AdjustTemplateTab()
if dlgmain ~= nil then --Templates tab exists
for i = 1,5 do
HideContainer(boxTemplateEdit[i], options["UseField"..i] == "OFF") --hide the container if it is no in use
boxTemplateEdit[i][1].title = options["Field"..i.."Label"] --modify the fild label
end
end
end
local function AdjustTasksTab()
if dlgmain ~= nil then --Tasks tab exists
HideContainer(boxGroupData, options.UseGroup == "OFF")
boxGroupData[1].title = options.GroupLabel
for i = 1,5 do
HideContainer(boxTaskEdit[i], options["UseField"..i] == "OFF") --hide the container if it is no in use
boxTaskEdit[i][1].title = options["Field"..i.."Label"] --modify the fild label
end
HideContainer(boxTFactDate, options.UseFactDate == "OFF")
HideContainer(boxTDueDate, options.UseDueDate == "OFF")
end
end
local function MakeFactDefinition()
local myFactSet = FactSet(cstrPluginName)
local factsentence = [[{label}: <{value}> ]]..
utils.choose(options.UseField2 =="OFF", "",
[[{=CombineText( "[]]..options.Field2Label..[[: ", GetLabelledText( %FACT.NOTE2%, "]]..options.Field2Label..[[: " ), "]", "" )}]])..
utils.choose(options.UseField3 =="OFF", "",
[[{=CombineText( "[]]..options.Field3Label..[[: ", GetLabelledText( %FACT.NOTE2%, "]]..options.Field3Label..[[: " ), "]", "" )}]])
local Anote = MakeNoteText{
GroupValue = "",
DueDateValue = "",
UpdateDateValue = "",
FactDateValue = "",
IncludeIndividual = false,
Individual = nil,
Tag = options.TagLabel,
UseField1 = options.UseField1,
Field1Value = "",
Field1Value = "",
Field2Value = "",
Field3Value = "",
Field4Value = "",
Field5Value = "",
Source = nil
}
local DateValue = utils.choose(options.UseFactDate =="OFF", 0, 1)
local factparms = {Record="I", Type="A",
Sentence = factsentence, Timeframe = "Post-Death", Date = DateValue, Age = 0,
Place = 0 ,Address = 0, Note = 1, FastAdd = "Y", Hidden = "N",
OverrideFactsTab = factsentence,
OverrideRecordWindow = factsentence,
AutoNote = Anote}
myFactSet.AddFact(cstrFactName,factparms)
myFactSet.Save()
--Check if Fact exists
local booFactExists = false
local booFactSetExists = false
booFactSetExists = myFactSet.Exists()
if cstrFactTag ~= "" and not booFactSetExists then
--the user has created their own fact so do nothing further
else
myFactSet.Install()
booFactExists, cstrFactTag = myFactTools.Exists(cstrFactName, true, true)
end
end
local function MakeQuery()
function Query (strqtype, strtitle, strdescription, booReadOnly, strOrientation)
--prerequisites: File handling boilerplate; Penlight libraries
local cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local cstrPluginDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local QueryDirectory = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Queries\\Custom\\"
local strqueryfiletype = ".fhq"
local strQueryTitle = strtitle
local QueryTypes = {}
QueryTypes.Individual = "INDI"
QueryTypes.Family="FAM"
QueryTypes.Note = "NOTE"
QueryTypes.Source = "SOURCE"
QueryTypes.Repository = "REPO"
QueryTypes.Submitter = "SUBM"
QueryTypes.Submission = "SUBN"
QueryTypes.Media = "OBJE"
QueryTypes.Fact = "FACT"
QueryTypes.PLace = "PLAC"
local RowType = {}
RowType["Add if"] = "ADD,IF"
RowType["Add unless"] = "ADD,UNLESS"
RowType["Exclude if"] = "EXC,IF"
RowType["Exclude unless"] = "EXC,UNLESS"
local Comparators = {}
Comparators["matches"] = "="
Comparators["comes after"] = "<"
Comparators["comes before"] = ">"
Comparators["begins with"]= "begins"
Comparators["ends with"] = "ends"
Comparators["contains"] = "contains"
Comparators["is null"] = "null"
Comparators["is true"] = "true"
local RelTypes = {}
RelTypes["Ancestor"] = "ANC"
RelTypes["Descendant"] = "DESC"
RelTypes["Spouse"] = "SPOUSE"
--construct preamble
local strPreamble = [[
[Family Historian Query]
VERSION=3.0
TYPE=QQQQ
DESC=DDDD
.
TITLE="YYYY"
SUBTITLE="%#x"
ORIENTATION="OOOO"
]]
strPreamble = stringx.replace(strPreamble, "DDDD", strdescription)
strPreamble = stringx.replace(strPreamble, "YYYY", strtitle)
if booReadOnly then
strPreamble = stringx.replace(strPreamble, "TYPE=QQQQ", "TYPE=QQQQ\nATTR=READ_ONLY")
end
strPreamble = stringx.replace(strPreamble, "QQQQ", QueryTypes[strqtype])
strPreamble = stringx.replace(strPreamble, "OOOO", string.upper(strOrientation))
local tblcolumns = {}
--[[column format is
TAG = "header", "datareference", "type", "sort"
header is a string
datareference is a string (exactly as taken from the expression field within a query)
optional type can be "HIDDEN" "BUDDY" or nil -- cannot be omitted
optional sort can be "ASC" "DESC" or nil -- cannot be omitted
]]--
local AddColumn = function(sheader,sdatareference, stype, ssort)
local str = [[TAG="]]..sheader..[[","]]..sdatareference..[["]]
if stype then str = str..[[,"]]..stype..[["]] end
if ssort then
str = str..utils.choose(stype,[[,]]..ssort, [[,,]]..ssort)
end
table.insert(tblcolumns,str)
end
local tblprompts = {}
--[[Prompts are not an fH concept, but it can be useful to set a prompt and default for a field within a query.
The Query class will create prompts before filters, so that the filters can refer to the values returned by prompts
Prompt parameters are is:
default is the default value -- use * for all and * for none and "" for no default
Label is the string to use (followed by a prefix determined by the default) in the prompt and later filters
]]--
local AddPrompt = function(default, label)
local valuepromptsuffix =""
if default == "*" then
valuepromptsuffix = " (* for all)"
elseif default == "-" then
valuepromptsuffix = " (- for none)"
elseif default ~= "" then
valuepromptsuffix =" ("..default..")"
end
local s = [[FILTER=GEN,EXC,IF,Y,"]]..label..valuepromptsuffix..[[","=Text(%INDI.NOTE[1000]%)",,"=",TEXT,"]]..default..[["]]
table.insert(tblprompts, s)
end
local tblfilters = {}
--TODO: Not fully TESTED
local AddGenFilter = function(rowtype, booparm, strparmlabel, expression, boomatchcase, comparator, valuetype, value)
local rowtext = RowType[rowtype]..","
local parametertext = utils.choose(booparm, "Y,", "N,")
local labeltext = utils.choose(booparm, [["]]..strparmlabel..[[",]],[["",]])
local expressiontext = [["]]..expression..[[",]]
local comparatortext = [["]]..Comparators[comparator]..[[",]]
local matchtext = utils.choose(boomatchcase, "MC", "")..[[,]]
local strRow = "FILTER=GEN,"..rowtext..parametertext..labeltext..expressiontext..matchtext..comparatortext
if valuetype ~= nil then strRow = strRow..string.upper(valuetype)..[[,]] end
if value ~= nil then strRow = strRow..utils.choose(valuetype=="NUMBER", value, [["]]..value..[["]]) end
strRow = stringx.rstrip(strRow,",") --remove any trailing commas
table.insert(tblfilters, strRow)
end
local AddRelFilter = function(rowtype, indichoice, strparmlabel, relationship, booincludeoriginal, booincludespouses, maxgen)
--TODO: NOT AT ALL TESTED
local rowtext = RowType[rowtype]..","
booparm = indichoice == "Individual"
local parametertext = utils.choose(booparm, "Y,", "N,")
local labeltext = utils.choose(booparm, [["]]..strparmlabel..[[",]],[["",]])
local relationshiptext = [["]]..RelTypes[relationship]..[[",]]
local originaltext = utils.choose(booincludeoriginal, "ORG", "")..[[,]]
local spousetext = utils.choose(booincludespouses, "SP", "")..[[,]]
local strRow = "FILTER=REL,"..rowtext..parametertext..labeltext..relationshiptext..originaltext..spousetext
if maxgen ~= nil then local strRow=strRow..maxgen end
strRow = stingx.rstrip(strRow,",") --remove any trailing commas
table.insert(tblfilters, strRow)
end
local AddListFilter = function(rowtype, list)
--TODO: NOT AT ALL TESTED
local rowtext = RowType[rowtype]..","
local parametertext = "N,"
local labeltext = ","
local listtext = [["]]..list..[["]]
local strRow = "FILTER=LST,"..rowtext..parametertext..labeltext..listtext
table.insert(tblfilters, strRow)
end
local Install = function()
local sQ = strPreamble.."\n"..table.concat(tblcolumns,"\n").."\n"..table.concat(tblprompts,"\n").."\n"..table.concat(tblfilters,"\n") --UTF8
WriteUTF16File(sQ, QueryDirectory..strQueryTitle..strqueryfiletype)
end
local Download = function(downloadlocation, strQueryTitle)
--TODO: NOT TESTED
FileDownload(downloadlocation, QueryDirectory..strQueryTitle..strqueryfiletype)
end
return {AddColumn = AddColumn, AddPrompt = AddPrompt, AddGenFilter = AddGenFilter, AddRelFilter= AddRelFilter, AddListFilter = AddListFilter, Install = Install, Download = Download}
end
local strQueryType = utils.choose(options.UseNotes == "ON", "Note", "Fact")
local strTextField = utils.choose(options.UseNotes == "ON", "%NOTE.TEXT%", "%FACT.NOTE2%")
local myQuery = Query(
strQueryType,
"Research Tasks ("..strQueryType.."s)",
"All Research Tasks ("..strQueryType.."s) optionally filtered by various criteria",
true, "LANDSCAPE"
)
do --make columns
if strQueryType == "Note" then --identify task
myQuery.AddColumn("Individual",
[[=Record(TextToNumber(GetLabelledText(%NOTE.TEXT%,""Individual: "")),""I"")]],nil,"ASC")
myQuery.AddColumn("Note", "%NOTE%", nil, "ASC")
else
myQuery.AddColumn("Individual","=FactOwner(%FACT%,1,MALES_FIRST)",nil,"ASC")
myQuery.AddColumn("Task", "FACT",nil,"ASC")
end
for _, f in ipairs{"Group", "Tag", "FactDate", "DueDate", "UpdateDate", "Field1", "Field2", "Field3", "Field4"} do
if options["Use"..f] == "ON" then
if f == "FactDate" then
myQuery.AddColumn("Date","FACT.DATE")
elseif f == "UpdateDate" then
myQuery.AddColumn("Updated",
utils.choose(strQueryType=="Note",
[[=LastUpdated()]],
[[=GetLabelledText(%FACT.NOTE2%,""Updated: "")]])
)
elseif f == "Tag" then
myQuery.AddColumn("Tag",[[=Text(""]]..options.PrefixTag..[["".GetLabelledText(]]..strTextField..[[,""]]..options.PrefixTag..[[""))]])
else
if strQueryType == "Note" or f ~= "Field1" then
myQuery.AddColumn(options[f.."Label"],
[[=GetLabelledText(]]..strTextField..[[,""]]..options[f.."Label"]..[[: "")]])
end
end
end
end
myQuery.AddColumn("Source",string.upper(strQueryType)..".SOUR>")
myQuery.AddColumn("Repository", string.upper(strQueryType)..".SOUR>REPO>")
end
do --make defaults/prompts
local tblDefaults = {}
for _,f in ipairs{"Group","Tag", "Field2", "Field3"} do
if options["Use"..f] == "ON" then
local strLabel = utils.escape(utils.choose(f == "Tag", "Tag", options[f.."Label"]))
myQuery.AddPrompt("*", strLabel)
end
end
for _, f in ipairs{"Task", "Source", "Repository"} do
myQuery.AddPrompt("-", f.." words")
end
end
do --make filters
for _,f in ipairs{"Tag","Group", "Field2", "Field3"} do
if options["Use"..f] == "ON" then
local expression ='=IsTrue((Text([""LLLL (* for all)""]) = ""*"") or (GetLabelledText(%NOTE.TEXT%,""AAAA"") = [""LLLL (* for all)""]))'
expression = stringx.replace(expression, "LLLL", utils.escape(utils.choose(f == "Tag", "Tag", options[f.."Label"])))
expression = stringx.replace(expression, "AAAA", utils.escape(utils.choose(f == "Tag", options.PrefixTag, options[f.."Label"]..": ")))
myQuery.AddGenFilter("Exclude unless", false, "", expression, false, "is true")
end
end
myQuery.AddGenFilter("Exclude unless", false, "", '=IsTrue((Text([""Task words (- for none)""]) = ""-"") or ContainsText(%'..string.upper(strQueryType)..'%,[""Task words (- for none)""],STD))', false, "is true")
myQuery.AddGenFilter("Exclude unless", false, "", '=IsTrue((Text([""Source words (- for none)""])= ""-"") or ContainsText(%'..string.upper(strQueryType)..'.SOUR>%,[""Source words (- for none)""],STD))', false, "is true")
myQuery.AddGenFilter("Exclude unless", false, "", '=IsTrue((Text([""Repository words (- for none)""]) = ""-"") or ContainsText(%'..string.upper(strQueryType)..'.SOUR>REPO>%,[""Repository words (- for none)""],STD))', false, "is true")
if strQueryType == "Fact" then
myQuery.AddGenFilter("Exclude unless", false, "", "=FactLabel(%FACT%)", false, "matches", "TEXT", "Task") --filter on Task facts only
end
end
myQuery.Install()
end
local function AdjustToolsTab()
HideContainer(htoolsFactsBox, options.UseAttribute == "OFF")
HideContainer(htoolsNotesBox, options.UseNotes == "OFF")
end
local function AdjustResults()
--set Results parameters to defaults for creating tasks -- if any of the options on tabTools are used, they will override this
myResults.Types({"item","item","text","text","text","text","text","text","text","text","text","item","item"})
myResults.Headings({"Individual", "Task",
"Tag", options.GroupLabel, options.FactDateLabel,
options.DueDateLabel, options.UpdateDateLabel,
options.Field1Label, options.Field2Label,
options.Field3Label,options.Field4Label,
"Source", "Repository"})
myResults.Visibility({true, true,
options.UseTag == "ON", options.UseGroup == "ON",
options.UseFactDate == "ON", options.UseDueDate =="ON", options.UseUpdateDate == "ON",
options.UseField1 =="ON" and options.UseNotes == "ON", options.UseField2 == "ON",
options.UseField3 == "ON", options.UseField4 == "ON",
true, true})
end
AdjustConfigDialog()
AdjustTemplateTab()
AdjustTasksTab()
_, cstrFactTag = myFactTools.Exists(cstrFactName, true, true)
if options.UseAttribute == "ON" then MakeFactDefinition() end
MakeQuery()
AdjustToolsTab()
AdjustResults()
end
return{
GetContainer = GetContainer,
GetDefaults = GetDefaults,
ActionConfig = ActionConfig
}
end
--[[Template handling class
A Templates class that implements a set ot text-based template deinitions. The contents of the templates are determined by the calling plugin, which interacts with the template class via defined methods and makes plug-in specific features (i.e. non text-fields) available via an associated PluginTemplates class.
Multiple instances of the Templates class can be created by a plugin, as long as the plugin specifies a separate subdirectory to hold templates other than the first set (which are held in the main plugin directory.
Templates can be defined to be Global (available to all fH projects), or Project (available to a single project only). Project templates will typically be required when a template includes project-specific data such as a source identifier.
Use as follows
MyTemplate = Template(initvalue) to instantiate
MyTemplate.DoSomething(parms) to call a method
TODO: (future enhancement): the ability to use templates that include tokens (text substitions when the template is used, with default values)
@Author: Helen Wright
@V1.0: Initial version.
@LastUpdated: 27 October 2019
]]--
function Templates (booAllowProjectTemplates, subdir, booTokens)
do --prerequisites
require("iuplua")
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
-- fH API boilerplate
-- Dialog boilerplate
-- File handling boilerplate
end
local caller = CallerTemplates(subdir) --initialise caller class
--variables and functions for handling global and project templates
local booProjectTemplatesAllowed = booAllowProjectTemplates
local strGlobalTemplateDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local strProjectTemplateDir = utils.choose(booProjectTemplatesAllowed, fhGetPluginDataFileName("CURRENT_PROJECT", true), nil)
if subdir then
strGlobalTemplateDir = path.join(strGlobalTemplateDir, subdir)
if not path.isdir(strGlobalTemplateDir) then
path.makepath(strGlobalTemplateDir)
end
if booProjectTemplatesAllowed then
strProjectTemplateDir = path.join(strProjectTemplateDir, subdir)
if not path.isdir(strProjectTemplateDir) then
path.makepath(strProjectTemplateDir)
end
end
end
local function TemplatePath(boop) --returns directory
return utils.choose(boop,strProjectTemplateDir,strGlobalTemplateDir)
end
--data and function for Template details
local tblTemplates = {} --keyed by Template Name which is unique; values are Template Directory and boolean 'IsProjectTemplate'
local tblSelectors = {} --list of selectors that have been created using tblTemplates
local GetTemplates = function()
local tcount = 1
local tname = ""
tblTemplates = {}
for _, t in ipairs(dir.getfiles(strGlobalTemplateDir, "*.dat")) do
tname = path.basename(path.splitext(t)) --isolate the file name which will be the template name
tblTemplates[tname] = {strGlobalTemplateDir, false}
tcount = tcount+1
end
if booProjectTemplatesAllowed then
for _, t in ipairs(dir.getfiles(strProjectTemplateDir, "*.dat")) do
tname = path.basename(path.splitext(t)) --isolate the file name which will be the template name
tblTemplates[tname] = {strProjectTemplateDir, true}
tcount = tcount+1
end
end
end
local function RepopulateSelectors()
for _, l in ipairs(tblSelectors) do
--save current selection
if l.MULTIPLE == "YES" then
local ltbl = GetSelectedValues(l)
PopulateList(l, tblTemplates)
SetSelectedValues(l, ltbl)
else
local lvalue = GetSingleValue(l)
PopulateList(l, tblTemplates)
GoToInList(lvalue, l)
end
end
end
GetTemplates() -- populate the template details when class is instantiated
local PromptTemplateName = function(prompt, default) --returns string
local booOK, strName = GetText(prompt, default, strAlphaNumeric)
return utils.choose(booOK, stringx.strip(strName), "")
end
local PromptTemplateNameAndType = function (prompt, default) --returns boolean, string
local booOK, strName, booProject = GetTextandTick(prompt, default, strAlphaNumeric, "Project template?")
return booProject, utils.choose(booOK, stringx.strip(strName), "")
end
local function PromptName() --returns name and project indicator
local boop = false
local name = ""
if booProjectTemplatesAllowed then
boop, name = PromptTemplateNameAndType("Enter new name", "")
else -- get target name
name = PromptTemplateName("Enter new name", "")
end
return boop, name
end
local function SaveTemplate(values, name, directory)
table.save(values, path.join(directory, name.."\.dat")) --save the file
end
local function ReadTemplate(name) --returns table of values
return table.load(path.join(tblTemplates[name][1], name.."\.dat"))
end
local function AddTemplate(values, name, booproject)
SaveTemplate(values, name, TemplatePath(booproject))
GetTemplates() --repopulate the template data
RepopulateSelectors()
end
local function DeleteTemplate(name)
DeleteFile(path.join(tblTemplates[name][1], name.."\.dat"))
GetTemplates() --repopulate the template data
RepopulateSelectors()
end
local function RenameTemplate(oldname, newname)
--rename the file
local oldpath = path.join(tblTemplates[oldname][1], oldname.."\.dat")
local newpath = path.join(tblTemplates[oldname][1], newname.."\.dat")
RenameFile(oldpath, newpath)
GetTemplates() --repopulate the template data
RepopulateSelectors()
end
local TemplatesExist = function() --returns boolean
return tablex.size(tblTemplates) > 0
end
--data and functions for Template container handling
local function TemplateGetFromContainer(container) --returns table of values
local tblvalues = GetContainerData(GetDataElements(container, {},{}, false))
return caller.Get(tblvalues, container)
end
local function TemplateLoadToContainer(container, tblvalues)
for k, v in ipairs(GetDataElements(container, {},{}, false)) do
v.VALUE = tblvalues[k]
end
caller.Load(tblvalues, container)
EnableContainer(container, {}, "YES")
end
local function ContainerClear(container)
ClearContainer(container, {})
caller.Clear(container)
end
--data for Template selection controls
--methods for selecting a single template
local btnUseNew =nil
local btnUseUpdate = nil
local UseContainer = nil --iup container for the use control
local ListUse = nil
local UseSelector = function(container) --returns list
UseContainer = container
local function Use(ctl, name) --load template into UseContainer
local values = ReadTemplate(name)
local booProject = tblTemplates[name][2]
TemplateLoadToContainer(UseContainer, values)
btnUseNew.active = "ON"
btnUseUpdate.active = "ON"
caller.Display(booProject, UseContainer)
caller.CheckUIStatus()
end
local function New()
local booProject, strNewName = PromptName()
if strNewName ~= "" then --not cancelled
if tblTemplates[strNewName] then --this is a duplicate name
Messagebox("New template failed","A template called "..strNewName.." already exists", false)
return
end
local booOK, strError = caller.OK(booProject, UseContainer)
if booOK then
AddTemplate(TemplateGetFromContainer(UseContainer), strNewName, booProject)
GoToInList(strNewName, ListUse)
btnUseNew.active = "ON"
btnUseUpdate.active = "ON"
caller.Display(booProject, UseContainer)
caller.CheckUIStatus()
Messagebox("New template succeeded","Template "..strNewName.." created",false)
else
Messagebox("Save failed","Cannot save "..strNewName.." - "..strError, true)
end
end
end
local function Update()
local strname = GetSingleValue(ListUse)
local booOK, strError = caller.OK(tblTemplates[strname][2],UseContainer)
if booOK then
SaveTemplate(TemplateGetFromContainer(UseContainer), strname, tblTemplates[strname][1])
else
Messagebox("Save failed","Cannot save "..strname.." - "..strError, true)
end
end
ListUse = MakeList{values = tblTemplates, action = Use,
tip = "Choose a template"}
table.insert(tblSelectors, ListUse)
btnUseNew = MakeButton{title = "New", callback = New, tip = "Create a new template from the contents"}
btnUseUpdate = MakeButton{title = "Update", callback = Update, tip = "Update the template from the contents"}
btnUseUpdate.active = "OFF"
btnUseNew.active = "ON"
return iup.vbox{iup.hbox{MakeLabel{title = "Template", tip = cstrTemplateTip}, ListUse}, iup.hbox{iup.fill{}, btnUseNew, btnUseUpdate};}
end
local ClearUseSelector = function()
ContainerClear(UseContainer)
ListUse.value = 0
btnUseNew.active = "ON"
btnUseUpdate.active = "OFF"
end
local GetUseName = function()
return GetSingleValue(ListUse)
end
local GetUseValues = function()
return TemplateGetFromContainer(UseContainer)
end
--methods for selecting templates in bulk
local listBulk = nil
local BulkSelector= function() --returns list
local function Action()
caller.CheckUIStatus()
end
listBulk = MakeList{dropdown="NO", visiblelines = "20", multiple = "YES",
values = tblTemplates, action = Action, tip = "Choose one or more templates"}
table.insert(tblSelectors, listBulk)
return listBulk
end
local ClearBulkSelector = function()
MultiListSelectionClear(listBulk)
caller.CheckUIStatus()
end
local BulkTemplates = function() --returns successive tables of values
--iterator through selected templates
local t = GetSelectedValues(listBulk)
local contents = ""
local k, v = next(t, nil) --v is a name
return function ()
if k then
contents = ReadTemplate(v)
k, v =next(t, k)
return contents
end
end
end
local BulkTemplatesAreSelected = function() --returns boolean
return MultiListSelectionTrue(listBulk)
end
--methods and controls to manipulate template definitions
local EditContainer = nil -- iup container for the edit control
local tblLastSaved = nil
local strLastEdited = ""
local tblbtnTemplates = {}
local listEdit = nil
local function Save()
local booOK, strError = caller.OK(tblTemplates[strLastEdited][2],EditContainer)
if booOK then
tblLastSaved = TemplateGetFromContainer(EditContainer)
SaveTemplate(tblLastSaved, strLastEdited, tblTemplates[strLastEdited][1])
else
Messagebox("Save failed","Cannot save "..strLastEdited.." - "..strError, true)
end
end
local function CheckEditChanges() --returns boolean
local values = TemplateGetFromContainer(EditContainer)
if tablex.compare(values, tblLastSaved, "==") then return true end --no changes since last save
if Messagebox("Unsaved template changes","You have template changes to save. Press OK to Save those changes and Continue, or Cancel to Discard them and Continue", true) == "OK" then
local booOK, strError = caller.OK(EditContainer)
if not booOK then --cannot save so will not continue
Messagebox("Save failed","Cannot save "..strLastEdited.." - "..strError.." so will not continue with selected operation", true)
return false--abort
end
Save() --save changes
else
TemplateLoadToContainer(EditContainer, tblLastSaved) --restore last saved value
end
return true --continue
end
local EditSelector = function(container) --returns vbox
local cstrProjectTemplate = "Project template"
local cstrGlobalTemplate = "Global template"
local labProjectTemplate = MakeLabel{title="", norm = norm2}
local ctlProjectTemplateContainer = iup.hbox{labProjectTemplate}
local function SetTemplateLabel(boop)
labProjectTemplate.title = utils.choose(boop, cstrProjectTemplate, cstrGlobalTemplate)
end
local function Edit(ctl, name)
if name == strLastEdited then return end --nothing changed
if CheckEditChanges() then
local values = ReadTemplate(name)
local booProject = tblTemplates[name][2]
TemplateLoadToContainer(EditContainer, values)
SetTemplateLabel(booProject)
EnableButtons(tblbtnTemplates, "YES")
caller.Display(booProject, EditContainer)
strLastEdited = name
tblLastSaved = values
else
GoToInList(strLastEdited, listEdit) -- return to the previous selection
end
end
local function New()
if CheckEditChanges() then
local booProject, strNewName = PromptName()
if strNewName ~= "" then --not cancelled
if tblTemplates[strNewName] then --this is a duplicate name
Messagebox("New template failed","A template called "..strNewName.." already exists", false)
return
end
ContainerClear(EditContainer)
tblLastSaved = TemplateGetFromContainer(EditContainer)
AddTemplate(tblLastSaved, strNewName, booProject)
GoToInList(strNewName, listEdit)
SetTemplateLabel(booProject)
EnableButtons(tblbtnTemplates, "YES")
EnableContainer(EditContainer, {}, "YES")
caller.Display(booProject, EditContainer)
caller.CheckUIStatus()
Messagebox("Create succeeded","Template "..strNewName.." created",false)
strLastEdited = strNewName
end
end
end
local function Copy()
local strOldName = GetSingleValue(listEdit)
if CheckEditChanges() then
local booProject, strNewName = PromptName()
if strNewName ~= "" then --not cancelled
if tblTemplates[strNewName] then --this is a duplicate name
Messagebox("Copy template failed","A template called "..strNewName.." already exists", false)
else
AddTemplate(tblLastSaved, strNewName, booProject)
GoToInList(strNewName, listEdit)
Messagebox("Copy template succeeded", strOldName.." copied to "..strNewName, false)
strLastEdited = strNewName
end
end
end
end
local function Rename()
local strNewName=PromptTemplateName("Enter new name","") --can't change type of an existing template
local strOldName = GetSingleValue(listEdit)
if strNewName ~= "" then --not cancelled
if tblTemplates[strNewName] then --this is a duplicate name
Messagebox("Rename template failed","A template called "..strNewName.." already exists", false)
else
RenameTemplate(strOldName, strNewName)
GoToInList(strNewName, listEdit)
Messagebox("Rename template succeeded", strOldName.." renamed to "..strNewName, false)
strLastEdited = strNewName
end
end
end
local function Delete()
local name = GetSingleValue(listEdit)
if Messagebox("Confirm template deletion","Confirm deletion of "..name, true) == "OK" then
DeleteTemplate(name)
ContainerClear(EditContainer)
EnableContainer(EditContainer, {}, "NO") --no selected template
SetTemplateLabel(false) --default is not a project
caller.CheckUIStatus()
Messagebox("Delete template succeeded","Template "..name.." deleted",false)
strLastEdited = ""
tblLastSaved = TemplateGetFromContainer(EditContainer)
end
end
local function MakeButtons() --returns hbox
local btnCopy = MakeButton{title="Copy", callback = Copy, tip = "Copy selected template"}
local btnRename = MakeButton{title="Rename", callback = Rename, tip = "Rename selected template"}
local btnDelete = MakeButton{title="Delete", callback = Delete, tip = "Delete selected template"}
local btnSave = MakeButton{title="Save", callback = Save, tip = "Save changes to selected template"}
tblbtnTemplates = {btnCopy, btnSave, btnRename, btnDelete}
EnableButtons(tblbtnTemplates, "NO") -- can't do any of this until a template is selected
return iup.hbox{iup.fill{}, btnCopy, btnSave, btnRename, btnDelete, iup.fill{}}
end
local cstrTemplateTip = "Choose a template to modify or view"
EditContainer = container
tblLastSaved = TemplateGetFromContainer(EditContainer) --empty values
EnableContainer(EditContainer, {}, "NO")
listEdit = MakeList{values=tblTemplates, action = Edit, tip = cstrTemplateTip}
table.insert(tblSelectors, listEdit)
local vbox = iup.vbox{
iup.hbox{MakeLabel{title = "Template", tip = cstrTemplateTip},
listEdit,
MakeButton{title="New", callback = New, tip = "Create new template"};
alignment = "ACENTER"},
ctlProjectTemplateContainer,
MakeButtons()
}
SetTemplateLabel(false) --default is not a project
if booProjectTemplatesAllowed == false then
HideContainer(ctlProjectTemplateContainer)
end
return vbox
end
local function CheckForUnsavedEditChanges()
if CheckEditChanges() then caller.CheckUIStatus() return true end
return false
end
local function ClearEditSelector()
ContainerClear(EditContainer)
tblLastSaved = TemplateGetFromContainer(EditContainer)
strLastEdited = ""
listEdit.value = 0
EnableButtons(tblbtnTemplates, "NO")
caller.CheckUIStatus()
end
--expose public methods
return{TemplatesExist = TemplatesExist,
UseSelector = UseSelector,
ClearUseSelector = ClearUseSelector,
GetUseName = GetUseName,
GetUseValues = GetUseValues,
BulkSelector = BulkSelector,
BulkTemplates = BulkTemplates,
BulkTemplatesAreSelected = BulkTemplatesAreSelected,
ClearBulkSelector = ClearBulkSelector,
EditSelector = EditSelector,
ClearEditSelector = ClearEditSelector,
CheckForUnsavedEditChanges = CheckForUnsavedEditChanges}
end
function CallerTemplates(subdir)
local source = nil
local function IdentifySource(container)
return utils.choose(container == boxTemplateEdit, TemplateSource, TaskSource)
end
--public methods
local Display = function(boop, container)
source = IdentifySource(container)
source.DisableSelector(not boop and container ~= boxTaskEdit)
end
local Load = function(tblvalues, container)
--handle the source as the last item in the template
local source = IdentifySource(container)
if tblvalues[#tblvalues] ~= "" then
local ptr = fhNewItemPtr()
ptr:MoveToRecordById('SOUR', tblvalues[#tblvalues])
source.Load({ptr})
else
source.ClearSelector()
end
end
local Get = function(tblvalues, container) --returns a set of values suitable to save as a template
local source = IdentifySource(container)
--now handle the source as the last item in the template
local tbls = source.SourceList()
if #tbls > 0 then
tblvalues[#tblvalues] = fhGetRecordId(tbls[1])
else
tblvalues[#tblvalues] = ""
end
return tblvalues
end
local Clear = function(container)
local source = IdentifySource(container)
source.ClearSelector()
EnableContainer(container, {}, utils.choose(container == boxTemplateEdit, "NO", "YES"))
end
local OK = function(boop, container) --returns boolean and optional error message
local source = IdentifySource(container)
local slist = source.SourceList()
local booOK = boop == true or #slist == 0
local strError = utils.choose(booOK, "", "A global template cannot specify a source")
return booOK, strError
end
local InitialiseTemplate = function(boop, container)
return --do nothing
end
local CheckUIStatus = function()
AdjustButtons()
end
return{
Display = Display,
Load = Load,
Get = Get,
Clear = Clear,
OK = OK,
InitialiseTemplate = InitialiseTemplate,
CheckUIStatus = CheckUIStatus,
}
end
--[[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()
return --no need to do anything
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 = "HORIZONTAL", norm = norm2, tip = strTip, multiline = utils.choose(imaxIndividuals ~= 1, "YES", "NO")}
Initialise()
btnIndividuals = MakeButton{title="Individual...", callback=Choose, tip = strTip}
if Orientation == "HORIZONTAL" then
return iup.hbox{btnIndividuals, txtIndividuals; alignment = "ACENTER"}
else
return iup.vbox{btnIndividuals, txtIndividuals; alignment = "ACENTER"}
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
--[[Results And Activity Log classes
@Author: Helen Wright
@Version: 1.0
@LastUpdated: 18 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
--[[Search class
A class that implements a Generic TextSearch object
]]--
function Search (strPattern, booPlain, booWhole, booSensitive)
--TODO: Fully test Search class
--TODO: Generalise to take case sensitivity into account
--pre-requisites
if not fhloadrequire("pl","pl.init") then return end -- Load required Penlight modules
pl = require("pl.import_into")
local function isWordFoundInString(w,s)
return select(2,s:gsub('^' .. w .. '%W+','')) +
select(2,s:gsub('%W+' .. w .. '$','')) +
select(2,s:gsub('^' .. w .. '$','')) +
select(2,s:gsub('%W+' .. w .. '%W+','')) > 0
end
--private state variables
local plain = booPlain or true
local whole = booWhole or true
local spattern = strPattern
if plain then spattern = utils.escape(spattern) end --escape magic characters
local Found = function (strSearch)
local ssearch = strSearch
if whole then
return isWordFoundInString(spattern, ssearch)
else
return string.find(spattern, ssearch) ~= nil
end
end
--expose public methods
return{Found = Found}
end
function Report ()
--class to create reports
--prerequisites: File handling boilerplate; Penlight libraries
local cstrPluginName = fhGetContextInfo("CI_PLUGIN_NAME")
local cstrPluginDir = fhGetPluginDataFileName("LOCAL_MACHINE",true)
local ReportDirectory = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Reports\\Custom\\"
local strreportfiletype = ".fhr"
local Download = function(strReportTitle)
local downloadlocation = "http://www.fhug.org.uk/colevalleygirl/"..strReportTitle..strreportfiletype
FileDownload(downloadlocation, ReportDirectory..strReportTitle..strreportfiletype)
end
return {Download = Download}
end
myReports = Report()
--------------------------------------------------------------
--MAIN DIALOG ACTIONS
--------------------------------------------------------------
function AdjustButtons()
local tblIndividuals = indi.IndividualList()
local booBulkOK = myTemplates.TemplatesExist() and myTemplates.BulkTemplatesAreSelected() and #tblIndividuals > 0
btnBulkMake.active = utils.choose(booBulkOK, "YES", "NO")
local booTextOK = #tblIndividuals > 0
btnTaskMake.active = utils.choose(booTextOK, "YES", "NO") --can only make a task from text when an individual is selected
end
--[[taskoptions components; there are no defaults
These are used in both MakeNoteText and MakeTask
GroupValue
DueDateValue
UpdateDateValue
FactDateValue
IncludeIndividual = true|false
Individual = ptr
Tag
UseField1
Field1Value
Field2Value
Field3Value
Field4Value
Field5Value
Source = ptr
]]--
function MakeNoteHeader(note, taskoptions)
local strNote = note.."\[\[\n" --privacy markers
if taskoptions.UseField1 == "ON" then --set first line of note as title
strNote = strNote..options.Field1Label..strSep..taskoptions.Field1Value.."\n"..strRule.."\n"
end
if taskoptions.IncludeIndividual then
local txtIndi = fhGetDisplayText(taskoptions.Individual)
local intIndi = fhGetRecordId(taskoptions.Individual)
strNote = strNote.."Individual: "..tostring(intIndi).." "..txtIndi.."\n"..strRule.."\n"--add the identity of the individual
end
return strNote
end
function MakeNoteTail(note)
return note.."\n\]\]"
end
function AddTagTextandGroup(note, booTag, tag, booGroup, group)
if booTag and tag then
note = note..options.PrefixTag..tag.."\n"
end
if booGroup then
note = note..options.GroupLabel..strSep..group.."\n"
end
if booTag or booGroup then
note = note..strRule.."\n"
end
return note
end
function MakeNoteText(taskoptions)
local strNote = ""
local booRuleNeeded = false
local function InsertRuleIfNeeded()
if booRuleNeeded == true then
strNote = strNote..strRule.."\n"
booRuleNeeded = false
end
end
if options.UseNotes == "ON" then strNote = MakeNoteHeader(strNote, taskoptions) end
strNote = AddTagTextandGroup(strNote, options.UseTag =="ON", taskoptions.Tag, options.UseGroup == "ON", taskoptions.GroupValue)
if options.UseDueDate == "ON" then
strNote = strNote..options.DueDateLabel..strSep..taskoptions.DueDateValue .."\n"
booRuleNeeded = true
end
if options.UseUpdateDate == "ON" and options.UseAttribute == "ON" then
strNote = strNote..options.UpdateDateLabel..strSep..taskoptions.UpdateDateValue .."\n"
booRuleNeeded = true
end
InsertRuleIfNeeded()
for i = 2,3 do
if options["UseField"..tostring(i)] == "ON" then
strNote = strNote..options["Field"..tostring(i).."Label"]..strSep..taskoptions["Field"..tostring(i).."Value"].."\n"
booRuleNeeded = true
end
end
for i = 4,5 do
if options["UseField"..tostring(i)] == "ON" then
InsertRuleIfNeeded()
strNote = strNote..options["Field"..tostring(i).."Label"]..strSep..taskoptions["Field"..tostring(i).."Value"].."\n"
booRuleNeeded = true
end
end
if options.UseNotes == "ON" then strNote = MakeNoteTail(strNote) end
return strNote
end
function MakeTask(strNote, taskoptions)
local ptrTask = nil --will hold a pointer to the new task item
if options.UseAttribute == "ON" then
local values = {}
values.attributevalue = utils.choose(options.UseField1 == "ON", taskoptions.Field1Value, "")
if options.UseFactDate == "ON" then
values.factdate = taskoptions.FactDateValue
end
values.factnote = strNote
ptrTask = myFactTools.Create(cstrFactTag, taskoptions.Individual, values) --create the task fact linked to the individual with fact date and note if appropriate
else --using Notes
ptrTask = CreateSharedNote(strNote) --create the note
CreateNoteLink(taskoptions.Individual, ptrTask) --link the note to the individuals
end
local ptrS = taskoptions.Source
local ptrR = nil
if ptrS:IsNotNull() then
mySourceTools.Link(ptrTask,ptrS) --add source link if specified
if fhGetItemPtr(ptrS,'~.REPO>'):IsNotNull() then
ptrR = fhGetItemPtr(ptrS,'~.REPO>')
end
end
local txtIndi = fhGetDisplayText(taskoptions.Individual)
local strLogText = txtIndi.." | ".. taskoptions.Field1Value
if options.UseGroup == "ON" then
if taskoptions.GroupValue ~= "" then
strLogText = strLogText .." | "..options.GroupLabel..": "..taskoptions.GroupValue
end
end
myActivity.Update(strLogText)
local resultstable = {taskoptions.Individual, ptrTask, options.PrefixTag..options.TagLabel, taskoptions.GroupValue,
taskoptions.FactDateValue, taskoptions.DueDateValue, taskoptions.UpdateDateValue,
taskoptions.Field1Value, taskoptions.Field2Value, taskoptions.Field3Value, taskoptions.Field4Value,
ptrS, ptrR}
myResults.Update(resultstable)
myResults.Title("Tasks created")
EnableContainer(tabTools,{},"NO")--disable tools because results tables are being used for task creation
end
function MakeMainDialog()
local function MakeBulkTasks()
local tblTemplateValues = {}
for tblTemplateValues in myTemplates.BulkTemplates() do --returns a table of values 1-6, 6 being the source
local taskoptions = {
DueDateValue = "",
UpdateDateValue = os.date("%x"),
FactDateValue = os.date("%x"),
GroupValue = boxGroupData[2].value,
Field1Value = tblTemplateValues[1],
Field2Value = tblTemplateValues[2],
Field3Value = tblTemplateValues[3],
Field4Value = tblTemplateValues[4],
Field5Value = tblTemplateValues[5],
UseField1 = options.UseField1,
Tag = options.TagLabel
}
for k, v in pairs(taskoptions) do
taskoptions[k] = utils.choose(type(v) == 'string', v, "")
end
taskoptions.IncludeIndividual = true
local tblindi = indi.IndividualList()
taskoptions.Individual = tblindi[1]
local sptr = fhNewItemPtr()
local ints = tonumber(tblTemplateValues[6]) --SourceID
if type(ints) == 'number' then
sptr:MoveToRecordById('SOUR', ints)
else
sptr:SetNull()
end
taskoptions.Source = sptr
local strTaskNote = MakeNoteText(taskoptions)
MakeTask(strTaskNote, taskoptions)
end
end
local function MakeTaskFromText()
local tblTemplateValues = myTemplates.GetUseValues() --returns a table of values 1-6, 6 being the source
local taskoptions = {
DueDateValue = boxTDueDate[2].value,
UpdateDateValue = os.date("%x"),
FactDateValue = boxTFactDate[2].value,
GroupValue = boxGroupData[2].value,
Field1Value = tblTemplateValues[1],
Field2Value = tblTemplateValues[2],
Field3Value = tblTemplateValues[3],
Field4Value = tblTemplateValues[4],
Field5Value = tblTemplateValues[5],
UseField1 = options.UseField1,
Tag = options.TagLabel
}
for k, v in pairs(taskoptions) do
taskoptions[k] = utils.choose(type(v) == 'string', v, "")
end
taskoptions.IncludeIndividual = true
local tblindi = indi.IndividualList()
taskoptions.Individual = tblindi[1]
local sptr = fhNewItemPtr()
local ints = tonumber(tblTemplateValues[6]) --SourceID
if type(ints) == 'number' then
sptr:MoveToRecordById('SOUR', ints)
else
sptr:SetNull()
end
taskoptions.Source = sptr
local strTaskNote = MakeNoteText(taskoptions)
MakeTask(strTaskNote, taskoptions)
end
local function FindTag()
--TODO: Possibly search for multiple tags or all tags
local TagSearch = nil
--get search text
local booOK, strTag = GetText("Specify tag, which will be prefixed with "..options.PrefixTag, options.TagLabel, strMin1Letter)
if not booOK then
return false
else
strTag = options.PrefixTag..strTag
TagSearch = Search(strTag, true, true)
end
--identify data classes that will be searched
local tblClass = {text=1,longtext=1,name=0,place=0,wordlist=1, word = 1}
for strType, v in pairs(tblClass) do
tblClass[strType] = (v==1) --make values boolean
end
--Process all items in data
local booFound = false
for ptrItem in allItems() do
local strDataClass = fhGetDataClass(ptrItem)
if fhGetValueType(ptrItem) == 'text' and tblClass[strDataClass] == true then
local strPtrText = fhGetValueAsText(ptrItem)
if TagSearch.Found(strPtrText) then -- add to results
booFound = true
local strRecordType = fhGetTag(ptrItem)
--get the parent item
local ptrParent =fhNewItemPtr()
ptrParent:MoveToParentItem(ptrItem)
--get a parent item date if one exists
local strDate = ""
local dtptr = fhGetItemPtr(ptrParent,'~.DATE')
if dtptr:IsNotNull() then
strDate = fhGetValueAsDate(dtptr):GetValueAsText()
end
--get an updated date if possible
local strUpdated = fhGetLabelledText(ptrItem, options.UpdateDateLabel..strSep) --look for labelled text first
if strUpdated == "" and fhGetDataClass(ptrParent) == "record" then
local dtupdated = fhCallBuiltInFunction ("LastUpdated", ptrParent) --look for a record updated date, returns a datept
if dtupdated:IsNull() == false then
local d = fhNewDate()
d:SetSimpleDate(dtupdated)
strUpdated = d:GetDisplayText("ABBREV")
end
end
--get the parent source and corresponding repository if they exist
local ptrS = nil
local ptrR = nil
ptrS = fhGetItemPtr(ptrParent, '~.SOUR>')
if ptrS:IsNull() then
ptrS = nil
else
ptrR = fhGetItemPtr(ptrS,'~.REPO>')
if ptrR:IsNull() then
ptrR = nil
end
end
myResults.Update({ptrParent:Clone(),ptrItem:Clone(),
strTag, fhGetLabelledText(ptrItem,options.GroupLabel..strSep),
strDate, fhGetLabelledText(ptrItem,options.DueDateLabel..strSep), strUpdated,
fhGetLabelledText(ptrItem,options.Field1Label..strSep),
fhGetLabelledText(ptrItem,options.Field2Label..strSep),
fhGetLabelledText(ptrItem,options.Field3Label..strSep),
fhGetLabelledText(ptrItem,options.Field4Label..strSep),
ptrS, ptrR})
end
end
end
if booFound == false then
Messagebox("No results found", "No results found")
else
--results will be displayed on exit
myResults.Title("Tagged items")
myResults.Headings({"Parent", "Item",
"Tag", options.GroupLabel, options.FactDateLabel,
options.DueDateLabel, options.UpdateDateLabel,
options.Field1Label, options.Field2Label,
options.Field3Label,options.Field4Label,
"Source", "Repository"})
myResults.Visibility({true, true,
true, options.UseGroup == "ON",
options.UseFactDate == "ON", options.UseDueDate =="ON", options.UseUpdateDate == "ON",
options.UseField1 =="ON" and options.UseNotes == "ON", options.UseField2 == "ON",
options.UseField3 == "ON", options.UseField4 == "ON",
true, true})
end
return booFound
end
local function ConvertFacts()
local function MakeNewNote(pi,pf,strTag)
local ptrt = fhNewItemPtr()
ptrt:MoveTo(pf, "~.NOTE2")
local strText = fhGetValueAsText(ptrt)
local strNote = ""
local taskoptions = {
IncludeIndividual = true,
Individual = pi,
}
--set UseField1 and Field1Value
taskoptions.Field1Value = fhGetValueAsText(pf)
taskoptions.UseField1 = utils.choose(taskoptions.Field1Value == "", "OFF", "ON")
strNote = MakeNoteHeader(strNote, taskoptions)
--set tag and group if in use
local strGroup = fhGetLabelledText(ptrt, options.GroupLabel..strSep)
local existingTag = fhGetLabelledText(ptrt, options.PrefixTag)
local usetag = utils.choose(existingTag == "", strTag, existingTag)
strNote = AddTagTextandGroup(strNote, true, utils.choose(existingTag == "", strTag, existingTag), options.UseGroup == "ON", strGroup)
if existingTag ~= "" then
strText = stringx.replace(strText, options.PrefixTag..existingTag, "")
end--remove tag from the text
if options.UseGroup == "ON" and strGroup ~= "" then
strText = stringx.replace(strText, options.GroupLabel..strSep..strGroup, "") --remove group
end
strText = stringx.replace(strText, options.GroupLabel..strSep, "") --remove group label
--concertt old separators to new, and then remove double separators
strText = stringx.replace(strText, "====================", strRule)
strText = stringx.replace(strText, strRule.."%s."..strRule, strRule)
--remove leading and trailing whitespace
strText = stringx.strip(strText)
--add anything left to the note without changing it; worst case, the user gets some extraneous separators but doesn't lose any data
strNote = strNote..strText
strNote = MakeNoteTail(strNote)
return strNote
end
local booconversionsdone = false
local booOK, strDefaultTag, boodeletefacts = GetTextandTick("Specify tag to use if one isn't found", options.TagLabel, strMin1Letter, "Delete facts after conversion?")
if not booOK then return false end --abort
for pi in records('INDI') do --iterate all individuals
for pf in myFactTools.Iterate(pi) do --iterate all facts
if fhGetTag(pf) == cstrFactTag then --test if Fact is a Task
booconversionsdone = true
local notetext = MakeNewNote(pi, pf, strDefaultTag)
ptrTask = CreateSharedNote(notetext) --create the note
CreateNoteLink(pi, ptrTask) --link the note to the individuals
CopyChildren(pf, ptrTask) --copy the sources
if boodeletefacts then
fhDeleteItem(pf)
end
local ptrt = fhNewItemPtr()
ptrt:MoveTo(ptrTask, "~.TEXT")
local strText = fhGetValueAsText(ptrt)
local resultstable = {pi:Clone(), ptrTask:Clone(), fhGetLabelledText(ptrt, options.PrefixTag),
fhGetLabelledText(ptrt, options.GroupLabel..strSep),
"", "", "", fhGetLabelledText(ptrt, options.Field1Label..strSep), "", "", "", pf:Clone(), nil}
myResults.Update(resultstable)
end
end
end
if booconversionsdone then
myResults.Title("Converted facts")
myResults.Headings({"Individual", "Task",
"Tag", options.GroupLabel, options.FactDateLabel,
options.DueDateLabel, options.UpdateDateLabel,
options.Field1Label, options.Field2Label,
options.Field3Label,options.Field4Label,
"Source fact", ""})
myResults.Visibility({true, true, true, options.UseGroup == "ON",
false, false, false, true, false, false, false, utils.choose(boodeletefacts, false, true), false})
Messagebox("Things to delete?", [[Once you've converted all your Task facts to notes, you can safely delete the query "Research Tasks (Facts)", the "Research Planner" Fact Set, and any queries you downloaded associated with version 1 of the Research PLanner plugin.]])
return true
else
Messagebox("No facts found to convert", "No facts found to convert")
return false
end
end
local function ConvertNotes()
local function MakeNewNote(ptrtext)
local strText = fhGetValueAsText(ptrtext)
--remove privacy markers
strText = stringx.replace(strText,"[[\n","")
strText = stringx.replace(strText,"\n]]","")
--Get field1 if it exists and remove it from the note
local f1value = fhGetLabelledText(ptrtext, options.Field1Label..strSep)
if f1value ~= "" then
strText = stringx.replace(strText, options.Field1Label..strSep..f1value.."\n"..strRule,"")
end
--get the individual
local strIndividual = fhGetLabelledText(ptrtext, "Individual: ")
-- local iIndividual = TextToNumber(strIndividual)
local indi = fhNewItemPtr()
if strIndividual ~= "" then
-- -- indi:MoveToRecordById("INDI", tonumber(strIndividual))
indi:MoveToRecordById("INDI", fhCallBuiltInFunction("TextToNumber", strIndividual))
strText = stringx.replace(strText, "Individual: "..strIndividual.."\n"..strRule, "") --remove the individual identifiers
end
--convert old separators to new, and then remove double separators
strText = stringx.replace(strText, "====================", strRule)
strText = stringx.replace(strText, strRule.."%s."..strRule, strRule)
strText = stringx.strip(strText)
return strText, indi, f1value
end
local booconversionsdone = false
local booOK, strTag, boodeletenotes = GetTextandTick("Specify tag to search for", options.TagLabel, strMin1Letter, "Delete notes after conversion?")
if not booOK then return false end --abort
for pn in records('NOTE') do --iterate all Notes
local ptrt = fhNewItemPtr()
ptrt:MoveTo(pn, "~.TEXT")
local thistag = fhGetLabelledText(ptrt, options.PrefixTag)
if thistag == strTag then
booconversionsdone = true
local notetext, pi, f1value = MakeNewNote(ptrt) --get the note individual and field1 value
if pi ~= nil then --can't make a fact if the individual isn't identified
ptrTask = myFactTools.Create(cstrFactTag, pi, f1value) --create the task fact linked to the individual
if notetext ~= "" then CreateNote(ptrTask, notetext) end --add the note if it isn't blank
if options.UseFactDate == "ON" then
local dt = fhNewDate()
local DateOK = dt:SetValueAsText(os.date("%x"), true) --defaults to today
SetDate(ptrTask, dt)
end
CopyChildren(pn, ptrTask) --copy the sources
if boodeletenotes then
fhDeleteItem(pn)
end
local ptrt = fhNewItemPtr()
ptrt:MoveTo(ptrTask, "~.TEXT")
local strText = fhGetValueAsText(ptrt)
local resultstable = {pi:Clone(), ptrTask:Clone(), strTag,
fhGetLabelledText(ptrt, options.GroupLabel..strSep),
"", "", "", fhGetLabelledText(ptrt, options.Field1Label..strSep), "", "", "", pn:Clone(), nil}
myResults.Update(resultstable)
end
end
end
if booconversionsdone then
myResults.Title("Converted notes")
myResults.Headings({"Individual", "Task",
"Tag", options.GroupLabel, options.FactDateLabel,
options.DueDateLabel, options.UpdateDateLabel,
options.Field1Label, options.Field2Label,
options.Field3Label,options.Field4Label,
"Original note", ""})
myResults.Visibility({true, true, true, options.UseGroup == "ON",
false, false, false, false, false, false, false, utils.choose(boodeletefacts, false, true), false})
Messagebox("Things to delete?", [[Once you've converted all your Task notes, you can safely delete the query "Research Tasks (Notes)"]])
return true
else
Messagebox("No notes found to convert", "No notes found to convert")
return false
end
end
local function FactsReport()
myReports.Download("Tasks - Individual (Facts)")
Messagebox("Report installed", "Report installed")
end
local function NotesReport()
myReports.Download("Tasks - Individual (Notes)")
Messagebox("Report installed", "Report installed")
end
local function MakeTemplateBox(source)
--The template box contains 6 containers each with 2 items in them (either a button or a label, and a text field)
--the elements can be accessed using mybox[n] for the containers or mybox[n][m] for the elements
local txtField1 = MakeText{expand="HORIZONTAL", wordwrap="NO"}
local labField1= MakeLabel{title=options.Field1Label}
local hboxField1 = iup.hbox{labField1, txtField1}
local txtField2 = MakeText{expand="HORIZONTAL", wordwrap="NO"}
local labField2 = MakeLabel{title=options.Field2Label}
local hboxField2 = iup.hbox{labField2, txtField2}
local txtField3 = MakeText{expand="HORIZONTAL", wordwrap="NO"}
local labField3 = MakeLabel{title=options.Field3Label}
local hboxField3 = iup.hbox{labField3, txtField3}
local txtField4 = MakeText{wordwrap="YES", multiline="YES", scrollbar = "VERTICAL", visiblelines = "2"}
local labField4 = MakeLabel{title=options.Field4Label}
local vboxField4 = iup.vbox{labField4, txtField4}
local txtField5 = MakeText{wordwrap="YES", multiline="YES", scrollbar = "VERTICAL", visiblelines = "2"}
local labField5 = MakeLabel{title=options.Field5Label}
local vboxField5 = iup.vbox{labField5, txtField5}
return iup.vbox{
hboxField1, hboxField2, hboxField3, vboxField4, vboxField5, source.Selector("Use the Source button to choose a source from within Family Historian.","HORIZONTAL");
}
end
local function MakeTabTasks(strTabTitle)
local UseSelector = nil
boxTDueDate = nil
boxTFactDate = nil
local function TabMakeText()
local function ClearTabMakeText()
myTemplates.ClearUseSelector(UseSelector) --clears Template selector and container
boxTDueDate[2].value = ""
boxTDueDate[2].value = ""
end
--define buttons
btnTaskMake = MakeButton{title="Make", callback = MakeTaskFromText, tip = "Make task from specified details"}
local btnClear = MakeButton{title="Clear", callback = ClearTabMakeText, tip = "Clear task details"}
local btns = iup.hbox{iup.fill{}, btnClear, btnTaskMake}
btnTaskMake.active = "YES"
--define template fields
TaskSource = Source(1)
boxTaskEdit = MakeTemplateBox(TaskSource)
--define date fields
boxTDueDate = MakeDateField{title=options.DueDateLabel}
boxTFactDate = MakeDateField{title=options.FactDateLabel}
UseSelector = myTemplates.UseSelector(boxTaskEdit)
return iup.vbox{UseSelector,
UseSelector, boxTaskEdit, iup.vbox{boxTDueDate, boxTFactDate}, btns;
tabtitle = "From Text"}
end
local function TabMakeBulk()
--define buttons
btnBulkMake = MakeButton{title="Make", callback = MakeBulkTasks, tip = "Make tasks from selected templates"}
local btnClear = MakeButton{title="Clear", callback = myTemplates.ClearBulkSelector, tip = "Clear selected templates"}
local btns = iup.hbox{iup.fill{}, btnClear, btnBulkMake}
btnBulkMake.active = "NO"
local boxBulkMake = iup.vbox{
myTemplates.BulkSelector(),
btns;
} --define container
return iup.vbox{boxBulkMake; tabtitle = "In Bulk"}
end
--define tab-wide buttons and fields
local btns = iup.hbox{iup.fill{}, myHelp.Button(norm, "creating_and_editing_tasks" ), myConfig.OptionsButton(), MakeButton{title="Exit"}}
--Project field
local labGroup = MakeLabel{title = options.GroupLabel, tip = "Grouping to use when creating tasks"}
local txtGroup = MakeText{tip = "Grouping to use when creating tasks", default = ""}
boxGroupData = iup.hbox{labGroup, txtGroup}
return iup.vbox{
indi.Selector("Use the button to choose an individual from within Family Historian.\nYou cannot create tasks until you have selected an individual."),
boxGroupData,
iup.tabs{TabMakeText(), TabMakeBulk()},
btns, myActivity.Make("OPEN", "Task history"); tabtitle = strTabTitle}
end
local function MakeTabTemplates(strTabTitle)
TemplateSource = Source(1)
boxTemplateEdit = MakeTemplateBox(TemplateSource)
local btnExit = MakeButton{title="Exit", callback = myTemplates.CheckForUnsavedEditChanges, close = "YES"}
local btns = iup.hbox{iup.fill{}, myHelp.Button(norm, "managing_task_templates"), myConfig.OptionsButton(), btnExit}
return iup.vbox{myTemplates.EditSelector(boxTemplateEdit), iup.frame{boxTemplateEdit}, btns; tabtitle = strTabTitle}
end
local function MakeTabTools(strTabTitle)
local btns = iup.hbox{iup.fill{}, myHelp.Button(norm,"miscellaneous_tools" ), myConfig.OptionsButton(), MakeButton{title="Exit"}}
hFindBox = iup.hbox{
MakeButton{title = "Find Items", callback = FindTag, close = "YES", tip = "Find items with a specified tag"}
}
htoolsFactsBox = iup.hbox{
MakeButton{title = "Convert Notes", callback = ConvertNotes, close = "YES", tip = "Convert any Note records with a specified tag to Task facts"},
MakeButton{title = "Download Report", callback = FactsReport, close = "YES", tip = "Download a Report for Tasks using Facts"}
} --tools available when facts are in use
htoolsNotesBox = iup.hbox{
MakeButton{title = "Convert Facts", callback = ConvertFacts, close = "YES", tip = "Convert any Task facts to Note records with a specified tag"},
MakeButton{title = "Download Report", callback = NotesReport, close = "YES", tip = "Download a Report for Tasks using Notes"}
} --tools available when notes are in use
return iup.vbox{hFindBox, htoolsFactsBox, htoolsNotesBox, btns;
tabtitle = "Tools"}
end
local strtabTasksTitle = "Make tasks"
local strtabTemplatesTitle = "Manage templates"
local strtabToolsTitle = "Tools"
local tabTasks = MakeTabTasks(strtabTasksTitle)
local tabTemplates = MakeTabTemplates(strtabTemplatesTitle)
tabTools = MakeTabTools(strtabToolsTitle)
local MainTabs = iup.tabs{tabTasks, tabTemplates, tabTools}
MainTabs.tabchange_cb = function(self, newTab, oldTab)
if oldTab.tabTitle == strtabTemplatesTitle and myTemplates.CheckForUnsavedEditChanges() == false then
tabs.value = tabTemplates --revert tab
end
end
local d = MakeDialog(MainTabs, {title = cstrPluginName})
d.close_cb = function()
if myTemplates.CheckForUnsavedEditChanges() then
d.show_cb = nil --avoid iup bug
return iup.CLOSE
else
return iup.IGNORE
end
end
DoNormalize()
return d
end
-------------------------------------
--EXECUTE
-------------------------------------
do
SetUTF8IfPossible()
SetTextSize()
myHelp = HTMLHelp(cstrPluginVersion) --create Help object to display Help with a browser interface
myConfig = Config(cstrPluginVersion, myHelp ) --create config object to make all methods available
myTemplates = Templates(true) --create template object to make all methods available -- allow project templates; do not use a subdirectory and don't use tokens
myResults = Results(13) --initialise results handling with 13 results tables; remaining details will depend on user actions
myActivity = ActivityLog()
indi = Individuals(1) --create an object of the Individuals class that allows a single individual
dlgmain = MakeMainDialog()
myConfig.Initialise() --initialise the configuration
dlgmain:show()
iup.MainLoop()
DestroyAllDialogs()
myResults.Display()
end
--DONE
Source:Research-Planner-3.fh_lua