Trim Date and Text Fields.fh_lua

--[[
@Title:			Trim Date and Text Fields
@Type:				Standard
@Author:			Mike Tate
@Contributors:	
@Version:			1.1
@Keywords:		
@LastUpdated:	19 Dec 2020
@Licence:			This plugin is copyright (c) 2020 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:	Reduce the details held in Date, Note, Address & Place fields.
@V1.1:				FH V7 Lua 3.5 IUP 3.28; progbar 3.0; 
@V1.0:				First published version;
@V0.3:				Simplified AddResults() function; Support Note field; Allow both standard Date formats; Inhibit Result Set option; Add progress bar.
@V0.2:				Make options globally sticky; Allow fields to be deleted; Support Address field.
@V0.1:				1st prototype.
]]

require "iuplua"																		-- To access GUI window builder
require "lfs"																			-- To access LUA filing system

local strTitle = "Trim Date and Text Fields  1.1 "

if fhGetAppVersion() > 5 then														-- Cater for Unicode UTF-8 from FH Version 6 onwards
	fhSetStringEncoding("UTF-8")
	iup.SetGlobal("UTF8MODE","YES")
	iup.SetGlobal("CUSTOMQUITMESSAGE","YES")										-- Needed for IUP 3.28
end
if fhGetAppVersion() > 6 then unpack = table.unpack end						-- V1.1

-- Split a string using "," or chosen separator --

