Where Used Record Links.fh_lua--[[
@Title: Where Used Record Links
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 2.1
@Keywords:
@LastUpdated: 08 Feb 2024
@Licence: This plugin is copyright (c) 2024 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: List all Where Used Record Links for any selected Records and mark duplicate usage with * and rich text links with @ in 'Data Ref Where Used' column.
@V2.1: Fix default Source record type; Fix =LinksTo() check for prepared Citation Media & Note records; Fix message "No 'Where Used Record Links' found"?;
@V2.0: Fix for _FIELD metafield type = AD in FH V5/V6;
@V1.9: Fix for _FIELD metafield type = AD in doResultSet(); Fix Check Version in Store;
@V1.8: Monthly check version in Plugin Store; Let same record be selected more than once; Fix *@ prefix combo; Fix _FIELD metafield Class;
@V1.7: Fix progbar % miscalculation & add memory garbage collection; Make linked record detection more efficient; In doResultSet(...) correct unique Citation key and duplicate link script and add memory garbage collection to reduce 'Not responding';
@V1.6: Variable width Result Set columns; Templated source/citation metafield and other extra columns; Fix Place record links; Fix LinksTo count for "HEAD.SUB[MN]" in FH V7;
@V1.5: Include rich text @ Record Links allowing multiple instances; Make prompt retainable;
@V1.4: FH V7 Lua 5.3 IUP 3.28 compatible; progbar 3.0; Added unique key for Source Citations; Exclude HEAD link count;
@V1.3: Added test for duplicate usage marked with an * on Data Ref Where Used column.
@V1.2: Added Witness Role & Notes, and correct Witness Citation details to Result Set columns.
@V1.1: Added Media count Result Set columns, fix links from/to validity, and double check counts.
@V1.0: First published in Plugin Store.
]]
local strVersion = "2.1" -- Update when version changes
require "iuplua" -- To access GUI window builder
require "lfs" -- To access LUA filing system
StrPilcrow = " ¶ " -- Newline Pilcrow symbol -- V1.6
if fhGetAppVersion() > 5 then -- Cater for Unicode UTF-8 from FH Version 6 onwards
fhSetStringEncoding("UTF-8")
iup.SetGlobal("UTF8MODE","YES")
iup.SetGlobal("UTF8MODE_FILE","NO")
StrPilcrow = fhConvertANSItoUTF8(StrPilcrow)
end
--[[
@Module: +fh+progbar_v3
@Author: Mike Tate
@Version: 3.0
@LastUpdated: 27 Aug 2020
@Description: Progress Bar library module.
@V3.0: Function Prototype Closure version.
@V1.0: Initial version.
]]
local function progbar_v3()
local fh = {} -- Local environment table
require "iuplua" -- To access GUI window builder
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28
local tblBars = {} -- Table for optional external attributes
local strBack = "255 255 255" -- Background colour default is white
local strBody = "0 0 0" -- Body text colour default is black
local strFont = nil -- Font dialogue default is current font
local strStop = "255 0 0" -- Stop button colour default is red
local intPosX = iup.CENTER -- Show window default position is central
local intPosY = iup.CENTER
local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
local lblText, barGauge, lblDelta, btnStop, dlgGauge
local function doFocus() -- Bring the Progress Bar window into Focus
dlgGauge.BringFront="YES" -- If used too often, inhibits other windows scroll bars, etc
end -- local function doFocus
local function doUpdate() -- Update the Progress Gauge and the Delta % with clock
barGauge.Value = intVal
lblDelta.Title = string.format("%4d %% %s ",math.floor(intPercent),strClock)
end -- local function doUpdate
local function doReset() -- Reset all dialogue variables and Update display
intVal = 0 -- Current value of Progress Bar
intPercent= 0.01 -- Percentage of progress
intStart = os.time() -- Start time of progress
intDelta = 0 -- Delta time of progress
intScale = math.ceil( intMax / 1000 ) -- Scale of percentage per second of progress (initial guess is corrected in Step function)
strClock = "00 : 00 : 00" -- Clock delta time display
isBarStop = false -- Stop button pressed signal
doUpdate()
doFocus()
end -- local function doReset
function fh.Start(strTitle,intMaximum) -- Create & start Progress Bar window
if not dlgGauge then
strTitle = strTitle or "" -- Dialogue and button title
intMax = intMaximum or 100 -- Maximun range of Progress Bar, default is 100
local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30" -- Adjust Stop button size to Title
lblText = iup.label { Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Progress Message"; }
barGauge = iup.progressbar { RasterSize="400x30"; Value=0; Max=intMax; Tip="Progress Bar"; }
lblDelta = iup.label { Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Percentage and Elapsed Time"; }
btnStop = iup.button { Title=" Stop "..strTitle; RasterSize=strSize; FgColor=strStop; Tip="Stop Progress Button"; action=function() isBarStop = true end; } -- Signal Stop button pressed return iup.CLOSE -- Often caused main GUI to close !!!
dlgGauge = iup.dialog { Title=strTitle.." Progress "; Font=strFont; FgColor=strBody; Background=strBack; DialogFrame="YES"; -- Remove Windows minimize/maximize menu
iup.vbox{ Alignment="ACENTER"; Gap="10"; Margin="10x10";
lblText;
barGauge;
lblDelta;
btnStop;
};
move_cb = function(self,x,y) tblBars.X = x tblBars.Y = y end;
close_cb = btnStop.action; -- Windows Close button = Stop button
}
if type(tblBars.GUI) == "table"
and type(tblBars.GUI.ShowDialogue) == "function" then
dlgGauge.move_cb = nil -- Use GUI library to show & move window
tblBars.GUI.ShowDialogue("Bars",dlgGauge,btnStop,"showxy")
else
dlgGauge:showxy(intPosX,intPosY) -- Show the Progress Bar window
end
doReset() -- Reset the Progress Bar display
end
end -- function Start
function fh.Message(strText) -- Show the Progress Bar message
if dlgGauge then lblText.Title = strText end
end -- function Message
function fh.Step(intStep) -- Step the Progress Bar forward
if dlgGauge then
intVal = intVal + ( intStep or 1 ) -- Default step is 1
local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
if intPercent ~= intNew then -- Update progress once per percent or per second, whichever is smaller
intPercent = math.max( 0.1, intNew ) -- Ensure percentage is greater than zero
if intVal > intMax then intVal = intMax intPercent = 100 end -- Ensure values do not exceed maximum
intNew = os.difftime(os.time(),intStart)
if intDelta < intNew then -- Update clock of elapsed time
intDelta = intNew
intScale = math.ceil( intDelta / intPercent ) -- Scale of seconds per percentage step
local intHour = math.floor( intDelta / 3600 )
local intMins = math.floor( intDelta / 60 - intHour * 60 )
local intSecs = intDelta - intMins * 60 - intHour * 3600
strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
end
doUpdate() -- Update the Progress Bar display
end
iup.LoopStep()
end
end -- function Step
function fh.Focus() -- Bring the Progress Bar window to front
if dlgGauge then doFocus() end
end -- function Focus
function fh.Reset() -- Reset the Progress Bar display
if dlgGauge then doReset() end
end -- function Reset
function fh.Stop() -- Check if Stop button pressed
iup.LoopStep()
return isBarStop
end -- function Stop
function fh.Close() -- Close the Progress Bar window
isBarStop = false
if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
end -- function Close
function fh.Setup(tblSetup) -- Setup optional table of external attributes
if tblSetup then
tblBars = tblSetup
strBack = tblBars.Back or strBack -- Background colour
strBody = tblBars.Body or strBody -- Body text colour
strFont = tblBars.Font or strFont -- Font dialogue
strStop = tblBars.Stop or strStop -- Stop button colour
intPosX = tblBars.X or intPosX -- Window position
intPosY = tblBars.Y or intPosY
end
end -- function Setup
return fh
end -- local function progbar_v3
local progbar = progbar_v3() -- To access FH progress bars module
--[[
@Function: BuildDataRef
@Description: Get Full Data Reference for Pointer
@Parameters: Item Pointer
@Returns: Data Reference String, Record Id Integer, Record Type Tag String
@Requires: None
]]
function BuildDataRef(ptrRef)
local strDataRef = "" -- Data Reference with instance indices e.g. INDI.RESI[2].ADDR
local intRecId = 0 -- Record Id for associated Record
local strRecTag = "" -- Record Tag of associated Record type i.e. INDI, FAM, NOTE, SOUR, etc
-- getDataRef() is called recursively per level of the Data Ref
-- ptrRef points to the upper Data Ref levels yet to be analysed
-- strRef compiles the lower Data Ref levels including instances
local function getDataRef(ptrRef,strRef)
local ptrTag = ptrRef:Clone()
local strTag = fhGetTag(ptrTag) -- Current level Tag
ptrTag:MoveToParentItem(ptrTag)
if ptrTag:IsNotNull() then -- Parent level exists
local intSib = 1
local ptrSib = ptrRef:Clone() -- Pointer to siblings with same Tag
ptrSib:MovePrev("SAME_TAG")
while ptrSib:IsNotNull() do -- Count previous siblings with same Tag
intSib = intSib + 1
ptrSib:MovePrev("SAME_TAG")
end
if intSib > 1 then strTag = strTag.."["..intSib.."]" end
getDataRef(ptrTag,"."..strTag..strRef) -- Now analyse the parent level
else
strDataRef = strTag..strRef -- Record level reached, so set return values
intRecId = fhGetRecordId(ptrRef)
strRecTag = strTag
if not fhIsValidDataRef(strDataRef) then print(strDataRef.." is Invalid") end
end
end -- local function getDataRef
if type(ptrRef) == "userdata" then getDataRef(ptrRef,"") end
return strDataRef, intRecId, strRecTag
end -- function BuildDataRef
function intRecordCount(strType) -- Count number of records of chosen Record Type
-- strType ~ Record type tag
local ptrType = fhNewItemPtr()
local intCount = 0
ptrType:MoveToFirstRecord(strType)
while ptrType:IsNotNull() do
intCount = intCount + 1
ptrType:MoveNext()
end
return intCount
end -- function intRecordCount
function Main()
local strRecTag = nil -- Selected Record Type tag
local arrLinks = {} -- Count of Where Used Links per Record
local dicWhere = {} -- Dictionary of Where Used to detect duplicates -- V1.3
local intTwice = 0 -- Count where same record used same place twice -- V1.3
local intUnique = 0 -- Unique key for Citations with identical fields -- V1.4
local intRecMax = fhGetRecordTypeCount() + 1 -- Number of record types including HEAD
local tblRecord = { Min=6; Max=50; } -- Result Set tables
local tblRec_Id = {}
local tblRecObj = {} -- V1.1
local tblUseTyp = {}
local tblUseRec = { Min=6; Max=50; }
local tblUse_Id = {}
local tblUseObj = {} -- V1.1
local tblUseRef = {}
local tblUseTxt = { Min=6; Max=50; }
local tblUsePtr = {}
local tblColumn = {} -- Table of more columns for Record Type
local dicType =
{ INDI = { Col={}; Name="Individual"; }; -- Record Type dictionary
FAM = { Col={}; Name="Family"; };
NOTE = { Col={}; Name="Note"; };
SOUR = { Col={}; Name="Source"; };
REPO = { Col={}; Name="Repository"; };
SUBM = { Col={}; Name="Submitter"; };
SUBN = { Col={}; Name="Submission"; };
OBJE = { Col={}; Name="Media"; };
_PLAC = { Col={}; Name="Place"; };
_RNOT = { Col={}; Name="Research Note"; }; -- V1.4
_SRCT = { Col={}; Name="Source Template"; }; -- V1.4
HEAD = { Col={}; Name="Head"; };
}
local function doTypeDetails(strLink,strRef,intMax,strName,strMode) -- More Result Set Column details for Record Type dictionary
-- strLink ~ Link record type tag
-- strRef ~ Data ref relative to link or "Media" or "Key"
-- intMax ~ Max char width of Result Set column
-- strName ~ Name of Result Set column
-- strMode ~ Mode of Result Set column if not "item"
local dicType = dicType[strLink]
if strRef == "Media" then -- V1.1
dicType.Media = true
elseif strRef then
local intMin = 3 -- Min char width of Result Set column for "item" pointers -- V1.6
if strMode then intMin = 6 end -- Larger width for other column modes "integer" or "text"
local intMore = #dicType.Col + 1 -- Column number index -- V1.6
table.insert(dicType.Col,{ More=intMore; Text=0; Char=0; Ref=strRef; Min=intMin; Max=intMax; Name=strName; Mode=(strMode or "item"); }) -- V1.6
tblColumn[intMore] = {} -- dicType.Col and tblColumn share same index -- V1.6
end
end -- local function doTypeDetails
doTypeDetails( "INDI" , "Media" ) -- V1.1
doTypeDetails( "INDI" , "~.ROLE" , 25, "Role/Association") -- V1.2
doTypeDetails( "INDI" , "~.NOTE2" , 50, "Local Note" ) -- V1.2
doTypeDetails( "INDI" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails( "FAM" , "Media" ) -- V1.1
doTypeDetails( "FAM" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails( "NOTE" )
doTypeDetails( "NOTE" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails( "SOUR" , "Media" ) -- V1.1
doTypeDetails( "SOUR" , "Key" , 5, "Key", "integer" ) -- Unique Citation Key -- V1.4
doTypeDetails( "SOUR" , "~.DATA.DATE" , 25, "Entry Date" )
doTypeDetails( "SOUR" , "~.QUAY" , 25, "Assessment" ) -- FH V7 also finds "~._QUAY"
doTypeDetails( "SOUR" , "~.PAGE" , 50, "Where within Source" )
doTypeDetails( "SOUR" , "~.DATA.TEXT" , 50, "Text From Source" )
doTypeDetails( "SOUR" , "~.NOTE2" , 50, "Citation Note" )
doTypeDetails( "SOUR" , "~._FIELD[1]" , 50, "Metafield 1" ) -- V1.6
doTypeDetails( "SOUR" , "~._FIELD[2]" , 50, "Metafield 2" ) -- V1.6
doTypeDetails( "SOUR" , "~._FIELD[3]" , 50, "Metafield 3" ) -- V1.6
doTypeDetails( "REPO" , "+._TYPE" , 25, "Generic Type" )
doTypeDetails( "REPO" , "+.AUTH" , 25, "Author" )
doTypeDetails( "REPO" , "+.PUBL" , 50, "Publication Information" )
doTypeDetails( "REPO" , "+.TEXT" , 50, "Text From Source" )-- V1.6
doTypeDetails( "REPO" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails( "REPO" , "+._FIELD[1]" , 50, "Metafield 1" ) -- V1.6
doTypeDetails( "REPO" , "+._FIELD[2]" , 50, "Metafield 2" ) -- V1.6
doTypeDetails( "REPO" , "+._FIELD[3]" , 50, "Metafield 3" ) -- V1.6
doTypeDetails( "REPO" , "+._FIELD[4]" , 50, "Metafield 4" ) -- V1.6
doTypeDetails( "REPO" , "+._FIELD[5]" , 50, "Metafield 5" ) -- V1.6
doTypeDetails( "SUBM" )
doTypeDetails( "SUBM" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails( "SUBN" )
doTypeDetails( "SUBN" , "+.SUBM" , 50, "Submitter" ) -- V1.6
local strAnnotation = ">NOTE2" -- FH V6
if fhGetAppVersion() > 6 then
strAnnotation = ">NOTE2._NOTA" -- FH V7 -- V1.4
end
doTypeDetails( "OBJE" , ">NOTE2._AREA", 25, "Frame Area" )
doTypeDetails( "OBJE" , ">NOTE2._EXCL", 25, "Exclude From" )
doTypeDetails( "OBJE" , ">NOTE2._CAPT", 25, "Caption?" )
doTypeDetails( "OBJE" , strAnnotation , 50, "Link Note" ) -- FH V7 -- V1.4 -- V1.6
doTypeDetails( "OBJE" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails("_PLAC" , "Media" ) -- V1.1
doTypeDetails("_PLAC" , "<.ADDR" , 50, "Fact Address" )
doTypeDetails("_PLAC" , "<.NOTE2" , 50, "Fact Note" )
doTypeDetails("_PLAC" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails("_RNOT" ) -- V1.4
doTypeDetails("_RNOT" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails("_SRCT" ) -- V1.4
doTypeDetails("_SRCT" , "+._TYPE" , 25, "Generic Type" ) -- V1.6
doTypeDetails("_SRCT" , "+.AUTH" , 25, "Author" ) -- V1.6
doTypeDetails("_SRCT" , "+.PUBL" , 50, "Publication Information" )
doTypeDetails("_SRCT" , "+.TEXT" , 50, "Text From Source" )-- V1.6
doTypeDetails("_SRCT" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails("_SRCT" , "+._FIELD[1]" , 50, "Metafield 1" ) -- V1.6
doTypeDetails("_SRCT" , "+._FIELD[2]" , 50, "Metafield 2" ) -- V1.6
doTypeDetails("_SRCT" , "+._FIELD[3]" , 50, "Metafield 3" ) -- V1.6
doTypeDetails("_SRCT" , "+._FIELD[4]" , 50, "Metafield 4" ) -- V1.6
doTypeDetails("_SRCT" , "+._FIELD[5]" , 50, "Metafield 5" ) -- V1.6
doTypeDetails( "HEAD" )
doTypeDetails( "HEAD" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
local function arrMediaCount(arrObje) -- Count Media Objects -- V1.1
-- arrObje ~ Array listing media
local arrMedia = {}
for intMedia = 1, #arrObje do arrMedia[intMedia] = 0 end -- Zero Media counters -- V1.1
repeat
local anyMedia = false
for intMedia, ptrMedia in ipairs (arrObje) do
if ptrMedia:IsNotNull() then
anyMedia = true
arrMedia[intMedia] = arrMedia[intMedia] + 1 -- Count each kind of Media -- V1.1
ptrMedia:MoveNext("SAME_TAG")
end
end
until not anyMedia
return arrMedia
end -- local function arrMediaCount
local function doTextLength(tblItem,anyItem) -- tblItem.Long = longest text in column, tblItem.Char = next to longest -- V1.6
-- tblItem ~ Table that holds parameters
-- anyItem ~ Length of text or pointer to text
local intItem = anyItem
if type(anyItem) ~= "number" then
intItem = #fhGetDisplayText(anyItem,"","MIN") + 1 -- Get display text length for pointer item + 1 for record icon
end
if intItem > 0 then
intItem = intItem - math.floor(intItem/10) -- Reduce length by 10% otherwise columns are too wide
if intItem > ( tblItem.Long or 0 ) then
tblItem.Char = tblItem.Long or intItem -- Previous longest item or initial item length
tblItem.Long = intItem -- Save length of longest item so far
elseif intItem < tblItem.Long then
if tblItem.Char == tblItem.Long then
tblItem.Char = 0 -- Ensure subsequent item char length is used
end
tblItem.Char = math.max(intItem,tblItem.Char) -- Save length of next to longest item so far
end
end
end -- local function doTextLength
local function doResultSet(intRecord,ptrRecord,intType,ptrUsed,ptrItem)
-- intRecord ~ Index into current record list
-- ptrRecord ~ Pointer to current record item
-- intType ~ Index to the current record type
-- ptrUsed ~ Pointer to parent record where current record used
-- ptrItem ~ Pointer to the data item where current record used
doTextLength(tblRecord,ptrRecord)
table.insert(tblRecord,ptrRecord:Clone()) -- Update Result Set table columns
table.insert(tblRec_Id,fhGetRecordId(ptrRecord))
if dicType[strRecTag].Media then -- V1.1
local arrObje = {
fhGetItemPtr(ptrRecord,"~.OBJE") ; -- Media pointers -- V1.1
fhGetItemPtr(ptrRecord,"~.OBJE2");
}
local arrMedia = arrMediaCount(arrObje) -- Media counts -- V1.1
table.insert(tblRecObj,tostring(arrMedia[1]+arrMedia[2]))
end
table.insert(tblUseTyp,intType or 99)
if ptrUsed then -- Record has Where Used Link
local strData, intRecId = BuildDataRef(ptrItem)
local ptrRoot = fhNewItemPtr()
ptrRoot:MoveToParentItem(ptrItem)
local arrObje = {
fhGetItemPtr(ptrRoot,"~.OBJE") ; -- Media pointers -- V1.1
fhGetItemPtr(ptrRoot,"~.OBJE2");
fhGetItemPtr(ptrItem,"~.OBJE") ;
fhGetItemPtr(ptrItem,"~.OBJE2");
}
local strUnique, intKeyCol -- Citation unique fields and "Key" column -- V1.4
local arrMedia = arrMediaCount(arrObje) -- Media counts -- V1.1
local strRole = fhGetValueAsText(fhGetItemPtr(ptrItem,"~.ROLE")) -- Need Role only for Individuals in duplicate detection below -- V1.3
doTextLength(tblUseRec,ptrUsed)
table.insert(tblUseRec,ptrUsed:Clone())
table.insert(tblUse_Id,intRecId)
table.insert(tblUseObj,tostring(arrMedia[1]+arrMedia[2]).." / "..tostring(arrMedia[3]+arrMedia[4])) -- V1.1
doTextLength(tblUseTxt,ptrRoot)
table.insert(tblUseTxt,fhGetDisplayText(ptrRoot))
if strData:match("_SHAR") then
table.insert(tblUsePtr,ptrRoot:Clone()) -- ptrRoot is better for Witness links for this field -- V1.2
else
table.insert(tblUsePtr,ptrItem:Clone()) -- but must retain ptrItem for all subsequent fields -- V1.2
end
for intCol, tblCol in ipairs (dicType[strRecTag].Col) do
local strRef = tblCol.Ref -- Update optional Result Set columns depending on search record type
if strRef == "Key" then
strUnique = tostring(intRecord)..":" -- Unique source for replica citation Key column -- V1.4
intKeyCol = tblCol.More
else
if strRef:match("ROLE") and strData:match("INDI.ASSO") then -- V1.6
strRef = strRef:gsub("ROLE","RELA")
elseif strRef:match("^%+") then -- Reference is relative to record where used -- V1.6
strRef = strRef:gsub("^%+","~")
ptrItem = ptrUsed:Clone()
elseif strRef:match("^<") then -- Reference is relative to parent of link -- V1.6
strRef = strRef:gsub("^<","~")
ptrItem = ptrRoot:Clone()
elseif strRef:match("^>NOTE2") then -- Reference is a media link note
strRef = strRef:gsub("^>NOTE2","~")
if fhGetAppVersion() <= 6 then -- V1.4
local strAsid = fhGetValueAsText(fhGetItemPtr(ptrItem,"~._ASID"))
local ptrNote = fhNewItemPtr()
ptrNote:MoveTo(ptrRecord,"~.NOTE2")
while ptrNote:IsNotNull() do
if strAsid == fhGetValueAsText(fhGetItemPtr(ptrNote,"~._ASID")) then
ptrItem = ptrNote:Clone()
break
end
ptrNote:MoveNext("SAME_TAG")
end
end
end
local anyRef = fhGetItemPtr(ptrItem,strRef)
local strRef = ""
if tblCol.Mode == "text" then
strRef = fhGetDisplayText(anyRef)
anyRef = fhGetValueAsText(anyRef)
doTextLength(tblCol,#anyRef) -- Set length of text to which column can be shrunk -- V1.6
elseif tblCol.Mode == "item" then
local strClass = fhGetDataClass(anyRef)
if strClass == "longtext" or strClass == "richtext"
or ( fhGetAppVersion() > 6 and fhGetMetafieldType(anyRef) == "AD" ) then -- _FIELD is an Address -- V2.0
tblCol.Min = 6
tblCol.Class = "longtext" -- Get field long/richtext to go with a buddy column -- V1.6
strRef = fhGetValueAsText(anyRef):gsub("\n",StrPilcrow)
else
strRef = fhGetDisplayText(anyRef,"","MIN") -- Get display text for other field types
end
doTextLength(tblCol,#strRef) -- Set length of text to which column can be shrunk -- V1.6
if tblCol.Text == 0 then
tblColumn[#tblColumn+1] = { } -- Add "text" column to go with "item" buddy column -- V1.6
tblCol.Text = #tblColumn
end
table.insert(tblColumn[tblCol.Text],strRef)
end
table.insert(tblColumn[tblCol.More],anyRef)
if intKeyCol then
strUnique = strUnique..strRef -- Unique text for replica citation detection -- V1.7 code move here
end
end
end
if intKeyCol then
if not dicWhere[strUnique] then -- Unique key number for each replica citation
intUnique = intUnique + 1
dicWhere[strUnique] = intUnique
end
table.insert(tblColumn[intKeyCol],dicWhere[strUnique])
end
arrLinks[intRecord] = arrLinks[intRecord] or 0 -- V2.1
if intType < intRecMax
or strData ~= "HEAD._PCIT.SOUR" then -- HEAD._PCIT.SOUR record links discounted by LinksTo() function -- V1.4 -- V1.6 -- V2.1
arrLinks[intRecord] = arrLinks[intRecord] + 1
end
-- Detect duplicate usage for intRecord of intType in intRecId for strData Ref & Role -- V1.3 -- V1.7 code moved here
local strWhat = strData:gsub("%[%d+%]$","") -- Remove a trailing Data Reference [index] -- V1.3
strWhat = tostring(intRecord).."of"..intType.."in"..intRecId..strWhat..strRole..(dicWhere[strUnique] or "") -- V1.7
local intUsed = dicWhere[strWhat] -- Lookup dictionary to see if used before -- V1.3
if intUsed then -- Used before so have old Result Set entry -- V1.3
intTwice = intTwice + 1
strData = "*"..strData -- Asterisk the new & old duplicate entries -- V1.3
tblUseRef[intUsed] = tblUseRef[intUsed]:gsub("^%**","*")
else
dicWhere[strWhat] = #tblUseRef+1 -- Dictionary entry with Result Set index -- V1.3
end
strData = strData:gsub("^(%*?)(.-%._LINK_%u)","%1@%2") -- Mark rich text Record Links with @ symbol -- V1.5 -- V1.8
table.insert(tblUseRef,strData)
else
local strUnused = "< No 'Where Used Record Links' found. >"
table.insert(tblUseRec,fhNewItemPtr()) -- Record is unused
table.insert(tblUse_Id,nil)
doTextLength(tblUseTxt,#strUnused) -- V1.6
table.insert(tblUseTxt, strUnused)
table.insert(tblUsePtr,fhNewItemPtr())
end
collectgarbage("step",0) -- Memory garbage collection to avoid 'Not responding' -- V1.7
end -- local function doResultSet
local function isLink(ptrItem) -- Detect fhGetDataClass() & fhGetMetafieldType() links -- V1.8
local dicLink = {
place = true ; -- Place _PLAC record link
link = true ; -- Any other record link
PL = true ; -- Metafield _FIELD with Place record link
RP = true ; -- Metafield _FIELD with Repository link
}
local strClass = fhGetDataClass(ptrItem)
if strClass == "metafield" then
strClass = fhGetMetafieldType(ptrItem)
end
return dicLink[strClass]
end -- local function isLink
local tblLinks = { -- Links from/to validity table and Names -- V1.1 -- V1.4 -- V1.5 only HEAD has no rich text links
INDI = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Individuals"; };
FAM = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Families"; };
NOTE = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Notes"; };
SOUR = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Sources"; };
REPO = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Repositories"; };
SUBM = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Submitters"; };
SUBN = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Submissions"; };
OBJE = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Multimedia"; };
_PLAC= { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Places"; };
_RNOT= { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Research Notes"; }; -- V1.4
_SRCT= { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Source Templates"; }; -- V1.4
HEAD = { INDI=nil; FAM=nil; NOTE=true; SOUR=true; REPO=nil; SUBM=true; SUBN=true; OBJE=true; _PLAC=nil; _RNOT=nil; _SRCT=nil; HEAD=nil; Name="Headers"; };
}
local strFileName = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\Where Used Record Links.dat"
local lfsFileName = lfs.attributes(strFileName,"mode") == "file"
for intType = 1, intRecMax do -- Search each record type
local strType = ""
local arrRecs = {} -- Array of selected records
local dicRecs = {} -- Dictionary of Record Id (or Placenames) to array index of selected records
if intType < intRecMax and lfsFileName then -- Check if any Records pre-selected -- V1.5
strType = fhGetRecordTypeTag(intType)
arrRecs = fhGetCurrentRecordSel(strType)
else
local ptrItem = fhNewItemPtr()
local strForm = " Record Type: %l| " -- Otherwise prompt for Record selection
local strKeep = " Keep Prompt: %b[ Prompt hidden, Hover for advice]{Untick to hide this prompt and always select records before running plugin}\n" -- V1.5
local intKeep = 1 -- V1.5
if lfsFileName then intKeep = 0 end
local dicData = {}
local intData = 0
for intType = 1, intRecMax-1 do -- Build iup.GetParam droplist format
strType = fhGetRecordTypeTag(intType)
ptrItem:MoveToFirstRecord(strType)
if ptrItem:IsNotNull() then -- Records of this Type exist
strForm = strForm..dicType[strType].Name.." | "
table.insert(dicData,intType)
if strType == "SOUR" then intData = #dicData-1 end -- Default to Source Record Type -- V2.1
end
end
strForm = strForm.."|{Choose the required record type to analyse}\n" -- V1.5
local isOK, intData, intKeep = iup.GetParam("Choose Record Type",nil,strForm..strKeep,intData,intKeep)
if isOK then
strType = fhGetRecordTypeTag(dicData[intData+1])
arrRecs = fhPromptUserForRecordSel(strType) -- Prompt for Record selection
if intKeep == 0 then
local fileHandle = io.open(strFileName,"w") -- When file exists prompt is hidden -- V1.5
assert(fileHandle:close())
else
os.remove(strFileName) -- When file missing prompt is shown -- V1.5
end
else
break
end
end
if #arrRecs > 0 then -- Selected records exist
local intStep = 100 / #arrRecs -- Progress bar step increment
strRecTag = strType
progbar.Start("Where Used Record Links looking for "..tblLinks[strType].Name,100)
progbar.Message("Building Search Index")
for intRecord, ptrRecord in ipairs (arrRecs) do -- Build a Record Id index to target records -- V1.7
local intRecId = fhGetRecordId(ptrRecord)
dicRecs[intRecId] = intRecord -- This avoids searching the array of records
progbar.Step(intStep)
end
fhSleep(400,300)
for intType = 1, intRecMax do -- Search each record type
local strType = fhGetRecordTypeTag(intType)
local intStep = 100 / intRecordCount(strType) -- V1.7
local ptrItem = fhNewItemPtr()
local ptrUsed = fhNewItemPtr()
if intType == intRecMax then -- Special HEAD case
strType = "HEAD"
intStep = 1
end
if tblLinks[strType][strRecTag] then -- This record type can link to selected records -- V1.1
progbar.Message("Searching "..tblLinks[strType].Name)
progbar.Reset()
progbar.Step(1)
ptrItem:MoveToFirstRecord(strType) -- Search every field
while ptrItem:IsNotNull() do
if not fhHasParentItem(ptrItem) then -- Next record reached
ptrUsed = ptrItem:Clone()
progbar.Step(intStep) -- Advance progress bar
if progbar.Stop() then
error("User Cancelled")
end
collectgarbage("step",0) -- Memory garbage collection to avoid 'Not responding' -- V1.7
end
if isLink(ptrItem) then -- Found any kind of record link -- V1.7 -- V1.8
local ptrLink = fhGetValueAsLink(ptrItem)
if strRecTag == fhGetTag(ptrLink) then -- Found right type of record link -- V1.7
local intRecord = dicRecs[fhGetRecordId(ptrLink)]
if intRecord then -- Found a where used record link -- V1.7
doResultSet(intRecord,arrRecs[intRecord],intType,ptrUsed,ptrItem)
end
end
end
ptrItem:MoveNextSpecial()
end
end
end
if #tblRecord > 0 then -- Found some record links
for intRecId, intRecord in pairs(dicRecs) do -- This lets same record be selected more than once -- V1.8
local ptrRecord = arrRecs[intRecord]
if not arrLinks[intRecord] then
doResultSet(intRecord,ptrRecord) -- Report any missing links
arrLinks[intRecord] = 0
end
local intLinks = fhCallBuiltInFunction("LinksTo",ptrRecord)
if intLinks ~= arrLinks[intRecord] then -- V1.1 double check counts
fhMessageBox("\n =LinksTo() "..intLinks.." disagrees with "..arrLinks[intRecord].." links found! \n")
end
end
end
break
end
end
if #tblRecord > 0 then
local function intSize(tblCol) -- Set width of column based on length of contents -- V1.6
local intChar = tblCol.Char or 0 -- Character length of penultimate longest content
local intLong = tblCol.Long or 0 -- Character length of longest content
local intMax = tblCol.Max or 50 -- Maximum size in characters
local intMin = tblCol.Min or 6 -- Minimum size in characters
local intSize = math.min( math.max(intMin,intChar), intMax ) * 4
-- print(intChar,intLong,intMin,intMax,intSize,tblCol.Mode or "item",tblCol.Class,tblCol.Text)
return intSize
end
progbar.Reset("Loading Result Set")
local strRecordName = dicType[strRecTag].Name.." Record Name"
fhOutputResultSetTitles("Where Used Record Links "..strVersion)
fhOutputResultSetColumn(strRecordName , "item" , tblRecord, #tblRecord, intSize(tblRecord) , "align_left")
fhOutputResultSetColumn("Rec Id" , "integer", tblRec_Id, #tblRecord, 28 , "align_mid" , 1)
if #tblRecObj > 0 then
fhOutputResultSetColumn("Media" , "text" , tblRecObj, #tblRecord, 28 , "align_mid" ) -- V1.1
end
fhOutputResultSetColumn("Hide Record Type" , "integer", tblUseTyp, #tblRecord, 10 , "align_left", 2, true, "default", "hide")
fhOutputResultSetColumn("Record Where Used" , "item" , tblUseRec, #tblRecord, intSize(tblUseRec) , "align_left")
fhOutputResultSetColumn("Rec Id" , "integer", tblUse_Id, #tblRecord, 28 , "align_mid" , 3)
fhOutputResultSetColumn("Media" , "text" , tblUseObj, #tblRecord, 28 , "align_mid" ) -- V1.1
fhOutputResultSetColumn("Data Ref Where Used", "text" , tblUseRef, #tblRecord, 90 , "align_left", 4)
fhOutputResultSetColumn("Buddy Where Used" , "item" , tblUsePtr, #tblRecord, 10 , "align_left", 0, true, "default", "buddy")
fhOutputResultSetColumn("Field Where Used" , "text" , tblUseTxt, #tblRecord, intSize(tblUseTxt) , "align_left", 5)
fhOutputResultSetColumn("Buddy Where Used" , "item" , tblUsePtr, #tblRecord, 10 , "align_left", 0, true, "default", "buddy")
local dicCol = dicType[strRecTag].Col
for intCol = #dicCol, 1, -1 do -- Remove empty rightmost Metafield columns -- V1.6
local tblCol = dicCol[intCol]
if tblCol.Ref:match("_FIELD") and tblCol.Char == 0 then
dicCol[intCol] = nil
else
break
end
end
for intCol, tblCol in ipairs (dicCol) do -- Optional columns
if intCol == #dicCol then
tblCol.Max = 1000 -- Rightmost column can be very wide -- V1.6
tblCol.Char = tblCol.Long
end
if tblCol.Class == "longtext" then -- Long text displayed in full with buddy item pointer -- V1.6
fhOutputResultSetColumn(tblCol.Name, "text" , tblColumn[tblCol.Text], #tblRecord, intSize(tblCol) , "align_left")
fhOutputResultSetColumn(tblCol.Name, "item" , tblColumn[tblCol.More], #tblRecord, 10 , "align_left", 0, true, "default", "buddy" )
else
fhOutputResultSetColumn(tblCol.Name,tblCol.Mode,tblColumn[tblCol.More], #tblRecord, intSize(tblCol) , "align_left")
end
end
progbar.Close()
if intTwice > 0 then -- V1.3
fhMessageBox("\n "..intTwice.." duplicate 'Where Used Record Links' found. \n\n See 'Data Ref Where Used' asterisked entries. \n Click its column header to list together at top. \n")
end
else
if strRecTag then
progbar.Close()
fhMessageBox("\n No 'Where Used Record Links' found. \n")
else
fhMessageBox("\n Please select one or more Records then run the Plugin. \n")
end
end
end -- function Main()
--[[
@Function: CheckVersionInStore
@Author: Mike Tate
@Version: 1.3
@LastUpdated: 03 May 2022
@Description: Check plugin version against version in Plugin Store
@Parameter: Plugin name and version
@Returns: None
@Requires: lfs & luacom
@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 "lfs"
require "luacom"
local function OpenFile(strFileName,strMode) -- Open File and return 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 -- local function OpenFile
local function SaveStringToFile(strString,strFileName) -- Save string to file
local fileHandle = OpenFile(strFileName,"w")
fileHandle:write(strString)
assert(fileHandle:close())
end -- local function SaveStringToFile
local function StrLoadFromFile(strFileName) -- Load string from file
local fileHandle = OpenFile(strFileName,"r")
local strContents = fileHandle:read("*all")
assert(fileHandle:close())
return strContents
end -- local function StrLoadFromFile
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 strPath = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\"
local strFile = strPath.."VersionInStore "..strPlugin..".dat"
local intTime = os.time() - 2600000 -- Time in seconds a month ago
local tblAttr, strError = lfs.attributes(strFile) -- Obtain file attributes
if not tblAttr or tblAttr.modification < intTime then -- File does not exist or was modified long ago
if lfs.attributes(strPath,"mode") ~= "directory" then
if not lfs.mkdir(strPath) then return end -- Ensure the Plugin Data folder exists
end
local strErrFile = strPath.."VersionInStoreInternetError.dat"
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
local intTime = os.time() - 36000 -- Time in seconds 10 hours ago
local tblAttr, strError = lfs.attributes(strErrFile) -- Obtain file attributes
if not tblAttr or tblAttr.modification < intTime then -- File does not exist or was modified long ago
fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ")
end
SaveStringToFile(strErrFile,strErrFile) -- Update file modified time
else
os.remove(strErrFile) -- Delete file if Internet is OK
if strReturn then
strLatest = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits
SaveStringToFile(strLatest,strFile) -- Update file modified time and save version
end
end
else
strLatest = StrLoadFromFile(strFile) -- Retrieve saved latest version
end
end
if intVersion(strLatest) > intVersion(strVersion or "0") then
fhMessageBox("Later Version "..strLatest.." of this Plugin is available from the Plugin Store.")
end
end -- function CheckVersionInStore
-- Main Code Section Starts Here --
fhInitialise(5,0,0,"save_recommended")
CheckVersionInStore("Where Used Record Links",strVersion) -- Notify if later Version -- V1.8
Main()
--[[
@Title: Where Used Record Links
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 2.1
@Keywords:
@LastUpdated: 08 Feb 2024
@Licence: This plugin is copyright (c) 2024 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: List all Where Used Record Links for any selected Records and mark duplicate usage with * and rich text links with @ in 'Data Ref Where Used' column.
@V2.1: Fix default Source record type; Fix =LinksTo() check for prepared Citation Media & Note records; Fix message "No 'Where Used Record Links' found"?;
@V2.0: Fix for _FIELD metafield type = AD in FH V5/V6;
@V1.9: Fix for _FIELD metafield type = AD in doResultSet(); Fix Check Version in Store;
@V1.8: Monthly check version in Plugin Store; Let same record be selected more than once; Fix *@ prefix combo; Fix _FIELD metafield Class;
@V1.7: Fix progbar % miscalculation & add memory garbage collection; Make linked record detection more efficient; In doResultSet(...) correct unique Citation key and duplicate link script and add memory garbage collection to reduce 'Not responding';
@V1.6: Variable width Result Set columns; Templated source/citation metafield and other extra columns; Fix Place record links; Fix LinksTo count for "HEAD.SUB[MN]" in FH V7;
@V1.5: Include rich text @ Record Links allowing multiple instances; Make prompt retainable;
@V1.4: FH V7 Lua 5.3 IUP 3.28 compatible; progbar 3.0; Added unique key for Source Citations; Exclude HEAD link count;
@V1.3: Added test for duplicate usage marked with an * on Data Ref Where Used column.
@V1.2: Added Witness Role & Notes, and correct Witness Citation details to Result Set columns.
@V1.1: Added Media count Result Set columns, fix links from/to validity, and double check counts.
@V1.0: First published in Plugin Store.
]]
local strVersion = "2.1" -- Update when version changes
require "iuplua" -- To access GUI window builder
require "lfs" -- To access LUA filing system
StrPilcrow = " ¶ " -- Newline Pilcrow symbol -- V1.6
if fhGetAppVersion() > 5 then -- Cater for Unicode UTF-8 from FH Version 6 onwards
fhSetStringEncoding("UTF-8")
iup.SetGlobal("UTF8MODE","YES")
iup.SetGlobal("UTF8MODE_FILE","NO")
StrPilcrow = fhConvertANSItoUTF8(StrPilcrow)
end
--[[
@Module: +fh+progbar_v3
@Author: Mike Tate
@Version: 3.0
@LastUpdated: 27 Aug 2020
@Description: Progress Bar library module.
@V3.0: Function Prototype Closure version.
@V1.0: Initial version.
]]
local function progbar_v3()
local fh = {} -- Local environment table
require "iuplua" -- To access GUI window builder
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28
local tblBars = {} -- Table for optional external attributes
local strBack = "255 255 255" -- Background colour default is white
local strBody = "0 0 0" -- Body text colour default is black
local strFont = nil -- Font dialogue default is current font
local strStop = "255 0 0" -- Stop button colour default is red
local intPosX = iup.CENTER -- Show window default position is central
local intPosY = iup.CENTER
local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
local lblText, barGauge, lblDelta, btnStop, dlgGauge
local function doFocus() -- Bring the Progress Bar window into Focus
dlgGauge.BringFront="YES" -- If used too often, inhibits other windows scroll bars, etc
end -- local function doFocus
local function doUpdate() -- Update the Progress Gauge and the Delta % with clock
barGauge.Value = intVal
lblDelta.Title = string.format("%4d %% %s ",math.floor(intPercent),strClock)
end -- local function doUpdate
local function doReset() -- Reset all dialogue variables and Update display
intVal = 0 -- Current value of Progress Bar
intPercent= 0.01 -- Percentage of progress
intStart = os.time() -- Start time of progress
intDelta = 0 -- Delta time of progress
intScale = math.ceil( intMax / 1000 ) -- Scale of percentage per second of progress (initial guess is corrected in Step function)
strClock = "00 : 00 : 00" -- Clock delta time display
isBarStop = false -- Stop button pressed signal
doUpdate()
doFocus()
end -- local function doReset
function fh.Start(strTitle,intMaximum) -- Create & start Progress Bar window
if not dlgGauge then
strTitle = strTitle or "" -- Dialogue and button title
intMax = intMaximum or 100 -- Maximun range of Progress Bar, default is 100
local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30" -- Adjust Stop button size to Title
lblText = iup.label { Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Progress Message"; }
barGauge = iup.progressbar { RasterSize="400x30"; Value=0; Max=intMax; Tip="Progress Bar"; }
lblDelta = iup.label { Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Percentage and Elapsed Time"; }
btnStop = iup.button { Title=" Stop "..strTitle; RasterSize=strSize; FgColor=strStop; Tip="Stop Progress Button"; action=function() isBarStop = true end; } -- Signal Stop button pressed return iup.CLOSE -- Often caused main GUI to close !!!
dlgGauge = iup.dialog { Title=strTitle.." Progress "; Font=strFont; FgColor=strBody; Background=strBack; DialogFrame="YES"; -- Remove Windows minimize/maximize menu
iup.vbox{ Alignment="ACENTER"; Gap="10"; Margin="10x10";
lblText;
barGauge;
lblDelta;
btnStop;
};
move_cb = function(self,x,y) tblBars.X = x tblBars.Y = y end;
close_cb = btnStop.action; -- Windows Close button = Stop button
}
if type(tblBars.GUI) == "table"
and type(tblBars.GUI.ShowDialogue) == "function" then
dlgGauge.move_cb = nil -- Use GUI library to show & move window
tblBars.GUI.ShowDialogue("Bars",dlgGauge,btnStop,"showxy")
else
dlgGauge:showxy(intPosX,intPosY) -- Show the Progress Bar window
end
doReset() -- Reset the Progress Bar display
end
end -- function Start
function fh.Message(strText) -- Show the Progress Bar message
if dlgGauge then lblText.Title = strText end
end -- function Message
function fh.Step(intStep) -- Step the Progress Bar forward
if dlgGauge then
intVal = intVal + ( intStep or 1 ) -- Default step is 1
local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
if intPercent ~= intNew then -- Update progress once per percent or per second, whichever is smaller
intPercent = math.max( 0.1, intNew ) -- Ensure percentage is greater than zero
if intVal > intMax then intVal = intMax intPercent = 100 end -- Ensure values do not exceed maximum
intNew = os.difftime(os.time(),intStart)
if intDelta < intNew then -- Update clock of elapsed time
intDelta = intNew
intScale = math.ceil( intDelta / intPercent ) -- Scale of seconds per percentage step
local intHour = math.floor( intDelta / 3600 )
local intMins = math.floor( intDelta / 60 - intHour * 60 )
local intSecs = intDelta - intMins * 60 - intHour * 3600
strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
end
doUpdate() -- Update the Progress Bar display
end
iup.LoopStep()
end
end -- function Step
function fh.Focus() -- Bring the Progress Bar window to front
if dlgGauge then doFocus() end
end -- function Focus
function fh.Reset() -- Reset the Progress Bar display
if dlgGauge then doReset() end
end -- function Reset
function fh.Stop() -- Check if Stop button pressed
iup.LoopStep()
return isBarStop
end -- function Stop
function fh.Close() -- Close the Progress Bar window
isBarStop = false
if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
end -- function Close
function fh.Setup(tblSetup) -- Setup optional table of external attributes
if tblSetup then
tblBars = tblSetup
strBack = tblBars.Back or strBack -- Background colour
strBody = tblBars.Body or strBody -- Body text colour
strFont = tblBars.Font or strFont -- Font dialogue
strStop = tblBars.Stop or strStop -- Stop button colour
intPosX = tblBars.X or intPosX -- Window position
intPosY = tblBars.Y or intPosY
end
end -- function Setup
return fh
end -- local function progbar_v3
local progbar = progbar_v3() -- To access FH progress bars module
--[[
@Function: BuildDataRef
@Description: Get Full Data Reference for Pointer
@Parameters: Item Pointer
@Returns: Data Reference String, Record Id Integer, Record Type Tag String
@Requires: None
]]
function BuildDataRef(ptrRef)
local strDataRef = "" -- Data Reference with instance indices e.g. INDI.RESI[2].ADDR
local intRecId = 0 -- Record Id for associated Record
local strRecTag = "" -- Record Tag of associated Record type i.e. INDI, FAM, NOTE, SOUR, etc
-- getDataRef() is called recursively per level of the Data Ref
-- ptrRef points to the upper Data Ref levels yet to be analysed
-- strRef compiles the lower Data Ref levels including instances
local function getDataRef(ptrRef,strRef)
local ptrTag = ptrRef:Clone()
local strTag = fhGetTag(ptrTag) -- Current level Tag
ptrTag:MoveToParentItem(ptrTag)
if ptrTag:IsNotNull() then -- Parent level exists
local intSib = 1
local ptrSib = ptrRef:Clone() -- Pointer to siblings with same Tag
ptrSib:MovePrev("SAME_TAG")
while ptrSib:IsNotNull() do -- Count previous siblings with same Tag
intSib = intSib + 1
ptrSib:MovePrev("SAME_TAG")
end
if intSib > 1 then strTag = strTag.."["..intSib.."]" end
getDataRef(ptrTag,"."..strTag..strRef) -- Now analyse the parent level
else
strDataRef = strTag..strRef -- Record level reached, so set return values
intRecId = fhGetRecordId(ptrRef)
strRecTag = strTag
if not fhIsValidDataRef(strDataRef) then print(strDataRef.." is Invalid") end
end
end -- local function getDataRef
if type(ptrRef) == "userdata" then getDataRef(ptrRef,"") end
return strDataRef, intRecId, strRecTag
end -- function BuildDataRef
function intRecordCount(strType) -- Count number of records of chosen Record Type
-- strType ~ Record type tag
local ptrType = fhNewItemPtr()
local intCount = 0
ptrType:MoveToFirstRecord(strType)
while ptrType:IsNotNull() do
intCount = intCount + 1
ptrType:MoveNext()
end
return intCount
end -- function intRecordCount
function Main()
local strRecTag = nil -- Selected Record Type tag
local arrLinks = {} -- Count of Where Used Links per Record
local dicWhere = {} -- Dictionary of Where Used to detect duplicates -- V1.3
local intTwice = 0 -- Count where same record used same place twice -- V1.3
local intUnique = 0 -- Unique key for Citations with identical fields -- V1.4
local intRecMax = fhGetRecordTypeCount() + 1 -- Number of record types including HEAD
local tblRecord = { Min=6; Max=50; } -- Result Set tables
local tblRec_Id = {}
local tblRecObj = {} -- V1.1
local tblUseTyp = {}
local tblUseRec = { Min=6; Max=50; }
local tblUse_Id = {}
local tblUseObj = {} -- V1.1
local tblUseRef = {}
local tblUseTxt = { Min=6; Max=50; }
local tblUsePtr = {}
local tblColumn = {} -- Table of more columns for Record Type
local dicType =
{ INDI = { Col={}; Name="Individual"; }; -- Record Type dictionary
FAM = { Col={}; Name="Family"; };
NOTE = { Col={}; Name="Note"; };
SOUR = { Col={}; Name="Source"; };
REPO = { Col={}; Name="Repository"; };
SUBM = { Col={}; Name="Submitter"; };
SUBN = { Col={}; Name="Submission"; };
OBJE = { Col={}; Name="Media"; };
_PLAC = { Col={}; Name="Place"; };
_RNOT = { Col={}; Name="Research Note"; }; -- V1.4
_SRCT = { Col={}; Name="Source Template"; }; -- V1.4
HEAD = { Col={}; Name="Head"; };
}
local function doTypeDetails(strLink,strRef,intMax,strName,strMode) -- More Result Set Column details for Record Type dictionary
-- strLink ~ Link record type tag
-- strRef ~ Data ref relative to link or "Media" or "Key"
-- intMax ~ Max char width of Result Set column
-- strName ~ Name of Result Set column
-- strMode ~ Mode of Result Set column if not "item"
local dicType = dicType[strLink]
if strRef == "Media" then -- V1.1
dicType.Media = true
elseif strRef then
local intMin = 3 -- Min char width of Result Set column for "item" pointers -- V1.6
if strMode then intMin = 6 end -- Larger width for other column modes "integer" or "text"
local intMore = #dicType.Col + 1 -- Column number index -- V1.6
table.insert(dicType.Col,{ More=intMore; Text=0; Char=0; Ref=strRef; Min=intMin; Max=intMax; Name=strName; Mode=(strMode or "item"); }) -- V1.6
tblColumn[intMore] = {} -- dicType.Col and tblColumn share same index -- V1.6
end
end -- local function doTypeDetails
doTypeDetails( "INDI" , "Media" ) -- V1.1
doTypeDetails( "INDI" , "~.ROLE" , 25, "Role/Association") -- V1.2
doTypeDetails( "INDI" , "~.NOTE2" , 50, "Local Note" ) -- V1.2
doTypeDetails( "INDI" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails( "FAM" , "Media" ) -- V1.1
doTypeDetails( "FAM" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails( "NOTE" )
doTypeDetails( "NOTE" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails( "SOUR" , "Media" ) -- V1.1
doTypeDetails( "SOUR" , "Key" , 5, "Key", "integer" ) -- Unique Citation Key -- V1.4
doTypeDetails( "SOUR" , "~.DATA.DATE" , 25, "Entry Date" )
doTypeDetails( "SOUR" , "~.QUAY" , 25, "Assessment" ) -- FH V7 also finds "~._QUAY"
doTypeDetails( "SOUR" , "~.PAGE" , 50, "Where within Source" )
doTypeDetails( "SOUR" , "~.DATA.TEXT" , 50, "Text From Source" )
doTypeDetails( "SOUR" , "~.NOTE2" , 50, "Citation Note" )
doTypeDetails( "SOUR" , "~._FIELD[1]" , 50, "Metafield 1" ) -- V1.6
doTypeDetails( "SOUR" , "~._FIELD[2]" , 50, "Metafield 2" ) -- V1.6
doTypeDetails( "SOUR" , "~._FIELD[3]" , 50, "Metafield 3" ) -- V1.6
doTypeDetails( "REPO" , "+._TYPE" , 25, "Generic Type" )
doTypeDetails( "REPO" , "+.AUTH" , 25, "Author" )
doTypeDetails( "REPO" , "+.PUBL" , 50, "Publication Information" )
doTypeDetails( "REPO" , "+.TEXT" , 50, "Text From Source" )-- V1.6
doTypeDetails( "REPO" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails( "REPO" , "+._FIELD[1]" , 50, "Metafield 1" ) -- V1.6
doTypeDetails( "REPO" , "+._FIELD[2]" , 50, "Metafield 2" ) -- V1.6
doTypeDetails( "REPO" , "+._FIELD[3]" , 50, "Metafield 3" ) -- V1.6
doTypeDetails( "REPO" , "+._FIELD[4]" , 50, "Metafield 4" ) -- V1.6
doTypeDetails( "REPO" , "+._FIELD[5]" , 50, "Metafield 5" ) -- V1.6
doTypeDetails( "SUBM" )
doTypeDetails( "SUBM" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails( "SUBN" )
doTypeDetails( "SUBN" , "+.SUBM" , 50, "Submitter" ) -- V1.6
local strAnnotation = ">NOTE2" -- FH V6
if fhGetAppVersion() > 6 then
strAnnotation = ">NOTE2._NOTA" -- FH V7 -- V1.4
end
doTypeDetails( "OBJE" , ">NOTE2._AREA", 25, "Frame Area" )
doTypeDetails( "OBJE" , ">NOTE2._EXCL", 25, "Exclude From" )
doTypeDetails( "OBJE" , ">NOTE2._CAPT", 25, "Caption?" )
doTypeDetails( "OBJE" , strAnnotation , 50, "Link Note" ) -- FH V7 -- V1.4 -- V1.6
doTypeDetails( "OBJE" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails("_PLAC" , "Media" ) -- V1.1
doTypeDetails("_PLAC" , "<.ADDR" , 50, "Fact Address" )
doTypeDetails("_PLAC" , "<.NOTE2" , 50, "Fact Note" )
doTypeDetails("_PLAC" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails("_RNOT" ) -- V1.4
doTypeDetails("_RNOT" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails("_SRCT" ) -- V1.4
doTypeDetails("_SRCT" , "+._TYPE" , 25, "Generic Type" ) -- V1.6
doTypeDetails("_SRCT" , "+.AUTH" , 25, "Author" ) -- V1.6
doTypeDetails("_SRCT" , "+.PUBL" , 50, "Publication Information" )
doTypeDetails("_SRCT" , "+.TEXT" , 50, "Text From Source" )-- V1.6
doTypeDetails("_SRCT" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
doTypeDetails("_SRCT" , "+._FIELD[1]" , 50, "Metafield 1" ) -- V1.6
doTypeDetails("_SRCT" , "+._FIELD[2]" , 50, "Metafield 2" ) -- V1.6
doTypeDetails("_SRCT" , "+._FIELD[3]" , 50, "Metafield 3" ) -- V1.6
doTypeDetails("_SRCT" , "+._FIELD[4]" , 50, "Metafield 4" ) -- V1.6
doTypeDetails("_SRCT" , "+._FIELD[5]" , 50, "Metafield 5" ) -- V1.6
doTypeDetails( "HEAD" )
doTypeDetails( "HEAD" , "+.NOTE2" , 50, "Record Note" ) -- V1.6
local function arrMediaCount(arrObje) -- Count Media Objects -- V1.1
-- arrObje ~ Array listing media
local arrMedia = {}
for intMedia = 1, #arrObje do arrMedia[intMedia] = 0 end -- Zero Media counters -- V1.1
repeat
local anyMedia = false
for intMedia, ptrMedia in ipairs (arrObje) do
if ptrMedia:IsNotNull() then
anyMedia = true
arrMedia[intMedia] = arrMedia[intMedia] + 1 -- Count each kind of Media -- V1.1
ptrMedia:MoveNext("SAME_TAG")
end
end
until not anyMedia
return arrMedia
end -- local function arrMediaCount
local function doTextLength(tblItem,anyItem) -- tblItem.Long = longest text in column, tblItem.Char = next to longest -- V1.6
-- tblItem ~ Table that holds parameters
-- anyItem ~ Length of text or pointer to text
local intItem = anyItem
if type(anyItem) ~= "number" then
intItem = #fhGetDisplayText(anyItem,"","MIN") + 1 -- Get display text length for pointer item + 1 for record icon
end
if intItem > 0 then
intItem = intItem - math.floor(intItem/10) -- Reduce length by 10% otherwise columns are too wide
if intItem > ( tblItem.Long or 0 ) then
tblItem.Char = tblItem.Long or intItem -- Previous longest item or initial item length
tblItem.Long = intItem -- Save length of longest item so far
elseif intItem < tblItem.Long then
if tblItem.Char == tblItem.Long then
tblItem.Char = 0 -- Ensure subsequent item char length is used
end
tblItem.Char = math.max(intItem,tblItem.Char) -- Save length of next to longest item so far
end
end
end -- local function doTextLength
local function doResultSet(intRecord,ptrRecord,intType,ptrUsed,ptrItem)
-- intRecord ~ Index into current record list
-- ptrRecord ~ Pointer to current record item
-- intType ~ Index to the current record type
-- ptrUsed ~ Pointer to parent record where current record used
-- ptrItem ~ Pointer to the data item where current record used
doTextLength(tblRecord,ptrRecord)
table.insert(tblRecord,ptrRecord:Clone()) -- Update Result Set table columns
table.insert(tblRec_Id,fhGetRecordId(ptrRecord))
if dicType[strRecTag].Media then -- V1.1
local arrObje = {
fhGetItemPtr(ptrRecord,"~.OBJE") ; -- Media pointers -- V1.1
fhGetItemPtr(ptrRecord,"~.OBJE2");
}
local arrMedia = arrMediaCount(arrObje) -- Media counts -- V1.1
table.insert(tblRecObj,tostring(arrMedia[1]+arrMedia[2]))
end
table.insert(tblUseTyp,intType or 99)
if ptrUsed then -- Record has Where Used Link
local strData, intRecId = BuildDataRef(ptrItem)
local ptrRoot = fhNewItemPtr()
ptrRoot:MoveToParentItem(ptrItem)
local arrObje = {
fhGetItemPtr(ptrRoot,"~.OBJE") ; -- Media pointers -- V1.1
fhGetItemPtr(ptrRoot,"~.OBJE2");
fhGetItemPtr(ptrItem,"~.OBJE") ;
fhGetItemPtr(ptrItem,"~.OBJE2");
}
local strUnique, intKeyCol -- Citation unique fields and "Key" column -- V1.4
local arrMedia = arrMediaCount(arrObje) -- Media counts -- V1.1
local strRole = fhGetValueAsText(fhGetItemPtr(ptrItem,"~.ROLE")) -- Need Role only for Individuals in duplicate detection below -- V1.3
doTextLength(tblUseRec,ptrUsed)
table.insert(tblUseRec,ptrUsed:Clone())
table.insert(tblUse_Id,intRecId)
table.insert(tblUseObj,tostring(arrMedia[1]+arrMedia[2]).." / "..tostring(arrMedia[3]+arrMedia[4])) -- V1.1
doTextLength(tblUseTxt,ptrRoot)
table.insert(tblUseTxt,fhGetDisplayText(ptrRoot))
if strData:match("_SHAR") then
table.insert(tblUsePtr,ptrRoot:Clone()) -- ptrRoot is better for Witness links for this field -- V1.2
else
table.insert(tblUsePtr,ptrItem:Clone()) -- but must retain ptrItem for all subsequent fields -- V1.2
end
for intCol, tblCol in ipairs (dicType[strRecTag].Col) do
local strRef = tblCol.Ref -- Update optional Result Set columns depending on search record type
if strRef == "Key" then
strUnique = tostring(intRecord)..":" -- Unique source for replica citation Key column -- V1.4
intKeyCol = tblCol.More
else
if strRef:match("ROLE") and strData:match("INDI.ASSO") then -- V1.6
strRef = strRef:gsub("ROLE","RELA")
elseif strRef:match("^%+") then -- Reference is relative to record where used -- V1.6
strRef = strRef:gsub("^%+","~")
ptrItem = ptrUsed:Clone()
elseif strRef:match("^<") then -- Reference is relative to parent of link -- V1.6
strRef = strRef:gsub("^<","~")
ptrItem = ptrRoot:Clone()
elseif strRef:match("^>NOTE2") then -- Reference is a media link note
strRef = strRef:gsub("^>NOTE2","~")
if fhGetAppVersion() <= 6 then -- V1.4
local strAsid = fhGetValueAsText(fhGetItemPtr(ptrItem,"~._ASID"))
local ptrNote = fhNewItemPtr()
ptrNote:MoveTo(ptrRecord,"~.NOTE2")
while ptrNote:IsNotNull() do
if strAsid == fhGetValueAsText(fhGetItemPtr(ptrNote,"~._ASID")) then
ptrItem = ptrNote:Clone()
break
end
ptrNote:MoveNext("SAME_TAG")
end
end
end
local anyRef = fhGetItemPtr(ptrItem,strRef)
local strRef = ""
if tblCol.Mode == "text" then
strRef = fhGetDisplayText(anyRef)
anyRef = fhGetValueAsText(anyRef)
doTextLength(tblCol,#anyRef) -- Set length of text to which column can be shrunk -- V1.6
elseif tblCol.Mode == "item" then
local strClass = fhGetDataClass(anyRef)
if strClass == "longtext" or strClass == "richtext"
or ( fhGetAppVersion() > 6 and fhGetMetafieldType(anyRef) == "AD" ) then -- _FIELD is an Address -- V2.0
tblCol.Min = 6
tblCol.Class = "longtext" -- Get field long/richtext to go with a buddy column -- V1.6
strRef = fhGetValueAsText(anyRef):gsub("\n",StrPilcrow)
else
strRef = fhGetDisplayText(anyRef,"","MIN") -- Get display text for other field types
end
doTextLength(tblCol,#strRef) -- Set length of text to which column can be shrunk -- V1.6
if tblCol.Text == 0 then
tblColumn[#tblColumn+1] = { } -- Add "text" column to go with "item" buddy column -- V1.6
tblCol.Text = #tblColumn
end
table.insert(tblColumn[tblCol.Text],strRef)
end
table.insert(tblColumn[tblCol.More],anyRef)
if intKeyCol then
strUnique = strUnique..strRef -- Unique text for replica citation detection -- V1.7 code move here
end
end
end
if intKeyCol then
if not dicWhere[strUnique] then -- Unique key number for each replica citation
intUnique = intUnique + 1
dicWhere[strUnique] = intUnique
end
table.insert(tblColumn[intKeyCol],dicWhere[strUnique])
end
arrLinks[intRecord] = arrLinks[intRecord] or 0 -- V2.1
if intType < intRecMax
or strData ~= "HEAD._PCIT.SOUR" then -- HEAD._PCIT.SOUR record links discounted by LinksTo() function -- V1.4 -- V1.6 -- V2.1
arrLinks[intRecord] = arrLinks[intRecord] + 1
end
-- Detect duplicate usage for intRecord of intType in intRecId for strData Ref & Role -- V1.3 -- V1.7 code moved here
local strWhat = strData:gsub("%[%d+%]$","") -- Remove a trailing Data Reference [index] -- V1.3
strWhat = tostring(intRecord).."of"..intType.."in"..intRecId..strWhat..strRole..(dicWhere[strUnique] or "") -- V1.7
local intUsed = dicWhere[strWhat] -- Lookup dictionary to see if used before -- V1.3
if intUsed then -- Used before so have old Result Set entry -- V1.3
intTwice = intTwice + 1
strData = "*"..strData -- Asterisk the new & old duplicate entries -- V1.3
tblUseRef[intUsed] = tblUseRef[intUsed]:gsub("^%**","*")
else
dicWhere[strWhat] = #tblUseRef+1 -- Dictionary entry with Result Set index -- V1.3
end
strData = strData:gsub("^(%*?)(.-%._LINK_%u)","%1@%2") -- Mark rich text Record Links with @ symbol -- V1.5 -- V1.8
table.insert(tblUseRef,strData)
else
local strUnused = "< No 'Where Used Record Links' found. >"
table.insert(tblUseRec,fhNewItemPtr()) -- Record is unused
table.insert(tblUse_Id,nil)
doTextLength(tblUseTxt,#strUnused) -- V1.6
table.insert(tblUseTxt, strUnused)
table.insert(tblUsePtr,fhNewItemPtr())
end
collectgarbage("step",0) -- Memory garbage collection to avoid 'Not responding' -- V1.7
end -- local function doResultSet
local function isLink(ptrItem) -- Detect fhGetDataClass() & fhGetMetafieldType() links -- V1.8
local dicLink = {
place = true ; -- Place _PLAC record link
link = true ; -- Any other record link
PL = true ; -- Metafield _FIELD with Place record link
RP = true ; -- Metafield _FIELD with Repository link
}
local strClass = fhGetDataClass(ptrItem)
if strClass == "metafield" then
strClass = fhGetMetafieldType(ptrItem)
end
return dicLink[strClass]
end -- local function isLink
local tblLinks = { -- Links from/to validity table and Names -- V1.1 -- V1.4 -- V1.5 only HEAD has no rich text links
INDI = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Individuals"; };
FAM = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Families"; };
NOTE = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Notes"; };
SOUR = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Sources"; };
REPO = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Repositories"; };
SUBM = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Submitters"; };
SUBN = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Submissions"; };
OBJE = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Multimedia"; };
_PLAC= { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Places"; };
_RNOT= { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Research Notes"; }; -- V1.4
_SRCT= { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Source Templates"; }; -- V1.4
HEAD = { INDI=nil; FAM=nil; NOTE=true; SOUR=true; REPO=nil; SUBM=true; SUBN=true; OBJE=true; _PLAC=nil; _RNOT=nil; _SRCT=nil; HEAD=nil; Name="Headers"; };
}
local strFileName = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\Where Used Record Links.dat"
local lfsFileName = lfs.attributes(strFileName,"mode") == "file"
for intType = 1, intRecMax do -- Search each record type
local strType = ""
local arrRecs = {} -- Array of selected records
local dicRecs = {} -- Dictionary of Record Id (or Placenames) to array index of selected records
if intType < intRecMax and lfsFileName then -- Check if any Records pre-selected -- V1.5
strType = fhGetRecordTypeTag(intType)
arrRecs = fhGetCurrentRecordSel(strType)
else
local ptrItem = fhNewItemPtr()
local strForm = " Record Type: %l| " -- Otherwise prompt for Record selection
local strKeep = " Keep Prompt: %b[ Prompt hidden, Hover for advice]{Untick to hide this prompt and always select records before running plugin}\n" -- V1.5
local intKeep = 1 -- V1.5
if lfsFileName then intKeep = 0 end
local dicData = {}
local intData = 0
for intType = 1, intRecMax-1 do -- Build iup.GetParam droplist format
strType = fhGetRecordTypeTag(intType)
ptrItem:MoveToFirstRecord(strType)
if ptrItem:IsNotNull() then -- Records of this Type exist
strForm = strForm..dicType[strType].Name.." | "
table.insert(dicData,intType)
if strType == "SOUR" then intData = #dicData-1 end -- Default to Source Record Type -- V2.1
end
end
strForm = strForm.."|{Choose the required record type to analyse}\n" -- V1.5
local isOK, intData, intKeep = iup.GetParam("Choose Record Type",nil,strForm..strKeep,intData,intKeep)
if isOK then
strType = fhGetRecordTypeTag(dicData[intData+1])
arrRecs = fhPromptUserForRecordSel(strType) -- Prompt for Record selection
if intKeep == 0 then
local fileHandle = io.open(strFileName,"w") -- When file exists prompt is hidden -- V1.5
assert(fileHandle:close())
else
os.remove(strFileName) -- When file missing prompt is shown -- V1.5
end
else
break
end
end
if #arrRecs > 0 then -- Selected records exist
local intStep = 100 / #arrRecs -- Progress bar step increment
strRecTag = strType
progbar.Start("Where Used Record Links looking for "..tblLinks[strType].Name,100)
progbar.Message("Building Search Index")
for intRecord, ptrRecord in ipairs (arrRecs) do -- Build a Record Id index to target records -- V1.7
local intRecId = fhGetRecordId(ptrRecord)
dicRecs[intRecId] = intRecord -- This avoids searching the array of records
progbar.Step(intStep)
end
fhSleep(400,300)
for intType = 1, intRecMax do -- Search each record type
local strType = fhGetRecordTypeTag(intType)
local intStep = 100 / intRecordCount(strType) -- V1.7
local ptrItem = fhNewItemPtr()
local ptrUsed = fhNewItemPtr()
if intType == intRecMax then -- Special HEAD case
strType = "HEAD"
intStep = 1
end
if tblLinks[strType][strRecTag] then -- This record type can link to selected records -- V1.1
progbar.Message("Searching "..tblLinks[strType].Name)
progbar.Reset()
progbar.Step(1)
ptrItem:MoveToFirstRecord(strType) -- Search every field
while ptrItem:IsNotNull() do
if not fhHasParentItem(ptrItem) then -- Next record reached
ptrUsed = ptrItem:Clone()
progbar.Step(intStep) -- Advance progress bar
if progbar.Stop() then
error("User Cancelled")
end
collectgarbage("step",0) -- Memory garbage collection to avoid 'Not responding' -- V1.7
end
if isLink(ptrItem) then -- Found any kind of record link -- V1.7 -- V1.8
local ptrLink = fhGetValueAsLink(ptrItem)
if strRecTag == fhGetTag(ptrLink) then -- Found right type of record link -- V1.7
local intRecord = dicRecs[fhGetRecordId(ptrLink)]
if intRecord then -- Found a where used record link -- V1.7
doResultSet(intRecord,arrRecs[intRecord],intType,ptrUsed,ptrItem)
end
end
end
ptrItem:MoveNextSpecial()
end
end
end
if #tblRecord > 0 then -- Found some record links
for intRecId, intRecord in pairs(dicRecs) do -- This lets same record be selected more than once -- V1.8
local ptrRecord = arrRecs[intRecord]
if not arrLinks[intRecord] then
doResultSet(intRecord,ptrRecord) -- Report any missing links
arrLinks[intRecord] = 0
end
local intLinks = fhCallBuiltInFunction("LinksTo",ptrRecord)
if intLinks ~= arrLinks[intRecord] then -- V1.1 double check counts
fhMessageBox("\n =LinksTo() "..intLinks.." disagrees with "..arrLinks[intRecord].." links found! \n")
end
end
end
break
end
end
if #tblRecord > 0 then
local function intSize(tblCol) -- Set width of column based on length of contents -- V1.6
local intChar = tblCol.Char or 0 -- Character length of penultimate longest content
local intLong = tblCol.Long or 0 -- Character length of longest content
local intMax = tblCol.Max or 50 -- Maximum size in characters
local intMin = tblCol.Min or 6 -- Minimum size in characters
local intSize = math.min( math.max(intMin,intChar), intMax ) * 4
-- print(intChar,intLong,intMin,intMax,intSize,tblCol.Mode or "item",tblCol.Class,tblCol.Text)
return intSize
end
progbar.Reset("Loading Result Set")
local strRecordName = dicType[strRecTag].Name.." Record Name"
fhOutputResultSetTitles("Where Used Record Links "..strVersion)
fhOutputResultSetColumn(strRecordName , "item" , tblRecord, #tblRecord, intSize(tblRecord) , "align_left")
fhOutputResultSetColumn("Rec Id" , "integer", tblRec_Id, #tblRecord, 28 , "align_mid" , 1)
if #tblRecObj > 0 then
fhOutputResultSetColumn("Media" , "text" , tblRecObj, #tblRecord, 28 , "align_mid" ) -- V1.1
end
fhOutputResultSetColumn("Hide Record Type" , "integer", tblUseTyp, #tblRecord, 10 , "align_left", 2, true, "default", "hide")
fhOutputResultSetColumn("Record Where Used" , "item" , tblUseRec, #tblRecord, intSize(tblUseRec) , "align_left")
fhOutputResultSetColumn("Rec Id" , "integer", tblUse_Id, #tblRecord, 28 , "align_mid" , 3)
fhOutputResultSetColumn("Media" , "text" , tblUseObj, #tblRecord, 28 , "align_mid" ) -- V1.1
fhOutputResultSetColumn("Data Ref Where Used", "text" , tblUseRef, #tblRecord, 90 , "align_left", 4)
fhOutputResultSetColumn("Buddy Where Used" , "item" , tblUsePtr, #tblRecord, 10 , "align_left", 0, true, "default", "buddy")
fhOutputResultSetColumn("Field Where Used" , "text" , tblUseTxt, #tblRecord, intSize(tblUseTxt) , "align_left", 5)
fhOutputResultSetColumn("Buddy Where Used" , "item" , tblUsePtr, #tblRecord, 10 , "align_left", 0, true, "default", "buddy")
local dicCol = dicType[strRecTag].Col
for intCol = #dicCol, 1, -1 do -- Remove empty rightmost Metafield columns -- V1.6
local tblCol = dicCol[intCol]
if tblCol.Ref:match("_FIELD") and tblCol.Char == 0 then
dicCol[intCol] = nil
else
break
end
end
for intCol, tblCol in ipairs (dicCol) do -- Optional columns
if intCol == #dicCol then
tblCol.Max = 1000 -- Rightmost column can be very wide -- V1.6
tblCol.Char = tblCol.Long
end
if tblCol.Class == "longtext" then -- Long text displayed in full with buddy item pointer -- V1.6
fhOutputResultSetColumn(tblCol.Name, "text" , tblColumn[tblCol.Text], #tblRecord, intSize(tblCol) , "align_left")
fhOutputResultSetColumn(tblCol.Name, "item" , tblColumn[tblCol.More], #tblRecord, 10 , "align_left", 0, true, "default", "buddy" )
else
fhOutputResultSetColumn(tblCol.Name,tblCol.Mode,tblColumn[tblCol.More], #tblRecord, intSize(tblCol) , "align_left")
end
end
progbar.Close()
if intTwice > 0 then -- V1.3
fhMessageBox("\n "..intTwice.." duplicate 'Where Used Record Links' found. \n\n See 'Data Ref Where Used' asterisked entries. \n Click its column header to list together at top. \n")
end
else
if strRecTag then
progbar.Close()
fhMessageBox("\n No 'Where Used Record Links' found. \n")
else
fhMessageBox("\n Please select one or more Records then run the Plugin. \n")
end
end
end -- function Main()
--[[
@Function: CheckVersionInStore
@Author: Mike Tate
@Version: 1.3
@LastUpdated: 03 May 2022
@Description: Check plugin version against version in Plugin Store
@Parameter: Plugin name and version
@Returns: None
@Requires: lfs & luacom
@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 "lfs"
require "luacom"
local function OpenFile(strFileName,strMode) -- Open File and return 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 -- local function OpenFile
local function SaveStringToFile(strString,strFileName) -- Save string to file
local fileHandle = OpenFile(strFileName,"w")
fileHandle:write(strString)
assert(fileHandle:close())
end -- local function SaveStringToFile
local function StrLoadFromFile(strFileName) -- Load string from file
local fileHandle = OpenFile(strFileName,"r")
local strContents = fileHandle:read("*all")
assert(fileHandle:close())
return strContents
end -- local function StrLoadFromFile
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 strPath = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\"
local strFile = strPath.."VersionInStore "..strPlugin..".dat"
local intTime = os.time() - 2600000 -- Time in seconds a month ago
local tblAttr, strError = lfs.attributes(strFile) -- Obtain file attributes
if not tblAttr or tblAttr.modification < intTime then -- File does not exist or was modified long ago
if lfs.attributes(strPath,"mode") ~= "directory" then
if not lfs.mkdir(strPath) then return end -- Ensure the Plugin Data folder exists
end
local strErrFile = strPath.."VersionInStoreInternetError.dat"
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
local intTime = os.time() - 36000 -- Time in seconds 10 hours ago
local tblAttr, strError = lfs.attributes(strErrFile) -- Obtain file attributes
if not tblAttr or tblAttr.modification < intTime then -- File does not exist or was modified long ago
fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ")
end
SaveStringToFile(strErrFile,strErrFile) -- Update file modified time
else
os.remove(strErrFile) -- Delete file if Internet is OK
if strReturn then
strLatest = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits
SaveStringToFile(strLatest,strFile) -- Update file modified time and save version
end
end
else
strLatest = StrLoadFromFile(strFile) -- Retrieve saved latest version
end
end
if intVersion(strLatest) > intVersion(strVersion or "0") then
fhMessageBox("Later Version "..strLatest.." of this Plugin is available from the Plugin Store.")
end
end -- function CheckVersionInStore
-- Main Code Section Starts Here --
fhInitialise(5,0,0,"save_recommended")
CheckVersionInStore("Where Used Record Links",strVersion) -- Notify if later Version -- V1.8
Main()
Source:Where-Used-Record-Links-8.fh_lua