File SMDialog.mys

SMDialog class

Table of contents

  1. Introduction
  2. Skeleton
  3. Concepts
    1. DialogItems
    2. Components
    3. Panels
    4. Components and panels are tables
    5. Relative positionning
    6. Events
    7. Keyboard navigation and focus
    8. MIDI controllers
    9. Dialog.UserTable
    10. Components creation in Dialog.Init()
    11. Event functions
    12. Attach event to button
    13. Button properties
  4. Panels
    1. What is a panel?
    2. Create panels, in function Init(dialog)
    3. Show and hide panels
    4. Move panel
    5. Add an item in a panel
  5. Dialog.UserTable
  6. Dialog.DrawContent
  7. Component zoo

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:

  • like in palettes, color change when mouse pointer is over a button
  • like some palette button, you can display 2 states: active, inactive
  • No minimal size restriction
  • You can insert text (caption), image or draw a polygon

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

Active layers script by Danièl

(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!

Skeleton?

SMDialog is part of SMCore library, bundled with Harmony Assistant. In case of update between two releases of Harmony Assistant, the following lines will download automatically latest SMCore version.
Last update version is announced on the forum.

In a new floatting or Composer window, remove the pre-defined functions, and copy/past this skeleton.

function Init(dialog)
	-- download/update library
	Include "SMCore-Updater"
	SMCoreRequireVersion(20250501)
 
	Include "SMDialog"
	smDialog = SMDialog:new(dialog)
	-- Uncomment for Composer window (modal dialog)
	-- smDialog.ListenMidiEvents = true
	
	-- Uncomment for more messages in Console in Run mode
	-- LOG_LEVEL = LOG_LEVEL_DEBUG -- LOG_LEVEL_TRACE
	
	-- Uncomment for less messages in Console in Debug mode
	-- LOG_LEVEL = LOG_LEVEL_INFO
	
	-- dialog.AreaWidth, dialog.AreaHeight = x, x
	-- dialog.AreaLeft, dialog.AreaTop = x, x
	
	-- Add components
	-- smDialog:addButton, :addSlider, :addKnob, :addGrid...
	
end -- /Init

function KeyDown(dialog, dummy, key) -- KeyDown : called each time a key is pressed
	local continueProcessing, keyAsString, ctrl, shift = smDialog:handleKeyDown(dummy, key)
	if continueProcessing == true then
		-- No component handled the pressed key, perform dialog-level processing
		-- print(keyAsString, ctrl, shift)
	end
	-- Return true if the key will be processed normaly
	return continueProcessing
end -- /KeyDown

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

function Exit(dialog) -- Exit : called just before the dialog is closed
	-- close resources, may print some debug messages
	smDialog:handleExit()
end

Concepts

DialogItems

MyrScript native items are DialogItems, called "items" in this documentation.

Components

All elements used by SMDialog are called "component". Most of them are drawn with a background Graph.DrawPaletteButton().

The basic ones are button, label (without background). More complex ones have been created: slider, spinner, grid....

Panels

SMDialog introduces the concept of "panel" grouping components and items, which can be hidden or shown, like a contextual menu (overlapping other items and components), 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"

Components and panels are tables

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

Relative positionning

Components 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

Components have "event" functions: OnClick, OnclickRelease, OnMouseWheel, OnMouseEnter, OnMouseLeave, OnInit, OnChange, OnMidiController...

Provided component (Slider, Grid, Knob...) have their own event handler functions, it's up to you to implement the OnChange

Simplest Component (buttons and labels) have no event handler implemented.

Keyboard navigation and focus

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

MIDI controllers

If you enable the Midi event listener, you can assign CC# [+ channel] shortcuts to components. This is not recommanded for a distributed script, because CC# are very hardware dependant: MIDI-master keyboards have plenty of knobs, switches, cursors (sliders) while simpler keyboard only have pitch bend and modulation wheels.
Run the script Midi > Midi input monitor to discover channel and CC# of your Midi device.

If only one component has OnMidiNoteOn or OnMidiNoteOff or OnMidiPitchBend functions, they will receive respectively all 'Note On', 'Note Off' and 'Pitch Bend' messages and be focused.

By default, once Midi listener is enabled, focused or under mouse pointer component will behave the following way:

  • pitch bend wheel scrolls the Grids
  • pitch bend wheel turns Knobs and move Sliders,
  • sustain pedal produce a click, or Space stroke,
  • all other controllers selects/change value of grids, sliders and knobs.
  • Last, if nothing above happened (the dialog only contain simple buttons), pitch bend wheel and controllers move the focus across buttons in the dialog.

Given the above behaviors and considerations, it's very safe to enable the Midi event listener for Composer window (modal dialogs), not recommanded for floatting windows as they may conflict with Midi recording, unless you really know what you are doing, for your personal use, not for distributed script.

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.

Components creation in Dialog.Init()

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

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

	-- B2 is a button wide as the dialog with contextual help
	local B2 = smDialog:addButton("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(...)
	local btnHelp = smDialog:addButton("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 Component for full list.

  • Resource allocation, init and exit:
    • B1.OnInit(dialog, compo)
      Fired only once, after the Dialog initialization. Can allocate resources that should be closed by OnExit() event.
    • B1.OnExit(dialog, compo)
      Fired only once, before dialog exit. Free resources that have been allocated by OnInit event.
  • Mouse event:
    • B1.OnClick(dialog, compo, x, y, click)
      fired when mouse button is pressed inside the component's area.
    • B1.OnClickRelease(dialog, compo, x, y, click)
      fired when mouse button is released inside the component's area.
    • B1.OnMouseEnter(dialog, compo)
      fired when mouse pointer enter the component's area.
    • B1.OnMouseLeave(dialog, compo)
      fired when mouse pointer exit the component's area.
    • B1.OnMouseMove(dialog, compo, x, y, click)
      fired each time mouse pointer move within the component's area.
      This is less common event, but can be used for a Slider (cursor) while a mouse button is pressed.
    • B1.OnMouseOver(dialog, compo, x, y, click)
      fired each time the dialog is idle and mouse pointer is in component's area, even not moving.
      uncommon but why not? It can scroll a long caption or play a little animation.
    • B1.OnMouseWheel(dialog, score, amount, compo)
      fired when mouse wheel is scrolling while mouse pointer is in component's area.
      Note that in debug mode, mouse wheel is not fired.
  • Computer keyboard:
    • B1.OnKeyDown(dialog, dummy, key, compo, ctrl, shift, keyAsString)
      fired to the focused component when a key is pressed.
  • Midi device (or other sources, see MidiAllowedSources):
    • B1.OnMidiNoteOn(dialog, compo, channel, pitch, velocity
      When Midi source plays a note while component is focused or under mouse pointer, e.g. a key of a Midi keyboard is pressed. Only if MidiAllowNoteOn is true.
    • B1.OnMidiNoteOff(dialog, compo, channel, pitch, velocity
      When Midi source stop play a note while component is focused or under mouse pointer, e.g. a key of a Midi keyboard is released. Only if MidiAllowNoteOff is true.
    • B1.OnMidiController(dialog, compo, channel, controller, value)
      When Midi hardware send a CC message (modulation wheel, pedals, switches, knobs, cursors) while component is focused or under mouse pointer. Enabled by default, MidiAllowCC is set to true.
    • B1.OnMidiPitchBend(dialog, compo, channel, value)
      When Midi hardware send a pitch bend message from the pitch wheel while component is focused or under mouse pointer. Enabled by default, MidiAllowPitchBend is set to true.
      If OnMidiPitchBend is not implemented, the pitch bend send mouse wheel event.
    • B1.OnMidiEvent(dialog, compo, event, channel, pitch, velocity, timeMs, timePos, source:
      When a midi event is received while component is focused or under mouse pointer, note on/off, controller, program change, pitch bend, aftertouch...
      Only if MidiAllowOthers is set to tru.
  • OnChange(dialog, compo, ...):
    Some component fire OnChange event you have to implement after mouse, keyboard or Midi input, e.g. a knob is rotated, selected row of a Grid has changed...
    Refer to each component for list of arguments.
  • B1.OnDraw(dialog, compo)
    called each time the button is drawn, after drawing the bacgkground palette button and the caption.
    This allow to add picture, polygons, and is massively used by other components.
  • B1.OnExit(dialog, compo)
    Called at Dialog.Exit, to free open resources.

Attach event to button

  1. Still in the function Init(dialog), get the result of SMDialog:addButton in a local variable, this is a Component:
    local B1 = smDialog:addButton("B1", 0, 0, 180, 25, "My button caption")
  2. Then, for each event required, write B1.<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, "anotherArgument")
    		end
    end
    --... and at the bottom of your script:
    function MyComplexFunction(dialog, button, x, y, click, anotherArgument)
    	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 Component for complete list of properties.

	local B1 = smDialog:addButton("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 components 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 ptjer components 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:

  • first tab show first panel, and hide all others
  • second tab show second panel, and hide all others...
The components and items of the panel are the tab content. In this screen capture, the panel only contains a static text.

Schema of a tabbed panel
>

Create panels, in function Init(dialog)

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

default, all components 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 full 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 (components 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:addButton(...). Other components (see below) have also a panel argument.
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 component 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 components.

Component zoo

A Spinner and a Slider, the spinner change the unit of the slider. The last one show the focus on the spinner.

Grid, with default parameters
Grid with folders

XY slider. Parameters: MinX=-50, MaxX=50, MinY=-30, MaxY=30, OrientationY=ORIENTATION_SOUTH

Switchable and Knob, the first turned off disable the 4 first knobs.

SMMenu use buttons with text, drawings and images.

Author
Sylvain Machefert

Class

ClassSummary
SMDialogPowerful user interface.

Tables

TableSummary
ComponentSMDialog components: buttons, label, slider, grid...
Dialog.UserTableFunctions and variables in MyrScript's Dialog.UserTable
GridGrid component, a flexible 2D "table" with customizable events and draw for each column.
GridColumnColumn definition for Grid.
KnobKnob: a component to pick a value between a min and a max, that accept keyboard and mouse wheel inputs.
PanelSMDialog panel: group of Components 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.
SwitchableSwitchable component: checkbox, radio or switch.
XYSliderXYSlider: a component to pick a couple of x,y values within bounds, that accept keyboard and mouse wheel inputs.

Summary

ConstantTypeSummary
CLICK_NONEintNo mouse click
DUAL_CLICKintLeft+Right (dual) mouse button click
FOCUS_HIGHLIGHT_CAPTIONintHighlight component's text on focus
FOCUS_HIGHLIGHT_FRAMEintHighlighted frame around focused component
FOCUS_HIGHLIGHT_FULLintHighlight full component on focus
FOCUS_HIGHLIGHT_NONEintNo highlight on focus
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

int FOCUS_HIGHLIGHT_NONE

No highlight on focus

int FOCUS_HIGHLIGHT_FULL

Highlight full component on focus

int FOCUS_HIGHLIGHT_FRAME

Highlighted frame around focused component

int FOCUS_HIGHLIGHT_CAPTION

Highlight component's text on focus