File SMDialog.mys

SMDialog class

Introduction

Hi devs!

When you create a floatting window or interface composer, MyrScript allows you to use DialogItem such as button, static text, radio, checkbox, text editor, note head/duration selector...
These items are very useful but you may bump into their limit, for example create a small square button is impossible, it has minimal width.
An example of script made of "native" DialogItems is Target Editor.

Danièl's scripts are very inspirating.

Danièl's approch is to graphically create buttons with background of palettes' buttons (Graph.DrawPaletteButton(...)), display a text, and handle in Idle() and Draw() functions, what to do for each button while you move mouse pointer or click.
The result is really more like a palette than a "dialog", and there is no such limitations as DialogItem have:

A simple example of what Danièl scripts look like: Active layers

(but I'm sure you already installed and enjoyed all his wonderful scripts!)

If you have a look to the source code, you'll see a lot of variables for each button B1G, B1H, B1D, B1B (gauche, haut, droite, bas) which stands for "button #1" left, top, right and bottom.
You'll see also two large Idle() and Draw() functions.

So I was looking for a way to simplify the code, and makie it more human readable...

After this long introduction, let's go discovering my powerful componants and code!

How to start?

Init function, SMCore is required

SMDialog is part of SMCore library.

The next lines of code will download automatically the library and made it available.
In a new floatting window, click the title bar and add these lines to Init() function:

function Init(dialog)
	-- download/update core
	local coreUpdater = GetSystemSettingsPathName("Scripts/Includes").."SMCore-Updater.mys"
	local downloadCore = false
	if FileExist(coreUpdater)==false then
		coreUpdater = GetUserSettingsPathName("Scripts/Includes").."SMCore-Updater.mys"
		if FileExist(coreUpdater)==false then downloadCore = true; end
	end
	if downloadCore==true then
		print("SMCore libraries not found.")
		print("Downloading SMCore-Updater.mys...")
		local errorCode = Internet.DownloadFile(
			"sylvain-machefert.myriad-users.com",
			"archive/scripts/SMCore/SMCore-Updater.mys",
			coreUpdater)
		assert(errorCode==0, "Error occured while downloading. Error code: "..tostring(errorCode))
		local file,errorMsg = OpenFile(coreUpdater, "r", UNKNOWN_STR_ENCODING, UNKNOWN_CR)
		if file then
			local line = file.Read("l")
			while line do assert(strfind(line,"404 Not Found",1,1)==nil,"404 Not Found"); line=file.Read("l"); end
			file.Close()
		else error(errorMsg); end
	end
	Include "SMCore-Updater"
	SMCoreRequireVersion(20231130)
 
	Include "SMDialog"
	smDialog = SMDialog:new(dialog)
end

In Harmony > 9.9.6c, you'll have debug infos in the Console, except when the script is run from the menu.
If you want to remove debug infos, add a line LOG_LEVEL = LOG_LEVEL_INFO. Of course, change the size of your dialog (AreaWidth and AreaHEight).

Last update version is published on the forum.
Older version of SMCore is automatically updated.

KeyDown, OnMouseWheel, Idle and Draw

OnMouseWheel doesn't exist in floatting window skeleton, the others already exist. They will have one line of code, calling our smDialog object's functions. Just copy/paste ;)

function KeyDown(dialog,dummy,key) -- KeyDown : called each time a key is pressed
	local ret = smDialog:handleKeyDown(dummy, key)
	-- Return true if the key will be processed normaly
	return ret
end

function OnMouseWheel(dialog,score,amount)
	smDialog:handleMouseWheel(score, amount)
end

function Idle(dialog) -- Idle : called when nothing happens to the dialog box
	smDialog:handleIdle()
end

function Draw(dialog) -- Draw : called each time the dialog need to be redrawn
	-- default font size and face
	-- size is a number, face is a constant from MSDefine
	-- 11 and FACE_NONE should do the job.
	smDialog:handleDraw(11, FACE_NONE)
end

Concepts

DialogItems

MyrScript native items are DialogItems, called "items" in the source code.

Palette buttons

All elements used by SMDialog are called "button" because they are drawn with a background Graph.DrawPaletteButton().
By removing the background, we can create static texts, but for the code these are also... "buttons".

Panels

