Jump to content

Macros/ScriptForge/BasicMacroOrganizer

From The Document Foundation Wiki

"Basic Macro Organizer" clone - WiP

The objective is to illustrate that ‘Tabbed Dialogs’ can be designed with ScriptForge in a modular eased way. This reduces the need to develop dynamic dialogs scripts/macros - such as those using com.sun.star.awt.tab services - that require numerous and tedious graphical adjustments.

As far as ‘Basic Macro Organizer’ is concerned, this example serves no purpose at all.

Basic Macro Organizer reproduced layout.
Basic Macro Organizer reproduced layout.

How to run it in BASIC

  1. Download & Open the Writer file
  2. Select Tools-Macros-Run macro...
  3. Run Organizer.View.Main() Basic macro

How to run it in Python - WiP

  1. Download & Open the Writer file
  2. Select Tools-Macros-Run macro...
  3. Run document organizer.view.__init__.Main() script

Cloning "Basic Macro Organizer" Code

The above tabbed dialog embeds three pre-defined dialogs – ’Dialogs’, ’Libraries’ and ’Modules' – for the sake of design simplicity. Basic scripts are adapted from ”Query and update events actions” by JesperB and rewritten using ScriptForge. Routines are grouped in modules figuring out the Model-View-Controller design pattern. Tab click events are handled, tabs keys shortcuts are not supported. Most buttons controls are intentionally left unset.

Embedded controls versus Imported Controls

Using Abstract Windowing Toolkit tabbed dialog Uno Api requires to build a dynamic dialog in order to embed controls in API defined tabs. This is error-prone and subject to numerous graphical adjustments.

This example suggests to design each tab as individual dialogs via Basic IDE dialog editor. Dialog controls can be imported – using ScriptForge facilities - including associated event routines.

REM  SCRIPTFORGE WIKI EXAMPLE
REM 	How to simulate a tabbed interface in a dialog ?
REM		Minimal required version: LibreOffice 25.8
REM 	Used services
REM 		Dialog, DialogControl

Option Explicit
Option Compatible

Private SF_dialog As Object ' SFDialogs.Dialog
Private ui As Object ' ScriptForge.UI

'*******************************************************************
'* Run the demo below
'*******************************************************************
Public Sub Main(Optional Event As Object)
	''' The VIEW part of the Model-View-Controller design pattern '''
	'GlobalScope.BasicLibraries.loadLibrary("XrayTool")
	GlobalScope.BasicLibraries.loadLibrary("ScriptForge")
	
	ui = CreateScriptService("ScriptForge.UI") ' To display a progress status bar
	ui.setStatusBar("🥁 Loading Macro Organizer 🥁")
	ui.showProgressBar(,"🥁 Loading Macro Organizer 🥁")	
		Organizer.View.setupCompoundDialog()
		Organizer.Controller.startListeners()
		SF_dialog.Page = 1 : SF_dialog.Controls("Tabs").Value = 1
		Organizer.Model.setModulesContent(SF_dialog) ' default tab 1
	ui.setStatusBar() ' clear progress status bar
	ui.showProgressBar() ' clear progress bar dialog

	Organizer.View.SF_dialog.Execute() ' Tabbed compound dialog
	Organizer.View.SF_dialog.Terminate
End Sub ' Main

