Advanced RapaGUI techniques

Discuss GUI programming with the RapaGUI plugin here
SamuraiCrow
Posts: 475
Joined: Fri May 15, 2015 5:15 pm
Location: Waterville, Minnesota USA

Advanced RapaGUI techniques

Post by SamuraiCrow »

Hello all!

I've been putting some work into a GUI builder with RapaGUI as the tool and target. The link is at https://github.com/SamuraiCrow/RapaEdit if you would like to monitor my progress. I've got about a week's worth of work put in so far.

This thread is to make note of some of my techniques in programming somewhat object oriented and extremely modular code. Note first of all that all the gadget types are stored in the gadgets directory. Currently I have 3: Application, Window and Group. As I expand into more territory with more gadget types, I'll post my progress here.
I'm on registered MorphOS using FlowStudio.
SamuraiCrow
Posts: 475
Joined: Fri May 15, 2015 5:15 pm
Location: Waterville, Minnesota USA

Re: Advanced RapaGUI techniques

Post by SamuraiCrow »

For my second post, I'll illustrate how to keep the RapaGUI event handler small. In order to be routed to different handler subroutines, all IDs must have a prefix punctuated by an underscore at the end. The proper handler is selected from the "prefixes" table using the prefix as the key and the function as the value.

Code: Select all

/*
**	Global Event Handler
*/
Function p_ProcessGUI(message)
	Switch message.action
		Case "RapaGUI":
			Local prefixLength=FindStr(message.id, "_")+1
			Assert(prefixLength>1)
			;Check for Local prefixes and event handlers
			Local prefix$=LeftStr(message.id, prefixLength)
			Local handler=RawGet(prefixes, prefix$)
			;Invoke local event handler from class
			handler(message, prefixLength)
	EndSwitch
EndFunction
There it is, short and sweet. The only requirement is that the "prefixes" table be initialized at startup and the handlers have to all use the same parameter list. To show an example how it is initialized, I'll take another clip from RapaEdit:

Code: Select all

;add global prefixes to registry
prefixes["win_"]=p_handler
prefixes["app_"]=p_handler
prefixes["mn_"]=p_MenuToolBar
prefixes["tb_"]=p_MenuToolBar
prefixes["wa_"]=application.AddWindow
prefixes["wr_"]=application.RemWindow
Notice how prefixes mn and tb have the same handler? Those correspond to the menus and toolbar buttons. Those often call the same functions so I use identical suffixes for each function, stripping off the prefix using the UnrightStr(message.id, prefixLength) command.

Notice also, that the wa and wr have corresponding handlers in the application namespace table to add and remove windows respectively to the GUI being edited. For my next post I'll explain why namespace tables are an easy way to avoid name collisions in the global heap.
I'm on registered MorphOS using FlowStudio.
tolkien
Posts: 190
Joined: Sun Oct 17, 2010 10:40 pm
Location: Spain

Re: Advanced RapaGUI techniques

Post by tolkien »

Great. I'll follow your github link and this thread. Thanks.
SamuraiCrow
Posts: 475
Joined: Fri May 15, 2015 5:15 pm
Location: Waterville, Minnesota USA

Re: Advanced RapaGUI techniques

Post by SamuraiCrow »

tolkien wrote: Sun Dec 13, 2020 9:33 pm Great. I'll follow your github link and this thread. Thanks.
You're welcome!

Last time I used a form of polymorphism to invoke a different handler depending on the prefix of the ID sent to the input handler. One thing I forgot to mention was that for it to work properly, each handler has to process the same parameters passed to each function and in the same order. It's also possible to invoke handlers that return parameters as long as they are consistent between all of them.

Now I'm going to use a form of encapsulation that reduces all of the global identifiers to one table in the global scope. There will only be one instance of this table and it will not be copied. This is the equivalent function of the "namespace" directive in C++. Here we go:

Code: Select all

Global application={}
application["count"]=0
application["kind$"]="application"

/*
**	Application factory method
*/
Function application:new()
	Local data={}
	data["id$"]="app"..StrStr(self.count)
	data["name$"]=data.id$
	self.count=self.count+1
	...	
	moai.DoMethod("tree_hierarchy", "InsertNode", data.id$, "root", "tail", "application "..data.name$)
	moai.DoMethod("tree_hierarchy", "Open", data.id$)
	moai.Set("tree_hierarchy", "Active", data.id$)
	treeitems[moai.Get(data.id$, "UID")]=data
EndFunction
Notice at the top how application was declared as an empty table and variables are now declared inside the table? This is the equivalent of a "static local" variable in an object-oriented language such as C++. Then carrying this farther, the "new" method is declared as a member of the application class, so to speak, so that it can access all of the static variables using the "self" designation. A C++ factory method would call its new function inside a static method so that the pointer returned by the initialization could be NULL rather than throwing an exception (error code).

