Quick Family Facts.fh_lua--[[
@Title: Quick Family Facts
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 1.4
@Keywords:
@LastUpdated: 03 Feb 2026
@Licence: This plugin is copyright (c) 2026 Mike Tate & contributors and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Quick family details popup snapshot can be invoked with Alt T Q
@V1.8: Add Check for Updates button; fhInitialise(); Use "NATIVEPARENT"; Reset Window button;
@V1.3: Cater for UTF-8 accented characters & symbols in .dat filepath;
@V1.2: Ensure all versions use one Plugin Data .dat file; FH V7 Lua 3.5 IUP 3.28;
@V1.1: Optional [Rec Id] for each family & person, improve Formatting, avoid duplicated partners, and rename Root Person as Family Root;
@V1.0: First published version in Plugin Store;
]]
require("iuplua") -- To access IUP GUI library -- V1.2
require("luacom") -- To create File System Object -- V1.8
local FSO = luacom.CreateObject("Scripting.FileSystemObject")
local strVersion = "1.4"
local strPluginTitle = " Quick Family Facts "..strVersion -- V1.4
if fhGetAppVersion() > 5 then -- Cater for Unicode UTF-8 from FH Version 6 onwards
fhSetStringEncoding("UTF-8")
iup.SetGlobal("UTF8MODE","YES")
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28 -- V1.2
end
function Encoding()
if fhGetAppVersion() > 5 then return fhGetStringEncoding() end
return "ANSI"
end -- function Encoding
-- Split a string into numbers using " " or "," or "x" separators -- Any non-number remains as a string
function splitnumbers(strTxt)
local tblNum = {}
strTxt = tostring(strTxt or "")
strTxt:gsub("([^ ,x]+)", function(strNum) tblNum[#tblNum+1] = tonumber(strNum) or strNum end)
return tblNum[1],tblNum[2],tblNum[3],tblNum[4]
end -- function splitnumbers
-- Report error message --
local function doError(strMessage,errFunction)
-- strMessage ~ error message text
-- errFunction ~ optional error reporting function
if type(errFunction) == "function" then
errFunction(strMessage)
else
error(strMessage)
end
end -- function doError
-- Convert filename to ANSI alternative and indicate success --
function FileNameToANSI(strFileName,strAnsiName)
-- strFileName ~ full file path
-- strAnsiFile ~ ANSI file name & type
-- return values ~ ANSI file path, true if original path was ANSI compatible
if Encoding() == "ANSI" then return strFileName, true end
local isFlag = fhIsConversionLossFlagSet()
fhSetConversionLossFlag(false)
local strAnsi = fhConvertUTF8toANSI(strFileName)
local wasAnsi = true
if fhIsConversionLossFlagSet() then
strAnsiName = strAnsiName or "ANSI.ANSI"
strAnsi = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\"..strAnsiName
wasAnsi = false
end
fhSetConversionLossFlag(isFlag)
return strAnsi, wasAnsi
end -- function FileNameToANSI
-- Get parent folder --
function GetParentFolder(strFileName)
-- strFileName ~ full file path
-- return value ~ parent folder path
local strParent = FSO:GetParentFolderName(strFileName) --! Faulty in FH v6 with Unicode chars in path
if fhGetAppVersion() == 6 then
local _, wasAnsi = FileNameToANSI(strFileName)
if not wasAnsi then
strParent = strFileName:match("^(.+)[\\/][^\\/]+[\\/]?$")
end
end
return strParent
end -- function GetParentFolder
-- Check if file exists --
function FlgFileExists(strFileName)
-- strFileName ~ full file path
-- return value ~ true if it exists
return FSO:FileExists(strFileName)
end -- function FlgFileExists
-- Delete a file if it exists --
function DeleteFile(strFileName,errFunction)
-- strFileName ~ full file path
-- errFunction ~ optional error reporting function
-- return value ~ true if file does not exist or is deleted else false
if FSO:FileExists(strFileName) then
FSO:DeleteFile(strFileName,true)
if FSO:FileExists(strFileName) then
doError("File Not Deleted:\n"..strFileName.."\n",errFunction)
return false
end
end
return true
end -- function DeleteFile
-- Copy a file if it exists and destination is not a folder --
function CopyFile(strFileName,strDestination)
-- strFileName ~ full source file path
-- strDestination~ full target file path
-- return value ~ true if file exists and is copied else false
if MakeFolder(GetParentFolder(strDestination)) and FSO:FileExists(strFileName) and not FSO:FolderExists(strDestination) then
FSO:CopyFile(strFileName,strDestination)
if FSO:FileExists(strDestination) then
return true
end
end
return false
end -- function CopyFile
-- Move a file if it exists and destination is not a folder --
function MoveFile(strFileName,strDestination)
-- strFileName ~ full source file path
-- strDestination~ full target file path
-- return value ~ true if file exists and is moved else false
if MakeFolder(GetParentFolder(strDestination)) and FSO:FileExists(strFileName) and not FSO:FolderExists(strDestination) then
if DeleteFile(strDestination) then
FSO:MoveFile(strFileName,strDestination)
if FSO:FileExists(strDestination) then
return true
end
end
end
return false
end -- function MoveFile
-- Open File with ANSI path and return Handle --
function OpenFile(strFileName,strMode)
-- strFileName ~ full file path
-- strMode ~ "r", "w", "a" optionally suffixed with "+" &/or "b"
-- return value ~ file handle
local fileHandle, strError = io.open(strFileName,strMode)
if fileHandle == nil then
error("\n Unable to open file in \""..strMode.."\" mode. \n "..strFileName.." \n "..strError.." \n")
end
return fileHandle
end -- function OpenFile
-- Save string to file --
function SaveStringToFile(strContents,strFileName,strFormat)
-- strContents ~ text string
-- strFileName ~ full file path
-- strFormat ~ optional "UTF-8" or "UTF-16LE"
-- return value ~ true if successful else false
strFormat = strFormat or "UTF-8"
if fhGetAppVersion() > 6 then
return fhSaveTextFile(strFileName,strContents,strFormat)
end
local strAnsi, wasAnsi = FileNameToANSI(strFileName)
local fileHandle = OpenFile(strAnsi,"w")
fileHandle:write(strContents)
assert(fileHandle:close())
if not wasAnsi then
MoveFile(strAnsi,strFileName)
end
return true
end -- function SaveStringToFile
-- Load string from file --
function StrLoadFromFile(strFileName,strFormat)
-- strFileName ~ full file path
-- strFormat ~ optional "UTF-8" or "UTF-16LE"
-- return value ~ file contents
strFormat = strFormat or "UTF-8"
if fhGetAppVersion() > 6 then
return fhLoadTextFile(strFileName,strFormat)
end
local strAnsi, wasAnsi = FileNameToANSI(strFileName)
if not wasAnsi then
CopyFile(strFileName,strAnsi)
end
local fileHandle = OpenFile(strAnsi,"r")
local strContents = fileHandle:read("*all")
assert(fileHandle:close())
return strContents
end -- function StrLoadFromFile
--[[
@Function: CheckVersionInStore
@Author: Mike Tate
@Version: 1.4
@LastUpdated: 20 Jan 2026
@Description: Check plugin version against version in Plugin Store
@Parameter: Plugin name and version
@Returns: None
@Requires: luacom
@V1.4: Dispense with files and assume called via IUP button;
@V1.3: Save and retrieve latest version in file;
@V1.2: Ensure the Plugin Data folder exists;
@V1.1: Monthly interval between checks; Report if Internet is inaccessible;
@V1.0: Initial version;
]]
function CheckVersionInStore(strPlugin,strVersion) -- Check if later Version available in Plugin Store
require "luacom"
local function httpRequest(strRequest) -- Luacom http request protected by pcall() below
local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
http:Open("GET",strRequest,false)
http:Send()
return http.Responsebody
end -- local function httpRequest
local function intVersion(strVersion) -- Convert version string to comparable integer
local intVersion = 0
local arrNumbers = {}
strVersion:gsub("(%d+)", function(strDigits) table.insert(arrNumbers,strDigits) end)
for i = 1, 5 do
intVersion = intVersion * 100 + tonumber(arrNumbers[i] or 0)
end
return intVersion
end -- local function intVersion
local strLatest = "0"
if strPlugin then
local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..tostring(strPlugin)
local isOK, strReturn = pcall(httpRequest,strRequest)
if not isOK then -- Problem with Internet access
fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ")
elseif strReturn then
strLatest = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits
end
end
local strMessage = "No later Version"
if intVersion(strLatest) > intVersion(strVersion or "0") then
strMessage = "Later Version "..strLatest
end
fhMessageBox(strMessage.." of this Plugin is available from the 'Plugin Store'.")
end -- function CheckVersionInStore
local strMaroon = "128 0 0" -- Color attributes
local strPurple = "128 0 128"
local strMagenta= "255 0 255"
local strAmber = "210 110 0"
local strOlive = "128 128 0"
local strGreen = "0 128 0"
local strTeal = "0 128 128"
local strNavy = "0 0 128"
local strOrange = "255 190 0"
local strGray = "128 128 128"
local strRed = "255 0 0"
local strBlue = "0 0 255"
local strLime = "0 255 0"
local strCyan = "0 255 255"
local strYellow = "255 255 0"
local strSilver = "192 192 192"
local strSmoke = "220 220 220"
local strWhite = "255 255 255"
local strBlack = "0 0 0"
function ResetData()
ArrFather = { "Father"; Parent=true; } -- List of title, plural, class, events, format, people, weight for each type of person
ArrMother = { "Mother"; Parent=true; }
ArrPerson = { "Family Root"; Root = true; } -- V1.1
ArrSpouse = { "Partner"; Plural="s"; Partner=true; }
ArrSiblng = { "Sibling"; Plural="s"; Sibling=true; }
ArrChilds = { "Child"; Plural="ren"; Children=true; }
for intArray, arrArray in ipairs ({ArrFather,ArrMother,ArrPerson,ArrSpouse,ArrSiblng,ArrChilds}) do
arrArray.Events = { { "~.BIRT"; "~.BAPM"; "~.CHR"; }; { "~.MARR"; "~.DIV"; "~.EVEN-SEPARATION"; }; { "~.DEAT"; "~.BURI"; "~.CREM"; }; }
arrArray.Format = { } -- Formatting applied to text
arrArray.Linked = { } -- Linked line per person with Head and Tail char position of Name
arrArray.Select = 0 -- Current selected line
arrArray.People = 0 -- People count per pane
arrArray.Weight = 0 -- ExpandWeight per row
end
ArrRow = { ArrMother; ArrSpouse; ArrChilds; }
ArrFamily = { } -- Family and associated colours
ArrColour = {strMaroon;strPurple;strMagenta;strAmber;strOlive;strTeal;strNavy;strOrange;strGray;strRed;strBlue;strLime;strCyan;strYellow;}
IntColour = 1 -- Next available colour
end -- ResetData()
local ArrPeople = { } -- Array of Family Root pointers for Go Backward/Forward
local TblField = { } -- Sticky Settings File fields
local IntPoints = 9 -- 9=default font Point size
local IntEvents = 1 -- 1=Full, 2=Brief, 3=None
local StrRec_Id = "OFF" -- [Record Id] hidden -- V1.1
local FontScale = 1.2 -- FontScale of Titles needed for ExpandWeight adjustment
local PluginData = fhGetPluginDataFileName():gsub(" %- V%d.*%.dat$",".dat") -- Ensure root name is used -- V1.2 -- V1.3 -- V1.4
function LoadSettings() -- Load Settings File in lines with key & val fields -- V1.4
if FlgFileExists(PluginData) then
local strData = StrLoadFromFile(PluginData)
for strLine in strData:gmatch("[^\r\n]+") do
local strKey,strVal = strLine:match("([%a_]+)=(.+)$")
TblField[strKey] = strVal
end
IntPoints = tonumber(TblField.Points) or IntPoints
IntEvents = tonumber(TblField.Events) or IntEvents
StrRec_Id = TblField.Rec_Id or StrRec_Id -- V1.1
end
end -- function LoadSettings
function SaveSettings() -- Save Settings File lines with key & val fields -- V1.4
local tblData = {}
for strKey,strVal in pairs(TblField) do
table.insert(tblData,strKey.."="..strVal.."\n")
end
local strData = table.concat(tblData,"\n")
if not SaveStringToFile(strData,PluginData) then
doError("\nSettings file not saved successfully.\n\nMay need to Delete the following Plugin Data .dat file:\n\n"..PluginData.."\n\nError detected.")
end
end -- function SaveSettings
function LoopFacts(arrList,arrFacts,ptrHome) -- Loop through Facts
-- arrList List of facts to update
-- arrFacts List of Tags to find
-- ptrHome Record to search
if IntEvents < 3 then
for intFact, strFact in ipairs (arrFacts) do -- Loop on each fact Tag unless "None" chosen
strFact = fhGetDisplayText(ptrHome,strFact)
if #strFact > 0 then
table.insert(arrList,"\t"..strFact) -- Add each Fact to List and break if "Brief" chosen
if IntEvents == 2 then break end
end
end
end
end -- function LoopFacts
function GetRecId(ptrRec) -- Get Record Id conditionally -- V1.1
if StrRec_Id == "OFF" then return "" end
return " ["..fhGetRecordId(ptrRec).."]"
end -- function GetRecId
function ShowName(ptrIndi) -- Show Name (LifeDates) [RecId]
return fhGetDisplayText(ptrIndi).." ("..fhCallBuiltInFunction("LifeDates2",ptrIndi,"STD")..")"..GetRecId(ptrIndi) -- V1.1
end -- function ShowName
function IntLength(strName) -- Length of name in characters -- V1.1
if fhGetAppVersion() > 5 then
strName = fhConvertUTF8toANSI(strName) -- Convert each multi-byte UTF8 char to one byte ANSI char
fhSetConversionLossFlag(false)
end
return #strName
end
function ListFacts(arrList,ptrIndi,ptrRoot) -- List the name and facts for one person
-- arrList List of facts to fill and parameters
-- ptrIndi Link to the current Individual record
-- ptrRoot Link to root person Individual record
if ptrIndi:IsNotNull() then
arrList.People = arrList.People + 1 -- List starts with Name & (LifeDates) of person
local strName = ShowName(ptrIndi)
table.insert(arrList," "..strName)
arrList.Linked[#arrList] = { Indi=ptrIndi:Clone(); Head=2; Tail=2+IntLength(strName); } -- V1.1
for intFamily, arrFamily in ipairs (ArrFamily) do -- Obtain the colour of primary individuals
local isMatch = false
if fhGetItemPtr(ptrIndi,"~.FAMC[1]>"):IsSame(arrFamily.Family) then
isMatch = true
else
local ptrFams = fhGetItemPtr(ptrIndi,"~.FAMS[1]") -- Move to 1st Spouse Family of Individual
while ptrFams:IsNotNull() do
if fhGetValueAsLink(ptrFams):IsSame(arrFamily.Family) then
isMatch = true
break
end
ptrFams:MoveNext("SAME_TAG") -- Move to next Spouse Family of Individual
end
end
if isMatch then
arrList.Colour = arrFamily.Colour
break
end
end
local isRoot = FormatName(arrList,ptrIndi,ptrRoot)
if not isRoot or arrList.Root then
for intFacts, arrFacts in ipairs (arrList.Events) do -- Loop on each event array
if intFacts == 2 then
local intSpou = 1
local ptrFams = fhGetItemPtr(ptrIndi,"~.FAMS") -- For family facts loop on Family as Spouse tags
while ptrFams:IsNotNull() do
local ptrFrec = fhGetValueAsLink(ptrFams) -- Family record -- V1.1
local ptrSpou = fhGetItemPtr(ptrIndi,"~.~SPOU["..intSpou.."]>")
if ptrSpou:IsNotNull() then
local strSpou = ShowName(ptrSpou)
local strTail = ""
if IntEvents == 3 then -- If No BMD Events then add (m.year) or (d.year) or (s.year) -- V1.1
for intFact, strFact in ipairs (arrFacts) do
local strYear = fhGetItemText(ptrFrec,strFact..".DATE:YEAR")
if #strYear > 2 then -- If year exists extract initial letter of fact
local strInit = strFact:match("~%.[EVEN-]*(%u)"):lower()
strTail = " ("..strInit.."."..strYear..")"
break
end
end
end
local strRecId = GetRecId(ptrFrec) -- Add each spouse/partner Family [RecId] and Name (LifeDates) [RecId] to List -- V1.1
table.insert(arrList,"\t="..strRecId.." "..strSpou..strTail)
arrList.Linked[#arrList] = { Indi=ptrSpou:Clone(); Head=#strRecId+4; Tail=#strRecId+4+IntLength(strSpou); } -- V1.1
if ptrSpou:IsSame(ptrRoot) then -- Partner's spouse's colour must match Root's Parents
arrList.Colour = ArrFamily[#ArrFamily].Colour
else
arrList.Colour = nil
for intFamily, arrFamily in ipairs (ArrFamily) do
if ptrFrec:IsSame(arrFamily.Family) then
arrList.Colour = arrFamily.Colour -- Assign existing Spouse Family colour
break
end
end
if not arrList.Colour -- or arrList.Partner or arrList.Sibling or arrList.Children or fhGetItemPtr(ptrSpou,"~.~CHIL[1]>"):IsNull() ) then
and ( arrList.Parent or arrList.Root )
and fhGetItemPtr(ptrSpou,"~.~CHIL[1]>"):IsNotNull() then
arrList.Colour = ArrColour[IntColour] -- Assign next colour to next Parent/Root Spouse Family
IntColour = IntColour + 1 -- Recycle through colours if necessary
if IntColour > #ArrColour then IntColour = 1 end
table.insert(ArrFamily,1,{ Family=ptrFrec; Colour=arrList.Colour; })
end
end
FormatName(arrList,ptrSpou,ptrRoot)
end
LoopFacts(arrList,arrFacts,ptrFrec)
intSpou = intSpou + 1
ptrFams:MoveNext("SAME_TAG")
end
else
LoopFacts(arrList,arrFacts,ptrIndi)
end
end
end
end
end -- function ListFacts
function FormatName(arrList,ptrIndi,ptrRoot) -- Format name with Bold & Underline for root
-- arrList List to hold format
-- ptrIndi Individual record
-- ptrRoot Family Root record
local isRoot = ptrIndi:IsSame(ptrRoot)
if isRoot then
AddFormat(arrList,"BOLD","SINGLE")
else
AddFormat(arrList,"BOLD")
end
return isRoot
end -- function FormatName
function Selection(intLine,intHead,intTail) -- Format the text selection line and chars -- V1.1
-- intLine Line of selection
-- intHead Head of selection
-- intTail Tail of selection
return intLine..","..intHead..":"..intLine..","..intTail
end -- function Selection
function AddFormat(arrList,strWeight,strUnder,strScale) -- Add formatting for bold, underline, font scale & size
-- arrList List of parameters
-- strWeight Bold weight format
-- strUnder Underline format optional
-- strScale Scale sizing for Title only
local strColour = arrList.Colour
local strAlign = "LEFT"
local intLine = #arrList
local intHead = 2 -- Head & Tail char positions
local intTail = 999
if strScale then
strColour = strBlack -- Format for Title
strAlign = "CENTER"
intLine = 1
else
local dicLink = arrList.Linked[#arrList] -- Format for Name from Head to Tail -- V1.1
intHead = dicLink.Head
intTail = dicLink.Tail
if intHead > 2 then
local strSelect = Selection(intLine,2,intHead) -- Format for Head = [Rec Id] is green bold -- V1.1
table.insert(arrList.Format,{ Selection=strSelect; FgColor=strGreen; Weight="BOLD"; })
local strSelect = Selection(intLine,intTail,999) -- Format for Tail (m.year) is black unbold -- V1.1
table.insert(arrList.Format,{ Selection=strSelect; FgColor=strBlack; Weight="NONE"; })
end
end
local strSelect = Selection(intLine,intHead,intTail) -- Format for line between Head & Tail -- V1.1
table.insert(arrList.Format,{ Selection=strSelect; FgColor=strColour or strBlack; Weight=strWeight or "NONE"; Underline=strUnder or "NONE"; Alignment=strAlign or "LEFT"; FontScale=strScale or "1.0"; FontSize=IntPoints; })
arrList.Colour = strBlack
end -- function AddFormat
function ListTitle(arrList) -- List Title
local strTitle = arrList[1]
local strPlural = arrList.Plural or ""
if arrList.People == 1 and arrList.Sibling then -- One Sibling
arrList.People = 0
arrList[2] = nil
end
if arrList.People == 0 then -- No people
strTitle = "No "..strTitle..strPlural.." Listed"
elseif arrList.People > 1 then -- Multiple people
strTitle = strTitle..strPlural
end
arrList[1] = " "..strTitle
AddFormat(arrList,"BOLD","NONE",FontScale) -- FontScale was "1.1" but upsets ExpandWeight especially when only Title, unless adjustment added
end -- function ListTitle
function LoadText(arrList,txtText,isReset) -- Load variable text and format into Text pane
-- arrList List of parameters
-- txtText GUI text pane for Father, Mother, Person, etc
-- isReset Exists if reset needed
local iupTags = iup.user{ Bulk="YES"; CleanOut="YES"; }
iup.Append( iupTags, iup.user{ Selection="1,1:999,999"; TabsArray="15 LEFT "; FontSize=IntPoints; })
for intFormat, iupFormat in ipairs (arrList.Format) do -- Append the formatting tags
local iupUser = iup.user{}
for iupName, iupData in pairs (iupFormat) do -- Create the user data
iupUser[iupName] = iupData
end
iup.Append( iupTags, iupUser )
end
txtText.VisibleLines = math.min(#arrList-1,2) -- Improves initial height of dialog
if isReset then txtText.Value = " " end -- Ensure local pane font is reset
local strText = txtText.Value or ""
local strList = table.concat(arrList,"\n")
if arrList.Linked[#arrList] then -- Name is last so needs an extra blank line for doMotion()
strList = strList.."\n "
end
if strText ~= strList then -- Load new list of names & facts
txtText.Value = strList
end
txtText.AddFormatTag = iupTags -- Add the formatting tags
end -- function LoadText
function MakeFacts(ptrRoot) -- Make the names & facts for each pane
local ptrFath = fhNewItemPtr()
local ptrMoth = fhNewItemPtr()
local ptrIndi = fhNewItemPtr()
local ptrFams = fhGetItemPtr(ptrRoot,"~.FAMS>")
local ptrFamc = fhGetItemPtr(ptrRoot,"~.FAMC>")
if ptrFamc:IsNull() then ptrFamc = ptrFams end -- Add colour for spouse/parent family of root
table.insert(ArrFamily,{ Family=ptrFamc:Clone(); Colour=ArrColour[IntColour]; })
IntColour = IntColour + 1
ListFacts(ArrFather,fhGetItemPtr(ptrRoot,"~.~FATH>"),ptrRoot) -- List the Father of root Individual
ListTitle(ArrFather)
ListFacts(ArrMother,fhGetItemPtr(ptrRoot,"~.~MOTH>"),ptrRoot) -- List the Mother of root Individual
ListTitle(ArrMother)
local arrSibling = {}
local nxtSibling = 1
local intIndi = 1 -- List the Siblings of root Individual (must be before root Indiviudal)
ptrMoth:MoveTo(ptrRoot,"~.~MOTH[1]>")
ptrIndi:MoveTo(ptrMoth,"~.~CHIL[1]>") -- Move to 1st Child of Mother i.e. Sibling of root Individual
while ptrIndi:IsNotNull() do
table.insert(arrSibling,ptrIndi:Clone()) -- Add next sibling to list
arrSibling["ID"..fhGetRecordId(ptrIndi)] = true
intIndi = intIndi + 1
ptrIndi:MoveTo(ptrMoth,"~.~CHIL["..intIndi.."]>") -- Move to next Sibling of root Individual
end
local intIndi = 1
ptrFath:MoveTo(ptrRoot,"~.~FATH[1]>")
ptrIndi:MoveTo(ptrFath,"~.~CHIL[1]>") -- Move to 1st Child of Father i.e. Sibling of root Individual
while ptrIndi:IsNotNull() do
if arrSibling["ID"..fhGetRecordId(ptrIndi)] then -- Skip existing sibling
if nxtSibling < #arrSibling and
arrSibling[#arrSibling]:IsSame(ptrIndi) then
nxtSibling = nxtSibling + 1 -- Ensure more siblings go after last existing sibling
end
else
table.insert(arrSibling,nxtSibling,ptrIndi:Clone()) -- Add new sibling to list
end
nxtSibling = nxtSibling + 1
intIndi = intIndi + 1
ptrIndi:MoveTo(ptrFath,"~.~CHIL["..intIndi.."]>") -- Move to next Sibling of root Individual
end
for intIndi, ptrIndi in ipairs (arrSibling) do
ListFacts(ArrSiblng,ptrIndi,ptrRoot) -- List each Sibling of root Individual
end
ListTitle(ArrSiblng)
ListFacts(ArrPerson,ptrRoot,ptrRoot) -- List the root Individual Person
ListTitle(ArrPerson)
local intIndi = 1 -- List the Partners of root Individual
local arrSpou = {}
ptrFams:MoveTo(ptrRoot,"~.FAMS") -- Move to 1st Family of root Individual
while ptrFams:IsNotNull() do
ptrIndi:MoveTo(ptrRoot,"~.~SPOU["..intIndi.."]>")
for _, ptrSpou in ipairs (arrSpou) do -- Has this Spouse already been listed? -- V1.1
if ptrIndi:IsSame(ptrSpou) then ptrIndi:SetNull() break end
end
if ptrIndi:IsNotNull() then -- V1.1
table.insert(arrSpou,ptrIndi:Clone())
ListFacts(ArrSpouse,ptrIndi,ptrRoot) -- List each Spouse of root Individual
end
intIndi = intIndi + 1
ptrFams:MoveNext("SAME_TAG") -- Move to next Family of root Individual
end
ListTitle(ArrSpouse)
local intIndi = 1 -- List the Children of root Individual
ptrIndi:MoveTo(ptrRoot,"~.~CHIL[1]>") -- Move to 1st Child of root Individual
while ptrIndi:IsNotNull() do
ListFacts(ArrChilds,ptrIndi,ptrRoot) -- List each Child of root Individual
intIndi = intIndi + 1
ptrIndi:MoveTo(ptrRoot,"~.~CHIL["..intIndi.."]>") -- Move to next Child of root Individual
end
ListTitle(ArrChilds)
end -- function MakeFacts
function TextPane() -- Compose IUP Multiline Read-Only Text pane with Auto-Hidden Scroll-Bars and Formatting
return iup.text{ MultiLine="YES"; ReadOnly="YES"; Expand="YES"; CanFocus="NO"; ScrollBar="YES"; AutoHide="YES"; Formatting="YES"; TipDelay=9000; Tip="Click highlighted Name to select as new Family Root\nUse click and drag or mouse-wheel to scroll up/down\nUse the Ctrl key + mouse-wheel to alter text font size"; } -- V1.1
end -- function TextPane
function Hbox(iupPaneA,iupPaneB,intRow) -- Compose IUP Horizontal box for two panes with row number
local btnInc = iup.button{ Title="+"; Expand="VERTICAL"; Padding="2x0"; CanFocus="NO"; Tip="Increase height of this row"; action=function() SetWeight(ArrRow[intRow], 1) end; }
local btnDec = iup.button{ Title="-"; Expand="VERTICAL"; Padding="3x0"; CanFocus="NO"; Tip="Decrease height of this row"; action=function() SetWeight(ArrRow[intRow],-1) end; }
return iup.hbox{ Alignment="ATOP"; Margin="2x2"; Gap="2"; iupPaneA; iupPaneB; iup.vbox{ btnInc; btnDec; }; }
end -- function Hbox
function Main()
local arrRoot = fhGetCurrentRecordSel("INDI") -- Check if any Records pre-selected
if #arrRoot == 0 then
arrRoot = fhPromptUserForRecordSel("INDI",1) -- Prompt for Record selection
end
if #arrRoot ~= 1 then return end
local ptrRoot = arrRoot[1]
ArrPeople.Index = 1
table.insert(ArrPeople,ptrRoot:Clone()) -- Initialise list of people for Go Backward/Forwards
ResetData()
LoadSettings() -- Load sticky settings
local function strPoints() -- Format the Font Point size
return string.format(" Font %2u Point ",IntPoints)
end -- local function strPoints
local tipPoints = "Choose text font point size with either\nclick and drag, or mouse-wheel, or the\nHome, End, Page Up/Down, arrow keys"
txtFather = TextPane() -- Compose popup window of family panes
txtMother = TextPane()
boxParent = Hbox(txtFather,txtMother,1) -- Row 1
txtPerson = TextPane()
txtSpouse = TextPane()
boxSpouse = Hbox(txtPerson,txtSpouse,2) -- Row 2
txtSiblng = TextPane()
txtChilds = TextPane()
boxJunior = Hbox(txtSiblng,txtChilds,3) -- Row 3
lblPrefix = iup.label { CanFocus="NO"; Title="20"; Tip=tipPoints; }
valPoints = iup.val { Expand="HORIZONTAL"; Value=IntPoints; Inverted="YES"; Min=4.2; Max=20.2; PageStep=1/16; Step=1/16; ShowTicks=16; Tip=tipPoints; }
lblSuffix = iup.label { CanFocus="NO"; Title="4" ; Tip=tipPoints; }
lblPoints = iup.label { CanFocus="NO"; Title=strPoints() ; FgColor=strGreen; Tip=tipPoints; }
btnToPrev = iup.button{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Padding="3x1"; Title="To previous Root" ; Tip="Go backward to previous Family Root"; Active="NO" ; } -- V1.1
btnToNext = iup.button{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Padding="3x1"; Title="To following Root"; Tip="Go forward to following Family Root"; Active="NO" ; } -- V1.1
btnUpdate = iup.button{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Padding="3x1"; Title="Check for Updates"; Tip="Check for Updates in Plugin Store"; } -- V1.4
lstEvents = iup.list { CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Value=IntEvents; DropDown="YES"; VisibleColumns="9"; Tip="Choose the events to show with\nclick and select, or mouse-wheel"; " Full BMD Events"; " Brief BMD Events"; " No BMD Events"; }
tglRec_Id = iup.toggle{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Value=StrRec_Id; Title=" [Rec Id]"; Tip="Include the Record Id ?"; } -- V1.1
btnCentre = iup.button{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Padding="1x1"; Title="Centre Window"; Tip="Centre plugin window over the FH window"; } -- V1.4
btnCancel = iup.button{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strRed ; Padding="1x1"; Title="Close Window" ; Tip="Close this facts popup window and keep settings"; } -- V1.4
boxPoints = iup.vbox { Margin="0x0"; Gap="0"; Alignment="ACENTER"; iup.hbox { Gap="2"; lblPrefix; valPoints; lblSuffix; }; lblPoints; }
boxOption = iup.hbox { Margin="2x0"; Gap="9"; Alignment="ACENTER"; btnToPrev; btnToNext; btnUpdate; boxPoints; lstEvents; tglRec_Id; btnCentre; btnCancel; Homogeneous="NO"; } -- V1.1 -- V1.4
boxColumn = iup.vbox { Margin="2x2"; Gap="2"; Alignment="ACENTER"; boxParent; boxSpouse; boxJunior; boxOption; }
iupDialog = iup.dialog{ boxColumn; Title=strPluginTitle.." ~ with colour coded relations"; Shrink="YES"; MinBox="NO"; MaxBox="NO"; StartFocus=valPoints; } -- V1.2 -- V1.4
iupDialog.move_cb = function(self,x,y) if iup.MainLoopLevel() > 0 then TblField.CoordX=x TblField.CoordY=y end end -- Save window coordinates & size only after displayed -- V1.4
iupDialog.resize_cb = function(self) if iup.MainLoopLevel() > 0 then TblField.Raster=self.RasterSize end end -- Removed iup.Map(iupDialog) iup.Redraw(iupDialog,1) -- V1.4
local function doUpdate() -- Update all panes, events, styles, colors
ResetData()
MakeFacts(ptrRoot)
LoadText(ArrFather,txtFather,"Reset") -- Reload all text panes
LoadText(ArrMother,txtMother,"Reset")
LoadText(ArrPerson,txtPerson,"Reset")
LoadText(ArrSpouse,txtSpouse,"Reset")
LoadText(ArrSiblng,txtSiblng,"Reset")
LoadText(ArrChilds,txtChilds,"Reset")
SetWeight() -- Set their ExpandWeight
end -- local function doUpdate
local function goLinked(dicLink,intChar) -- Go to linked person as new Family Root
if dicLink and intChar > dicLink.Head and intChar < dicLink.Tail then -- V1.1
local ptrLink = dicLink.Indi -- Cursor is within linked person name
if not ptrLink:IsSame(ptrRoot) then
ptrRoot = ptrLink:Clone() -- Switch to person who is not current Root
ArrPeople.Index = ArrPeople.Index + 1
table.insert(ArrPeople,ptrRoot:Clone()) -- Add to list of people
btnToPrev.Active = "YES"
doUpdate()
iup.SetFocus(valPoints) -- Ensure Font slider keeps focus
end
end
end -- local function goLinked
function txtFather:caret_cb(intLine,intChar) -- Action for Father pane mouse click
goLinked(ArrFather.Linked[intLine],intChar)
end -- function txtFather:caret_cb
function txtMother:caret_cb(intLine,intChar) -- Action for Mother pane mouse click
goLinked(ArrMother.Linked[intLine],intChar)
end -- function txtMother:caret_cb
function txtPerson:caret_cb(intLine,intChar) -- Action for Person pane mouse click
goLinked(ArrPerson.Linked[intLine],intChar)
end -- function txtPerson:caret_cb
function txtSpouse:caret_cb(intLine,intChar) -- Action for Spouse pane mouse click
goLinked(ArrSpouse.Linked[intLine],intChar)
end -- function txtSpouse:caret_cb
function txtSiblng:caret_cb(intLine,intChar) -- Action for Sibling pane mouse click
goLinked(ArrSiblng.Linked[intLine],intChar)
end -- function txtSiblng:caret_cb
function txtChilds:caret_cb(intLine,intChar) -- Action for Children pane mouse click
goLinked(ArrChilds.Linked[intLine],intChar)
end -- function txtChilds:caret_cb
local function doMotion(arrList,txtText,intX,intY) -- Highlight person when mouse moves over
-- arrList List of parameters
-- txtText GUI text pane for Father, Mother, Person, etc
-- intX,intY Coordinates of mouse cursor position
local intPosn = iup.ConvertXYToPos(txtText,intX,intY) -- Convert coordinates to line & char position
local intLine,intChar = iup.TextConvertPosToLinCol(txtText,intPosn)
local dicLink = arrList.Linked[intLine] or { Indi=fhNewItemPtr(); Head=0; Tail=0; } -- V1.1
local intHead = dicLink.Head
local intTail = dicLink.Tail
if not dicLink.Indi:IsSame(ptrRoot)
and intChar > intHead and intChar < intTail then -- Cursor is in linked person name other than Root person -- V1.1
if arrList.Select ~= intLine then
arrList.Select = intLine
local strSelect = Selection(intLine,intHead,intTail) -- Temporarily highlight & underline person -- V1.1
table.insert(arrList.Format,{ Selection=strSelect; BgColor=strSmoke; Underline="SINGLE"; })
LoadText(arrList,txtText)
table.remove(arrList.Format) -- Cancel that formatting
end
else
if arrList.Select ~= 0 then -- Mouse over highlight has been applied
arrList.Select = 0
LoadText(arrList,txtText) -- Remove highlight & underline from pane
end
end
end -- local function doMotion
function txtFather:motion_cb(intX,intY) -- Action for Father pane mouse move
doMotion(ArrFather,txtFather,intX,intY)
end -- function txtFather:motion_cb
function txtMother:motion_cb(intX,intY) -- Action for Mother pane mouse move
doMotion(ArrMother,txtMother,intX,intY)
end -- function txtMother:motion_cb
function txtPerson:motion_cb(intX,intY) -- Action for Person pane mouse move
doMotion(ArrPerson,txtPerson,intX,intY)
end -- function txtPerson:motion_cb
function txtSpouse:motion_cb(intX,intY) -- Action for Spouse pane mouse move
doMotion(ArrSpouse,txtSpouse,intX,intY)
end -- function txtSpouse:motion_cb
function txtSiblng:motion_cb(intX,intY) -- Action for Sibling pane mouse move
doMotion(ArrSiblng,txtSiblng,intX,intY)
end -- function txtSiblng:motion_cb
function txtChilds:motion_cb(intX,intY) -- Action for Children pane mouse move
doMotion(ArrChilds,txtChilds,intX,intY)
end -- function txtChilds:motion_cb
function btnToPrev:action() -- Action for To Previous Root button
if ArrPeople.Index > 1 then
ArrPeople.Index = ArrPeople.Index - 1
ptrRoot = ArrPeople[ArrPeople.Index]:Clone() -- Select previous Family Root
doUpdate()
btnToNext.Active = "YES"
if ArrPeople.Index == 1 then
btnToPrev.Active = "NO"
end
end
end -- function btnToPrev:action
function btnToNext:action() -- Action for To Following Root button
if ArrPeople.Index < #ArrPeople then
ArrPeople.Index = ArrPeople.Index + 1
ptrRoot = ArrPeople[ArrPeople.Index]:Clone() -- Select next Family Root
doUpdate()
btnToPrev.Active = "YES"
if ArrPeople.Index == #ArrPeople then
btnToNext.Active = "NO"
end
end
end -- function btnToNext:action
function btnUpdate:action() -- Action for Check for Updates button -- V1.4
iupDialog.Active = "NO"
CheckVersionInStore("Quick Family Facts",strVersion)
iupDialog.BringFront = "YES"
iupDialog.Active = "YES"
end -- function btnUpdate
function valPoints:valuechanged_cb() -- Action for font Points size slider
local intPoints = IntPoints
IntPoints = math.floor(valPoints.Value)
if IntPoints ~= intPoints then
lblPoints.Title = strPoints()
doUpdate()
end
end -- function valPoints:valuechanged_cb
function lstEvents:action(strText,intItem,intState) -- Action for choose BMD Events dropdown
if intState == 1 then
IntEvents = intItem
doUpdate()
iup.SetFocus(valPoints) -- Ensure Font slider keeps focus
end
end -- function lstEvents:action
function tglRec_Id:action(intState) -- Action for the [Record Id] toggle -- V1.1
tglRec_Id.Tip = tglRec_Id.Tip -- Refresh XP Tooltip
StrRec_Id = tglRec_Id.Value
doUpdate()
end -- function tglRec_Id:action
if fhGetAppVersion() > 6 then iup.SetAttribute( iupDialog, "NATIVEPARENT", fhGetContextInfo("CI_PARENT_HWND") ) end -- V1.4
local function showWindow() -- Display the plugin window -- V1.4
if not TblField.Raster then
iupDialog:map()
local intWinW,intWinH = splitnumbers(iupDialog.NaturalSize) -- Set default window size -- V1.4
TblField.Raster = intWinW.."x"..(intWinH * 1.5) -- Increase height by 50% -- V1.4
end
iupDialog.RasterSize = TblField.Raster
iupDialog.MinSize = iupDialog.NaturalSize
local intMinX,intMinY,intMaxW,intMaxH = splitnumbers(iup.GetGlobal("VIRTUALSCREEN"))
intMaxH = intMaxH - 100 -- Screen size of all monitors allowing for the Taskbar -- V1.4
local intWinW,intWinH = splitnumbers(TblField.Raster) -- Get window dimensions from the previous use of plugin -- V1.4
local intX = tonumber(TblField.CoordX)
local intY = tonumber(TblField.CoordY)
if intX and intY -- Check window dimensions are inside screen dimensions -- V1.4
and intX >= intMinX and intY >= intMinY
and intX + intWinW <= intMaxW
and intY + intWinH <= intMaxH then
iup.ShowXY(iupDialog,intX,intY) -- Show window with previous saved position and size -- V1.4
else
iup.ShowXY(iupDialog,iup.CENTERPARENT,iup.CENTERPARENT) -- Otherwise centre window on FH window with saved size -- V1.4
TblField.CoordX = nil
TblField.CoordY = nil
end
local intX = math.max(tonumber(iupDialog.X),intMinX) -- Ensure window is all within all monitor screen(s) -- V1.4
local intY = math.max(tonumber(iupDialog.Y),intMinY)
if intX + intWinW > intMaxW then intX = intMaxW - intWinW end
if intY + intWinH > intMaxH then intY = intMaxH - intWinH end
iup.ShowXY(iupDialog,intX,intY)
end -- local function showWindow
function btnCentre:action() -- Action for Centre Window button -- V1.4
TblField.CoordX = nil
TblField.CoordY = nil
local MinSize = iupDialog.MinSize
showWindow()
iupDialog.MinSize = MinSize
TblField.CoordX = nil
TblField.CoordY = nil
end -- function btnCentre
function btnCancel:action() -- Action for Close Window button -- V1.4
return iup.CLOSE
end -- function btnCancel
doUpdate() -- Update all panes
showWindow()
iup.MainLoop()
iup.Close()
TblField.Points = IntPoints -- Save sticky settings
TblField.Events = IntEvents
TblField.Rec_Id = StrRec_Id
SaveSettings()
end -- function Main
function SetWeight(arrRow,intStep) -- Set the ExpandWeight per row of panes (must be after Main() so global boxParent/Spouse/Junior exist)
local function setRow(arrA,arrB)
local intRow = math.min(math.max(#arrA,#arrB),40) -- Limit large rows to 40 lines to allow other rows to auto-hide scroll-bar
intRow = intRow + arrB.Weight + FontScale - 1.1 -- Adjust row proportion for +/- Weight buttons and Title FontScale
intRow = intRow - math.min(intRow,(20-IntPoints)/5) + 0.1 -- Adjust inverse of Font Point Size, but must never be 0 that inhibits enlarging
return intRow
end -- local function setRow
if arrRow and intStep then -- Inc/Decrement row Weight in ArrMother/Spouse/Childs driven from +/- buttons
arrRow.Weight = arrRow.Weight + intStep -- or use + ( intStep * 0.2 * #arrRow )
end
local intParent = setRow(ArrFather,ArrMother)
local intSpouse = setRow(ArrPerson,ArrSpouse)
local intJunior = setRow(ArrSiblng,ArrChilds)
local intMean = ( intParent + intSpouse + intJunior ) / 3
boxParent.ExpandWeight = intParent / intMean -- Weight = 0.5 means row is 1/6 of window
boxSpouse.ExpandWeight = intSpouse / intMean -- Weight = 1.0 means row is 1/3 of window
boxJunior.ExpandWeight = intJunior / intMean -- Weight = 1.5 means row is 1/2 of window
iup.Map(iupDialog)
iup.Redraw(iupDialog,1) -- Ensure scroll-bars get redrawn
end -- local function SetWeight
fhInitialise(6,0,0,"save_recommended") -- V1.4
Main()
--[[
@Title: Quick Family Facts
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 1.4
@Keywords:
@LastUpdated: 03 Feb 2026
@Licence: This plugin is copyright (c) 2026 Mike Tate & contributors and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Quick family details popup snapshot can be invoked with Alt T Q
@V1.8: Add Check for Updates button; fhInitialise(); Use "NATIVEPARENT"; Reset Window button;
@V1.3: Cater for UTF-8 accented characters & symbols in .dat filepath;
@V1.2: Ensure all versions use one Plugin Data .dat file; FH V7 Lua 3.5 IUP 3.28;
@V1.1: Optional [Rec Id] for each family & person, improve Formatting, avoid duplicated partners, and rename Root Person as Family Root;
@V1.0: First published version in Plugin Store;
]]
require("iuplua") -- To access IUP GUI library -- V1.2
require("luacom") -- To create File System Object -- V1.8
local FSO = luacom.CreateObject("Scripting.FileSystemObject")
local strVersion = "1.4"
local strPluginTitle = " Quick Family Facts "..strVersion -- V1.4
if fhGetAppVersion() > 5 then -- Cater for Unicode UTF-8 from FH Version 6 onwards
fhSetStringEncoding("UTF-8")
iup.SetGlobal("UTF8MODE","YES")
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28 -- V1.2
end
function Encoding()
if fhGetAppVersion() > 5 then return fhGetStringEncoding() end
return "ANSI"
end -- function Encoding
-- Split a string into numbers using " " or "," or "x" separators -- Any non-number remains as a string
function splitnumbers(strTxt)
local tblNum = {}
strTxt = tostring(strTxt or "")
strTxt:gsub("([^ ,x]+)", function(strNum) tblNum[#tblNum+1] = tonumber(strNum) or strNum end)
return tblNum[1],tblNum[2],tblNum[3],tblNum[4]
end -- function splitnumbers
-- Report error message --
local function doError(strMessage,errFunction)
-- strMessage ~ error message text
-- errFunction ~ optional error reporting function
if type(errFunction) == "function" then
errFunction(strMessage)
else
error(strMessage)
end
end -- function doError
-- Convert filename to ANSI alternative and indicate success --
function FileNameToANSI(strFileName,strAnsiName)
-- strFileName ~ full file path
-- strAnsiFile ~ ANSI file name & type
-- return values ~ ANSI file path, true if original path was ANSI compatible
if Encoding() == "ANSI" then return strFileName, true end
local isFlag = fhIsConversionLossFlagSet()
fhSetConversionLossFlag(false)
local strAnsi = fhConvertUTF8toANSI(strFileName)
local wasAnsi = true
if fhIsConversionLossFlagSet() then
strAnsiName = strAnsiName or "ANSI.ANSI"
strAnsi = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\"..strAnsiName
wasAnsi = false
end
fhSetConversionLossFlag(isFlag)
return strAnsi, wasAnsi
end -- function FileNameToANSI
-- Get parent folder --
function GetParentFolder(strFileName)
-- strFileName ~ full file path
-- return value ~ parent folder path
local strParent = FSO:GetParentFolderName(strFileName) --! Faulty in FH v6 with Unicode chars in path
if fhGetAppVersion() == 6 then
local _, wasAnsi = FileNameToANSI(strFileName)
if not wasAnsi then
strParent = strFileName:match("^(.+)[\\/][^\\/]+[\\/]?$")
end
end
return strParent
end -- function GetParentFolder
-- Check if file exists --
function FlgFileExists(strFileName)
-- strFileName ~ full file path
-- return value ~ true if it exists
return FSO:FileExists(strFileName)
end -- function FlgFileExists
-- Delete a file if it exists --
function DeleteFile(strFileName,errFunction)
-- strFileName ~ full file path
-- errFunction ~ optional error reporting function
-- return value ~ true if file does not exist or is deleted else false
if FSO:FileExists(strFileName) then
FSO:DeleteFile(strFileName,true)
if FSO:FileExists(strFileName) then
doError("File Not Deleted:\n"..strFileName.."\n",errFunction)
return false
end
end
return true
end -- function DeleteFile
-- Copy a file if it exists and destination is not a folder --
function CopyFile(strFileName,strDestination)
-- strFileName ~ full source file path
-- strDestination~ full target file path
-- return value ~ true if file exists and is copied else false
if MakeFolder(GetParentFolder(strDestination)) and FSO:FileExists(strFileName) and not FSO:FolderExists(strDestination) then
FSO:CopyFile(strFileName,strDestination)
if FSO:FileExists(strDestination) then
return true
end
end
return false
end -- function CopyFile
-- Move a file if it exists and destination is not a folder --
function MoveFile(strFileName,strDestination)
-- strFileName ~ full source file path
-- strDestination~ full target file path
-- return value ~ true if file exists and is moved else false
if MakeFolder(GetParentFolder(strDestination)) and FSO:FileExists(strFileName) and not FSO:FolderExists(strDestination) then
if DeleteFile(strDestination) then
FSO:MoveFile(strFileName,strDestination)
if FSO:FileExists(strDestination) then
return true
end
end
end
return false
end -- function MoveFile
-- Open File with ANSI path and return Handle --
function OpenFile(strFileName,strMode)
-- strFileName ~ full file path
-- strMode ~ "r", "w", "a" optionally suffixed with "+" &/or "b"
-- return value ~ file handle
local fileHandle, strError = io.open(strFileName,strMode)
if fileHandle == nil then
error("\n Unable to open file in \""..strMode.."\" mode. \n "..strFileName.." \n "..strError.." \n")
end
return fileHandle
end -- function OpenFile
-- Save string to file --
function SaveStringToFile(strContents,strFileName,strFormat)
-- strContents ~ text string
-- strFileName ~ full file path
-- strFormat ~ optional "UTF-8" or "UTF-16LE"
-- return value ~ true if successful else false
strFormat = strFormat or "UTF-8"
if fhGetAppVersion() > 6 then
return fhSaveTextFile(strFileName,strContents,strFormat)
end
local strAnsi, wasAnsi = FileNameToANSI(strFileName)
local fileHandle = OpenFile(strAnsi,"w")
fileHandle:write(strContents)
assert(fileHandle:close())
if not wasAnsi then
MoveFile(strAnsi,strFileName)
end
return true
end -- function SaveStringToFile
-- Load string from file --
function StrLoadFromFile(strFileName,strFormat)
-- strFileName ~ full file path
-- strFormat ~ optional "UTF-8" or "UTF-16LE"
-- return value ~ file contents
strFormat = strFormat or "UTF-8"
if fhGetAppVersion() > 6 then
return fhLoadTextFile(strFileName,strFormat)
end
local strAnsi, wasAnsi = FileNameToANSI(strFileName)
if not wasAnsi then
CopyFile(strFileName,strAnsi)
end
local fileHandle = OpenFile(strAnsi,"r")
local strContents = fileHandle:read("*all")
assert(fileHandle:close())
return strContents
end -- function StrLoadFromFile
--[[
@Function: CheckVersionInStore
@Author: Mike Tate
@Version: 1.4
@LastUpdated: 20 Jan 2026
@Description: Check plugin version against version in Plugin Store
@Parameter: Plugin name and version
@Returns: None
@Requires: luacom
@V1.4: Dispense with files and assume called via IUP button;
@V1.3: Save and retrieve latest version in file;
@V1.2: Ensure the Plugin Data folder exists;
@V1.1: Monthly interval between checks; Report if Internet is inaccessible;
@V1.0: Initial version;
]]
function CheckVersionInStore(strPlugin,strVersion) -- Check if later Version available in Plugin Store
require "luacom"
local function httpRequest(strRequest) -- Luacom http request protected by pcall() below
local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
http:Open("GET",strRequest,false)
http:Send()
return http.Responsebody
end -- local function httpRequest
local function intVersion(strVersion) -- Convert version string to comparable integer
local intVersion = 0
local arrNumbers = {}
strVersion:gsub("(%d+)", function(strDigits) table.insert(arrNumbers,strDigits) end)
for i = 1, 5 do
intVersion = intVersion * 100 + tonumber(arrNumbers[i] or 0)
end
return intVersion
end -- local function intVersion
local strLatest = "0"
if strPlugin then
local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..tostring(strPlugin)
local isOK, strReturn = pcall(httpRequest,strRequest)
if not isOK then -- Problem with Internet access
fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ")
elseif strReturn then
strLatest = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits
end
end
local strMessage = "No later Version"
if intVersion(strLatest) > intVersion(strVersion or "0") then
strMessage = "Later Version "..strLatest
end
fhMessageBox(strMessage.." of this Plugin is available from the 'Plugin Store'.")
end -- function CheckVersionInStore
local strMaroon = "128 0 0" -- Color attributes
local strPurple = "128 0 128"
local strMagenta= "255 0 255"
local strAmber = "210 110 0"
local strOlive = "128 128 0"
local strGreen = "0 128 0"
local strTeal = "0 128 128"
local strNavy = "0 0 128"
local strOrange = "255 190 0"
local strGray = "128 128 128"
local strRed = "255 0 0"
local strBlue = "0 0 255"
local strLime = "0 255 0"
local strCyan = "0 255 255"
local strYellow = "255 255 0"
local strSilver = "192 192 192"
local strSmoke = "220 220 220"
local strWhite = "255 255 255"
local strBlack = "0 0 0"
function ResetData()
ArrFather = { "Father"; Parent=true; } -- List of title, plural, class, events, format, people, weight for each type of person
ArrMother = { "Mother"; Parent=true; }
ArrPerson = { "Family Root"; Root = true; } -- V1.1
ArrSpouse = { "Partner"; Plural="s"; Partner=true; }
ArrSiblng = { "Sibling"; Plural="s"; Sibling=true; }
ArrChilds = { "Child"; Plural="ren"; Children=true; }
for intArray, arrArray in ipairs ({ArrFather,ArrMother,ArrPerson,ArrSpouse,ArrSiblng,ArrChilds}) do
arrArray.Events = { { "~.BIRT"; "~.BAPM"; "~.CHR"; }; { "~.MARR"; "~.DIV"; "~.EVEN-SEPARATION"; }; { "~.DEAT"; "~.BURI"; "~.CREM"; }; }
arrArray.Format = { } -- Formatting applied to text
arrArray.Linked = { } -- Linked line per person with Head and Tail char position of Name
arrArray.Select = 0 -- Current selected line
arrArray.People = 0 -- People count per pane
arrArray.Weight = 0 -- ExpandWeight per row
end
ArrRow = { ArrMother; ArrSpouse; ArrChilds; }
ArrFamily = { } -- Family and associated colours
ArrColour = {strMaroon;strPurple;strMagenta;strAmber;strOlive;strTeal;strNavy;strOrange;strGray;strRed;strBlue;strLime;strCyan;strYellow;}
IntColour = 1 -- Next available colour
end -- ResetData()
local ArrPeople = { } -- Array of Family Root pointers for Go Backward/Forward
local TblField = { } -- Sticky Settings File fields
local IntPoints = 9 -- 9=default font Point size
local IntEvents = 1 -- 1=Full, 2=Brief, 3=None
local StrRec_Id = "OFF" -- [Record Id] hidden -- V1.1
local FontScale = 1.2 -- FontScale of Titles needed for ExpandWeight adjustment
local PluginData = fhGetPluginDataFileName():gsub(" %- V%d.*%.dat$",".dat") -- Ensure root name is used -- V1.2 -- V1.3 -- V1.4
function LoadSettings() -- Load Settings File in lines with key & val fields -- V1.4
if FlgFileExists(PluginData) then
local strData = StrLoadFromFile(PluginData)
for strLine in strData:gmatch("[^\r\n]+") do
local strKey,strVal = strLine:match("([%a_]+)=(.+)$")
TblField[strKey] = strVal
end
IntPoints = tonumber(TblField.Points) or IntPoints
IntEvents = tonumber(TblField.Events) or IntEvents
StrRec_Id = TblField.Rec_Id or StrRec_Id -- V1.1
end
end -- function LoadSettings
function SaveSettings() -- Save Settings File lines with key & val fields -- V1.4
local tblData = {}
for strKey,strVal in pairs(TblField) do
table.insert(tblData,strKey.."="..strVal.."\n")
end
local strData = table.concat(tblData,"\n")
if not SaveStringToFile(strData,PluginData) then
doError("\nSettings file not saved successfully.\n\nMay need to Delete the following Plugin Data .dat file:\n\n"..PluginData.."\n\nError detected.")
end
end -- function SaveSettings
function LoopFacts(arrList,arrFacts,ptrHome) -- Loop through Facts
-- arrList List of facts to update
-- arrFacts List of Tags to find
-- ptrHome Record to search
if IntEvents < 3 then
for intFact, strFact in ipairs (arrFacts) do -- Loop on each fact Tag unless "None" chosen
strFact = fhGetDisplayText(ptrHome,strFact)
if #strFact > 0 then
table.insert(arrList,"\t"..strFact) -- Add each Fact to List and break if "Brief" chosen
if IntEvents == 2 then break end
end
end
end
end -- function LoopFacts
function GetRecId(ptrRec) -- Get Record Id conditionally -- V1.1
if StrRec_Id == "OFF" then return "" end
return " ["..fhGetRecordId(ptrRec).."]"
end -- function GetRecId
function ShowName(ptrIndi) -- Show Name (LifeDates) [RecId]
return fhGetDisplayText(ptrIndi).." ("..fhCallBuiltInFunction("LifeDates2",ptrIndi,"STD")..")"..GetRecId(ptrIndi) -- V1.1
end -- function ShowName
function IntLength(strName) -- Length of name in characters -- V1.1
if fhGetAppVersion() > 5 then
strName = fhConvertUTF8toANSI(strName) -- Convert each multi-byte UTF8 char to one byte ANSI char
fhSetConversionLossFlag(false)
end
return #strName
end
function ListFacts(arrList,ptrIndi,ptrRoot) -- List the name and facts for one person
-- arrList List of facts to fill and parameters
-- ptrIndi Link to the current Individual record
-- ptrRoot Link to root person Individual record
if ptrIndi:IsNotNull() then
arrList.People = arrList.People + 1 -- List starts with Name & (LifeDates) of person
local strName = ShowName(ptrIndi)
table.insert(arrList," "..strName)
arrList.Linked[#arrList] = { Indi=ptrIndi:Clone(); Head=2; Tail=2+IntLength(strName); } -- V1.1
for intFamily, arrFamily in ipairs (ArrFamily) do -- Obtain the colour of primary individuals
local isMatch = false
if fhGetItemPtr(ptrIndi,"~.FAMC[1]>"):IsSame(arrFamily.Family) then
isMatch = true
else
local ptrFams = fhGetItemPtr(ptrIndi,"~.FAMS[1]") -- Move to 1st Spouse Family of Individual
while ptrFams:IsNotNull() do
if fhGetValueAsLink(ptrFams):IsSame(arrFamily.Family) then
isMatch = true
break
end
ptrFams:MoveNext("SAME_TAG") -- Move to next Spouse Family of Individual
end
end
if isMatch then
arrList.Colour = arrFamily.Colour
break
end
end
local isRoot = FormatName(arrList,ptrIndi,ptrRoot)
if not isRoot or arrList.Root then
for intFacts, arrFacts in ipairs (arrList.Events) do -- Loop on each event array
if intFacts == 2 then
local intSpou = 1
local ptrFams = fhGetItemPtr(ptrIndi,"~.FAMS") -- For family facts loop on Family as Spouse tags
while ptrFams:IsNotNull() do
local ptrFrec = fhGetValueAsLink(ptrFams) -- Family record -- V1.1
local ptrSpou = fhGetItemPtr(ptrIndi,"~.~SPOU["..intSpou.."]>")
if ptrSpou:IsNotNull() then
local strSpou = ShowName(ptrSpou)
local strTail = ""
if IntEvents == 3 then -- If No BMD Events then add (m.year) or (d.year) or (s.year) -- V1.1
for intFact, strFact in ipairs (arrFacts) do
local strYear = fhGetItemText(ptrFrec,strFact..".DATE:YEAR")
if #strYear > 2 then -- If year exists extract initial letter of fact
local strInit = strFact:match("~%.[EVEN-]*(%u)"):lower()
strTail = " ("..strInit.."."..strYear..")"
break
end
end
end
local strRecId = GetRecId(ptrFrec) -- Add each spouse/partner Family [RecId] and Name (LifeDates) [RecId] to List -- V1.1
table.insert(arrList,"\t="..strRecId.." "..strSpou..strTail)
arrList.Linked[#arrList] = { Indi=ptrSpou:Clone(); Head=#strRecId+4; Tail=#strRecId+4+IntLength(strSpou); } -- V1.1
if ptrSpou:IsSame(ptrRoot) then -- Partner's spouse's colour must match Root's Parents
arrList.Colour = ArrFamily[#ArrFamily].Colour
else
arrList.Colour = nil
for intFamily, arrFamily in ipairs (ArrFamily) do
if ptrFrec:IsSame(arrFamily.Family) then
arrList.Colour = arrFamily.Colour -- Assign existing Spouse Family colour
break
end
end
if not arrList.Colour -- or arrList.Partner or arrList.Sibling or arrList.Children or fhGetItemPtr(ptrSpou,"~.~CHIL[1]>"):IsNull() ) then
and ( arrList.Parent or arrList.Root )
and fhGetItemPtr(ptrSpou,"~.~CHIL[1]>"):IsNotNull() then
arrList.Colour = ArrColour[IntColour] -- Assign next colour to next Parent/Root Spouse Family
IntColour = IntColour + 1 -- Recycle through colours if necessary
if IntColour > #ArrColour then IntColour = 1 end
table.insert(ArrFamily,1,{ Family=ptrFrec; Colour=arrList.Colour; })
end
end
FormatName(arrList,ptrSpou,ptrRoot)
end
LoopFacts(arrList,arrFacts,ptrFrec)
intSpou = intSpou + 1
ptrFams:MoveNext("SAME_TAG")
end
else
LoopFacts(arrList,arrFacts,ptrIndi)
end
end
end
end
end -- function ListFacts
function FormatName(arrList,ptrIndi,ptrRoot) -- Format name with Bold & Underline for root
-- arrList List to hold format
-- ptrIndi Individual record
-- ptrRoot Family Root record
local isRoot = ptrIndi:IsSame(ptrRoot)
if isRoot then
AddFormat(arrList,"BOLD","SINGLE")
else
AddFormat(arrList,"BOLD")
end
return isRoot
end -- function FormatName
function Selection(intLine,intHead,intTail) -- Format the text selection line and chars -- V1.1
-- intLine Line of selection
-- intHead Head of selection
-- intTail Tail of selection
return intLine..","..intHead..":"..intLine..","..intTail
end -- function Selection
function AddFormat(arrList,strWeight,strUnder,strScale) -- Add formatting for bold, underline, font scale & size
-- arrList List of parameters
-- strWeight Bold weight format
-- strUnder Underline format optional
-- strScale Scale sizing for Title only
local strColour = arrList.Colour
local strAlign = "LEFT"
local intLine = #arrList
local intHead = 2 -- Head & Tail char positions
local intTail = 999
if strScale then
strColour = strBlack -- Format for Title
strAlign = "CENTER"
intLine = 1
else
local dicLink = arrList.Linked[#arrList] -- Format for Name from Head to Tail -- V1.1
intHead = dicLink.Head
intTail = dicLink.Tail
if intHead > 2 then
local strSelect = Selection(intLine,2,intHead) -- Format for Head = [Rec Id] is green bold -- V1.1
table.insert(arrList.Format,{ Selection=strSelect; FgColor=strGreen; Weight="BOLD"; })
local strSelect = Selection(intLine,intTail,999) -- Format for Tail (m.year) is black unbold -- V1.1
table.insert(arrList.Format,{ Selection=strSelect; FgColor=strBlack; Weight="NONE"; })
end
end
local strSelect = Selection(intLine,intHead,intTail) -- Format for line between Head & Tail -- V1.1
table.insert(arrList.Format,{ Selection=strSelect; FgColor=strColour or strBlack; Weight=strWeight or "NONE"; Underline=strUnder or "NONE"; Alignment=strAlign or "LEFT"; FontScale=strScale or "1.0"; FontSize=IntPoints; })
arrList.Colour = strBlack
end -- function AddFormat
function ListTitle(arrList) -- List Title
local strTitle = arrList[1]
local strPlural = arrList.Plural or ""
if arrList.People == 1 and arrList.Sibling then -- One Sibling
arrList.People = 0
arrList[2] = nil
end
if arrList.People == 0 then -- No people
strTitle = "No "..strTitle..strPlural.." Listed"
elseif arrList.People > 1 then -- Multiple people
strTitle = strTitle..strPlural
end
arrList[1] = " "..strTitle
AddFormat(arrList,"BOLD","NONE",FontScale) -- FontScale was "1.1" but upsets ExpandWeight especially when only Title, unless adjustment added
end -- function ListTitle
function LoadText(arrList,txtText,isReset) -- Load variable text and format into Text pane
-- arrList List of parameters
-- txtText GUI text pane for Father, Mother, Person, etc
-- isReset Exists if reset needed
local iupTags = iup.user{ Bulk="YES"; CleanOut="YES"; }
iup.Append( iupTags, iup.user{ Selection="1,1:999,999"; TabsArray="15 LEFT "; FontSize=IntPoints; })
for intFormat, iupFormat in ipairs (arrList.Format) do -- Append the formatting tags
local iupUser = iup.user{}
for iupName, iupData in pairs (iupFormat) do -- Create the user data
iupUser[iupName] = iupData
end
iup.Append( iupTags, iupUser )
end
txtText.VisibleLines = math.min(#arrList-1,2) -- Improves initial height of dialog
if isReset then txtText.Value = " " end -- Ensure local pane font is reset
local strText = txtText.Value or ""
local strList = table.concat(arrList,"\n")
if arrList.Linked[#arrList] then -- Name is last so needs an extra blank line for doMotion()
strList = strList.."\n "
end
if strText ~= strList then -- Load new list of names & facts
txtText.Value = strList
end
txtText.AddFormatTag = iupTags -- Add the formatting tags
end -- function LoadText
function MakeFacts(ptrRoot) -- Make the names & facts for each pane
local ptrFath = fhNewItemPtr()
local ptrMoth = fhNewItemPtr()
local ptrIndi = fhNewItemPtr()
local ptrFams = fhGetItemPtr(ptrRoot,"~.FAMS>")
local ptrFamc = fhGetItemPtr(ptrRoot,"~.FAMC>")
if ptrFamc:IsNull() then ptrFamc = ptrFams end -- Add colour for spouse/parent family of root
table.insert(ArrFamily,{ Family=ptrFamc:Clone(); Colour=ArrColour[IntColour]; })
IntColour = IntColour + 1
ListFacts(ArrFather,fhGetItemPtr(ptrRoot,"~.~FATH>"),ptrRoot) -- List the Father of root Individual
ListTitle(ArrFather)
ListFacts(ArrMother,fhGetItemPtr(ptrRoot,"~.~MOTH>"),ptrRoot) -- List the Mother of root Individual
ListTitle(ArrMother)
local arrSibling = {}
local nxtSibling = 1
local intIndi = 1 -- List the Siblings of root Individual (must be before root Indiviudal)
ptrMoth:MoveTo(ptrRoot,"~.~MOTH[1]>")
ptrIndi:MoveTo(ptrMoth,"~.~CHIL[1]>") -- Move to 1st Child of Mother i.e. Sibling of root Individual
while ptrIndi:IsNotNull() do
table.insert(arrSibling,ptrIndi:Clone()) -- Add next sibling to list
arrSibling["ID"..fhGetRecordId(ptrIndi)] = true
intIndi = intIndi + 1
ptrIndi:MoveTo(ptrMoth,"~.~CHIL["..intIndi.."]>") -- Move to next Sibling of root Individual
end
local intIndi = 1
ptrFath:MoveTo(ptrRoot,"~.~FATH[1]>")
ptrIndi:MoveTo(ptrFath,"~.~CHIL[1]>") -- Move to 1st Child of Father i.e. Sibling of root Individual
while ptrIndi:IsNotNull() do
if arrSibling["ID"..fhGetRecordId(ptrIndi)] then -- Skip existing sibling
if nxtSibling < #arrSibling and
arrSibling[#arrSibling]:IsSame(ptrIndi) then
nxtSibling = nxtSibling + 1 -- Ensure more siblings go after last existing sibling
end
else
table.insert(arrSibling,nxtSibling,ptrIndi:Clone()) -- Add new sibling to list
end
nxtSibling = nxtSibling + 1
intIndi = intIndi + 1
ptrIndi:MoveTo(ptrFath,"~.~CHIL["..intIndi.."]>") -- Move to next Sibling of root Individual
end
for intIndi, ptrIndi in ipairs (arrSibling) do
ListFacts(ArrSiblng,ptrIndi,ptrRoot) -- List each Sibling of root Individual
end
ListTitle(ArrSiblng)
ListFacts(ArrPerson,ptrRoot,ptrRoot) -- List the root Individual Person
ListTitle(ArrPerson)
local intIndi = 1 -- List the Partners of root Individual
local arrSpou = {}
ptrFams:MoveTo(ptrRoot,"~.FAMS") -- Move to 1st Family of root Individual
while ptrFams:IsNotNull() do
ptrIndi:MoveTo(ptrRoot,"~.~SPOU["..intIndi.."]>")
for _, ptrSpou in ipairs (arrSpou) do -- Has this Spouse already been listed? -- V1.1
if ptrIndi:IsSame(ptrSpou) then ptrIndi:SetNull() break end
end
if ptrIndi:IsNotNull() then -- V1.1
table.insert(arrSpou,ptrIndi:Clone())
ListFacts(ArrSpouse,ptrIndi,ptrRoot) -- List each Spouse of root Individual
end
intIndi = intIndi + 1
ptrFams:MoveNext("SAME_TAG") -- Move to next Family of root Individual
end
ListTitle(ArrSpouse)
local intIndi = 1 -- List the Children of root Individual
ptrIndi:MoveTo(ptrRoot,"~.~CHIL[1]>") -- Move to 1st Child of root Individual
while ptrIndi:IsNotNull() do
ListFacts(ArrChilds,ptrIndi,ptrRoot) -- List each Child of root Individual
intIndi = intIndi + 1
ptrIndi:MoveTo(ptrRoot,"~.~CHIL["..intIndi.."]>") -- Move to next Child of root Individual
end
ListTitle(ArrChilds)
end -- function MakeFacts
function TextPane() -- Compose IUP Multiline Read-Only Text pane with Auto-Hidden Scroll-Bars and Formatting
return iup.text{ MultiLine="YES"; ReadOnly="YES"; Expand="YES"; CanFocus="NO"; ScrollBar="YES"; AutoHide="YES"; Formatting="YES"; TipDelay=9000; Tip="Click highlighted Name to select as new Family Root\nUse click and drag or mouse-wheel to scroll up/down\nUse the Ctrl key + mouse-wheel to alter text font size"; } -- V1.1
end -- function TextPane
function Hbox(iupPaneA,iupPaneB,intRow) -- Compose IUP Horizontal box for two panes with row number
local btnInc = iup.button{ Title="+"; Expand="VERTICAL"; Padding="2x0"; CanFocus="NO"; Tip="Increase height of this row"; action=function() SetWeight(ArrRow[intRow], 1) end; }
local btnDec = iup.button{ Title="-"; Expand="VERTICAL"; Padding="3x0"; CanFocus="NO"; Tip="Decrease height of this row"; action=function() SetWeight(ArrRow[intRow],-1) end; }
return iup.hbox{ Alignment="ATOP"; Margin="2x2"; Gap="2"; iupPaneA; iupPaneB; iup.vbox{ btnInc; btnDec; }; }
end -- function Hbox
function Main()
local arrRoot = fhGetCurrentRecordSel("INDI") -- Check if any Records pre-selected
if #arrRoot == 0 then
arrRoot = fhPromptUserForRecordSel("INDI",1) -- Prompt for Record selection
end
if #arrRoot ~= 1 then return end
local ptrRoot = arrRoot[1]
ArrPeople.Index = 1
table.insert(ArrPeople,ptrRoot:Clone()) -- Initialise list of people for Go Backward/Forwards
ResetData()
LoadSettings() -- Load sticky settings
local function strPoints() -- Format the Font Point size
return string.format(" Font %2u Point ",IntPoints)
end -- local function strPoints
local tipPoints = "Choose text font point size with either\nclick and drag, or mouse-wheel, or the\nHome, End, Page Up/Down, arrow keys"
txtFather = TextPane() -- Compose popup window of family panes
txtMother = TextPane()
boxParent = Hbox(txtFather,txtMother,1) -- Row 1
txtPerson = TextPane()
txtSpouse = TextPane()
boxSpouse = Hbox(txtPerson,txtSpouse,2) -- Row 2
txtSiblng = TextPane()
txtChilds = TextPane()
boxJunior = Hbox(txtSiblng,txtChilds,3) -- Row 3
lblPrefix = iup.label { CanFocus="NO"; Title="20"; Tip=tipPoints; }
valPoints = iup.val { Expand="HORIZONTAL"; Value=IntPoints; Inverted="YES"; Min=4.2; Max=20.2; PageStep=1/16; Step=1/16; ShowTicks=16; Tip=tipPoints; }
lblSuffix = iup.label { CanFocus="NO"; Title="4" ; Tip=tipPoints; }
lblPoints = iup.label { CanFocus="NO"; Title=strPoints() ; FgColor=strGreen; Tip=tipPoints; }
btnToPrev = iup.button{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Padding="3x1"; Title="To previous Root" ; Tip="Go backward to previous Family Root"; Active="NO" ; } -- V1.1
btnToNext = iup.button{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Padding="3x1"; Title="To following Root"; Tip="Go forward to following Family Root"; Active="NO" ; } -- V1.1
btnUpdate = iup.button{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Padding="3x1"; Title="Check for Updates"; Tip="Check for Updates in Plugin Store"; } -- V1.4
lstEvents = iup.list { CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Value=IntEvents; DropDown="YES"; VisibleColumns="9"; Tip="Choose the events to show with\nclick and select, or mouse-wheel"; " Full BMD Events"; " Brief BMD Events"; " No BMD Events"; }
tglRec_Id = iup.toggle{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Value=StrRec_Id; Title=" [Rec Id]"; Tip="Include the Record Id ?"; } -- V1.1
btnCentre = iup.button{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strGreen; Padding="1x1"; Title="Centre Window"; Tip="Centre plugin window over the FH window"; } -- V1.4
btnCancel = iup.button{ CanFocus="NO"; Expand="HORIZONTAL"; FgColor=strRed ; Padding="1x1"; Title="Close Window" ; Tip="Close this facts popup window and keep settings"; } -- V1.4
boxPoints = iup.vbox { Margin="0x0"; Gap="0"; Alignment="ACENTER"; iup.hbox { Gap="2"; lblPrefix; valPoints; lblSuffix; }; lblPoints; }
boxOption = iup.hbox { Margin="2x0"; Gap="9"; Alignment="ACENTER"; btnToPrev; btnToNext; btnUpdate; boxPoints; lstEvents; tglRec_Id; btnCentre; btnCancel; Homogeneous="NO"; } -- V1.1 -- V1.4
boxColumn = iup.vbox { Margin="2x2"; Gap="2"; Alignment="ACENTER"; boxParent; boxSpouse; boxJunior; boxOption; }
iupDialog = iup.dialog{ boxColumn; Title=strPluginTitle.." ~ with colour coded relations"; Shrink="YES"; MinBox="NO"; MaxBox="NO"; StartFocus=valPoints; } -- V1.2 -- V1.4
iupDialog.move_cb = function(self,x,y) if iup.MainLoopLevel() > 0 then TblField.CoordX=x TblField.CoordY=y end end -- Save window coordinates & size only after displayed -- V1.4
iupDialog.resize_cb = function(self) if iup.MainLoopLevel() > 0 then TblField.Raster=self.RasterSize end end -- Removed iup.Map(iupDialog) iup.Redraw(iupDialog,1) -- V1.4
local function doUpdate() -- Update all panes, events, styles, colors
ResetData()
MakeFacts(ptrRoot)
LoadText(ArrFather,txtFather,"Reset") -- Reload all text panes
LoadText(ArrMother,txtMother,"Reset")
LoadText(ArrPerson,txtPerson,"Reset")
LoadText(ArrSpouse,txtSpouse,"Reset")
LoadText(ArrSiblng,txtSiblng,"Reset")
LoadText(ArrChilds,txtChilds,"Reset")
SetWeight() -- Set their ExpandWeight
end -- local function doUpdate
local function goLinked(dicLink,intChar) -- Go to linked person as new Family Root
if dicLink and intChar > dicLink.Head and intChar < dicLink.Tail then -- V1.1
local ptrLink = dicLink.Indi -- Cursor is within linked person name
if not ptrLink:IsSame(ptrRoot) then
ptrRoot = ptrLink:Clone() -- Switch to person who is not current Root
ArrPeople.Index = ArrPeople.Index + 1
table.insert(ArrPeople,ptrRoot:Clone()) -- Add to list of people
btnToPrev.Active = "YES"
doUpdate()
iup.SetFocus(valPoints) -- Ensure Font slider keeps focus
end
end
end -- local function goLinked
function txtFather:caret_cb(intLine,intChar) -- Action for Father pane mouse click
goLinked(ArrFather.Linked[intLine],intChar)
end -- function txtFather:caret_cb
function txtMother:caret_cb(intLine,intChar) -- Action for Mother pane mouse click
goLinked(ArrMother.Linked[intLine],intChar)
end -- function txtMother:caret_cb
function txtPerson:caret_cb(intLine,intChar) -- Action for Person pane mouse click
goLinked(ArrPerson.Linked[intLine],intChar)
end -- function txtPerson:caret_cb
function txtSpouse:caret_cb(intLine,intChar) -- Action for Spouse pane mouse click
goLinked(ArrSpouse.Linked[intLine],intChar)
end -- function txtSpouse:caret_cb
function txtSiblng:caret_cb(intLine,intChar) -- Action for Sibling pane mouse click
goLinked(ArrSiblng.Linked[intLine],intChar)
end -- function txtSiblng:caret_cb
function txtChilds:caret_cb(intLine,intChar) -- Action for Children pane mouse click
goLinked(ArrChilds.Linked[intLine],intChar)
end -- function txtChilds:caret_cb
local function doMotion(arrList,txtText,intX,intY) -- Highlight person when mouse moves over
-- arrList List of parameters
-- txtText GUI text pane for Father, Mother, Person, etc
-- intX,intY Coordinates of mouse cursor position
local intPosn = iup.ConvertXYToPos(txtText,intX,intY) -- Convert coordinates to line & char position
local intLine,intChar = iup.TextConvertPosToLinCol(txtText,intPosn)
local dicLink = arrList.Linked[intLine] or { Indi=fhNewItemPtr(); Head=0; Tail=0; } -- V1.1
local intHead = dicLink.Head
local intTail = dicLink.Tail
if not dicLink.Indi:IsSame(ptrRoot)
and intChar > intHead and intChar < intTail then -- Cursor is in linked person name other than Root person -- V1.1
if arrList.Select ~= intLine then
arrList.Select = intLine
local strSelect = Selection(intLine,intHead,intTail) -- Temporarily highlight & underline person -- V1.1
table.insert(arrList.Format,{ Selection=strSelect; BgColor=strSmoke; Underline="SINGLE"; })
LoadText(arrList,txtText)
table.remove(arrList.Format) -- Cancel that formatting
end
else
if arrList.Select ~= 0 then -- Mouse over highlight has been applied
arrList.Select = 0
LoadText(arrList,txtText) -- Remove highlight & underline from pane
end
end
end -- local function doMotion
function txtFather:motion_cb(intX,intY) -- Action for Father pane mouse move
doMotion(ArrFather,txtFather,intX,intY)
end -- function txtFather:motion_cb
function txtMother:motion_cb(intX,intY) -- Action for Mother pane mouse move
doMotion(ArrMother,txtMother,intX,intY)
end -- function txtMother:motion_cb
function txtPerson:motion_cb(intX,intY) -- Action for Person pane mouse move
doMotion(ArrPerson,txtPerson,intX,intY)
end -- function txtPerson:motion_cb
function txtSpouse:motion_cb(intX,intY) -- Action for Spouse pane mouse move
doMotion(ArrSpouse,txtSpouse,intX,intY)
end -- function txtSpouse:motion_cb
function txtSiblng:motion_cb(intX,intY) -- Action for Sibling pane mouse move
doMotion(ArrSiblng,txtSiblng,intX,intY)
end -- function txtSiblng:motion_cb
function txtChilds:motion_cb(intX,intY) -- Action for Children pane mouse move
doMotion(ArrChilds,txtChilds,intX,intY)
end -- function txtChilds:motion_cb
function btnToPrev:action() -- Action for To Previous Root button
if ArrPeople.Index > 1 then
ArrPeople.Index = ArrPeople.Index - 1
ptrRoot = ArrPeople[ArrPeople.Index]:Clone() -- Select previous Family Root
doUpdate()
btnToNext.Active = "YES"
if ArrPeople.Index == 1 then
btnToPrev.Active = "NO"
end
end
end -- function btnToPrev:action
function btnToNext:action() -- Action for To Following Root button
if ArrPeople.Index < #ArrPeople then
ArrPeople.Index = ArrPeople.Index + 1
ptrRoot = ArrPeople[ArrPeople.Index]:Clone() -- Select next Family Root
doUpdate()
btnToPrev.Active = "YES"
if ArrPeople.Index == #ArrPeople then
btnToNext.Active = "NO"
end
end
end -- function btnToNext:action
function btnUpdate:action() -- Action for Check for Updates button -- V1.4
iupDialog.Active = "NO"
CheckVersionInStore("Quick Family Facts",strVersion)
iupDialog.BringFront = "YES"
iupDialog.Active = "YES"
end -- function btnUpdate
function valPoints:valuechanged_cb() -- Action for font Points size slider
local intPoints = IntPoints
IntPoints = math.floor(valPoints.Value)
if IntPoints ~= intPoints then
lblPoints.Title = strPoints()
doUpdate()
end
end -- function valPoints:valuechanged_cb
function lstEvents:action(strText,intItem,intState) -- Action for choose BMD Events dropdown
if intState == 1 then
IntEvents = intItem
doUpdate()
iup.SetFocus(valPoints) -- Ensure Font slider keeps focus
end
end -- function lstEvents:action
function tglRec_Id:action(intState) -- Action for the [Record Id] toggle -- V1.1
tglRec_Id.Tip = tglRec_Id.Tip -- Refresh XP Tooltip
StrRec_Id = tglRec_Id.Value
doUpdate()
end -- function tglRec_Id:action
if fhGetAppVersion() > 6 then iup.SetAttribute( iupDialog, "NATIVEPARENT", fhGetContextInfo("CI_PARENT_HWND") ) end -- V1.4
local function showWindow() -- Display the plugin window -- V1.4
if not TblField.Raster then
iupDialog:map()
local intWinW,intWinH = splitnumbers(iupDialog.NaturalSize) -- Set default window size -- V1.4
TblField.Raster = intWinW.."x"..(intWinH * 1.5) -- Increase height by 50% -- V1.4
end
iupDialog.RasterSize = TblField.Raster
iupDialog.MinSize = iupDialog.NaturalSize
local intMinX,intMinY,intMaxW,intMaxH = splitnumbers(iup.GetGlobal("VIRTUALSCREEN"))
intMaxH = intMaxH - 100 -- Screen size of all monitors allowing for the Taskbar -- V1.4
local intWinW,intWinH = splitnumbers(TblField.Raster) -- Get window dimensions from the previous use of plugin -- V1.4
local intX = tonumber(TblField.CoordX)
local intY = tonumber(TblField.CoordY)
if intX and intY -- Check window dimensions are inside screen dimensions -- V1.4
and intX >= intMinX and intY >= intMinY
and intX + intWinW <= intMaxW
and intY + intWinH <= intMaxH then
iup.ShowXY(iupDialog,intX,intY) -- Show window with previous saved position and size -- V1.4
else
iup.ShowXY(iupDialog,iup.CENTERPARENT,iup.CENTERPARENT) -- Otherwise centre window on FH window with saved size -- V1.4
TblField.CoordX = nil
TblField.CoordY = nil
end
local intX = math.max(tonumber(iupDialog.X),intMinX) -- Ensure window is all within all monitor screen(s) -- V1.4
local intY = math.max(tonumber(iupDialog.Y),intMinY)
if intX + intWinW > intMaxW then intX = intMaxW - intWinW end
if intY + intWinH > intMaxH then intY = intMaxH - intWinH end
iup.ShowXY(iupDialog,intX,intY)
end -- local function showWindow
function btnCentre:action() -- Action for Centre Window button -- V1.4
TblField.CoordX = nil
TblField.CoordY = nil
local MinSize = iupDialog.MinSize
showWindow()
iupDialog.MinSize = MinSize
TblField.CoordX = nil
TblField.CoordY = nil
end -- function btnCentre
function btnCancel:action() -- Action for Close Window button -- V1.4
return iup.CLOSE
end -- function btnCancel
doUpdate() -- Update all panes
showWindow()
iup.MainLoop()
iup.Close()
TblField.Points = IntPoints -- Save sticky settings
TblField.Events = IntEvents
TblField.Rec_Id = StrRec_Id
SaveSettings()
end -- function Main
function SetWeight(arrRow,intStep) -- Set the ExpandWeight per row of panes (must be after Main() so global boxParent/Spouse/Junior exist)
local function setRow(arrA,arrB)
local intRow = math.min(math.max(#arrA,#arrB),40) -- Limit large rows to 40 lines to allow other rows to auto-hide scroll-bar
intRow = intRow + arrB.Weight + FontScale - 1.1 -- Adjust row proportion for +/- Weight buttons and Title FontScale
intRow = intRow - math.min(intRow,(20-IntPoints)/5) + 0.1 -- Adjust inverse of Font Point Size, but must never be 0 that inhibits enlarging
return intRow
end -- local function setRow
if arrRow and intStep then -- Inc/Decrement row Weight in ArrMother/Spouse/Childs driven from +/- buttons
arrRow.Weight = arrRow.Weight + intStep -- or use + ( intStep * 0.2 * #arrRow )
end
local intParent = setRow(ArrFather,ArrMother)
local intSpouse = setRow(ArrPerson,ArrSpouse)
local intJunior = setRow(ArrSiblng,ArrChilds)
local intMean = ( intParent + intSpouse + intJunior ) / 3
boxParent.ExpandWeight = intParent / intMean -- Weight = 0.5 means row is 1/6 of window
boxSpouse.ExpandWeight = intSpouse / intMean -- Weight = 1.0 means row is 1/3 of window
boxJunior.ExpandWeight = intJunior / intMean -- Weight = 1.5 means row is 1/2 of window
iup.Map(iupDialog)
iup.Redraw(iupDialog,1) -- Ensure scroll-bars get redrawn
end -- local function SetWeight
fhInitialise(6,0,0,"save_recommended") -- V1.4
Main()
Source:Quick-Family-Facts-3.fh_lua