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