There are a few other features that I'd like to point out about this factory method. First of all you see that the string named id$ always has a different number each time it's called so it uses the "count" variable as an enumerator. This allows the node in the "tree_hierarchy" inserted at the end to always have a unique identifier. The name$ variable defaults to this identifier but is separate so that the name displayed can be changed without affecting the ID field of the tree node gadget.

One finale note is that all of the gadget classes in my editor have the same "new" factory method with no parameters passed in but called as a method for the implied "self" variable. This allows another polymorphism example. In my next article I will demonstrate how the use of polymorphism can be expanded using single-inheritance but for now, this is enough to keep you observing.

In the interest of having a standardized interface, I may replace the tree nodes for each application with a tab in a page-view in order to avoid making you select the application node before adding a window or dialog box. In this way, a tab will always be selected even if a different icon within the tree gadget may be highlighted. In another future article I may show how it simplifies the window class and discuss refactoring.
I'm on registered MorphOS using FlowStudio.
SamuraiCrow
Posts: 475
Joined: Fri May 15, 2015 5:15 pm
Location: Waterville, Minnesota USA

Re: Advanced RapaGUI techniques

Post by SamuraiCrow »

Hello everyone! Now it's time for my demonstration of class polymorphism for Hollywood.

Don't worry too much about the object-oriented terminology, I define it later in this article. In order to set all the defaults in the window table for the child classes derived from it, I make an inner table called "define" because the word default is already used as a keyword:

Code: Select all

window["define"]={}
window.define["width"]=800
window.define["height"]=600
window.define["resizable"]=True
window.define["toolwindow"]=False
window.define["closegadget"]=True
window.define["draggable"]=True
This makes it simple to make a new class object in the "new" factory method using the CopyTable instruction. Then the factory can act as a constructor by setting the other fields needed:

Code: Select all

Function window:new(app)
	Local w=CopyTable(self.define)
...
	;enumerate count from static variable
	w["count"]=self.count
	self.count=w.count+1

	;set default id
	w["id$"]="win"..StrStr(w.count)
	w["title$"]=w.id$
	w["name$"]=w.id$

	Return(w)
EndFunction
Notice how the "new" factory is a child of the "window" namespace instead of the "w" object it created? This means the "self" table points to the static members of the namespace, rather than the newly created "w" object. This is significant when you look at the enumeration because the w["count"] item is local to the created class but self.count is static. The "static" keyword in C++ is used in various different ways in that language but in this context, it means that the members of the "window" table are effectively global even though they affect only the window "class" or type definition but are placed in a table anyway to avoid naming collisions with other classes.

The "define" table can also contain methods to operate on the object (represented in "new" as the "w" object).

Code: Select all

Function window.define:delete()
	If (Not IsNil(RawGet(self, "contents"))) Then self.contents:delete()
	Local uid$=moai.Get(self.id$, "UID")
	treeitems[uid$]=Nil
	moai.DoMethod("tree_hierarchy", "Remove", self.id$)
	self=Nil
EndFunction
The "delete" method is defined in the "define" table unlike the "new" factory. This means self table refers to the created object rather than the static class even though the "define" table is a member of the "window" class, the method will be transferred to all created objects. The first line If statement checks if the "contents" field of the created object is occupied so that all child objects can also be deleted. The next 2 lines just delete the item from the treeitems table in the global scope that is used to find the object when using the RapaGUI messages. The moai.DoMethod call removes the item from the treeview gadget. Lastly, the self object is assigned the Nil type to make it available to be garbage collected when Hollywood gets around to it. It's important that you do everything to deallocate everything used by the class before you do this step because once the self object is deallocated there is no way to access its fields including the self.id$ string used by the GUI.

There is one other method in the source called generateXML$ and next time I'll address how it functions using indirect recursion.
I'm on registered MorphOS using FlowStudio.
SamuraiCrow
Posts: 475
Joined: Fri May 15, 2015 5:15 pm
Location: Waterville, Minnesota USA

Re: Advanced RapaGUI techniques

Post by SamuraiCrow »

Before I get into the GenerateXML recursion, I realized I didn't finish my object-oriented polymorphism discussion before. If you look at the files in the Gadgets directory of the RapaEdit repository, you'll notice that all of them have equivalent functions with the same names in the namespace tables. This is not only because of the tables allowing this but using the same names for equivalent functions allows the main functionality in RapaEdit.hws to call the same functions in different gadget classes without considering which one it is talking about. This context sensitivity is called polymorphism.

Today I just added the Dialog.hws gadget to the repository and it is almost identical to the Window.hws file. In a future post I may discuss how to automate the generation of equivalent sections of code by using templates. This technique will use the search-and-replace functions of string.library to make equivalent labels for multiple gadgets while storing the definitions only once. Since that isn't implemented yet, it will have to wait.

