An Introduction to eXTension Builder Widgets

This subforum is for topics specifically about related to creating UI Widgets with eXTension Builder ( XTB / LCB ) and defining their properties for editing with the Property Inspector. Please stay on that topic or related in this forum.
Forum rules
This subforum is for topics specifically about related to creating UI Widgets with eXTension Builder ( XTB / LCB ) and defining their properties for editing with the Property Inspector. Please stay on that topic or related in this forum.
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

I'm going to attempt to write an clear introduction to eXTension Builder Widgets here.

eXTension Builder

I don't want to get into the language related discussion too much here as this topic is about Widgets, but In case you don't know... eXTension Builder (as I've been labeling it lately while thinking of adding support for the .3 filename extension to .xtb ), is a separate coding language from xTalk script. XTBuilder Serves as an alternative to creating Externals in a foreign coding language like C++, and they binds with engine internals using the same lcdi mechanism. XTBuilder has been design with an effort to making its syntax be xTalk-like and even compatible for select language keywords (the language is also extensible but not getting into that here). However, XTBuilder is still a different animal, may require learning some programming concepts more often found in a bit lower level languages such as C++ or Pascal.

Specifically you must declare variables before using them and variables can be (and usually should be) 'typed', that is to say you must provide info about what type of data the variable will contain. If you aren't sure of the data type yet you can always 'type' the variable 'as optional any' which means allow 'nothing'/void (optional) or anything (any). In general frequently in libraries you'll be using the built-in types 'String' (text), 'Number' (any sort of number) or 'Boolean' or 'Data' (a bunch of bytes) variable types, but there are quite a few more types, many of which are for use with foreign code.

There are variable types that may be defined in specific libraries, such as the Cavas drawing module. The Canvas language module includes definitions for variable types like Canvas, Color, Point, Rectangle, Oval, Path, etc. These are the types used when creating UI graphics to be drawn by libSkia into our Widget's bounding rectangle. Not to be confused with 'Native Layer' Widgets (that's a separate topic), Widgets render with Canvas should generally render identically on every platform. The exception being if the Widget allows use of user installed Fonts, which may appear different due to lack of a font's availability on the target computer (of course there's ways you could embed a copy of the font file, but that may go against a fonts distribution rights). The point is with Canvas Widgets you can get a consistent appearance on any platform.

So with that prelude said, lets look at a widget module's structure:

The module gets wrapped in a module declaration. All modules must have a unique module namespace reverse-DNS-style identifier, such as org.whatever.library.mylib or com.whatever.widget.mywidget.

Code: Select all

widget org.openxtalk.widget.oxtuikitbutton
	-- YOUR WIDGET CODE GOES HERE --
end widget
Meta Data

We need some Metadata that will help the scripting engine know how to deal with the module, we should tell it what properties it will have when new instances of the control are created, what its properties should have as their defaults, which editor to use for a given property, etc. In short metadata is how we give our widgets properties.

A Widget should have at minimum it's name, which is different from it's extension namespace identifier (org.openxtalk.widget.whatever), this name will be used when the control is displayed in the IDE, in Tools Palette, in the Extension Manager, and in the Standalone Builder.

Code: Select all

metadata title is "Button-Widget" -- will be the displayed name for the control in the IDE.
metadata author is "Paul McClernan for OpenXTalk.org" -- writing credits
metadata version is "0.0.1" -- version num gets tracked on to the package name when an extension is packed as .lce
metadata preferredSize is "64,128" -- default Height,Width for new instance if the templateWidget has was not set
metadata _ide is "true" -- indicates module is a part of the IDE therefore is non-unloadable.
metadata userVisible is "true" --indicates if users see the module in listings or hides widgets from the Tools palette.
metadata svgicon is "M0,0v69.6c0,1.4,1.1,2.5,2.... z" -- SVG path string to use for the modules Icon in listings in the IDE.
eXTension Builder special inline Block comments

The IDE uses a markdown format for creating documentation, the API.lcdoc that the eXTension Builder creates uses this formatting as the output which is then added to the Syntax Dictionary when an XBuilder module is loaded or installed.
The API data for the API.lcdoc is (typically) parsed from specially formatted block comments within the XTBuilder (or xTalk script) source code that was inserted by the module's author as they're coding.

The first block of these special comments usually appears before the module declaration and can contains info about the widget or library module as a whole such as copyright or license info and it typically includes a general description and uses for the widget or library module.