'*******************************************************************
'* May be defined with the Basic IDE, except steps # 2-3
'*******************************************************************
Private Sub setupCompoundDialog
	''' Setup 'Macro Organizer' dialog visual layout '''
	Const MARGIN = 6
	Const DLG_Width = 255 : Const DLG_Height = 245
	Const BTN_Width = 45 : Const BTN_Height = 20
	Dim btn As Object ' SFDialogs.DialogControl: Button
	Dim tpc As Object ' SFDialogs.DialogControl: TabPageContainer

	' 0. Dialog definition
	SF_dialog = CreateScriptService("NewDialog", _
		"MacroOrganizer", Array(250,60,DLG_Width,DLG_Height) )
	With SF_dialog 'dialogModel
		.Caption = "🥁 Basic Macro Organizer 🥁"
		.Name = .Caption+ " dialog"
	End With

	' 1. Add two Buttons at bottom of dialog
	btn = SF_dialog.createButton("Help", Array(MARGIN,220,BTN_Width,BTN_Height))
	btn.Caption = btn.Name : btn.TabIndex = 3
	btn = SF_dialog.createButton("Close", Array(34*MARGIN,220,BTN_Width,BTN_Height),push:="OK")
	btn.Caption = btn.Name : btn.TabIndex = 4

	' 2. Import tab controls from sub-dialog
	Organizer.View.importDialogControls(1, "Organizer.Modules")
	Organizer.View.importDialogControls(2, "Organizer.Dialogs")
	Organizer.View.importDialogControls(3, "Organizer.Libraries")

	' 3. Add TabPageContainer control
	tpc = SF_dialog.createTabPageContainer( "Tabs", _
		Place := Array(MARGIN,MARGIN,5*BTN_Width,BTN_Height), _
		TabHeaders := Array("🧻 ~Modules","💬 ~Dialogs","📚 ~Libraries"), _
		Border := "NONE" )
		'Place := Array(MARGIN,MARGIN,DLG_Width - 15*MARGIN,DLG_Height - 35)
	With tpc ' TabPageContainer
		.TipText = "Use Ctrl+PgUp/Down to navigate tabs."
		.Text = "Click to navigate tabs"
	End With ' TabPageContainer

End Sub ' Organizer.View.SetupCompoundDialog

Private Sub importDialogControls(tabIdx%, subDlgName As String)
	''' Embed sub-dialog controls into specified Page/Tab number
	' Arguments:
	' 	tabIdx		Tab number [1..] aka dialog Page
	' 	subDlgName	A fully qualified 'document' dialog name
	'''
	Const OFFSET = 25, CTRLS_PERCENT = 1/20*100 ' 20 controls to import
	Static progress As Single
	Dim subDlg As Object ' SFDialogs.Dialog
	Dim libraryName As String, dialogName As String, ctl As Object ' SFDialogs.Control: any
	Dim ctrl As String, currentCtrl As Object ' SFDialogs.Control: any  
	
	libraryName = Split(subDlgName,".")(0) : dialogName = Split(subDlgName,".")(1)
	subDlg = CreateScriptService( "Dialog", ThisComponent, libraryName, dialogName )
	For Each ctrl In subDlg.Controls
		currentCtrl = subDlg.Controls(ctrl) ' SFDialogs.DialogControl: Any
		ctl = SF_dialog.importControl(subDlg, currentCtrl.Name, _
			offsetY:=OFFSET, Page:=tabIdx, IncludeOnProperties:=True )
		ctl.Page = tabIdx ' Define which controls belong to each page/tab
		progress = progress + CTRLS_PERCENT
		ui.setStatusBar("🥁 Loading Macro Organizer 🥁", progress)
		ui.showProgressBar(, "🥁 Loading Macro Organizer 🥁", progress)
	Next ' SFDialogs.DialogControl: any
End Sub ' Organizer.View.importDialogControls
###  SCRIPTFORGE WIKI EXAMPLE
### 	How to simulate a tabbed interface in a dialog ?
###		Minimal required version: LibreOffice 25.8
### 	Used services
### 		Dialog, DialogControl

Completing Dialog Content

’Modules’, ’Dialogs’ and ’Libraries’ tabs are edited as separate dialogs in order to be dynamically imported in main tabbed dialog. They are designed and tested individually, then embedded in the main tabbed dialog.

’Modules’ and ’Dialogs’ tree controls and ’Libraries’ grid control contents are filled using ScriptForge. Associated event routines transfer seamlessly when imported with ScriptForge.

Option Explicit

Private oLibraries As Object, library As String, libNode As Object