SMDialog introduces the concept of "panel" grouping buttons and items, which can be hidden or shown, like a contextual menu (overlapping other items and buttons), or panels of a tab bar.
You can add items in your window and attach them to a panel in their own function Init(dialog, item), in one line:

item.UserTable.Panel="MyPanelName"

Buttons and panels are tables

Buttons and panels are tables with variables like Left, Top, Right, Bottom, Caption, Help... Table elements can be functions, and this is massively used by buttons.

Relative position

Buttons position are relative to their parent panel.
By default there is a "main" panel and positions are from the left top corner of the window, but in case of contextual menu, you'll see that relative positions is very handy.

Events

Buttons have "event" functions: OnClick, OnMouseWheel, OnMouseEnter, OnMouseLeave...

Keyboard navigation and focus

Mouse related events are sent to the button under the mouse pointer, but you can navigate throught buttons using Tab or Shift+Tab. Then, the focus is changed, and the focused button will receive keyboard event: OnKeyDown.
Some components, still based on buttons, such as Slider and Grid have pre-defined behavior when pressing keystrokes.

Dialog.UserTable

Inside these event functions, you don't have direct reference to smDialog object created in Dialog.Init(), so there are some shortcuts in dialog.UserTable to interact with SMDialog (e.g. hide a panel, get a button to change it's properties...). See Dialog.UserTable for list of functions and properties.

Button creation in Dialog.Init()

All buttons, visible or hidden, in main panel or another, *MUST* be created function Init(dialog).
See SMDialog:addPaletteButton(...).

	-- B1 is a button in dialog top left corner without contextual help
	smDialog:addPaletteButton("B1", 0, 0, 180, 25, "My button caption")

	-- B2 is a button wide as the dialog with contextual help
	smDialog:addPaletteButton("B2", 0, 25, nil, 50, "A wide button", "The wide button help")

	-- bigger and bold caption, button in near bottom right corner
	-- Default font size is set to 11 in Dialog.Draw -> smDialog:handleDraw(...)
	smDialog:addPaletteButton("help", -35, -35, -10, -10, "?",
		"Click to open help, right click to make coffee",
		16, FACE_BOLD)

All these buttons are in the "main" panel = the window. We will focus on panels later.

Run the script, the buttons are drawn, but nothing happen on click. Stop the script.
Let's define the OnClick events.

Event functions

See Button.

Attach event to button

  1. Still in the function Init(dialog), get the result of SMDialog:addPaletteButton in a local variable, this is a Button:
    local B1 = smDialog:addPaletteButton("B1", 0, 0, 180, 25, "My button caption")
  2. Then, for each event required, write button.Event = function(...) body end:
    	B1.OnMouseEnter = function(dialog, button)
    		print("Hello little button. How are you?")
    	end
    	B1.OnClick = function(dialog, button, x, y, click)
    		print("OK, you clicked me!")
    	end
    	-- one-line format works as well:
    	B1.OnMouseLeave = function(dialog, button) print("Good bye little button...") end
    
  3. If the bodies of the functions are short, you can write them as the above example.
    But if you have the same body for several buttons, or a more complex code, you'd better write it in a separate function in your Dialog this way:
    function Init(dialog)
    	--...
    	B1.OnClick = function(dialog, button, x, y, click)
    			dialog.MyComplexFunction(dialog, button, x, y, click)
    		end
    end
    --... and at the bottom of your script:
    function MyComplexFunction(dialog, button, x, y, click)
    	if button.Name=="B1" and click=LEFT_CLICK then print("Left click on B1")
    	elseif button.Name=="B2" and click==LEFT_CLICK then print("Left click on B2")
    	elseif button.Name=="help" and click==RIGHT_CLICK then dialog.MakeCoffee()
    	-- else ...
    	end
    end 

    This way will lighten a bit the function Init(dialog), and can be used by several buttons for complex tasks.

Button properties

As for events, once you have the button variable, you can read/change its properties.
See Button for complete list of properties.

	local B1 = smDialog:addPaletteButton("B1", 0, 0, 180, 25, "My button caption")
	B1.CaptionHorizontalAlign = ALIGN_LEFT
	B1.IsSelected = true
	-- ...

Note: do NOT change Name, Left, Top, Right, Bottom and Panel after creation.