In a the section above the widget declaration we can add some special inline markdown for any 'messages' that the control may emit using the 'post' command. This is also a way that the IDE knows which control can emit specific messages, which is a factor in creating 'defaultScripts' for use with the control.

For a 'Button' behavior you'll at minimum need to implement mouseUp/mouseDown messages, here's what the declaration for that looks:

Code: Select all

/**
Description: A button widget that has properties commonly found in xTalk 'Button' objects.

Name: mouseMove
Type: message

Name: mouseDown
Type: message

Name: mouseUp
Type: message

Name: mouseRelease
Type: message
**/
widget org.openxtalk.widget.oxtuikitbutton
	-- YOUR WIDGET CODE GOES HERE --
Now we can start to add actual code.
First we'll want to pull in any non-default languages modules from the eXTension Builder standard modules that we may want to use. This is done using the 'use' command, which is quite similar to 'include' statements you may have seen in other programming languages like C. For widgets you'll need 'com.livecode.widget', for a canvas drawn widget you're going to also want com.livecode.canvas, and there's a few others I use so often it's now cut-paste 'boiler plate' to add these for a widget as soon as I begin, but they may not all be needed depending on your code:

Code: Select all

-- dependancy declarations
use com.livecode.canvas
use com.livecode.string
use com.livecode.char
use com.livecode.array
use com.livecode.list
use com.livecode.widget
use com.livecode.engine
use com.livecode.library.widgetutils
...Continues...
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

Message Handlers
Now we can start adding handlers to make the widget actually do something.

There's a few 'built-in' messages a widget can receive, your handlers for these events must be named to match the messages.

Every widget will have at a minimum a handler called OnCreate.
OnCreate is triggered when a new instance of your widget is created. If you check the built-in property 'my scriptObject' within your OnCreate handler you would see that in that current context the current object is the 'templateWidget'. You can use script to set up the templateWidget ahead of time, just like you can for the classic controls. If you're familiar with object-oriented programming concepts you might think of the templateWidget as an object 'class' for a 'widget' type. Within the OnCreate handler you will typically fill in the module variables you may have assigned to hold widget related values with their default values, so that our widget is ready render something immediately after being created. If values such as these are not yet defined with a value then you'll likely have errors when the Widget goes to render its instances, which doesn't actually happen until the next event is triggered.

Here's an example of an OnCreate handler definition, along with a module-scoped variable definition before it:

Code: Select all

private variable mMouseButton as Number
handler OnCreate()
	put 0 into mMouseButton
end handler
With this handler and private module-scoped variable, as soon as a new instance of widget is created the module will also create mMouseButton, which might be used to hold the number of a mouseButton that may have been pressed and then OnCreate assigns that variable its default value of 0 (zero). During this event the engine is merging any property values of the widget with the default values set in 'the templateWidget' and then it creates an instance of the control. Obviously this was a very brief example, but provides the basic gist of OnCreate.

Once a new instance is placed onto a card, or a card with a pre-existing instance is navigated to, the engine triggers the OnOpen handler. If you check the built-in property 'my scriptObject' within your OnOpen handler you would see that in this handlers context the current object is NOT the 'templateWidget', but is now instead named according to whatever the name property for the instance is set to, for new instances that would be the name provided via metadata shown earlier (in the example I provided this would be 'widget "Button-Widget"').

The OnOpen handler may be thought of as a widget's equivalent to 'on openCard' in XTScript. One trick I picked up from Hermann Hoch is to create a module-scoped variable here and call it 'mE' or 'mMe' and put into it the instance's scriptObject, thereafter you can use that variable like you would the special 'me' variable in an xTalk Script. The OnOpen handler is optional and might not include much code at all. Here's a typical OnOpen handler:

Code: Select all

private variable mMe as scriptObject
handler OnOpen()
     put my script object into mE
     log [mE] -- the log command very useful for debugging, it outputs to the text field in the Extension Builder stack. 
end handler
When the widget control needs to be rendered the handler OnPaint is automatically triggered by the engine. Widgets don't render anything at all unless you add some code to render something. If you don't add any code to draw something you have an empty (invisible) rectangle in your stack/card. Regular Widgets (as opposed to 'Native Layer' widgets) are rendered with the libSkia drawing library. eXTension Builder comes with syntax that is bound to this library which is embedded in the engine(s) internally and so NO extra foreign-code libraries need to be included with the module. Typically this makes for modules that are small in byte size, of course lots of content may be include with a widget, which obviously would affect the overall file size of the module.