Sub setModulesContent( SF_dialog As Object ) ' SFDialog.Dialog object
	Static isInitialized As Boolean
	If IsInitialized Then Exit Sub
	
	Dim tree As Object ' SFDialogs.DialogControl: TreeControl
	Dim userRoot As Object, applRoot As Object, docRoot As Object, _
		oLibrary As Object, module As String, modNode As Object

	REM 1. Modules tab page content
	tree = Sf_dialog.Controls("modTreeCtrl")
	userRoot = tree.createRoot("My Macros", "My Macros")
	applRoot = tree.createRoot("Application Macros", "Application Macros")
	oLibraries = GlobalScope.BasicLibraries
	With oLibraries ' Let's explore libraries
	For Each library in .ElementNames
		If .isLibraryReadOnly(library) Then ' "Application Macros"
			libNode = tree.addSubNode(applRoot, library, library)
		Else ' "My Macros"
			libNode = tree.addSubNode(userRoot, library, library)
		EndIf
		oLibrary = .getByName(library)
		With oLibrary ' Let's explore modules
		For Each module in .ElementNames
			modNode = tree.addSubNode(libNode, module, library &"."& module)
		Next
		End With ' Basic modules
	Next
	End With ' GlobalScope.BasicLibraries

	docRoot = tree.createRoot(ThisComponent.Title, ThisComponent.URL)
	oLibraries = BasicLibraries
	With oLibraries ' Let's explore libraries
	For Each library In .ElementNames
		libNode = tree.addSubNode(docRoot, library, library)
		oLibrary = .getByName(library)
		With oLibrary ' Let's explore modules
		For Each module in .ElementNames
			modNode = tree.addSubNode(libNode, module, library &"."& module)
		Next
		End With ' Basic modules
	Next
	End With ' ThisComponent BasicLibraries

	isInitialized = True
End Sub ' Organizer.Model.setModulesContent

Sub setDialogsContent( SF_dialog As Object ) ' SFDialog.Dialog object
	Static isInitialized As Boolean
	If IsInitialized Then Exit Sub
	
	REM 2. Dialogs tab page content
	Dim tree As Object ' SFDialogs.DialogControl: TreeControl
	Dim userRoot As Object, applRoot As Object, docRoot As Object, _
		oLibrary As Object, dialog As String, dlgNode As Object

	REM 1. Dialogs tab page content
	tree = Sf_dialog.Controls("dlgTreeCtrl")
	userRoot = tree.createRoot("My Dialogs", "My Dialogs")
	applRoot = tree.createRoot("Application Dialogs", "Application Dialogs")
	oLibraries = GlobalScope.DialogLibraries
	With oLibraries ' Let's explore libraries
	For Each library in .ElementNames
		If .isLibraryReadOnly(library) Then ' "Application Dialogs"
			libNode = tree.addSubNode(applRoot, library, library)
		Else ' "My Dialogs"
			libNode = tree.addSubNode(userRoot, library, library)
		EndIf
		oLibrary = .getByName(library)
		With oLibrary ' Let's explore dialogs
		For Each dialog in .ElementNames
			dlgNode = tree.addSubNode(libNode, dialog, library &"."& dialog)
		Next
		End With ' Dialogs
	Next
	End With ' GlobalScope.DialogLibraries

	docRoot = tree.createRoot(ThisComponent.Title, ThisComponent.URL)
	oLibraries = DialogLibraries
	With oLibraries ' Let's explore libraries
	For Each library In .ElementNames
		libNode = tree.addSubNode(docRoot, library, library)
		oLibrary = .getByName(library)
		With oLibrary ' Let's explore dialogs
		For Each dialog in .ElementNames
			dlgNode = tree.addSubNode(libNode, dialog, library &"."& dialog)
		Next
		End With ' Dialogs
	Next
	End With ' ThisComponent DialogLibraries

	isInitialized = True
End Sub ' Organizer.Model.setDialogsContent

