--[[ @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