One advantage of object-oriented programming is that once the framework is set-up properly, you can add more functionality in less typing. One common way to polymorph the code is to use a technique called inheritance. This means that all the base functionality has the equivalent functions in each gadget class. Only those functions are inherited by the "child class" of a parent. Normal inheritance, whether single or multiple, often takes more calling overhead with nested tables than what is necessary. For that reason, RapaEdit only uses the simplest form of inheritance: interface inheritance.

Due to each type being stored in the same variables and not needing to have much concern over which variables hold what type, Hollywood is a "weakly typed" language. This makes the interfacing just as easy as following a convention of having all gadget classes defined equivalently with no enforcement necessary. Strongly typed languages like Java require you to define all the constants and methods (functions with class membership) that an interface can enforce by a special interface file. The disadvantage of not needing this enforcement is that, when the convention is not followed carefully, you're on your own to find the bug because Hollywood can't trace it down for you.

In the case of GenerateXML, there is a difference between the Application class and all the other gadget classes. The other gadget classes only accept an indention level parameter while the Application class accepts no parameters at all. This is because Application is the "root class" and is assumed to have an indentation level of zero. As with all of the other gadgets, however, the details of the xml are filled in using variables stored in the gadget's own fields and are rendered out to the XML output which is a string returned to the calling function. If Application weren't the root class there would be cryptic errors emerging about how the Application didn't have the same parameters passed to it. Here's where the technique of indirect recursion comes in.

The entire document is stored as a tree gadget for each application. This guarantees there are no self-referential "cycles" in the calling structure. Since every node in the tree has zero or more leaf nodes and the innermost nesting level of the tree has no nodes in it, we know there will be a "terminating condition". In other words there is a limit to the number of nesting levels of the nodes even though we don't know at the start how deep the nesting goes. This means that the recursion will not be infinite and will ultimately finish its job.

Now there's only one problem. The four types of nodes we have: Application, Window, Dialog and Group are all nodes with no leaves. Next time, I'll add another gadget type that is a terminal case that shows how the recursion should work. It will work now but will be kind of pointless since a group without gadgets in it is just emptiness. I'll catch you all later!
I'm on registered MorphOS using FlowStudio.
SamuraiCrow
Posts: 475
Joined: Fri May 15, 2015 5:15 pm
Location: Waterville, Minnesota USA

Re: Advanced RapaGUI techniques

Post by SamuraiCrow »

a few days ago, I added the rectangle and button classes to RapaEdit. It took surprisingly little effort. When I made the rectangle class, all I had to do was change a copy of the group class to be a single item of type rectangle. Rectangle class has no editable fields so most of the editing methods were just empty functions. I also had to add the generateXML$() method.

In the process of adding the generateXML$() method to the Rectangle class, I noticed that I hadn't added the counterpart to the group class either so I also added that. That was just a simple exercise of using the IPairs() iterator to maintain the ordering of the objects contained in the group and the y variable is never used but is a placeholder:

Code: Select all

Function group.define.GenerateXML$(indention)
	Local xml$
	xml$=RepeatStr("\t",indention).."<"..self:header$()..
		IIf(self.title$<>""," title=\""..self.title$.."\"","")..
		IIf(self.frame," frame=\"true\""..
			IIf(self.frametitle<>"", " frametitle=\"..self.frametitle..\"", ""), "")..
		IIf(self.same, "same=\"true\"", "")..
		" color=\""..self.color.."\" id=\""..self.id$.."\""..
		IIf(self.kind=2, " columns=\""..self.columns.."\"", "")
	Switch self.halign
		Case 1:
			xml$=xml$.." halign=\"left\""
		Case 2:
			xml$=xml$.." halign=\"center\""
		Case 3:
			xml$=xml$.." halign=\"right\""
	EndSwitch
	Switch self.valign
		Case 1:
			xml$=xml$.." valign=\"top\""
		Case 2:
			xml$=xml$.." valign=\"center\""
		Case 3:
			xml$=xml$.." valign=\"bottom\""
	EndSwitch
	xml$=xml$..">\n"
	For y,x In IPairs(self.contents) Do xml$=xml$..x.generateXML$(indention+1)
	xml$=xml$..RepeatStr("\t", indention).."</"..self.header$()..">\n"
	Return(xml$)
EndFunction
Notice how the two case statements expand the enumerations from the alignment enumerations. This is because of the way that the gadgets store the results as numbers but the XML needs the results as strings. I deliberately left out the default case because the parameters are ultimately optional and don't need to be specified. Also notice that there are a number of concatenated IIf statements that omit optional parameters if they aren't specified.