Sub setLocation( SF_dialog As Object ) ' SFDialog.Dialog object
	Dim location As Object ' SFDialog.DialogControl: ListBox
	Dim ui As Object ' com.sun.star.awt.XlistBox
	location = SF_dialog.Controls("libLocation")
	ui = location.XControlView 'com.sun.star.awt.XlistBox
	If ui.ItemCount = 2 Then
		ui.addItem(ThisComponent.Title,2)
	EndIf
End Sub

Sub setLibrariesContent( SF_dialog As Object ) ' SFDialog.Dialog object
	Static isInitialized As Boolean
	If IsInitialized Then Exit Sub
	
	Dim location As Object ' SFDialog.DialogControl: ListBox
	Dim ui As Object ' com.sun.star.awt.XlistBox

	' 3. Libraries grid/table content
	location = SF_dialog.Controls("libLocation")
	ui = location.XControlView 'com.sun.star.awt.XlistBox
	Organizer.Model.refreshLibraries(ui.SelectedItemPos)
	setLocation(SF_dialog)

	isInitialized = True
End Sub ' Organizer.Model.setLibrariesContent

Private Sub refreshLibraries( selectedItemPos As Integer )
	' Triggered upon location UI selection
	Dim grid As Object ' SFDialogs.DialogControl: TabPageContainer
	Dim svc As Object ' ScriptForge.Array
	Dim oContainer As Object 
	Dim location As String, tableData As Variant

	grid = Organizer.View.SF_dialog.Controls("libGridCtrl")
	svc = CreateScriptService("ScriptForge.Array")
	tableData = Array("Name", "Location") ' Grid headers
	oContainer = Switch( _
		selectedItemPos <= 1, GlobalScope.BasicLibraries, _
		selectedItemPos >= 2, BasicLibraries)
	With oContainer ' library container
	Select Case SelectedItemPos ' .selectedIem
		Case 0 ' USER "My Macros & Dialogs"
			For Each library in .ElementNames
				If .isLibraryLink(library) Then 
					location = .getLibraryLinkURL(library)
				Else
					location = ""
				End If
				If Not .isLibraryReadOnly(library) Then _
					tableData = svc.AppendRow(tableData, Array(library,location))
			Next ' library name
		Case 1 ' SHARED "Application Macros & Dialogs"
			For Each library in .ElementNames
				If .isLibraryLink(library) Then 
					location = .getLibraryLinkURL(library)
				Else
					location = ""
				End If
				If .isLibraryReadOnly(library) Then _
					tableData = svc.AppendRow(tableData, Array(library,location))
			Next ' library name
		Case 2 ' DOCUMENT ThisComponent.Title
			For Each library in .ElementNames
				tableData = svc.AppendRow(tableData, Array(library,""))
			Next ' library name
		Case Else :	tableData = svc.AppendRow(tableData, Array("🧨 bang!","Unexpected library location"))
	End Select
	End With ' oContainer library container

	grid.setTableData(tableData, widths:=Array(1,2), alignments:="LL")
End Sub ' Organizer.Model.refreshLibraries
# to be continued

Handling Dialog Events

Dialog Editor Events

Events defined with Basic IDE Dialog Editor – during individual dialog tests - are incompatible with ScriptForge. They need to be removed from dialogs – before import – and their code may require minor ScriptForge adaptation.

Dialog Event Handlers

Dialog Editor event handlers can not be used, although they offer coding advantages.

Controls Listeners

API listeners can be used. However ScriptForge ’SFDialogs’ library simplifies the management of events.

Option Explicit

