Ancestral Sources Launcher.fh_lua

--[[
@Title:        Ancestral Sources Launcher
@Type:         Standard
@Author:       Mark Draper
@Contributors: Mike Tate, Nick Walker
@Version:      2.1
@LastUpdated:  16 Mar 2025
@Licence:      This plugin is copyright (c) 2025 Mark Draper and is licensed under the MIT License
               which is hereby incorporated by reference
               (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description:  This plugin provides a simple method for immediately launching the
               separate Ancestral Sources application from within Family Historian. It is recommended
               that the plugin is added to the user's Tools Menu if used frequently.
]]

--[[
Mar 2025 - 2.1
	Workaround to ensure FH prompts for updates (see FHUG description)
]]

fhInitialise(5,0,0, 'save_recommended')
require 'luacom'
require('iuplua')
iup.SetGlobal('CUSTOMQUITMESSAGE', 'YES')

FSO = luacom.CreateObject('Scripting.FileSystemObject')

-- *********************************************************************

function main()

	-- get installed versions of AS

	local tblV = GetVersions()
	local versions = 0

	for k, v in pairs(tblV) do
		versions = versions + 1
	end

	-- warn if no version is installed

	local link = 'https://apps.microsoft.com/detail/9MXZ7Q1WZ1SS'
	if versions == 0 then
		local msg = 'Ancestral Sources is not installed on this PC.\n\nDo you want to be redirected ' ..
				'to the Microsoft Store for a download link?'
		if MessageBox(msg, 'YESNO', 'QUESTION') == 1 then fhShellExecute(link) end
		return
	end

	-- set path if just a single version
	
	local exe_file
	local tblS = {}
	if versions == 1 then
		if tblV.Store then exe_file = tblV.Store.Path
		elseif tblV.Local then exe_file = tblV.Local.Path end
	else
		-- check versions unchanged if previous selection used

		tblS, exe_file = GetSavedVersions()
		local display_menu
		if (tblV.Store.Version or '') ~= (tblS.StoreVersion or '') then display_menu = true end
		if tblV.Local and (tblV.Local.Version or '') ~= (tblS.LocalVersion or '') then display_menu = true end
		if iup.GetGlobal('SHIFTKEY') == 'ON' then display_menu = true end
		if display_menu then
			exe_file = SelectVersion(tblV)
			if not exe_file then return end
		end
	end

	local GEDCOM_file = fhGetContextInfo('CI_GEDCOM_FILE')
	if GEDCOM_file == '' then
		local msg = 'Ancestral Sources requires an existing GEDCOM file to operate.'
		MessageBox(msg, 'OK', 'ERROR')
		return
	end

	local file_option, id_option, msg = '-FILE "' .. GEDCOM_file .. '"', ''
	local pI = fhGetCurrentRecordSel('INDI')[1]
	if pI then
		id_option = ' -IND ' .. fhGetRecordId(pI)
	else
		local pF = fhGetCurrentRecordSel('FAM')[1]
		if pF then
			local tblSpouses = {}
			for _, reference in ipairs{'~.~SPOU[1]>', '~.~SPOU[2]>'} do
				local p = fhGetItemPtr(pF, reference)
				if p:IsNotNull() then table.insert(tblSpouses, p:Clone()) end
			end
			if #tblSpouses == 0 then
				msg = 'Selected family has no parents'
			elseif #tblSpouses == 1 then
				id_option = ' -IND ' .. fhGetRecordId(tblSpouses[1])
			else
				local i = SelectSpouse(tblSpouses)
				if not i then
					return
				elseif i == 0 then
					msg = 'No individual has been selected'
				else
					id_option = ' -IND ' .. fhGetRecordId(tblSpouses[i])
				end
			end
		else
			MessageBox('Selected Record is not an Individual or Family.', 'OK', 'ERROR')
			return			
		end
	end
	if msg then
		msg = msg .. ', so Ancestral Sources will be opened with default options.'
		if MessageBox(msg, 'OKCANCEL', 'WARNING') ~= 1 then return end
	else if MessageBox('Click OK to launch Ancestral Sources','OKCANCEL','INFORMATION') ~= 1 then return end
	end
	fhShellExecute(exe_file, file_option .. id_option)
end

-- *********************************************************************

function CheckStore()

	-- look for preferred Microsoft Store version

	local HKCUapp = 'HKCU\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\' ..
			'CurrentVersion\\AppModel\\Repository\\Packages'
	local RegQuery = 'REG QUERY "{HKCU}" /F "AncestralSources" /REG:64 > "{Keys}"'
	local QueryFile = os.getenv('TMP') .. '\\~fhASkeys.tmp'

	RegQuery = RegQuery:gsub('{HKCU}', HKCUapp)						-- Insert registry key
	RegQuery = RegQuery:gsub('{Keys}', QueryFile)					-- Insert keys file for pipe

	local luaShell = luacom.CreateObject("WScript.Shell")
	pcall(function() luaShell:Run("cmd.exe /C " .. RegQuery, 2, true) end)

	for Line in io.lines(QueryFile) do
		AppPath = Line:match("\\(AncestralSources.+)")	-- path to Store version in its protected space
		if AppPath then
			Version = AppPath:match("_([%d.]+)%.0_")				-- Extract the AS Version
			AppName = AppPath:gsub("_([%d.]+)%.0_x64_","")
			AppPath = 'shell:appsFolder\\' .. AppName .. '!App'
			break
		end
	end
	return AppPath, Version
end

-- *********************************************************************

function GetVersions()

	local tblV = {}

	-- check for Microsoft Store version

	local StorePath, StoreVersion = CheckStore()
	if StorePath then
		tblV.Store = {Path = StorePath, Version = StoreVersion}
	end

	-- check for local version

	local LocalPath = GetRegKey('HKLM\\SOFTWARE\\Ancestral Sources\\App Path')
	if FSO:FileExists(LocalPath) then
		tblV.Local = {Path = LocalPath, Version = GetRegKey('HKLM\\SOFTWARE\\Ancestral Sources\\Version')}
	end
	return tblV
end

-- *********************************************************************

function GetSavedVersions()

	local OptionsFile = fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Plugin Data\\' .. 
			fhGetContextInfo('CI_PLUGIN_NAME') .. '.ini'

	local tblS = {}
	local exe_file

	if FSO:FileExists(OptionsFile) then
		for line in io.lines(OptionsFile) do
			local i = line:find('=')
			if i and line:sub(1, i-1) == 'Store Version' then tblS.StoreVersion = line:sub(i+1) end
			if i and line:sub(1, i-1) == 'Local Version' then tblS.LocalVersion = line:sub(i+1) end
			if i and line:sub(1, i-1) == 'Path' then exe_file = line:sub(i+1) end
		end
	end
	return tblS, exe_file
end

-- *********************************************************************

function SaveVersions(tblV, optStore, optDirect, optRemember, exe_file)

	local OptionsFile = fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Plugin Data\\' .. 
			fhGetContextInfo('CI_PLUGIN_NAME') .. '.ini'

	-- save or clear options, as defined

	if optRemember.Value == 'OFF' and FSO:FileExists(OptionsFile) then
		FSO:DeleteFile(OptionsFile)
		return
	end

	local F = io.open(OptionsFile, 'w')
	if not F then
		MessageBox('Cannot open options file.', 'OK', 'ERROR', 'File Access Error')
		return
	end
	F:write('[Options]\n')
	if tblV.Store then F:write('Store Version=' .. tblV.Store.Version .. '\n') end
	if tblV.Local then F:write('Local Version=' .. tblV.Local.Version .. '\n') end
	if exe_file then F:write('Path=' .. exe_file .. '\n') end
	F:close()
end

-- *********************************************************************

function SelectVersion(tblV)

	-- display menu to show preferred source

	local optStore = iup.toggle{title = 'Microsoft Store', active = 'NO'}
	local optLocal = iup.toggle{title = 'Direct Download', active = 'NO'}

	local optRemember = iup.toggle{title = ' Remember this selection\n (hold down Shift key when\n' ..
			' launching plugin to reset)'}
	local vbox = iup.vbox{optStore, optLocal}
	local radio = iup.radio{vbox}
	local frame = iup.frame{radio, title = 'Installed Versions'}
	if tblV.Store then
		optStore.Title = 'Microsoft Store (Version ' .. tblV.Store.Version .. ')'
		optStore.Active = 'YES'
	end
	if tblV.Local then
		optLocal.Title = 'Direct Download (Version ' .. tblV.Local.Version .. ')'
		optLocal.Active = 'YES'
	end

	local ok
	local btnOK = iup.button{title = 'OK', action = function(self) ok = true return iup.CLOSE end}
	local btnHelp = iup.button{title = 'Help', action = function(self)
			fhShellExecute('https://pluginstore.family-historian.co.uk/page/help/ancestral-sources-launcher')
			end}
	local btnCancel = iup.button{title = 'Cancel', action = function(self) return iup.CLOSE end,
			padding = '10x3'}
	local buttons = iup.hbox{btnOK, btnHelp, btnCancel;
			normalizesize = 'BOTH'; gap = 20, margin = '20x20'}

	local vbox2 = iup.vbox{frame, optRemember, buttons; alignment = 'ACENTER', gap = 10,
			margin = '20x20'}

	local dialog = iup.dialog{vbox2, title = 'Select Ancestral Sources Version', border = '30x30',
			minbox='NO', maxbox='NO'; defaultesc = btnCancel}
	if fhGetAppVersion() > 6 then
		iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
	end

	dialog:popup()

	if not ok then return end

	local exe_file
	if optStore.Value == 'ON' then
		exe_file = tblV.Store.Path
	else
		exe_file = tblV.Local.Path
	end
	SaveVersions(tblV, optStore, optLocal, optRemember, exe_file)
	return exe_file
end

-- *********************************************************************

function SelectSpouse(tblSpouses)

	local dialog
	local selection, ScreenPosition
	local file = fhGetContextInfo('CI_APP_DATA_FOLDER') ..
			'\\Plugin Data\\Ancestral Sources Launcher.ini'

	local label = iup.label{title = 'Select spouse to load into Ancestral Sources', expand = 'YES'}
	local btnS1 = iup.button{title = fhGetDisplayText(tblSpouses[1]), padding = '10x3',
			action = function(self) selection = 1 return iup.CLOSE end}
	local btnS2 = iup.button{title = fhGetDisplayText(tblSpouses[2]), padding = '10x3',
			action = function(self) selection = 2 return iup.CLOSE end}
	local btnN = iup.button{title = 'Neither',
			action = function(self) selection = 0 return iup.CLOSE end}
	local btnX = iup.button{title = 'Cancel',
			action = function(self)
			return iup.CLOSE
			end}
	local vbox1 = iup.vbox{btnS1, btnS2, btnN, btnX; alignment = 'ACENTER', expand = 'YES', gap = 10,
			NORMALIZESIZE = 'BOTH'}
	local vbox2 = iup.vbox{label, vbox1; alignment = 'ACENTER', gap = 30, margin = '20x20'}
	dialog = iup.dialog{vbox2; title = fhGetContextInfo('CI_PLUGIN_NAME'), border = '30x30',
			minbox='NO', maxbox='NO'}
	if fhGetAppVersion() > 6 then
		iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))

		iup.ShowXY(dialog, IUP_CENTERPARENT, IUP_CENTERPARENT)
		local position = dialog.ScreenPosition
		local FHx, FHy = position:match('([^,]*),([^,]*)')
		FHx, FHy = tonumber(FHx), tonumber(FHy)
		local size = dialog.NaturalSize
		local x, y, w, h
		w, h = size:match('^(%d+)x(%d+)$')
		local xMid = FHx + tonumber(w) / 2
		local yMid = FHy + tonumber(h) / 2
		local mi = iup.GetGlobal('MONITORSINFO')
		for m in mi:gmatch('[^\r\n]+') do
			x, y, w, h = m:match('^(%S+)%s(%S+)%s(%S+)%s(%S+)$')
			x, y, w, h = tonumber(x), tonumber(y), tonumber(w), tonumber(h)
			if xMid >= x and xMid <= x + w and yMid >= y and yMid <= y + h then		--	this monitor
				break
			end
		end
		if FHx >= x and FHx <= x + w and FHy >= y and FHy <= y + h then				-- visible
			dialog:popup(FHx, FHy)
		else
			dialog:popup(x, y)														-- corner
		end
	else
		local F = io.open(file, 'r')
		local x, y
		if F then
			local options = F:read("*a")
			F:close()
			local vs = options:match('VirtualScreen=(%C+)')
			if vs and vs == iup.GetGlobal('VIRTUALSCREEN') and iup.GetGlobal('SHIFTKEY') == 'OFF' then
				x, y = options:match('ScreenPosition=(%S+),(%S+)\n')
			end
		end
		if x and y then
			dialog:popup(x, y)
		else
			dialog:popup(IUP_CENTERPARENT, IUP_CENTERPARENT)
		end
	end

	local F = io.open(file, 'w')
	if F then
		F:write('[Options]\nVirtualScreen=' .. iup.GetGlobal('VIRTUALSCREEN') .. 
				'\nScreenPosition=' .. dialog.ScreenPosition .. '\n')
		F:close()
	end
	return selection
end

-- *********************************************************************

function GetRegKey(key)
	local sh = luacom.CreateObject 'WScript.Shell'
	local ans
	if pcall(function () ans = sh:RegRead(key) end) then
		return ans
	else
		return nil,true
	end
end

-- *********************************************************************

function MessageBox(Message, Buttons, Icon)

	-- replaces built-in function with custom version containing more options

	local msgdlg = iup.messagedlg{value = Message, buttons = Buttons, dialogtype = Icon,
			title = fhGetContextInfo('CI_PLUGIN_NAME')}
	msgdlg:popup()
	return tonumber(msgdlg.ButtonResponse)
end

-- *********************************************************************

main()

Source:Ancestral-Sources-Launcher-6.fh_lua