The Rectangle class has a very simple generateXML$() function:

Code: Select all

Function rectangle.define.generateXML$(indention)
	Return(RepeatStr("\t",indention).."<rectangle id=\""..self.id$.."\" />\n")	
EndFunction
The only parameter is the ID field. Later, I could add an edit field to the dialog box but more realistically, I might remove the id field completely from the generateXML$() function for simplicity. There is no reason to edit a rectangle.

Now that I've got all the generateXML$() functions for four node types and two leaf types, we can start debugging the XML generation next time.
I'm on registered MorphOS using FlowStudio.
SamuraiCrow
Posts: 475
Joined: Fri May 15, 2015 5:15 pm
Location: Waterville, Minnesota USA

Re: Advanced RapaGUI techniques

Post by SamuraiCrow »

Debugging time!

As I was adding the generateXML$() functionality to the program, I discovered that I'd forgotten in multiple places to put a colon in front of the generateXML$() method calls instead of a dot. Also, I found that some of the recursion wasn't working right at the window level. Ultimately there still remains a bug in the button class called by the addGadget() function.

When there is a Rectangle only in the group in the window I get:

Code: Select all

<?xml version="1.0" encoding="utf8"?>
<application id="app0">
	<window id="win0" title="win0" width="800" height="600" dragbar="True" closegadget="True" toolwindow"False">
		<vgroup  color="0" id="grp0">
			<rectangle id="rect0" />
		</vgroup >
	</window>
</application>
However, when I add a button under the rectangle the group gets corrupted and reveals the following:

Code: Select all

<?xml version="1.0" encoding="utf8"?>
<application id="app0">
	<window id="win0" title="win0" width="800" height="600" dragbar="True" closegadget="True" toolwindow"False">
		<vgroup  color="0" id="grp0">
		</vgroup >
	</window>
</application>
Now the rectangle is gone and so is the button that I just added!

Stay tuned for updates! I'll be posting again soon once I find this subtle bug.
I'm on registered MorphOS using FlowStudio.
SamuraiCrow
Posts: 475
Joined: Fri May 15, 2015 5:15 pm
Location: Waterville, Minnesota USA

Re: Advanced RapaGUI techniques

Post by SamuraiCrow »

Update!

I finally found the bug! I had changed self.contents to RawGet(self, contents) instead of RawGet(self, "contents") in its place. When using a string key with RawGet, the key string must be in quotes! That's a subtle bug that I looked past without even seeing the problem. The Groups class should be much more reliable now.

I had noticed that any time a Group had more than one item in it it failed to work correctly. So I ran the "preview" option that outputs the XML to a Dialog box. After having stuffed the GenerateXML$() method with DebugPrint() statements, I discovered that the loop that iterated over all the keys in self.contents it was coming up empty every time. Now I know why.

Here is the corrected AddGadget() function:

Code: Select all

Function group.addGadget(dest, type)
	Local data=type:new(dest)
	If (IsNil(RawGet(dest, "contents"))) Then dest["contents"]={}
	dest.contents[dest.children]=data
	data["slot"]=dest.children
	dest.children=dest.children+1
	type.beAdded(data)
EndFunction
RawGet(dest, contents) is equivalent to RawGet(dest, 0) and was always resetting the contents table to be empty because it was always Nil.
I'm on registered MorphOS using FlowStudio.
SamuraiCrow
Posts: 475
Joined: Fri May 15, 2015 5:15 pm
Location: Waterville, Minnesota USA

Re: Advanced RapaGUI techniques

Post by SamuraiCrow »

Today I implemented loading and saving of documents. This was greatly simplified by the WriteTable() command but I still had to overcome some obstacles.

One such obstacle was that there were a great many self-referencing fields in the table containing the document. Since CopyTable() can only do shallow copies of each table's nesting levels, Since I had to go through all the save buffer's tables and remove invariant fields to reduce file size anyway, converting it to use shallow copies wasn't that much of a problem. Ultimately anything that referred to structures within the code (such as the namespace tables) had to be assigned to Nil as well. If I hadn't done that, future versions of RapaEdit wouldn't have been able to load documents without having to strip the data out later on.

Another obstacle was that, in order to be able to load multiple copies of the same document at the same time, I had to remove the enumerations associated with each node and leaf of the document. Since they could be regenerated at load time also, I pruned them out of the file format.

Among the other pruning examples were the backward references to the groups stored in the child classes. Since the numeric keys are stored in the group structure anyway it was no major loss and could, once again, be regenerated at load time.

The net result was a document containing an application with a window and a dialog, each containing two buttons and the dialog containing a rectangle, produced a save file less than 2k in length. Not bad! The pruning seems to have paid off!
I'm on registered MorphOS using FlowStudio.
Post Reply