To change a set of button positions you can group them in a panel and move the panel (see further).

Panels

What is a panel?

Panel is a convenient way to group buttons and items: hide, show or move them in one function call,

A panel can be seen as a layer in the case you want to overlap buttons and items (e.g. a context menu that pops up at the x,y location of the right click).

It's also the way used by tabbed user interface:

The buttons and items of the panel are the tab content. In this screen capture, the panel only contains a static text.

Tabbed panels

Create panels, in function Init(dialog)

Before adding buttons to a panel with the last argument of SMDialog:addPaletteButton, you must create it.
See SMDialog:addPanel(...).

default, all buttons are in "main" panel which is automatically created.

The returne table Panel describe panel's properties: Name, Left, Top, Right, Bottom that should not be modified.
AutoClose and BackgroundColor can be modified after creation, but there should not be a lot of case.
See Panel for list of properties.

Show and hide panels

There are three functions:

This will force a redraw of the window, in debug mode, this can create a short blink, but this was not perceptible in normal mode (run from the Scripts menu).

Move panel

In the case you want a panel appear at the click position, or some other strange cases, do not change Left and Top properties!
Call SMDialog:movePanel(...). This will move all its content (buttons and items).

Note: use movePanel as "one shot", not for frequente moves like a sliding effect or a falling Tetris brick ;)
This would consume more CPU and need more graphical refreshs.

Add an item in a panel

For button, you know, it's the last argument of SMDialog:addPaletteButton(...).
For item (knob, list, note head/length selector, text box...), just click on each item and add a line in their Init function:

function Init(dialog,item)
	item.UserTable.Panel="MyPanel"
end

If you also set item.AreaLeft=x and item.AreaTop=y, these are absolute positions from left/top corner of the window. This will be converted into relative position to the left/top of the panel so the item will follow the panel's moves.

Dialog.UserTable

smDialog is a variable (a SMDialog object) created in your dialog's Init function.
It is only visible by other dialog's function (Draw, Idle) where we call smDialog:handleDraw(), smDialog.handleIdle()...
But, in all events such as button click, you may need to access smDialog, to show/hide/move panels, to get a button and change its properties... and there, smDialog variable is not visible.

In events, the native MyrScript Dialog is visible, given as dialog argument.

Dialog.UserTable is the bridge

A Lua table can contain functions.
Due to technical limitations, it was not possible to add directly function to the native Dialog object, so I used Dialog.UserTable. See it for list of properties and functions.

Dialog.UserTable functions require dialog as first argument, else they will raise an error. This is also a Lua standard.
For Java devs, these are static function, while object:doSomething() calls a method of an object).

Example: dialog.UserTable.HidePanel(dialog, name) calls smDialog:hidePanel(name).

Dialog.DrawContent()

The Dialog.DrawContent() in MyrScript manual, say it redraw completely the window.
But it doesn't know SMDialog at all.

SMDialog optimizes drawing, only needed buttons are redrawn instead of the whole window (it's around 3 times faster).


Don't call Dialog.DrawContent(), buttons will disappear!
Call dialog.UserTable.DrawContent(dialog) instead.

It forces SMDialog:handleDraw(...) to redraw all buttons.

Author
Sylvain Machefert

Class

ClassSummary
SMDialogPowerful user interface.

Tables

TableSummary
ButtonSMDialog button: the most basic component.
Dialog.UserTableFunctions and variables in MyrScript's Dialog.UserTable
GridGrid component, a flexible "table" with customizable events and draw for each column.
GridColumnColumn definition for Grid.
PanelSMDialog panel: group of Buttons and DialogItemstr that move, show and hide together.
SliderSlider: a component to pick a value between a min and a max, that accept keyboard and mouse wheel inputs.
SpinnerSpinner: a component that show arrows to select value in a pre-defined list.

Summary

ConstantTypeSummary
CLICK_NONEintNo mouse click
DUAL_CLICKintLeft+Right (dual) mouse button click
LEFT_CLICKintLeft mouse button click
RIGHT_CLICKintRight mouse button click

Constants

int CLICK_NONE

No mouse click

int LEFT_CLICK

Left mouse button click

int RIGHT_CLICK

Right mouse button click

int DUAL_CLICK

Left+Right (dual) mouse button click