''' DIALOG EDITOR EVENTS '''''''''''''''''''''''''''''''''''''''''''

Sub notImplemented(event As com.sun.star.lang.EventObject)
	Print "Not implemented yet", ',, event.Source.Name
End Sub

Sub IDESelectedLocation(event As com.sun.star.awt.ItemEvent)
	'xray event
	Select Case event.Selected
	 	Case 0,1,2 : Organizer.Model.refreshLibraries(event.Source.selectedItemPos)
	 	Case Else : notImplemented(event)
	End Select
End Sub

''' SCRIPTFORGE EVENTS '''''''''''''''''''''''''''''''''''''''''''''

Sub TabSwitch(Optional event As com.sun.star.awt.tab.TabPageActivatedEvent)
	''' Fired by a tab selection (OnTabSelected) '''
	Dim tab As Object '	SFDialogs.DialogControl: TabPageContainer
	Set tab = CreateScriptService("DialogEvent", event)
	
	If IsNull(event) Then Exit Sub
	' The Value property returns the selected tab
	If Not IsNull(tab) Then
		SF_dialog.Page = tab.Value
	 	Select Case tab.Value
	 		Case 1 : Organizer.Model.SetModulesContent(tab.Parent)
	 		Case 2 : Organizer.Model.SetDialogsContent(tab.Parent)
	 		Case 3 : Organizer.Model.SetLibrariesContent(tab.Parent)
	 	End Select
	EndIf
End Sub

Sub onNodeExpanded(event As com.sun.star.awt.tree.TreeExpansionEvent)
	Dim node As Object, parent As Object, branch As String, path As String
	Set node = event.Node : Set parent = node.Parent
	branch = node.DisplayValue ': path = node.DataValue
	'Print branch, 'path, ' output when finished script
End Sub

Sub onNodeSelected(event As com.sun.star.lang.EventObject)
	''' Triggered by the OnNodeSelected event '''

	Dim treecontrol	As Object '	A SFDialogs.DialogControl service instance
	Dim node As Object ' com.sun.star.awt.tree.XTreeNode

	If IsNull(event) Then Exit Sub

	Set treecontrol = CreateScriptService("DialogEvent", event)
    If isEmpty(treecontrol.currentNode) Then Exit Sub ' This is a root
	' Display the content of the selected node when it is not expandable
	node = treecontrol.CurrentNode
	If node.ChildCount = 0 Then
		'Print "Selection is ", node.DisplayValue,
	End If
End Sub

''' 

Private Sub setupEvents(objName As String)
	''' ScriptForge handled events: Tabs, Modules, Dialogs ... ''' 
	Dim tree As Object ' SFDialogs.DialogControl: TreeControl

	Select Case objName
		Case "Tabs"
			Organizer.View.SF_dialog.Controls("Tabs").OnTabSelected = _
				"vnd.sun.star.script:Organizer.Controller.TabSwitch?"& _
				"language=Basic&location=document"
		Case "Modules"
			tree = Sf_dialog.Controls("modTreeCtrl")
			tree.OnNodeSelected = _
				"vnd.sun.star.script:Organizer.Controller.OnNodeSelected"& _
				"?language=Basic&location=document"
			tree.OnNodeExpanded = _
				"vnd.sun.star.script:Organizer.Controller.OnNodeExpanded"& _
				"?language=Basic&location=document"
		Case "Dialogs"
			tree = Organizer.View.Sf_dialog.Controls("dlgTreeCtrl")
			tree.OnNodeSelected = _
				"vnd.sun.star.script:Organizer.Controller.OnNodeSelected"& _
				"?language=Basic&location=document"
			tree.OnNodeExpanded = _
				"vnd.sun.star.script:Organizer.Controller.OnNodeExpanded"& _
				"?language=Basic&location=document"
		Case "Libraries"
			' Event originally validated using IDE Dialog Editor events
			Organizer.View.SF_dialog.Controls("libLocation").OnItemStateChanged = _
				 "vnd.sun.star.script:Organizer.Controller.IDESelectedLocation"& _
				 "?language=Basic&location=document"
	End Select
End Sub

Private Sub startListeners
	setupEvents("Tabs")
	setupEvents("Modules")
	setupEvents("Dialogs")
	setupEvents("Libraries")

End Sub ' startListeners

Sub stopListeners(Optional dlg As com.sun.star.awt.UnoControlDialog)
	''' Stop event handlers at dialog closing time
	'''
End Sub ' stopListeners

''' API LISTENERS BELOW ''''''''''''''''''''''''''''''''''''''''''''
# to be continued

Importing document-based libraries - Python only - WiP

REM not applicable

Open Document to run example dialog

Notes