function split(strTxt,strSep)
	local tblFields = {}
	local strPattern = string.format("([^%s]+)", strSep or ",")
	strTxt = tostring(strTxt or "")
	strTxt:gsub(strPattern, function(strField) tblFields[#tblFields+1] = strField end)
	return tblFields
end -- function split

-- Check if file exists --

function FlgFileExists(strFileName)
	return lfs.attributes(strFileName,"mode") == "file"
end -- function FlgFileExists

-- Open File and return Handle --

function OpenFile(strFileName,strMode)
	local fileHandle, strError = io.open(strFileName,strMode)
	if fileHandle == nil then
		error("\n Unable to open file in \""..strMode.."\" mode. \n "..strFileName.." \n "..strError.." \n")
	end
	return fileHandle
end -- function OpenFile

-- Load Options

function LoadOptions(strFileName)
	local dicOption = {}
	strFileName = strFileName or "?"
	if FlgFileExists(strFileName) then												-- Read the file in table lines
		for strLine in io.lines(strFileName) do
			local arrFields = split(strLine,"=")
			dicOption[arrFields[1]] = tonumber(arrFields[2])
		end
	end
	return dicOption
end -- function LoadOptions

-- Save Options

function SaveOptions(dicOption,strFileName)
	strFileName = strFileName or "?"
	local fileHandle = OpenFile(strFileName,"w")
	for strField, strValue in pairs(dicOption) do								-- Write the file in table lines
		fileHandle:write(strField.."="..strValue.."\n")
	end
	fileHandle:close()
end -- function SaveOptions

-- Result Set Tables used by function AddResults and Main
local arrAct = {}																		-- Trim Action Text
local arrRec = {}																		-- Record Name Text
local arrBud = {}																		-- Record Buddy Pointer
local arrTyp = {}																		-- Record Type Tag
local arrTid = {}																		-- Record Type Integer sort key
local arrRid = {}																		-- Record Id Integer sort key
local arrOld = {}																		-- Old Trim Field Text
local arrNew = {}																		-- New Trim Field Pointer
local intAct = 0																		-- Count trims when Result Set inhibited

do	-- Handle the Result Set table features

	local dicAct = {																	-- Action descriptions
		DATE  = "1. Date Field Trim " ;
		NOTE  = "2. Note Field Trim " ;
		ADDR  = "3. Address Field Trim " ;
		PLAC  = "4. Place Field Trim " ;
		Purge = "5. Place Record Purge " ;
	}
	local dicOK = {
		[false] = "Failure" ;
		[true]  = "Success" ;
	}

	function CountTrims()																-- Count trims when Result Set inhibited
		intAct = intAct + 1
	end -- function CountTrims

	function AddResults(strAct,isOK,intTid,strVal,ptrNew,intRid)				-- Update the Result Set tables
		-- strAct		Action tag index for dicAct
		-- isOK		Action true/false for dicOK
		-- intTid		Record type integer sort key
		-- strVal		Old text field value or purged record name 
		-- ptrNew		New text field pointer or nil
		-- intRid		Purged record id or nil

		local ptrRec = fhNewItemPtr()
		local strRec = strVal															-- Place record purge values
		local strTyp = "_PLAC"
		local strOld = ""
		ptrNew = ptrNew or fhNewItemPtr()
		if strAct ~= "Purge" then														-- Date or text field frim values
			ptrRec:MoveToRecordItem(ptrNew) 
			strRec = fhGetDisplayText(ptrRec)
			strTyp = fhGetTag(ptrRec)
			intRid = fhGetRecordId(ptrRec)
			strOld = strVal
		end
		table.insert(arrAct,dicAct[strAct]..dicOK[isOK or false])				-- Save the Result Set values
		table.insert(arrRec,strRec)
		table.insert(arrBud,ptrRec:Clone())
		table.insert(arrTyp,strTyp)
		table.insert(arrTid,intTid)
		table.insert(arrRid,intRid)
		table.insert(arrOld,strOld)
		table.insert(arrNew,ptrNew:Clone())
	end -- function AddResults

end -- do

function HandleDate(intDate)															-- Handle the Date field trimming features
	local strWord = "( ?[abcft]?[feniro]?[tfdro]?[eowcm]?[rea]?e?n? ?)"		-- Pattern to capture keywords:	after before between and circa from to
	local strMnth = "[JFMASOND]?[aepuco]?%l-,? ?"								-- Pattern to match the months: Ja Fe Ma Ap Ju Au Se Oc No De letters comma space
	local arrDate =
	{	-- gsub parameters for Date trim options > 0
		{ Pattern = "^.*$" ;																	Replace = "";		};
		{ Pattern = "^.-(%d%d%d%d).*$" ;													Replace = "%1";		};
		{ Pattern = strWord.."Q?%d?%d? ?"..strMnth.."%d?%d?,? ?(%d%d%d%d)" ;		Replace = "%1%2";	};
		{ Pattern = strWord.."Q?%d?%d? ?("..strMnth..")%d?%d?,? ?(%d%d%d%d)" ;		Replace = "%1%2%3";	};
	}
	local dicDate = arrDate[intDate] or { }										-- Set the Date trim parameters
	local strPatt = dicDate.Pattern
	local strRepl = dicDate.Replace

	function TrimDate(intTid,ptrItem)												-- Trim old Date field if enabled
		if intDate > 0 then	
			local isPhrase = false
			local strPhr = ""
			local datVal = fhGetValueAsDate(ptrItem)
			local strOld = datVal:GetValueAsText()
			if datVal:GetType() == "Phrase" then									-- Cope with Date Phrase and Interpreted value
				isPhrase = true
				strOld,strPhr = strOld:match('^(.-)(%(?".+"%)?)$')
			end
			local strNew = strOld:gsub(strPatt,strRepl)
			if #strNew < #strOld then													-- Save new Date field if different
				local isOK = true
				if #strNew == 0 then
					datVal:SetNull()
				else
					isOK = datVal:SetValueAsText(strNew..strPhr,isPhrase)		-- Update the Date value
				end
				if isOK then
					isOK = fhSetValueAsDate(ptrItem,datVal)						-- Update the Date field
				end
				AddResults("DATE",isOK,intTid,strOld..strPhr,ptrItem)			-- Update the Result Set
			end
		end
	end -- local function TrimDate

end -- function HandleDate

function HandleNote(intNote)															-- Handle the Note field trimming features
	local arrNote =
	{	-- gsub parameters for Note trim options > 0
		{ Pattern = "^.*$" ;			Replace = ""; };
		{ Pattern = "%[%[.-%]%]" ;	Replace = ""; };
	}
	local dicNote = arrNote[intNote] or { }										-- Set the Note trim parameters
	local strPatt = dicNote.Pattern
	local strRepl = dicNote.Replace

	function TrimNote(intTid,ptrItem)												-- Trim old Note field if enabled
		if intNote > 0 then
			local strOld = fhGetValueAsText(ptrItem)
			local strNew = strOld:gsub(strPatt,strRepl)
			if #strNew < #strOld then													-- Save new Note field if different
				local isOK = fhSetValueAsText(ptrItem,strNew)
				AddResults("NOTE",isOK,intTid,strOld,ptrItem)					-- Update the Result Set
			end
		end
	end -- local function TrimNote

end -- function HandleNote

function HandleAddr(intAddr)															-- Handle the Address field trimming features

	function TrimAddr(intTid,ptrItem)												-- Trim old Address field if enabled
		if intAddr > 0 then
			local strOld = fhGetValueAsText(ptrItem)
			local strNew = ""															-- For intAddr == 1 then delete field
			if intAddr > 1 then															-- For intAddr > 1 then keep last intAddr-1 columns
				strNew = fhCallBuiltInFunction("TextPart",strOld,-1,intAddr-1)
			end
			if #strNew < #strOld then													-- Save new Address field if different
				local isOK = fhSetValueAsText(ptrItem,strNew)
				AddResults("ADDR",isOK,intTid,strOld,ptrItem)					-- Update the Result Set
			end
		end
	end -- local function TrimAddr

end -- function HandleAddr

function HandlePlac(intPlac)															-- Handle the Place field trimming features

	function TrimPlac(intTid,ptrItem)												-- Trim old Place field if enabled
		if intPlac > 0 then
			local strOld = fhGetValueAsText(ptrItem)
			local strNew = ""															-- For intPlac == 1 then delete field
			if intPlac > 1 then															-- For intPlac > 1 then keep last intPlac-1 columns
				strNew = fhCallBuiltInFunction("TextPart",strOld,-1,intPlac-1)
			end
			if #strNew < #strOld then													-- Save new Place field if different
				local isOK = fhSetValueAsText(ptrItem,strNew)
				AddResults("PLAC",isOK,intTid,strOld,ptrItem)					-- Update the Result Set
			end
		end
	end -- local function TrimPlac

end -- function HandlePlac

function HandlePurge(intPurge)														-- Handle the Place record purging features

	function PurgePlac(intTid,ptrRec)												-- Purge the Place record if enabled
		if intPurge > 0 then
			if fhCallBuiltInFunction("LinksTo",ptrRec) == 0 then
				local strRec = fhGetDisplayText(ptrRec)							-- Save record Name & Id before deletion
				local intRid = fhGetRecordId(ptrRec)
				local isOK = fhDeleteItem(ptrRec)
				AddResults("Purge",isOK,intTid,strRec,nil,intRid)				-- Update the Result Set
			end
		end
	end -- function PurgePlac

end -- function HandlePurge

function TrimFields(dicOptions)
	-- dicOptions["DATE"]		Dates field trim mode :	0 = None, 1 = Delete, 2 = Year only, 3 = No Day/Month, 4 = No Day number
	-- dicOptions["NOTE"]		Notes field trim mode :	0 = None, 1 = Delete, 2 = Delete [[private]]
	-- dicOptions["ADDR"]		Addrs field trim mode :	0 = None, 1 = Delete, 2+ = One greater than number of trailing parts to keep
	-- dicOptions["PLAC"]		Place field trim mode :	0 = None, 1 = Delete, 2+ = One greater than number of trailing parts to keep
	-- dicOptions["Purge"]		Place record purge mode:	0 = No  , 1 = Yes
	-- dicOptions["ResSet"]	Create Result Set mode :	0 = No  , 1 = Yes

	if dicOptions["ResSet"] == 0 then AddResults = CountTrims end			-- When Result Set inhibited then just count trims

	HandleDate ( dicOptions["DATE"] )												-- Initialise the field trimming functions
	HandleNote ( dicOptions["NOTE"] )
	HandleAddr ( dicOptions["ADDR"] )
	HandlePlac ( dicOptions["PLAC"] )
	HandlePurge( dicOptions["Purge"])

	local function null()																-- Do nothing function for other fields
	end -- local function null

	local dicTrim = {																	-- Trim handler dictionary per field tag (should be faster than multiple if then ifelse then...)
		DATE	= TrimDate ;
		_DATE	= TrimDate ;
		NOTE2	= TrimNote ;
		_NOTE2	= TrimNote ;
		ADDR	= TrimAddr ;
		PLAC	= TrimPlac ;
		_PLAC	= TrimPlac ;
	}

	local intMaxType = fhGetRecordTypeCount()
	progbar.Start("Trimming Fields",intMaxType+1)								-- Initiate progress bar
	local ptrItem = fhNewItemPtr()
	for intType = 1, intMaxType do													-- Loop all record types
		progbar.Step()
		if progbar.Stop() then break end											-- Step or Stop the progress bar
		local intRec = 1
		local strType = fhGetRecordTypeTag(intType)
		ptrItem:MoveToFirstRecord(strType)
		while ptrItem:IsNotNull() do													-- Loop all fields
			local ptrParent = fhNewItemPtr()
			ptrParent:MoveToParentItem(ptrItem)
			if ptrParent:IsNull() then												-- No parent so must be next record
				intRec = intRec - 1
				if intRec == 0 then 													-- Only report some records to reduce run time 
					intRec = 31
					progbar.Message("Searching record "..strType.." ["..tostring(fhGetRecordId(ptrItem)).."]")
					if progbar.Stop() then break end
				end
			end
			if strType == "_PLAC" then												-- Purge Place records if enabled and no links
				local ptrRec = ptrItem:Clone()
				ptrItem:MoveNext()														-- Move pointer to next record before deletion
				PurgePlac(intType,ptrRec)
			else
				( dicTrim[fhGetTag(ptrItem)] or null )(intType,ptrItem)		-- Trim any matching text field
				ptrItem:MoveNextSpecial()												-- Move pointer to next item
			end 
		end
	end
	progbar.Close()
end -- function TrimFields

function GetOptions(dicOption)														-- Prompt User to Select Options (derived from Clean Living Persons)
	local tblName = {}																	-- Index names for dicOption settings
	local tblData = {}																	-- Parameter values for iup.GetParam
	local tblForm = {}																	-- Format strings for iup.GetParam

	local function paramAction(iupDialog,intIndex)								-- Display Help Page (derived from Clean Living Persons)
		if intIndex == iup.GETPARAM_MAP then										-- Correct button labels needed for IUP 3.28 bug	-- V1.1
			iupDialog.Button1.Title = " Trim Fields"								-- Otherwise remain as OK and Cancel
			iupDialog.Button2.Title = " Cancel Plugin"
		end
		if intIndex == (iup.GETPARAM_HELP or -4) then							-- FH V5 needs -4
			fhShellExecute("https://pluginstore.family-historian.co.uk/page/help/trim-date-and-text-fields","","","open")	-- V1.1
			fhSleep(3000,500)
			iupDialog.BringFront="YES"
		end
		return 1
	end -- local function paramAction

	local function setTables(strName,intData,strForm)							-- Set those lookup tables to values below
		table.insert(tblName,strName)												-- 'nil' values insert nothing
		table.insert(tblData,dicOption[strName] or intData)						-- Use dicOption or set default value
		table.insert(tblForm,strForm)
	end -- local function setTables

	--			Options	, Data	,	Parameter Format for iup.GetParam(...)
	setTables( "DATE"	,  0	, "Select trim Date fields mode :    %l| No Date field trim   | Delete entire Date | Keep first/only Year  | Omit Day & Month | Omit Day number |" )
	setTables( "NOTE"	,  0	, "Select trim Note fields mode :    %l| No Note field trim   | Delete entire Note | Delete [[private]] text|" )
	setTables( "ADDR"	,  0	, "Select trim Address fields mode : %l| No Address field trim| Delete all columns | Keep last column  | Keep last 2 columns  | Keep last 3 columns | Keep last 4 columns | Keep last 5 columns |" )
	setTables( "PLAC"	,  0	, "Select trim Place fields mode :   %l| No Place field trim  | Delete all columns | Keep last column  | Keep last 2 columns  | Keep last 3 columns | Keep last 4 columns | Keep last 5 columns |" )
	setTables( "Purge"	,  0	, "Purge unused Place records ?      %b" )
	setTables( "ResSet"	,  1	, "Create Result Set of changes ?    %b" )
	setTables( nil		, nil	, "Button Names                      %u[ Trim Fields, Cancel Plugin, Help && Advice]" )
	tblData = { iup.GetParam("Family Historian - "..strTitle,paramAction,table.concat(tblForm,"\n"),unpack(tblData)) }
	if not tblData[1] then return false end
	table.remove(tblData,1)															-- Align data with tables above
	for intName, strName in ipairs (tblName) do
		dicOption[strName] = tblData[intName]										-- Update options
	end
	return true
end -- function GetOptions

function CheckMode()																	-- Check for Project and Warn if in Project Mode (derived from Clean Living Persons)
	if fhGetContextInfo("CI_APP_MODE") == "Project Mode" then
		local strAns = fhMessageBox(
	[[
	Plugin Warning:
	This is designed to delete data from the current file.
	You have a Project open.
	Please confirm you want to trim the data from 
	]]..fhGetContextInfo("CI_PROJECT_NAME"),
		"MB_OKCANCEL",
		"MB_ICONEXCLAMATION")
		if strAns ~= "OK" then
			return false
		end
	end
	local strName = fhGetPluginDataFileName("LOCAL_MACHINE")					-- Global sticky options file needs V5.0.8
	return strName:gsub(" %- V%d.*%.dat$",".dat")								-- Use same .dat file for all Plugin versions
end -- function CheckMode

function Main()
	local fileOptions = CheckMode()													-- Check Project/Gedcom mode and compose sticky options filename
	if fileOptions then
		local dicOption = LoadOptions(fileOptions)								-- Load sticky options from file
		if GetOptions(dicOption) then												-- Get options choice from user
			SaveOptions(dicOption,fileOptions)										-- Save sticky options into file
			TrimFields(dicOption)														-- Trim fields as per options
			if #arrAct > 0 then
				fhOutputResultSetTitles(strTitle,strTitle)						-- Output Result Set
				fhOutputResultSetColumn("Action" , "text", arrAct, #arrAct, 104, "align_left", 1, true, "default", "show" )
				fhOutputResultSetColumn("Record" , "text", arrRec, #arrAct, 200, "align_left")
				fhOutputResultSetColumn("Buddy"  , "item", arrBud, #arrAct, 200, "align_left", 0, true, "default", "buddy")
				fhOutputResultSetColumn("Type"   , "text", arrTyp, #arrAct,  30, "align_mid" )
				fhOutputResultSetColumn("TypId","integer", arrTid, #arrAct,  30, "align_mid" , 2, true, "integer", "hide" )
				fhOutputResultSetColumn("RecId","integer", arrRid, #arrAct,  30, "align_mid" , 3, true, "integer", "show" )
				fhOutputResultSetColumn("Old Val", "text", arrOld, #arrAct, 200, "align_left")
				fhOutputResultSetColumn("New Val", "item", arrNew, #arrAct, 200, "align_left")
			else
				fhMessageBox(tostring(intAct).." field trims performed.")
			end
		end
	end
end -- function Main

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

progbar = progbar_v3()												-- To access FH progress bars module

Main()																-- Invoke main function

Source:Trim-Date-and-Text-Fields-1.fh_lua