The syntax for Canvas drawing with Skia is rather xTalk-like. It includes support for much of the capabilities of Skia, including drawing SVG-style vector-paths-strings, images, image-patterns, shape transformations for both vector and image data, setting fill and strokes styles, and gradient ramps. For now lets keep it really simple, we'll just stroke the bounding box of the widget:

Code: Select all

handler OnPaint()
     variable tRect as Rectangle -- remember to declare variables before using them
     put my rectangle path of my bounds into tRect -- 'my bounds' is the widget's view port / clipping rect
     stroke tRect on this canvas -- draw the line around our widget's rectangle
end handler
Now our Widget will actually have a visible form. You can test it by compiling it with 'Extension Builder' in the IDE, you'll only see a thin outline of the widgets rect, but you can add more elements to render later.

For our widget to be useful as a UI control, we can make it respond to a user by adding some input event handlers. Like 'classic controls' Widgets can respond mouse input messages. The mouse event handlers that you'll typically want to use are 'OnMouseDown' and 'OnMouseUp' and possibly also 'OnMouseEnter' and 'OnMouseLeave'.
Here's a most basic example of handling mouse down within in a widget:

Code: Select all

handler OnMouseDown()
     post "mouseDown"
end handler
The 'Post' command is used to send messages to the scripting Engine. Here we are simply sending the standard xTalk "mouseDown" message, just like the 'classic controls' normally receive. Because we already added metadata for 'mouseDown' at the top of the widgets source, the script editor automatically adds the 'on mouseDown' to the 'available handlers' list on the left side of the editor window. You can modify the script that gets inserted when you click by creating a special _defaultScript file in the project's 'support' folder that includes a script for 'on mouseDown'.

Now our Widget will respond to clicks within its rectangle by sending 'mouseDown' which can then be handled by a script in the widget (or elsewhere down the message path). Our widget is practically a 'button' already!
User avatar
tperry2x
Posts: 2571
Joined: Tue Dec 21, 2021 9:10 pm
Location: Somewhere in deepest darkest Norfolk, England
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by tperry2x »

I'm following along with this, with interest.
(Just in case you thought you were talking to yourself :lol: ).
I wonder if we can put it into a step-by-step stack, with working examples and such - to make it as straightforward as possible for everyone to get to grips with.
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

tperry2x wrote: Wed Sep 18, 2024 12:31 pm I'm following along with this, with interest.
(Just in case you thought you were talking to yourself :lol: ).
OK. Great, that's encouraging. I'll try to get back to updating it (though I'm rather busy with real life stuff right now).
I have a stack I've been working on while writing this. It's not so much a walk-trough as it is a tool for generating widget 'shells' / template generation and XBuilder editor. A tutorial with screen-shots and all of that would be good to have too.
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

A note about about the widget box stroke we applied... The width of strokes are split along the center of a path. Widgets are clipped at their bounding rectangle, so if you outline its bounding rectangle with a stroke you only will see the inner half of the stroke. Therefore if you want a 2 pixel inner-outline you would need to double the stroke width to 4 pixels, the outer two pixels will be 'clipped' off as they are outside of the widgets bounds.

Code: Select all

set the stroke of this canvas to 4
stroke tPath on this canvas
Now let's change the drawing so that the stroke if filled with the default (inherited) foreground color and the inside area of the widget with the default background colors. The fore/backColors properties ONLY allow RGB components, which is an important distinction from Skia Canvas colors that can be RGB+A (A=alpha / transparency). RGB colors component numbers for Canvas are REALs / floating point, like are percentages. Rather than the 0-255 numbers of traditional xTalk script colors these colors are in the range of 0 to 1.0. Normally we would need to convert between the two color standards, but with the built-in properties this largely handled for us. All we need is the right syntax.

Code: Select all

variable tColor as Color

put the color of my background paint into tColor
fill tPath on this canvas with solid paint tColor

put the color of my foreground paint into tColor
set the stroke of this canvas to 4
stroke tPath on this canvas with solid paint tColor
TerryL
Posts: 107
Joined: Sat Oct 16, 2021 5:05 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by TerryL »

Paul, thanks for the widget tutorial. I've not seen a detailed description of how to make one before. Attached is a .txt version without edit other than a few spell-check typos. Maybe with your permission tPerry could add it to the OXT Lite Lessons stack. Terry
Attachments
Widget Lesson.zip
(5.69 KiB) Downloaded 33 times
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

TerryL wrote: Fri Sep 20, 2024 5:06 pm Paul, thanks for the widget tutorial. I've not seen a detailed description of how to make one before. Attached is a .txt version without edit other than a few spell-check typos. Maybe with your permission tPerry could add it to the OXT Lite Lessons stack. Terry
Sure, that would be great! Maybe someone could actually walk through theses steps (and take screenshots along the way), in order to make sure I'm covering everything I might not think of, or that someone who hasn't built widgets before might get snagged on.

There is a github repo by macMikey called "LCB the missing manual", which is MIT licensed, that might also be helpful. https://github.com/macMikey/LCB-missing-manual
There are some useful script snippets in that repo as well like this one that makes a 'Answer' dialog bridge between XBuilder and XTScript: https://github.com/macMikey/LCB-missing ... Answer.lcb
User avatar
tperry2x
Posts: 2571
Joined: Tue Dec 21, 2021 9:10 pm
Location: Somewhere in deepest darkest Norfolk, England
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by tperry2x »

I wonder, what would cause "put url" to suddently break?
broken.png
broken.png (76.38 KiB) Viewed 1694 times
You can see here that the correct file should be 722.7kb in size. When uploaded it's still 722.7kb in size.
I can download this from a browser and open it, and the stack will open.

However, when downloaded via the update in OXT, the file comes into the download cache at 711.1kb in size instead, and unsurprisingly won't open.
User avatar
tperry2x
Posts: 2571
Joined: Tue Dec 21, 2021 9:10 pm
Location: Somewhere in deepest darkest Norfolk, England
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by tperry2x »

That is strange, but instead of using the engine & IDE's "put url" function, which now seems to be unreliable, I'm using curl instead.
So there are two updates which you can apply to v1.07 of OXT lite. The first one tells the updates stack to use CURL instead. The second update downloads the file correctly.
I've so far tested this in Linux and Windows - I haven't been able to test the revised update mechanism in MacOS, so if we can have a victim willing volunteer, that would be great!

Edit: tested on Catalina and Monterey, and can report that curl is working nicely.

So, if all goes to plan after these last two updates, you should have part 1 of Paul's guide in the Lessons (lesson 70):
lessons.png
lessons.png (59.46 KiB) Viewed 1687 times
Anyway, sorry to hijack your post here Paul - please continue. It's all much appreciated.
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

OK getting back to this I actually created this as an .lcb file and after a few corrections did get it to compile into a functional widget, (albeit the most basic shell of a widget). Make sure to use this one due to some corrections.
Here's the code so far:

Code: Select all

/**
Description: A button widget that has properties commonly found in xTalk 'Button' objects.

Name: mouseMove

Type: message

Name: mouseDown

Type: message

Name: mouseUp

Type: message

Name: mouseRelease

Type: message

Name: backgroundColor

Type: property

Name: foregroundColor

Type: property
*/
widget org.openxtalk.widget.oxtuikitbutton

metadata title is "Button-Widget-Demo" -- will be the displayed name for the control in the IDE.
metadata author is "Paul McClernan for OpenXTalk.org" -- writing credits
metadata version is "0.0.1" -- version num gets tracked on to the package name when an extension is packed as .lce
metadata preferredSize is "64,128" -- default Height,Width for new instance if the templateWidget has was not set
metadata _ide is "true" -- indicates module is a part of the IDE therefore is non-unloadable.
metadata userVisible is "true" --indicates if users see the module in listings or hides widgets from the Tools palette.
metadata svgicon is "M0,0v69.6c0,1.4,1.1,2.5,2.... z" -- SVG path string to use for the modules Icon in listings in the IDE.

-- dependancy declarations
use com.livecode.canvas
use com.livecode.string
use com.livecode.char
use com.livecode.array
use com.livecode.list
use com.livecode.widget
use com.livecode.engine
use com.livecode.library.widgetutils

---------  metadata for built-in properties ---------
private variable mForeColor	as Color
property foregroundColor		get getForeColor	set mForeColor
metadata foregroundColor.label is "Foreground Color"
private handler getForeColor() returns String
	return colorToString( mForeColor, false)
end handler

private variable mBackColor	as Color
property backgroundColor         get getBackColor set mBackColor
metadata backgroundColor.label is "Background Color"
private handler getBackColor() returns String
	return colorToString( mBackColor, false)
end handler

public handler OnSave(out rProperties as Array)
   put colorToString(mBackColor, false)  into rProperties["backgroundColor"]
   put colorToString(mForeColor, false) into rProperties["foregroundColor"]
end handler

-- this handler is called when the widget is loaded
public handler OnLoad(in pProperties as Array)
	log "Load"
   -- log [pProperties] --> logging this doesn't seem to work! -- moreover, it silently fails making debugging difficult
   put stringToColor(pProperties["backgroundColor"]) into mBackColor
   put stringToColor(pProperties["foregroundColor"]) into mForeColor
   -- OnGeometryChanged()
   redraw all
end handler

private variable mMouseButton as Number
public handler OnCreate()
	put 0 into mMouseButton
	put the color of my foreground paint into mForeColor
	put the color of my background paint into mBackColor
   redraw all
end handler

private variable mE as scriptObject
public handler OnOpen()
     put my script object into mE
     log [mE] -- the log command very useful for debugging, it outputs to the text field in the Extension Builder stack.
     redraw all
	  log "Open"
end handler

public handler OnPaint()
   variable tColor as Color
   variable tRectPath as Path -- remember to declare variables before using them
   put rectangle path of my bounds into tRectPath -- 'my bounds' is the widget's view port / clipping rect
   log[tRectPath]
   set the paint of this canvas to my background paint
   fill tRectPath on this canvas
   set the paint of this canvas to my foreground paint
   set the stroke width of this canvas to 4
   stroke tRectPath on this canvas -- draw the line around our widget's rectangle
end handler

public handler OnMouseDown()
     post "mouseDown"
end handler
public handler OnMouseUp()
     post "mouseUp"
end handler

end widget
Note in the 'comment' section at the top of the code there's now:

Code: Select all

Name: backgroundColor

Type: property

Name: foregroundColor

Type: property
Similarly to the lines that relate to events, this lets the engine know we will be using the default object properties, theoretically we don't have to define them because they're basic properties that all scriptObjects have (which the IDE gets from a tab-sep-values .tsv file). But in practice, once the user sets these values to something, they'll expect that that value 'sticks' or is saved along within the stack and stays across restarts of the engine.

The way the engine does this is with properties arrays. Widgets can read and write to this properties array, the data of which becomes embedded into the stack file (like any other control does), that is it saves the widget user's settings for the instance of a widget. This is done though two events that get triggered OnSave and OnLoad:

Code: Select all

public handler OnSave(out rProperties as Array)
   put colorToString(mBackColor, false)  into rProperties["backgroundColor"]
   put colorToString(mForeColor, false) into rProperties["foregroundColor"]
end handler
public handler OnLoad(in pProperties as Array)
   put stringToColor(pProperties["backgroundColor"]) into mBackColor
   put stringToColor(pProperties["foregroundColor"]) into mForeColor
   redraw all
end handler
Note there is an 'out' on the parameter of rProperties param in the OnSave handler, this is a return value parameter. Parqamaters in Extension Builder can be 'in','out',or 'inout' (please hold any lewd comments). This out-parameter is what the Engine expects to receive (the properties array to save into the stack). OnLoad simply reads back in the properties array for a widget instance that was save on a card. Both handlers call Builder's 'redraw all' in order to cause a refresh of the widgets on-screen rendering, reflecting any changes that had been made to any of its properties.

The only time i actually used the inherited 'backgroundColor' ('the color of my background paint') or foregroundColor ('the color of my foreground paint') is in the 'OnCreate'' handler. My widget will only inherit these colors from the card/stack/engine/os when a new instance of this widget is created. Once these colors are explicitly set on the widget, I store the values just like I would with any custom named property.

One thing to note about shadowing 'built-in' properties is that you must use the appropriate Property Inspector editor that provides the formatting and type of value that the property expects to receive. In the case of foregroundColor and backgroundColor they need to be an string formatted RGB triplet, which is important because colors in xBuilder can be RGBA values (A=AlphaChannel) and they're Floating Point values rater than values based on bytes (integers between 0-255), these color number go from 0.0 to 1.0. This means color values need to be converted between xTalk script values to xBuilder color values. Fortunately handlers to do this conversion already come with the xBuilder in the module 'use com.livecode.library.widgetutils'. These handlers can be seen in use in the property save/load handlers:
put stringToColor(pProperties["backgroundColor"]) into mBackColor
and:
put colorToString(mBackColor, false) into rProperties["backgroundColor"]
The second parameter in colorToString is a boolean that determines wether or not to include any Alpha component there may be in the converted comma separated string return value (xTalk script's color format), which we don't want for our built-in fore/back color properties.
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

You can build and test this widget out now. Notice that because we've included the 'lcdoc' formatted documentation comments for the mouse events they appear in the left 'handlers pane of the Script editor for the widget. However, the code in the widget currently only actually sends mouseUp and mouseDown messsages and does not send a 'mouseButtonNumber' parameter (for left-click/right-click/nidde-click etc.) value along with it.
Screen Shot 2024-10-06 at 4.26.34 PM.png
Screen Shot 2024-10-06 at 4.26.34 PM.png (262.36 KiB) Viewed 1212 times
We can add the mouse button-number to the message 'post' command by using the built-in syntax for that, which in xBuilder is 'the click button':

Code: Select all

	variable mButton as Number
	put the click button into mButton
	post "mouseDown" && mButton formatted as string
The variable mButton is of a data type 'Number' so it needs to be converted into a string format in order to be concatenated with the string 'mouseUp' so that it to be sent along as the paramaeter of the mouseUp message. For coercing between number and string types xBuilder includes syntax 'formatted as string' and the reverse 'parsed as number'. You can 'post' messages with multiple parameter values by making them into a comma seperated values string ( exp. 'post "mouseMove " & tX formatted as string & "," & tY formatted as string ).

Now when update and test run with 'on mouseDown pButtonNumber', my script receives the proper value (3) for a 'right-click' in the pButtonNumber parameter.

It's worth noting as an aside that while you'd normally handle a 'post' command within the script of the widget, the posted message can be targeted at a specific scriptObject, such as a 'parent' object of the widget (the group, or card it's on, substack, stack, etc.).
You can also use the 'post' command to execute lines of xTalk script within the context of the scriptObject, which is a trick I picked up from Hermann Hoch's work. For example adding this 'post' this line to one of our event handlers:

Code: Select all

post "set width of me to 230; set height of me to 115"
Will change the size of our widget when it's executed, just as if the line were in a xTalk script within the object.
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

OK let's give our widget a text label.
The label property is a 'built in' property that is not a part of the default properties for all objects so need to add the label property to our widget in order for that to show up in the property inspector.
We add the name: and type: for the property, so that the dictionary knows this widget scan use the associated property. For the 'label' property, since its a plain text string, we don't need a 'getter' handler, only a setter is needed:

Code: Select all

/**
Name: label

variable: property

*/
private variable mLabel as optional String
property label              get mLabel              set setLabel
private handler setLabel(in pLabel as String)
	put pLabel into mLabel
	redraw all
end handler
We add the property to the onload and onsave handlers so that it's value, if it has one, gets stored.

Code: Select all

-- OnSave add: 
        put mLabel into rProperties["label"]
-- OnLoad add:
	put pProperties["label"] into mLabel
Now we can use the label property to render as text.
First we check if the label has been assigned a value, if it's undefined then its value is 'nothing'.

Code: Select all

	if mLabel is not nothing then
		set the font of this canvas to my font
		fill text mLabel at point [ my width /2 , my height / 2 ] on this canvas
	end if
The textFont and the textSize are default properties that every UI object has, although it my be hidden from the property inspector using metadata and a widget might not impliment the property at all. The syntax 'my font' is used to retrieve the text settings for the widget instance, 'the font of this canvas' must be set to use these font settings before we can render with the font.

In the example code above, you'll see that 'my width' and 'my height' is also property syntax which refers to the current size of the widget, we can divide them by half to find the widget's center point, the equivalent to 'the loc' of our widget, and then render some text center starting at that point within our widgets graphics world. This does NOT center the text within the widget as one might expect, instead it starts the line of text AT the point given and renders left-to-right from there. (which leaves us room on its left side for adding an icon or other graphics later)

In order to actually center the text, there is syntax to center the text based on a rectangle that may be passed as a parameter like so:

Code: Select all

fill text mLabel at center of my bounds on this canvas
Again, 'my bounds' is a value of data type 'rectangle' (an array of four number) that reflects the current size of the widget, so we can pass that as the parameter in order to center the text within the widget.

We can define some default label text 'such as 'Button' by adding the appropriate metadata for the property:

Code: Select all

metadata label.default is "Label"
and then adding a line to intitialize the value in our OnCreate handler:

Code: Select all

	put "Label" into mLabel
	
Testing it out now you should see the default label text and be able to change the font/size used in the property inspector.
Screen Shot 2024-10-06 at 10.29.32 PM.png
Screen Shot 2024-10-06 at 10.29.32 PM.png (83.76 KiB) Viewed 1193 times
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

Because 'the color of my foreground paint', and 'my background ...", were used to draw the text & stroke, and the fill respectively, when there is no color explicitly set for these properties the color is inherited by the parent object, engine, or operating system. On macOS this means if you keep the default (emty) for the widget and then toggle the system appearance dark/light mode the widgets colors should automatically adjust for the change.

Let's get into 'path' drawing so we can add some more elaborate graphics. A 'path' in the context of canvas drawing is a set of instructions for drawing, based on 'turtle graphics' ( https://en.wikipedia.org/wiki/Turtle_graphics ). The drawing instructions have location, orientation (or direction), and painting properties (stroke width, color, etc.). xBuilder canvas module includes some handy handlers for creating paths, combining paths instructions, and converting/inspecting the path instructions.

So far we've only created and painted a single drawing path, created based on the bounding rectangle of the widget with the line:

Code: Select all

variable tRectPath as Path -- remember to declare variables before using them
put rectangle path of my bounds into tRectPath
We can't use the 'log' command to inspect a drawing path because 'log' is (apparently) unaware of the format of the 'Path' variable type. But we can convert between a 'Path' typed variable and text/string representation of the drawing instructions using the canvas module syntax: 'the instructions of tRectPath'

Code: Select all

log[the instructions of tRectPath]
logs something like the following to the Extension Builder window:
["M0.000000 0.000000L128.000000 0.000000 128.000000 36.000000 0.000000 36.000000Z"]'

If you've ever looked inside of an SVG vector graphics file, then likely you've seen similar drawing instructions strings. The format is fairly simple, a letter that represents a pen instruction followed by X,Y coordinate pairs. In our case above this string should match the current size of the widget.

The coordinates are local to the widget, so in the first pair, the 'M' means 'Move To' the coordinate 0.000000 0.000000, which will always be the topLeft of our widget, regardless of where the widget is positioned on a card (unlike the points of a classic 'Graphic Control'). Notice that the X,Y coordinates are floating point numbers rather than integers. That's because canvas drawing can do 'sub pixel' rendering. For a very long time now 'Pixels' (Picture Elements) have been based on 72nds of an inch (very close to traditional 'Points' measurements used in printing). However in the last decade and a half or so the Pixel density of computer monitors have increasingly become higher resolution than 72 dots per inch, like Apple's 'Rentina' Displays and today's 4K and 8K screens, now days we can render at a higher quality than 72nds of an inch allows (although it may be important to note that may incur higher CPU or GPU toll).

For the next instructions/coordinate pairs the 'L' means 'Line To', which in our case is followed by the X,Y that's based on the current width (topRight) of our widget, currently 128 pixels wide. The instruction for the next X,Y pair will be the same ('Line To') so we may exclude the instruction letter and drawing will continue to use 'Line To' instructions for all coordinates until the instruction is changed again. The next coordinate pair is 128.000000 36.000000 which reflects the current width of 128px and height of 36px (bottomRight) of our widget. Next pair, 0.000000 36.000000, which draws the line back to the starting X at 0, and Y height at 36 (bottomLeft) for the bottom line of our widget. But wait a minute, that's only three sides! The last coordinate is followed by the letter 'Z' which is the instruction to 'close the path' or complete the shape by connecting the drawing back to the begining point, which in our case would've been our topLeft coordinate again, but we don't need to include that coordinate again as it is assumed, this will always be the same as the first coordinate in the path string.

Most of the time you likely won't even need to look at or consider these drawing instructions strings, but it is good to understand what you're looking at when you do see them. They are part of SVG specification, a web standard, so understanding them may come in handy.
User avatar
richmond62
Posts: 3990
Joined: Sun Sep 12, 2021 11:03 am
Location: Bulgaria
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by richmond62 »

properties for edibility in the Property Inspector
How can we 'eat' widgets?

This bit needs sorting out.
https://richmondmathewson.owlstown.net/
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

richmond62 wrote: Mon Oct 07, 2024 3:02 pm
properties for edibility in the Property Inspector
How can we 'eat' widgets?

This bit needs sorting out.
Thanks, I corrected it, although because it was in the forum description the change may take a while to propagate.
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

"WE WANT BEVELED RECTANGLE BUTTONS LIKE IT'S THE 1990s!!!" You say? OK then let's continue...
We need a shape to be used as for 'bottomFill' shaded edges color. To get the idea of how this would work I did a quick test using the classic Graphic Objects. I created a rectangle, then get its Effective Points property to get a list of the rectangles x,y points. As I mentioned the Graphic controls Points coordinates are relative to the card, so we need to first move our graphic so that its topLeft is at 0,0 (the topLeft of the card it's on).
This makes it easier to decipher the list of points:
0,78
142,78
142,0
0,78
I can change the graphic to a polygon style now and paste this list into its 'Points' property to get an identical rectangle but using 'points list'. A bit of experimenting removing different single points from the list and have an idea of how I need this, translated to psuedo code:
left,bottom line to
right,bottom line to
right,top line to
left,bottom close path
Screen Shot 2024-10-07 at 2.18.15 PM.png
Screen Shot 2024-10-07 at 2.18.15 PM.png (119.15 KiB) Viewed 1139 times
User avatar
tperry2x
Posts: 2571
Joined: Tue Dec 21, 2021 9:10 pm
Location: Somewhere in deepest darkest Norfolk, England
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by tperry2x »

To help identify points, I usually put something in a button like this:

Code: Select all

local tDefaultcurs
on mouseup
   put the defaultcursor into tDefaultcurs
   set the defaultcursor to cross
   if not (the pendingmessages contains "tRepMouse") then send tRepMouse to me in 1 ticks
end mouseup
on tRepMouse
   put the mouseloc
   if the mouse is up then send tRepMouse to me in 5 ticks
   if the mouse is down then set the defaultcursor to tDefaultcurs
end tRepMouse
Then hover over a point to get the co-ords
hold mouse down for 5 ticks to cancel.

I sometimes turn on marker points too, to help increase visibility.
vertices.png
vertices.png (25.47 KiB) Viewed 1134 times
Attachments
points.oxtstack
(1.07 KiB) Downloaded 20 times
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

Nice tip, thanks!
But in this case I just wanted visual reference to figure out a pseudo list of points, which will be re-made as pairs of variables based on the widget's bounds rectangle's points.
left,bottom line to
right,bottom line to
right,top line to
left,bottom close path
So our canvas drawing commands for this shape should be similar to the following:

Code: Select all

	variable tPath as Path -- declare a canvas 'path' type variable to hold our drawing commands
	put the empty path into tPath -- init new blank path
	move to point [ 0, my height ] on tPath -- starting point left,bottom
	line to point [ my width, my height ] on tPath -- draw line to right,bottom
	line to point [ my width, 0 ] on tPath -- draw line upwards to right,top
	close path on tPath -- closing the path adds a line that connects it back to the starting point
	fill tPath on this canvas
	log[the instructions of tPath] -- just for development, check the canvas path string
(not on my dev machine so I will test this code later)
User avatar
richmond62
Posts: 3990
Joined: Sun Sep 12, 2021 11:03 am
Location: Bulgaria
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by richmond62 »

Still edible. :D
https://richmondmathewson.owlstown.net/
User avatar
OpenXTalkPaul
Posts: 2309
Joined: Sat Sep 11, 2021 4:19 pm
Contact:

Re: An Introduction to eXTension Builder Widgets

Post by OpenXTalkPaul »

richmond62 wrote: Tue Oct 08, 2024 4:51 am Still edible. :D
OK Thanks, yeah there was a second spot in the Forum's settings where I had to change it. Looks good now.

I also corrected that last bit of code I posted after trying to compile it, as there were some minor mistakes there too. I had written them from my head, without actually being able to test it.
Post Reply

Who is online

Users browsing this forum: No registered users and 0 guests