The C64 OS Programmer's Guide is being written
This guide is being written and released and few chapters at a time. If a chapter seems to be empty, or if you click a chapter in the table of contents but it loads up Chapter 1, that's mostly likely because the chapter you've clicked doesn't exist yet.
Discussion of development topics are on-going about what to put in this guide. The discusssions are happening in the C64 OS Community Support Discord server, available to licensed C64 OS users.
C64 OS PROGRAMMER'S GUIDE
Writing an Application: Tutorial 2: TestGround
Part 2: Main Application
This is the second part of the two-part tutorial on Writing An Appllication. If you haven't yet read the first part, it is highly recommended that you start with TestGround Part 1: Initialization.
The Part1: Initialization also describes how the Application's content and logic are split between the main binary (main.o) and an optional initialization binary (init.o) and how to assemble the complete Application. This part of the tutorial is shorter than the Part 1, and covers the following topics:
- Main vector table
- State variables and content
- Toolkit environment
- Quitting and clean up
- Drawing to screen
- Mouse and keyboard event handling
- Message handling
- Menu system enquiries and actions
- Toolkit object delegation and callbacks
The Application's Main Binary
The execution of init.a is more linear than main.a. Initialization flows more or less from top to bottom, completing one step after another until it reaches the bottom and then gets expunged from memory.
The code and content in main.a are accessed repeatedly and in a non-predictable order throughout the lifetime of the Application. It is structured into a number of general sections to help keep the code organized.
The structure of main.a
- Headers and includes
- Data structures and content
- App and screen layer vectored routines
- Toolkit delegates and callbacks
- Library and KERNAL link tables
Headers and Includes
In this Application there are many more headers and includes than were present in Hello World. Again, in the interest of clarity the headers and includes are grouped according to what they are.
Many of the same headers are included by main.a as were included by init.a, but not all of them. Which ones you need depends on what main.a actually needs to use. If you assemble main.o and you get an error saying that something is not defined, like, say, ff_r, it's because you forgot to include "file.s" (#inc_s "file"). Simply add the required include and try to assemble again.
The assembly start address of init.a floats around, it depends on the length of main.o. If main.o becomes longer, init.a has to be reassembled to an init.o that starts at the new end of main.o. Init.a gets its start address by importing main.xl, the exported label table from main.a. main.o is, naturally, the Application's main binary, and it is therefore always assembled to the Application's starting address, defined as appbase by the app.s include.
Data Structures
Unlike purely custom software for the Commodore 64, where you have complete freedom to structure your code in anyway imaginable, part of fitting into an operating system is following certain prescribed conventions. What you lose in freedom gets made up for by the benefits conferred from all the functionality provided by the rest of the system.
Integration with system APIs often comes in the form of data structures. In the code sample below there are 4 critical structures, plus some tables that make things easier to work with but which are not abssolutely required.
Main vector table
The first, topmost section is labelled "Exports." This is the only part of the Application that is found at a fixed address. It is called the Application's main vector table, and it exposes or exports to the operating system the only touch points that are minimally or absolutely required. Of these 5 vectors, only the first 2 need a custom implementation. These are:
- Application Initialization
- Message Handler
- App Will Quit (custom implementation optional)
- App Will Freeze (custom implementation optional)
- App Did Thaw (custom implementation optional)
The definitions of these are found in app.s, they are:
- appinit
- appmcmd
- appquit
- appfrze
- appthaw
Applications are loaded by one of two system components, the loader or the switcher. The switcher was introduced in v1.05 and replaces the loader if an REU is present. The switcher embeds all of the functionality of the loader, plus a bunch of additional functionality. The system's ability to make use of the loader or the switcher is determined by which service KERNAL module was loaded in during boot. For the sake of simplicity, this tutorial does not address fast app switching, and we will discuss only what the loader does. The switcher does almost exactly the same as the loader when launching an Application the first time.
The first thing the loader does is load in menu.m. This file must be present in the bundle. If it is not found, the loader will kick the user back to Homebase. The menu system is processed entirely by the C64 OS KERNAL. The Application does not need to do anything for the menus to be loaded or to be opened and closed by the mouse. We will learn in this tutorial how the Application interacts with the menus.
Second, the loader loads in main.o and marks in the page allocation map that the memory it occupies is allocated.
Third, the loader looks for init.o. If init.o is found in the Application's bundle, it loads it in. If it's not found, it just moves on to the next step.
Finally, it jumps through appinit, the first vector found at the beginning of main.o. As was discussed in Part 1: Initialization, if there is an init.o, appinit can be pointed directly to the start of init.o. It accomplishes this by putting the init label on the very last line of main.a. This is what TestGround does and you'll see that at the end of this tutorial.
The second vector is msgcmd or message command. For the Application to function correctly, this must be implemented. However, it is possible for it to have a very minimal implementation. The message vector is how the operating system and Utilities send messages to the Application. As we will see later in this tutorial, the menu system interacts with the Application by sending it messages and acting on the responses.
The third is the Application will quit vector. When the Application is going to be shut down and expunged from memory, so a new Application can be loaded in its place, the OS jumps through this vector. It is only necessary to implement this if you have specific resources to clean up. The loader handles some clean-up tasks automatically. Anything the loader does not handle, you have to clean up in your appquit routine. Anything you fail to clean up represents a resource leak. Leaks will eventually build up and lead to problems after some time.
The final two main vectors are appfrze and appthaw. These are used to inform the Application that it is about to be frozen into an REU bank, prior to fast App switching, and to inform the App that it was just restored from an REU bank. This tutorial will not discuss fast app switching. These vectors can be populated by placeholders, raw_rts. That constant points at a spot in workspace memory where an RTS is found. When these vectors are jumped through the result is an immediate return.
State and configurable state
State is different from content. Content is generally constant, whereas state is generally variable. It's not necessary to group your state variables in one place, but it's a good practice. The state variables can also be considered the data model in the model-view-controller (MVC) pattern.
Configurable state is a contiguous block of state that will be saved to a file in the App bundle called config.i. When the Application is initialized config.i (if it exists) is read in overtop of the block of configurable state, thus restoring it the next time the App is loaded. Loading from config.i was covered in Part 1: Initialization.
There is some state that is inappropriate to be saved and restored from config.i. For example, anything that has to be dynamically allocated during initialization. Other state properties are ideal for being saved.
Another role for the appquit vectored routine is to save config.i. Sometimes some state that would be nice to preserve and restore is in the Toolkit view hierarchy. Therefore, appquit can read some key properties from some views and write them into configurable state just prior to saving config.i. The reverse was shown in Part 1: Initialization.
TestGround saves 2 bytes of configurable state: The selected tab and the background color around the TKTabs view. We saw in Part 1: Initialization that as the main content view of the right have of the TKSplit was being intialized, its background color was set from one of these configurable state properties. And at the end of initialization, the selected tab was changed to the value stored in this configurable state.
There are two other main chunks of non-config state. First are three pointer stores: views, tstviews, and tabviews. These were discussed in Part 1: Initialization; they hold pointers to the three main sets of toolkit classes. Views holds the backbone classes, tstviews holds the many tiny controls that run down the left side, and tabviews holds the views presented in the TKTabs view to the right of the split. These stores are in main.o not init.o because the main business logic of the App needs to be able to get hold of the user interface objects to set or get their properties.
The other main chunk of state are three important structures.
Screen Layer
What gets displayed on the screen is built up from composited layers. The OS organizes the layering, and each layer only draws when it is told to draw. The layer with the lowest index draws itself to the screen first. The the layer above that is told to draw, and what it draws covers over some of what the layer below it drew, and this continues up to the topmost layer on which the menus draw themselves.
The OS also tracks which layers cause higher layers to become dirty. For example, when a menu is opened, it covers over a new area of the screen. But it's the topmost layer, so it cannot dirty anything above it. Thus, opening a menu is very efficient. Only that new menu draws to the screen. However, when that menu is closed, it uncovers an area of the screen that was below it. And this results in all of the layers from the bottom up being composited to the screen.
In another example, when the content of a Utility panel changes its layer has to be redrawn. The Application's layer is typically lower than the Utility, so the Application's layer does not get recomposited. However, a menu could be open above the Utility, so the menu layer gets redrawn after the Utility's layer is redrawn. If on the other hand the Utility panel is moved, much like a menu closing, the movement of the Utility uncovers areas of the screen below it, and now the compositing happens again from the bottom up.
The way that an Application injects itself into this process is by pushing its screen layer to the stack of layers. (The menu layer is a bit special. It's not on the layer stack proper, but it gets drawn last, making it conceptually always at the top of the stack.) The screen layer is a structure consisting of 4 routine vectors, followed by a single layer index byte.
The first is the draw vector. When it's time for your Application's layer to draw itself to the screen, the OS jumps through its draw vector.
The screen layers are also used to prioritize low-level mouse and keyboard event propagation. If there is a mouse click event it is the menu layer that has the first dibs at handling that event. If the menu layer determines that the event didn't interact with its content it signals this and allows the OS to propagate the event to the next layer down. That is perhaps a Utility which gets an opportunity to handle that event first. If the event were outside the bounds of the Utility's panel, it signals this, and the OS finally propagates the event down to the Application's screen layer.
If the mouse event is handled by a higher layer and not propagated, then the Application will have no idea that a mouse event even occurred. If the mouse event gets propagated to the Application's layer, the the OS jumps through its screen layer's mouse event handling vector.
There are two kinds of keyboard events and the order of keyboard event routing is slightly more complicated. However, if a keyboard event gets propagated to your Application the OS jumps through either the vector for key command events or printable key events. The difference between these is as follows:
Key command events are those which were generated by holding either the COMMODORE key or the CONTROL key in combination with some other key. LEFT-SHIFT is classified as a modifier only if used in conjunction with COMMDOORE, CONTROL or COMMODORE+CONTROL. The four function keys are also queued as key command events, and the function keys can be combined with other modifiers too. (i.e., CONTROL+F5 can be handled differently than F5, although both are queued as key command events.)
Printable key events are those which in principle affect the cursor in some way. These consist of all of the visibly printable numbers, characters and symbols, but also RETURN, DELETE, HOME, CLR, and the four CURSOR directions are all queued as printable key events. Holding SHIFT without any other modifier key and pressing any other key printable key produces a printable key event. (If SHIFT alone is used with a function key, that's queued as key command event.)
How each of these screen layer vectors works is covered in detail below, but here is a general principle: When any of these vectored routines is called, it is passing a notification of an event, it is not passing the event data itself. For example, upon notification that a mouse event has been propagated to your screen layer, your routine would then make a KERNAL call to read the topmost enqueued mouse event. Reading that event data does not dequeue it; you can read it as many times as you need to. It is the job of a lower-level part of the OS to dequeue the event after it has either been handled or passed through all the layers unhandled.
Draw Context
When your screen layer gets the opportunity to draw to the screen, there are different ways it could do this. The lowest-level most primitive way is to just poke some screen codes directly into the screen buffer. The screen buffer in C64 OS is even located at $0400 (1024), exactly where it is found from the READY prompt. So a C64 programmer may feel perfectly at home.
However, C64 OS also provides a more sophisticated context drawing system that makes drawing certain complicated things much easier. The C64's KERNAL ROM has a kind of context drawing system built-in. This is made up of a combination of the insertion cursor, the current color, the reverse flag, the insertion mode and/or depth, as well as implicit conversion from PETSCII to screencodes. When you press a key, the C64 scans and encodes the key's value in PETSCII, and places that byte in the keyboard buffer. When that character gets put on the screen it is converted to a screencode automatically. All of this, plus the linked line table, make up what is called the (full)screen editor. The C64 OS context drawing system is similar, but capable of more sophisticated transformations.
In the C64 KERNAL ROM's system, the properties of the draw context are permanently stored in zero page. In C64 OS, the properties are stored in a structure called a draw context. Before any drawing can be performed the KERNAL routine setctx has to be called with a RegPtr to the draw context. This moves properties from the structure into zero page, and sets up pointers ready to perform the actual output of characters.
This tutorial for TestGround will not discuss how context drawing works or any of its features. However, the context drawing system undergirds the Toolkit. All Toolkit classes draw themselves using the context drawing system. Therefore, if any Application is going to use Toolkit, it must provide a draw context structure.
The structure consist of 7 properties. Two pointers, three bytes, and two 16-bit offsets. The first two pointers point to a screen buffer and a color buffer. Although these could be pointed directly at the global screen buffer, this is not a good idea. Instead, Part 1: Initialization shows that part of the initalization of the App is to allocate 4 pages each for the screen and color buffers, and it sets the starting page of those buffers into the high bytes of these two pointers.
The next three bytes define the base dimensions of drawable region. For a fullscreen Application these are virtually always set as the dimension of the screen. The first is buffer width, which can be set to the width of the screen. A constant screen_cols is provided for this. Then draw width is next and can be set the same, and the draw height can be set to screen_rows. The reason for specifying the buffer width and the draw width separately, even though they start the same, is complicated and outside the scope of this tutorial.
The final two 16-bit properties are for a top offset and a left offset. Together these properties can be used to define a window, a rectangular area inside the draw buffer, with scrolled offsets inside a virtual drawing canvas that is much larger than the physical screen. How this is used is also outside the scope of this tutorial. Suffice it to say that Toolkit uses all of these advanced features of the context drawing system, and manipulates the properties in the draw context during its update cycle. Both of these scroll offsets should have initial values of zero.
Toolkit Environment
The C64's KERNAL ROM and BASIC have only one process drawing on the screen at a time. So the screen editor's properties can live permanently the global zero page space. Our Application needs its own draw context, and needs to set that context before drawing, because in C64 OS different processes share the screen.
Similarly, each process that draws to the screen may use the Toolkit. Each process that uses the Toolkit needs to define its own Toolkit environment structure. This structure holds permanent and variable properties about the environment that make it capable of drawing the view hierarchy, and processing and propagating mouse and keyboard events.
The Toolkit requires a draw context to perform its drawing tasks. To this end, the first pointer of the Toolkit environment is to the draw context. In TestGround, we won't do anything with the draw context manually. The draw context is defined and provided with allocated buffers for the sole purpose of backending the Toolkit environment.
A Toolkit object is instantiated with the KERNAL call tknew. As described in the KERNAL documentation, this call makes an implicit call to malloc. But, again because multiple processes share heap memory, the memory has to be divided into pools, and the pool from which malloc will make an allocation must be specified. Toolkit does this implicitly, but it needs to be provided with a memory pool to use. Part 1: Initialization showed how to calculate the minimum size of a pool necessary to fit all of the Toolkit objects that will be allocated. The start page of that memory pool is stored in the Tookit environment.
Next, the Toolkit user interface is composed of many objects, which are nested hierarchically. If for any reason a Toolkit object becomes dirty and needs to redraw itself (because of a mouse or keyboard event, or perhaps a timer or a network event) that object itself gets marked dirty. However, recursively traversing the entire Toolkit view hierarchy checking to see if any objects are dirty is a slow process. If there are no dirty objects (which is true most of the time) it would be much more efficient to know this during a draw cycle, and skip the slow recursive search. That is the purpose of the next property in the Toolkit environment. It is a dirty flag global to the entire Toolkit view hierarchy. If any one or more object gets marked dirty, the the Toolkit environment's global dirty flag needs to be set too.
This property is a set of bit flags. The other flags do things that are out of scope for this tutorial. However, the dirty flag is bit 0 (tf_dirty defined by //os/s/:toolkit.t) We want to mark the Toolkit as requiring an inital draw cycle, so this property should be set as 1.
The next property is the screen layer index. For reasons we won't discuss, the Toolkit needs to know what screen layer it belongs to. In the vast majority of cases, the Application's screen layer is the lowest layer, screen layer 0. Therefore we can just set this property to 0. In other situations, after pushing the screen layer to the stack, the screen layer index could be copied to the Toolkit environment.
The following four properties are pointers to objects in the view hierarchy. The first is to the root view. Every Toolkit view hierarchy must begin with root view. The instantiation and assignment of the root view to the Toolkit environment was covered in Part 1: Initialization. The other 3 pointers are managed automatically and facilitate event handling. They can be set to 0.
The last two properties are single byte values that represent offsets of where the Toolkit's draw context is on the screen. This is used internally to normalize low-level mouse events on the origin of the Toolkit's drawable area. In other words, at a low level, when the KERNAL and the input driver queue a mouse event, the event's coordinates are normalized on the whole screen; 0,0 means the top,left of the physical screen. If the Toolkit is in a floating panel, such as with a Utility, these offsets are used so that Toolkit can renormalize that low-level event so that 0,0 means the top,left corner of that Toolkit environment. For most fullscreen Applications these values are just 0 and stay zero, because the Application's Toolkit environment's drawable area coincides with the physical screen.
Content Section
Just as it's a good practice to put your state variables together, it's a good practice to put your Application's content in one place.
Most of these sections consist of a labels to string accessor macros, followed by the list of strings. The string accessor macros, #strxyget and #straxget, are described in Chapter 3: Development Environment → Common C64 OS Macros → String. These make it very easy to fetch a string pointer using an index in the accumulator. Most things in C64 OS take pointers in the format known as a RegPtr (X → Low Byte, Y → High Byte,) but occasionally something will take a pointer in the AX format (A → Low Byte, X → High Byte.)
TKLabel, for example, takes its string pointer in the AX format. If you have many things which all need the string pointer in the AX format, these can be grouped in a string store preceded by the #straxget accessor macro. The result returned is already in the format needed. If on the other hand, the majority of strings are needed in the RegPtr format, but just the odd one or two need the AX format, these can all be grouped together in a single #strxyget accessor, and a simple xytoax routine can be used to swap the pointer format. This routine is found here to accommodate the TKLabels in the UI.
Some of these strings have already been put to use during the initialization, and was described in Part 1, and use of some of the others will be shown later, when we look at how to create the callbacks and delegates for some of the Toolkit objects.
One special case in the content section is the table columns definition. Whether this structure is content or state is debatable. It could arguably be put in the state section.
The column definitions structure, as shown in Part 1: Initialization, is assigned to both the TKCols object and its child view, TKTable. Each column consists of a structure of 7 properties. If there are 3 column, that 7 property structure is repeated once for each column, and finally the overall structure is terminated by null bytes. The structure of a column is defined by (//os/tk/s/:tktable.s)
- Column header (tc_name)
- Identifier (tc_id)
- Resizable (tc_resz)
- Current width (tc_csiz)
- Minimum width (tc_mnsz)
- Maximum width (tc_mxsz)
- Content alignment (tc_algn)
If you wanted to preserve as configurable state the widths of all the columns, you could copy the tc_csiz property from each column into config state variables in the appquit routine. And during initialization, after loading config data from disk, these values could be copied back into this columns definition structure.
The first property of each column is a 2-byte pointer to the string that will be displayed in the column header. The second property is a unique column identifier. Here we just used the numbers 0, 1, and 2. How this is used is described later. The resizable property indicates if this column can be resized by the user. If it's 1, the column header displays a grippy icon whence the column can be resized.
The next three properties define the current width of the column, and its minimum and maximum sizes. These constrain how narrow or how wide the user can resize the columns. It would be natural to think, why have the resizable property at all? You could just set current, min and max widths all to the same value. Having the resize property makes it more efficient to determine whether the grippy should be drawn, and whether a mouse down on the area where the grippy would be drawn should trigger the start of a resize. An additional benefit is that a column can be temporarily locked by flipping the resize property to 0. It can later be unlocked by setting that property back to 1, and no manipulations of min and max values need to be made.
Lastly, the content alignment property allows the content of the column to be aligned left or right. This is very useful if you have a column of numbers. Aligning that column right makes the numbers line up nicely.
Will Quit (Vectored Routine)
We are now into the implementation of the Application's routines. The first is a willquit, this is the vectored routine from the App's main vector table. If init.o is not used, it is common that appinit, appquit, appfrze and appthaw will all come together at the top of main.o. However, in TestGround, init.o is handling the initialization for us, and the two routines for integrating with fast app switching are not implemented.
To summarize, willquit handles several clean up tasks: write out the configurable state data and delete the previously instantiated TKText object. The reason for deleting the TKText object explicitly, but not deleting any of the other objects, is because some objects (usually the complex ones) allocate additional resources. We don't have a handle on those resources and can't free them manually. Deleting the object allows the object to free up its own resources.
To save the config data we'll use fopen, fwrite and fclose. But we need a file reference to pass to fopen. Every Application has access to a special file reference that is configured for us called the appfileref (defined by //os/s/:service.s). This file reference is preconfigured to refer to the current Application's bundle directory. This is very convenient, as we are never required to lookup the system directory name or the system device or partition number. It also allows the user to rename the App bundle or to copy the Application bundle so there is more than one copy of the same Application. (This allows multiple copies of the Application to be open at the same time via fast app switching.) By using appfileref you get a dynamic reference to the current bundle directory.
If your code will reference multiple files from the bundle, a convenient routine to write is the one above called cnfappfref. It takes a pointer to a filename string, and writes it into the filename property of the appfileref.
App File References
The App File Reference (appfileref) is configured to point at the root of the Application's bundle. This defines the device number, the partition number, and the full path including the system directory name (which isn't required to be "os") and the Application's bundle name (which can be changed by the user.) The full path to TestGround will usually be "//os/applications/TestGround/".
The App File Reference does not define what filename it holds. Therefore, the App's code may change that filename. It is therefore safest to assume you don't know what the filename is and to set it explicitly before trying to use appfileref to reference a specific file.
An Application bundle could also store files in a subdirectory of the bundle. If you change the bundle's path, by say, appending a subdirectory, the appfileref is no longer valid. You can do this temporarily, but before returning control to the operating system, you must restore the path so it points to the root of the bundle. The reason is because more processes than just your code make use of the appfileref. Utilities, for example, store and retrieve their own resources from the App's bundle. An invalid appfileref could lead to strange and buggy behavior, or even a crash.
If you need a file reference that is based upon the appfileref, but that has an appended subdirectory, and persists beyond the momentary scope of its use, you should allocate a new page of memory, use memcpy to duplicate the existing appfileref into the new page, and then use that new file reference however you want.
Setting the filename of appfileref is done by passing a pointer to cnffile into the cnfappfref routine. cnffile is in the content section of main.a, a string "config.i". Now fopen refers to config.i in the Application's bundle. The file flags ff_w OR'd with ff_o are used. This will write a file, creating it if it doesn't exist and overwriting if it does exist. It then fwrites from the start of the block of configurable state data, with a fixed size 2 bytes, and then fcloses the file.
Before deleting the Toolkit object, we have to set the toolkit environment, shown above by passing RegPtr to tkenv to the settkenv KERNAL call. Then we get a pointer to the TKText object that was saved at tabviews,5 and establish it is as the current "this" object with ptrthis. Then we prepare and call its delete method.
That's all that's required for the clean up in willquit. What about all those other calls to TKNew? What about the many page allocations? What about the screen layer that good pushed to the stack? What about the Application's menus? The loader handles cleaning up all of this. The TKNew calls that make mallocs were performed within a pgalloc'd memory pool. That memory pool plus all the other pgalloc's were done with the type #mapapp. This tells the system that these pages are owned by the Application. And it deallocates them automatically.
The loader also deallocates the menus for us, and it pops all of the screen layers. Part of quiting an Application involves first quitting any open Utility. The Utility then handles popping its own screen layer. Between one App quitting and the next App loading, all screen layers are removed from the layer stack. Similarly, all enqueued timers are automatically removed from the queue. It would be very bad if an Application were quit but one of its timers was still being processed. This doesn't happen. Additionally, any queued mouse or keyboard events are removed and discard when an App is quit.
Screen Layer vectored routines
Following the routines implemented by the main vector table, the next routines are the implementation of those vectored by the screen layer. Drawmain, procmous, prockcmd and prockprt. We'll look at prockcmd last, because it does some custom work not handled by Toolkit. If your user interface is 100% driven by Toolkit, the implementation of these routines is very short. However, it's not uncommon to implement a few extra-Toolkit features.
Aside: About drawing to the screen
There are some key principles to understand when it comes to drawing. The system screen buffer is shared by all processes. Any process that writes something to the screen buffer cannot rely on that content remaining there indefinitely. This is an important difference from the way stand-alone Commodore 64 software works. If you write a program for the naked C64 and put some content on the screen, as long as you yourself never overwrite that area of the screen, it could reliably stay there until the user resets the computer.
This is not the case in C64 OS. Your Application does not own the screen; it shares the screen with other user and system processes. You should never write to the screen buffer when it is not the right time to do so. For example, setting a timer and writing something into the screen buffer every time the timer expires is a very bad idea. Writing something to the screen buffer in direct response to a mouse or keyboard event is also a bad idea. Writing to the screen buffer at an inappropriate time is very likely to produce an unexpected result and will probably look glitchy.
There is only one time when writing to the screen buffer is acceptable; when your screen layer's draw routine is called.
You yourself should never call your screen layer's draw routine. The correct order of is to implement your draw routine (called, drawmain, above.) And expect that this routine will be called hundreds and hundreds of times throughout the life of your Application. If you explicitly want to draw something on the screen, for example, some data has changed and what is on screen is out of date, call the KERNAL routine markredraw, and then wait. Let whatever routine called markredraw return to the main event loop without attempting to write anything to the screen. Some small number of cycles later, at the right time during screen compositing cycle, the OS will call your screen layer's draw routine for you.
TestGround uses the Toolkit exclusively for its user interface. The Toolkit environment knows if it itself is dirty and requires and update. It also has pointers to its draw context. Load a RegPtr to the toolkit environment struct and call tkupdate. This allows Toolkit to perform all necessary updates which it performs with the context drawing system to draw out to the screen layer's private buffers. If Toolkit has nothing to do, it will return immediately.
The second step is to copy your screen layer's drawing buffer to the screen buffer. This is done by calling ctx2scr. The KERNAL call ctx2scr requires the preparatory routine setctx, as it uses the values in the current draw context to locate the the buffers and their dimensions. However, tkupdate implicitly calls setctx for you.
Ctx2scr composites a draw context into the screen buffer at an offset from the origin (top,left) of the screen. The left offset is passed in the X register, and is signed, allowing the draw buffer to be composited starting off the left side of the screen. The top offset is passed in the Y register, which is unsigned. If the draw context is wider or taller than the screen or if its offsets cause the left, right or bottom to overflow the left, right or bottom of the screen, it clips automatically. This is not usually something you have to worry about for an Application, and most of the time these values are simply zero (0,0). In the above code, the values are read from the bottom of the Toolkit environment struct.
procmousHandling mouse events when using Toolkit is very easy. Recall that the screen layer's vectored routine being called is a notification that one or more mouse event is queued. It does not pass the event data in the call.
We need to inform Toolkit of the event and allow it to process it. Load a RegPtr to the Toolkit environment struct and call tkmouse. Upon return, the screen layer may require a redraw or it may not, depending on what the event did. To handle this, procmous jumps to another routine called chkdirt. That is implemented by TestGround and will be discussed below.
prockprtHandling key events using Toolkit is also very easy. The process is essentially identical to handling mouse events.
We need to inform Toolkit of the event and allow it to process the printable key event. Load a RegPtr to the Toolkit environment struct and call tkkprnt. Once again, the screen layer may require a redraw or it may not, depending on what the event did. To handle this, prockprnt calls the same chkdirt routine that is called by procmous. The logic it performs is in a separate routine specifically because it is identical for mouse and keyboard events.
prockcmdThe real TestGround Application is used as a playground for the developers at OpCoders Inc to test low-level features of the KERNAL, libraries and Toolkit. The following implementation has been simplified to show the basic idea of how to mix Toolkit- and non-Toolkit- based keyboard handling.
Remembering that the screen layer's vectored routine is only a notification, the first step is to call readkcmd. This fetches the top key commmand event from its queue, but does not remove it from the queue.
The KERNAL documentation for readkcmd says the carry is returned set if no event is queued. It is only necessary to check this under special circumstances. Because this routine is responding to a notification that a key command event is available, it is not necessary here to confirm it by checking the carry. The PETSCII value is returned in the accumulator, this is the key that was pressed in conjunction with the modifiers. The modifier key flags are returned in the Y register.
The modifier key flags are defined in //os/s/:input.s as follows:
lshftkey = %00000001 cbmkey = %00000010 ctrlkey = %00000100
Compare the Y register against the combination of modifier key flags required to trigger an action. In the above case, COMMODORE+PLUS (the + key) and COMMODORE+MINUS (the - key) are used to increment and decrement the border color. By comparing Y against #cbmkey alone, a match will not be found if the user has held down both COMMODORE+CONTROL or COMMODORE+SHIFT, etc. To compare against a combination, such as COMMODORE+CONTROL, OR the bit-flags together in the constant, like this:
cpy #cbmkey.ctrlkey
Within a branch for a specific modifier key combination, compare the accumulator against the keys for that set of modifiers to perform some action. In this case, if the accumulator holds "+" the VIC's $20 register (border color) is incrememented. If the accumulator holds "-" the border color register is decrememented. After any of these custom cases, you have to decide whether to allow the same keyboard combination to also be processed by the Toolkit or whether the custom case supersedes and takes control away from the Toolkit. In the above code, control is always being forwarded to Toolkit by jumping to pkdone.
At pkdone, a RegPtr is loaded to our App's toolkit environment, the KERNAL routine tkkcmd is called. And finally, chkdirt is jumped to so that the screen layer can marked for redraw if the keyboard command resulted in a change in Toolkit that dirtied the screen. If you don't want perform any custom key command event handling, the entire prockcmd routine can be implemented as just the final three lines found starting at pkdone.
That covers all of the routines that a screen layer provides. This gives your Application full integration with drawing itself, as well as handling mouse and keyboard events.
Message Handling
Message handling is lower-level than screen layers. For example, it is necessary to push a screen layer to integrate its vectored routines into the main event loop. Messages are delivered via the Application's main vector table, and there is no way to implement an Application without providing a valid handler routine for messages.
We saw at the beginning of this part of the tutorial that the message command vector is pointed to a routine called msgcmd. Because msgcmd gets called repeatedly throughout the life of the Application it is essential that this is in main.o and not in init.o. Although it is technically possible put the placeholder routine SEC_RTS in the main vector table, the Application would not handle any messages. The menu system communicates with the Application via messages, including sending the standard message to Go Home, therefore if messages are not handled in some minimal way the user is more-or-less trapped in the Application.1
In addition the the menu system, messages are also used to display custom content on the status bar, to receive notification that the system redraw flags have changed, or that the color theme has changed, and many other types of messages that can be sent from Utilities.
The following block of code covers the message command handler itself, plus four of the six supported messages: status bar, color changes, system redraw flag changes, and theme changes.
A message consists of a single command byte that comes in the accumulator, plus optional arguments in X and Y which are defined by the message command. The list of all currently defined message commands is found in //os/s/:app.s
There are, at the time of this writing, 25 defined message commands, which represent ~10% of the available command space. These are officially and judicially allocated by OpCoders Inc. Because they are designed to facilitate loosely coupled communication between unrelated Application/Utility pairs, they need to be kept sufficiently generic.
The switch macro automatically switches on the accumulator. We are handling 6 message commands so the switch macro has an argument of 6, followed by a table of 6 message command bytes, followed by the .RTA table of 6 corresponding routines. If the message command is anything other than those matched by our switch macro flow continues following the switch which sets the carry and returns. A set carry indicates that the message was not recognized or not handled.
Status BarThe status bar is automatically redrawn anytime the menu layer (of which the status bar is a part) gets redrawn. Therefore, if you want to force the status bar to redraw you can call markredraw on the mnulayer index, which is defined in //os/s/:menu.s
The status bar's mode can be toggled by the user. When the user clicks to toggle it into the Application custom mode, a mc_stptr message is sent to the Application. If the Application replies with the carry set, the message is unhandled, and the status bar skips this mode. To support this mode, the Application handles this message by returning a RegPtr to a string of printable PETSCII characters. Non-printable characters (such as color changes, or reverse, or cursor movements) are displayed like they are in quoted strings in BASIC. This string should be at least 40 characters long. However, if it is longer than 40 characters it will be trimmed to the edge of the screen.
The contents of this string are drawn directly to the status bar, in the theme-defined color of the menus and status bar. In the above example, the status is a static string showing some copyright information. Every time the status bar is redrawn it sends this message to the Application, and every time the Application can reply with something appropriate. Your Application could have multiple static strings with different fixed messages, and a variable in the App's state could help determine which pointer to return. Or, your Application could write dynamic information into a status string buffer, and then always return a pointer to that one buffer. Or any combination of the above.
When you return a status string RegPtr, it is essential to also return the carry clear, so the menu system knows the message was handled and the RegPtr is valid.
Color PickerC64 OS includes a standard color picker Utility called Colors. It is opened, for example, by a menu option in App Launcher to let the user pick a hint color for the current desktop or to assign a color to one more more selected desktop aliases. These messages are designed to be sent both ways. When the user picks an alias, the Application sends an mc_col message to the Utility, and the Utility highlights that color. If the user clicks a different color in the Utility, an mc_col message is sent back to the Application which changes the color of the selected alias(es). In this way, the two are kept synchronized. The Colors Utility functions as a floating color palette that reflects whatever is contextually selected in the Application.
Wherever possible, you too should try to integrate the mc_col messages. Other examples include the border color in the Eliza Application and the animated bars in the new Visualizer Application. In TestGround there is a colored border around the tab view and custom view that are inset from the edges of the right half of the split view. Whenever the mc_col message arrives, the color code is in the X register. Make a quick backup of that, and get get the pointer to the right content view with #storeget views,2 and call ptrthis to set the this object. Next, set the bcolor property on that view, and set the brdrcol state variable. The state variable is set so that the App will remember this color automatically when its configurable state gets saved, and will restore it automatically when the App starts up next time.
Lastly, call thisdirt to mark the this object as dirty and jmp to mkdirt so the Toolkit environment is marked dirty and the screen layer is marked as needing a redraw. Note that the Toolkit environment was never actually set in this process. This is okay because only properties are being set without calling any methods.
System Redraw FlagsThere is a byte in workspace memory that holds a set of bitflags identifying system-level elements that are to be drawn. redrawflgs and its bit values are defined by //os/s/:service.s
Some examples include whether the status bar is showing, whether the menu bar is showing, and if the menu bar is showing, whether the CPU busy indicator should be drawn and whether the clock should be drawn. The menu bar and status bar both take up one row each of screen real estate. The user has the ability to hide these using system-wide keyboard shortcuts.2 Whenever the system redraw flags change, a message about this is sent to the Application. It should, whenever possible, respond by shrinking or expanding its UI to fit the available space.
This is very easy to do with a Toolkit-based UI. The general idea is that the draw context of the screen layer fills the whole screen, and the root view's context is the full draw context. Therefore, to make room for the menu bar, the root view's offtop property can be set to 1. And to make room for the status bar, the root view's offbot property can be set to 1. The code above shows a simple technique make this adjustment in response to the mc_rflg message.
Because the root view has manually had its size changed, it is necessary to set the mf_resiz flag on the mflags property of the root view. Then branch or jmp to mkdirt so the Toolkit is marked dirty and the screen layer is marked for redraw.
Color ThemeThe entire user interface is themeable using the Themes Utility. Many of the themeable elements are common Toolkit classes, such as tabs, scroll bars, table column headers, buttons, etc. Each Toolkit class draws itself by dynamic reference to the current color theme. For example, every time a default button is drawn, is reads the color in which to draw itself directly from the theme table in workspace memory. The theme table and its elements are defined by //os/s/:ctxdraw.s and //os/s/:ctxcolors.s respectively.
Whenever the theme is changed the Application is sent an mc_theme message. If your user interface is entirely Toolkit-based, the solution is very simple. Just mark the root view as dirty. This forces all nested views to be redrawn, and they take the new colors automatically.
Handling Dirty Flags
In several places it is necessary to set the this object's df_dirty flag on its dflags property. Rather than repeating this code numerous times, and because it is so generic, it's put into short routine called thisdirt.
The screen layer has vectors for handling the low-level mouse events, printable key, and key command events. In these routines the event notification is forwarded to the Toolkit by calling tkmouse, tkkprnt, and tkkcmd respectively. Upon returning from any of these KERNAL routines the event may have affected a Toolkit object such that the Toolkit needs to be updated. This is indicated by the Toolkit environment having the tf_dirty flag set on its te_flags property. If this flag is set, the screen layer must be marked for redraw.
To handle this, the routine chkdirt is called after tkmouse, tkkprnt and tkkcmd. It checks if the tf_dirty bit is set on the Toolkit's te_flags property, and if so it branches to where the screen layer is marked for redraw.
Alternatively, you can manually set a property on an object, and then call thisdirt to set that object's dirty flag. After such a manual change, it is necessary to explicitly set the Toolkit Environment's dirty flag as well as marking the layer for redraw. This can be done by calling mkdirt.
Supporting the Menu System
The menu system uses messages to interact with the Application. There are two stages. Every time any menu item (with an action code) is about to be drawn, it first sends a menu enquiry message to the Application. The Application returns a set of flags that inform the KERNAL how to draw that particular menu item at this particular moment.
The second stage is, whenever any menu item (with an action code) is about to be triggered, whether by mouse or by keyboard shortcut, first another menu enquiry message is sent, and if the flag comes back indicating that it is enabled, then a menu command message is sent.
Menu Enquiries
The menu enquiry system is a delegation pattern which you may not be familiar with, but it is incredibly powerful, giving 100% control of the status of the menu items to the Application with a minimal amount of code required in the KERNAL.
Like all message comands, menu enquiry (mc_menq) is sent in the accumulator. The menu item's action code, which is always one byte and unique to a set of menus (i.e. unique per Application,) is sent in the X register. When the main message handler's switch macro jumps to the menuenq routine the X register is preserved. In order to pass it through to the next switch macro it must be transferred to the accumulator first.
There are two special flags that affect how a menu item is drawn: disabled (mnu_dis) and selected (mnu_sel).
A disabled menu item is drawn with the theme's disabled menu item color, it does not get a mouse rollover color change, and cannot have its action triggered.
A selected menu item that is drawn with a checkmark in the left gutter next to the title of them item. It is possible for a menu item to be both selected and disabled. The user expectation is that triggering a selected/selectable menu item will toggle its selected status. But while a menu item is disabled, its selected status cannot be toggled.
To use these flags, return them in the accumulator from the mc_menq message hander. The menu system disregards the state of the carry in the return from mc_menq messages.
Most menu items don't need to be enabled/disabled or selected/unselected. In this case simply return #0 in the accumulator. All bit flags low makes the menu item enabled and unselected.
Some of the menu items in TestGround are used to select between the three tabs and to toggle the state of some checkboxes. The checkboxes are for toggling the text wrapping in in the TKText view, and for showing and hiding the custom TKFooter object. The action codes assigned to these menu items are mnemonic, which is always recommended; "w" for wrap, "f" for footer, and then the numbers "1", "2" and "3" (PETSCII values, so bytes $31, $32, $33) for the three tabs. Using numbers (even in PETSCII) for the tabs is convenient, but handled separately from the switch macro.
The view menu, with selected menu items.
The switch macro switches on "w" and "f", which lead to a_wrapchk and a_footchk respectively. Each of these pulls a pointer from the tstviews pointer store to the right checkbox and branches or falls through to some common code. The cflags property is read from the checkbox object, this is a property defined by the TKCtrl superclass, from which checkbox buttons inherit. Then that is AND'd with the cf_state bit. If that cf_state bit is set, the checkbox is checked and it returns the accumulator with the value mnu_sel. The menu item will get the selected checkmark if the checkbox is checked. If the cf_state bit is clear, it branches to the common out that returns #0 in the accumulator.
Here is what is important. When the state of the actual checkbox is changed, such as by clicking on it, no information needs to be pushed into the menu system. When the menu item is ready to draw itself, it sends the enquiry message about its state. The enquiry message handler looks up the state, on-the-fly, from the source of truth, the checkbox itself, and returns the flags that determine how the menu item should be drawn. This makes it impossible for the state of the menu item and the state of the checkbox to ever be out of sync.
If the action code was not handled by the switch macro, flow continues. If the action code is greater than "3" or less than "1" simply branch to done and return #0 for all other menu items. Otherwise, a simple subtraction of the PETSCII value of "1" converts "1" through "3" to the ints $00 through $02. We saw in Part 1 of this tutorial that the selected tab index is saved to the configurable state variables, so when the Application quits and gets relaunched the last selected tab is still selected. That provides an easy number with which to compare this converted menu action code. If they match, return mnu_sel, otherwise, return #0.
We'll see later in this tutorial how the TKTabs delegate sets the seltab configurable state variable.
Menu Commands
A disabled menu item cannot be triggered by either the mouse or its keyboard shortcut if it has one. But if it is triggerable, regardless of whether it is triggered by mouse or keyboard shortcut, the result is a menu command message (mc_mnu) in the accumulator sent to the Application, again with the action code sent in the X register.
After the main message handler's switch macro jumps to the menucmd routine, the action code in the X register is preserved. To pass that through another switch macro it must first be transferred to the accumulator.
Being a testing ground for various features of the KERNAL and libraries, TestGround has some menu options for killing and hiding the mouse pointer, showing or loading the mouse pointer, and raising an exception, in addtion to the standard "Go Home" (i.e., quit the App,) plus the UI controls discussed earlier for switching the tabs or toggling the checkboxes.
The switch macro switches on the four actions codes for the mouse-related tests, which go to some routines that make KERNAL calls, killmouse, hidemouse or initmouse. The code for raising an exception performs a JSR to "exception", which should crash the App with the red SPLAT! dialog box.
The switch macro's RTA table has "quitapp" for the "!" action code, the one used in menu.m for Go Home. "quitapp" is found directly in the KERNAL link table at the end of main.o. There is not always the need to even create an intermediate routine.
The action codes for toggling the footer and the text wrapping jump to routines we'll look at next. If the action code was something other than these, it falls through, and the same logic from menuenq is used to check if the action is between "1" and "3", then convert to ints $00 to $02. Push this byte temporarily to the stack, and now we have to call a method on the TKTabs view. Start by setting the toolkit environment, get a reference to the TKTabs view from the views store, call ptrthis to make it the this object, prepare its seltab method. Pull the new tab index from the stack and call the method. Finish by jumping to mkdirt so the Toolkit environment is marked dirty and the layer is marked for redraw.
Toolkit Object Delegates
We are in the final leg of this tutorial now. The init.o binary was used to create and assemble the user interface by instantiating Toolkit classes, and then initializing them, snapping them together and setting the properties, parameters, callbacks and delegate pointers. Init.o then gets purged from memory, the Toolkit object hierarchy remains in dynamically allocated memory and primed to make calls to the callbacks and delegates which are implemented here at the end of main.o
The Text Wrap Checkbox
The routine, togwrap, is called directly by the toggle wrap checkbox when its state is changed as the result of a mouse click. When this routine is called internally by the checkbox itself, the this object is already set as the checkbox. That allows the callback routine to read and write properties directly from the calling checkbox. There is no need here to first lookup the checkbox.
Restore the this context
If the desired result of toggling the checkbox involves calling a method on some other object, it is critically important that the this context is restored to be the checkbox again before returning from this callback. Returning from this callback returns control flow into the checkbox (TKButton) class's code. The class assumes (correctly) that while it is executing, the this context is one of its own instantiated objects.
Changing the this context in a callback should be considered like pushing something to the stack. It must be pulled before returning to prevent a crash. The this context must be restored before returning.
The menu command handler jumps to a_togwrap, which conceptually means action-togwrap because it results from a menu action. a_togwrap is a very short leading routine that falls through to togwrap. When the menu action is triggered the toolkit environment has to be set, the checkbox object pointer looked up and made the this object with a call to ptrthis, the cf_state bit gets toggled on the cflags property and the checkbox is marked as dirty. Then it falls through to togwrap which does the rest.
When the checkbox is clicked and its callback is called, the toolkit environment is already set. The checkbox is already the this object. Clicking it already toggles its cf_state bit and marks it dirty. So you can see that the a_togwrap performs some minimal set up and then it falls through to the checkbox's callback with all the expected parameters set.
Toggling this checkbox is going to make some changes to the TKScroll view and the TKText view, so we back up a pointer to this to the stack with the macro #pushptr this. Next, we read the current state of the checkbox. The state of the checkbox is changed first, then the callback is called. This is why in a_togwrap we flip the cf_state bit first before falling through to togwrap.
The checkbox is checked we'll turn wrapping on. When text wrapping is on there is no need for a horizontal scroll bar because the nature of wrapping is that the text is never wider than the available horizontal space. Even a single word that is longer than the available width splits the word in the middle. Get a reference to the TKScroll view from tabviews,4, make it the this object. Prepare its setbar method. Configure which bars should be on (#1) and which off (#0). The X register is for the horizontal bar, so X is loaded with #0. The Y register is for the vertical bar, so Y is loaded with #1. Call the method.
Next get a reference to the TKText object from tabviews,5, make it the this object and prepare its setstrf method. Load the TKText object's current tstrflgs property, which is a set of bit flags defined by //os/tk/s/:tktext.s, and set the f_wrap bit. Call the method. It is not enough to just write the flags byte back to the tstrflgs property; TKText has to do a lot of work in response to a change in its flags.
Lastly, it jumps down to the common done, which pulls a RegPtr from the stack to the original this, calls ptrthis to restore the this context as the checkbox, and finally calls mkdirt to mark the Toolkit envirnment dirty and mark the layer as needing a redraw.
If the checkbox state is unchecked, it follows the same steps but does the reverse. It gets a reference to the TKScroll object and turns on both vertical and horizontal scroll bars. It gets a reference to the TKText, preps its setstrf method, reads the tstrflgs propery, clears the f_wrap bit and calls the method. Then falls through to done to restore the this context as the checkbox.
The Custom Footer Checkbox
The checkbox for toggling the visibility of the custom footer view follows an identical pattern. There is a routine called togfoot which is the callback of the toggle footer checkbox. There is a short leading routine called a_togfoot which is called by the menu action that prepares the checkbox as the this object, and flips the cf_state of the checkbox before falling through to the checkbox's callback.
If you have a keen eye, you may have noticed a subtle difference between a_togwrap and a_togfoot. The former first sets the toolkit environment but a_togfoot doesn't do that. The difference is that in togwrap methods are called; setbar on the TKScroll and setstrf on the TKText. But in togfoot only some properties are set on the objects. It is not a bug that the Toolkit environment was not first set.
If the checkbox is checked, we get a pointer to the custom footer from views,4 and make it the this object with ptrthis. Then the dflags are read, which is a property provided by its superclass, TKView. The df_visib flag is OR'd with the df_dirty flag and the results written back to the propery. This marks the object visible and dirty in the same step. The custom footer is anchored to the bottom of its content view. When it's visible the bottom of the TKTabs needs to be moved up to leave room for it. The pointer to the tktabs is fetched from views,3 and its offbot property is set to 6. Then the code jumps to the common end of this routine at done.
If the checkbox is not checked, almost the same thing happens but in reverse. On the footer object's dflags the df_dirty flag is again set but the df_visib flag is cleared, making this view invisible. It will not be drawn, events will not be routed to it, resizing will not recurse to its children. There is now room to expand the TKTabs, so its offbot property is set to 1. And then flow falls through to the common ending at done.
In both cases, at done the current this object is the TKTabs. Because its anchoring was changed it needs to have its mf_resiz bit set on the mflags property. This tells Toolkit that the object needs to be resized, which gets propagated recursively to its child views.
The right container view holds both the TKTabs and the TKFooter. Regardless of who apppears or disappears, or who gets resized, the easiest way to force that whole section to redraw cleanly is to mark the entire right container view as dirty. Lastly, the pointer to the checkbox that was pushed to the stack is pulled back off and made the this context again. Just as a reminder, if in a callback you are going to change the this object, it is important to preserve and restore it before returning from the callback. The Toolkit environment is marked dirty and the layer is marked for redraw with the final jmp mkdirt.
Cycle Button Titles
There is an example cycle button in the test views on the left side of the split. Cycle buttons are a bit unique in that they change their title depending on their value.
A cycle button is a type of TKButton whose btype property has been set to bt_cyc. This puts a small cycle icon (two arrows pointing at each other in a circle) on the button to the left of the title. TKButton is a subclass of TKCtrl, which provides properties for min and max values, as well as an action mechanism when the button is triggered.
When the button was configured, seen in Part 1 of this tutorial, a default value of 0 was set, and a min and max of 0 and 11 were set. Then a single initial call to mthcycle was made to configure the title. mthcycle is implemented here in main.a.
When mthcycle is called, the month cycle button is the this object. It prepares the settitle method, then reads the value from the object. The value is provided by all TKCtrl objects. The value is guaranteed to be between 0 and 11, the min and max values on the button. A call to months_s found in the content section of main.o is to a strxyget macro followed by a list of months. This call returns a pointer to the correct month, which is passed to the settitle method.
Whenever the user clicks the button, the current value is incremented automatically (or decremented if the COMMODORE key is held down). The value is also constrained to the min/max range. If it exceeds max it loops around to the min value, or vice versa, and then the action is called again. The action callback can be used to actually do something at the same time as updating the button's title. But this is just a test, so only the title on the button is updated. Cycle buttons can also use the TKCtrl's periodic feature, so that clicking and holding a cycle button cycles continously. Each time the value changes the action callback is called.
Input field delegation and validation
At the bottom of the left side three test input fields, TKInput, were created; name, age and address. The fields require a pointer to their content buffer and a buffer size. This limits the number of characters that can be typed or pasted into the field.
To work effectively with text fields there are several events or state changes that the Application needs to be able to be aware of. Sometimes when there are several possible callbacks that could be implemented on an object it is easier to implement them as delegation. TKInput does this. The difference between callbacks and delegation and the advantages of delegation is outside the scope of this tutorial.
When the age input field was instantiated, its idelegs property was set to agedel which is found here in main.a.
agedel is a delegate structure for TKInput objects. In order to be valid, it must provide a valid routine pointer for each of the elements in the delegate structure, which is defined by //os/tk/s/:tkinput.s
The delegate routines are: will insert (willins), did change (didchng), did focus (didfoc), and did blur (didblur). Delegate routines are always worded as will or did. The will routines are called before something occurs inside the object, and gives the routine an opportunity to accept or reject the change. To reject the change, return with the carry set. Or return carry clear to accept. The did routines inform about something which has just happened in the object. The did routines generally don't have return values.
In the context of TKInput objects, willins is called every time a character is about to be inserted into the field, regardless of whether it is being typed or pasted. The character being inserted is passed to willins in the accumulator. The routine can return with carry set to prevent that character from being inserted, or with the carry clear to allow it, or with the carry clear but a different character returned in the accumulator as a substitute.
In agedel only the willins gets a custom implementation, with the routine agevalid. The others are set to raw_rts, to return immediately. As a test we only want to accept numbers into our field. There is one complication, PETSCII cursor control characters, such as UP, DOWN, LEFT, RIGHT, DELETE, HOME and CLEAR, are sent as printable key events. In order to allow these to happen in the field, the willins routine has to return the carry clear for them too.
In agevalid, it first compares against #$20, the smallest visible character, a space. If it's less than this, it's in the block 1 control codes section of PETSCII, so it allows any of those. Next, if it compares to #$9f, the last character of the block 5 control codes. If it's above those controls it denies them. Then it compares to #$80, the first character of the block 5 control codes. If it's greater than or equal to $80 then it's in the block 5 control codes so it allows those. That permits all PETSCII codes, and denies everything higher than PETSCII codes. Finally, if checks if it's less than #$30 (PETSCII "0") and denies those, or greater than or equal to $3a (one more than PETSCII "9") and denies those. Lastly, it falls through to allow, thus allowing "0" to "9" in PETSCII. (See: Commodore 64 PETSCII Codes for reference.)
Tab view delegation
TKTabs, like TKInput, also uses delegation to provide a set of will and did routines to manage the control of the object. The TKTabs delegate structure is defined by //os/tk/s/:tktabs.s and provides the following routines: tab string (tabstr), will blur (willblur), will focus (willfoc), did blur (didblur), did focus (didfoc).
The first, tabstr, is called to fetch the title of the tab, and the tab index is passed in the accumulator. The tabsdel struct's first entry is set directly to tabs_s, which is to a strxyget macro in the content section of main.o. The strings in that content already contain the custom characters that were loaded as icons from the icon table in init.o. This puts the icons onto the tabs.
The title string on the tab is not retained by the TKTabs object. This delegate method is called everytime the TKTabs view needs to redraw itself. Interesting dynamic opportunities exist here. Rather than simply returning a string, this delegate routine could look at the width of its TKTabs view and customize the title based on the available space. For instance, if the tab became too narrow to fit the icon and the text, it could suddenly drop the icon. And if it became narrower still, too narrow to fit even the text, it could suddenly switch to showing only the icon. These little flourishes can make your App's UI very dyanmic. There are even opportunities to display status information in the tabs, such as an asterisk in a blurred tab to notify the user of changes, or a number in the tab to indicate how many of something is displayed within it.
The willblur and willfoc routines are called, in that order, just prior to any tab change. Both of these routines must return carry clear for the tab change to occur. didblur and didfoc are called after the tabs have changed. The multitudinous ways that these can be used is outside the scope of this tutorial. In the tabsdel for TestGround, clc_rts is used for both of the will routines, allowing them both. didblur calls raw_rts, but didfoc has a custom implementation of tabchng.
When a tab is changed, the first thing it does is fetch the current tab index from this and write it to seltab. We have seen before how seltab gets saved with configurable state, gets loaded into configurable state, gets used to set the initially selected tab when the TKTabs is created and how it is referenced to help with the menu enquiries. Finally we see here how that state variable is updated when the tabs change.
In each tab the main content view is always a TKScroll or a TKScroll subclass. And the content view of the TKScroll view is one which is able to take first key status. We want to set things up so that when the tabs are changed, the first key view is changed too. This will give better keyboard control. COMMODORE+3 triggers the menu to select tab 3, which calls the didfoc delegate method which sets that tab's content view as first key. Now pressing CURSOR-DOWN/UP is scrolling the text in that tab, without ever needing the mouse.
The this pointer is backed up first. Then the gettab method is prepped and called with the seltab as the requested index. A pointer to the TKScroll is returned which is the content view of the current tab. This is made the this object, and a RegPtr to its content view is fetched with getprop16, and then that is made the this object. The setfirst method is prepped and called on the content view of the TKScroll, and asked to take first key. Finally we restore the pointer to the the TKTabs from the stack and restore it as the this object before returning.
Table view callbacks
Tables are rather complex; a complete description of everything they can do is well outside the scope of this tutorial. Their behavior is driven mostly by callbacks. This means that there is some code necessary in your Application to get a tables fully working, but the upside is that there the App has a great deal of flexibility in managing how the tables behave.
We have already discussed how the columns, their size, resize range and content alignment are configured. TKTable is a subclass of TKList, which adds a second dimension, columns, to each row. The callback properties of a TKTable are provided by TKList and are defined by //os/tk/s/:tklist.s
The callbacks consist of:
- content at index (ctntaidx)
- content size (cbk_ctsz)
- key press (cbk_keyp)
- click (cbk_clik)
- double-click (cbk_dclk)
Of these, the two most important are content size and content at index.
Content size refers to the number of rows of data that the table is to present. The number of columns is fixed by the columns definition structure that was established during initialization of the table. t1_cs (tab 1, column size) was assigned as the callback. In this case, it returns a fixed number of 50 in a RegWrd. In a realistic context this number would be derived from a real source of data, such as a directory.
When a table draws, it draws only the visible cells. In order to draw, it requests the content string for the cell it is about to draw. It calls the ctntaidx callback with the row index in the Y register and the column index in the X register. This is implemented above as t1_ca (tab 1, content at.) Again, in a real context, this would be used to find an appropriate string to return from some 2-dimensional data structure. Such as using the Y index to find a directory entry, and then using the X index to lookup and return a property from the directory entry to be displayed in that column.
Row selection state is not maintained by the TKTable object. Much the same way that menu item selected and disabled states are not maintained by the menu system, the content at index callback specifies the selected state of the row being requested using the carry. The variable t1_sidx (tab 1, selected index) is one byte used to indicate the selected index. Maintaining multiple selections properly even when a table is sorted in some complex way is done by keeping a selection property on the structure that is presented by a table row. Regardless of how the structures are sorted, the selection status of that row can be read from the structure itself, along side the data to be drawn. What is shown here in TestGround, just one selection index variable, is a toy example.
In t1_ca, if the Y register (row index) is equal to t1_sidx, the carry is returned set so the row is drawn selected. Fetching the data to be drawn is also a toy example. In TestGround every row shows the same data. The X register (column index) is transfered to the accumulator and then a call to table_s to fetch a static string from the content section found at the start of main.a.
In this example, no column sorting is implemented. In init.o, there were two callbacks assigned to the TKCols object. t1_ci is to fetch column info, and t1_cc is to handle a column being clicked. When a column is clicked, a RegPtr to the column definition structure that was clicked is passed to the callback. Typically the callback uses this information to sort the data. The only return value is whether the result of the column click causes the table to require a redraw. If the carry is returned set, no change occurred and the table does not need to be redrawn. In the example above, since sorting is not implemented, the carry is always returned set.
The column info callback is called every time the TKCols object is about to draw one of the column headers. The value returned in the accumulator from the column info callback indicates the sort direction to use. Valid values are #$00, #$ff (i.e., -1) or #$01. If the value is #$00, no sort indicator is drawn and the theme color for the column is that of an unsorted column. If the value is -1 or 1, the appropriate directional arrow is drawn, and the theme color for a sorted column is used. In the example shown above, #$00 is always returned as none of the columns support sorting.
The callback t1_ck (tab 1, click) is called when a table row is clicked. The X register holds the table row index. In a real context this would be used to look up the data structure being presented by this row and change its selection state. In this toy example, the X register is simply being saved to the t1_sidx variable.
When a row is double clicked, the t1_dk (tab 1, double-click) callback is called, and the double-clicked row index is passed in the X register. This allows you do something with the data in the row. If it's a file, that may be to open it, for example. In this TestGround example, it merely calls the system alert so you get feedback that the double-click was recognized.
The last TKTable callback to discuss is t1_kp (tab 1, key pressed.) TKTables are capable of taking first key status. When the table is clicked, keyboard focus shifts to the table (it can also be set programmatically.) TKTable's implementation of the keypress handler is inherited from its superclass, TKList. This first performs some helpful transformations. It fetches the printable key event for you, and checks for CURSOR-UP or CURSOR-DOWN. If CURSOR-UP was pressed, the accumulator is loaded with #$ff (-1), if CURSOR-DOWN the accumulator is loaded with #$01. For any other key, the accumulator is loaded with #$00. Then the keypress callback is called.
The +1/-1 provided in the accumulator makes it easy to move a selection index, by simply adding the accumulator to the current selection index. In practice, other range checks will need to be performed so the selected index remains valid. Additionally, the Y register is passed to the callback with the key modifier byte set. Internally, the TKTable performs the same logic whether it is pressing a printable key event or a key command event, and there is only one callback. They modifier flags can be used to extended the selection (if, say, LEFT-SHIFT is held down,) or jump the selected index to the top or bottom of the table (if, say, the COMMODORE key is held down.) The full range of the possible logic that can be applied here is not covered by this tutorial.
If the selection changes, however, the carry must be returned clear, and the new selection index (or the newest, if multiple-row selection is supported) should be returned in the accumulator. Returning the new selection index allows the table to auto-scroll to put the new selection within the visible range.
List view callbacks
If you understood how TKTable works, understanding how TKList works is easier. The concepts are almost the same, except TKList has only a single column of data and now column headers. A TKList is typically embedded in a TKScroll class, but even this is not necessary if the list will not have to be scrolled.
A similar set of callbacks is applied to the TKList object.
- content at index (ctntaidx)
- content size (cbk_ctsz)
- key press (cbk_keyp)
- click (cbk_clik)
- double-click (cbk_dclk)
Just like for the table, TestGround is using a toy example of selection, a single variable t2_sidx (tab 2, selected index.) The callback t2_cs (tab 2, content size) is returning a fixed number of rows, 27. This is just an example, these 27 rows come from the string macro uitest_s found in the content section of main.a.
TKList is simpler than the TKTable. When TKTable calls the content at index callback it passes both a row index and a column index. When the TKList calls the content at index callback it passes only the row index in the X register. The callback must return a RegPtr to the string to be drawn at that index. The state of the carry indicates the row selection status. If the carry is returned set, the row is drawn as selected.
Similarly to TKTable, the TKList is capable of taking first key status. When either a printable key event or key command event is handled by the TKList it reads the appropriate low-level keyboard event and converts CURSOR-UP and CURSOR-DOWN keys to the accumulator values #$ff (-1) and #$01 respectively. If neither CURSOR direction was pressed the accumulator gets #$00. The Y register holds any modifier key flags. The value of the accumulator can be used to adjust the current selected index.
Since lists are simpler, don't have a column header, and often are not sorted, the toy model of a single variable for the selected index is much closer to how it might realistically be implemented. For example, if a TKList provides a fixed list of 10 options, and the sort order of those doesn't change, and if only one of them may be selected at a time, then having a single byte like t2_sidx to hold the selected index is perfectly suitable. Simply add the value of the accumulator to the selected index, and perform a bounds check to make sure the index is still valid (0 to 26 in this case.)
If the selected index does not change, return with the carry set. If the selected index does change as a result of the key press, return with the carry clear and the new selected index in the accumulator. This allows the list to coordinate with its enclosing TKScroll to scroll the newly selected index into view.
When a row is clicked the click callback is called, the row index that was clicked is passed in the X register. In the above example, we simply save the clicked row index to t2_sidx as the new selected index.
TKList also supports the double-click callback. The double-clicked row index is passed to the callback in the X register. In the example above, only an alert is being called to provide feedback that the double-click was registered and the callback was called.
Library and KERNAL link tables
Init.o also has its own library and KERNAL link tables. Those entries are for the library that was loaded, used, and then unloaded all within init.o. And for KERNAL calls that were used by init.o but that never need to be used by main.o.
Init.o was responsible for initializing the extern table. It did this twice, once for the KERNAL link table found at the end of init.a, at the label iextern, and then again for the table found at the end of main.a, at the label extern.
The structure of both KERNAL link tables is identical. Use the macro #inc_h to bring in the headers for a KERNAL module. This defines a KERNAL link table index (lfil, linp, lmem, lscr, lser, etc.) These may be then be used with the #syscall macro to establish links between the this table and the dynamically located address of the corresponding routine in the KERNAL.
The KERNAL routines in the header files have labels that end with an underscore. This is done so that in the link table a version of the same label without the underscore can be used in your code. For example:
alert #syscall lser,alert_
When the link table is initialized, the label alert becomes a JMP into the KERNAL's service module, to the alert routine.
Every KERNAL link table that is processed by initextern must be terminated with a single #$ff byte, shown above.
Lastly, the final line of main.a is the init label. The init label, if you recall, was put in the main vector table, in the Data Structures section way up at the beginning of main.a. That label resolves, dynamically, to the first byte following the last byte of main.o. The actual address of init, therefore, moves around as main.a grows or shrinks. After assembling main.a into main.o, the labels from main.a get exported to main.l and then processed by the BASIC tool //os/c64tools/:exlabel to reduce the set of labels to only those needed by init.a. The resultant file is main.xl which is included by init.a.
One of the labels found in main.xl is this init label from the very end of main.a, which is then used to establish the assemble start address of init.o. And we have come full circle.
That ends Part 1: Main Application of this tutorial. As you can see, the initialization part of the Application is actually longer and in some ways more complex than the main Application itself. The init.a source code is over 1400 lines, whereas main.a is just over 900 lines. This is the reason for the added complication of splitting the initialization code into its own file. Once init.o has completed, it is expunged from memory, leaving only main.o in memory, plus the other dynamic allocations.
The TestGround Application should be buildable using TurboMacroPro, natively on the C64, or using TurboMacroPro-Cross (TMPx) for cross assembly on a Mac or PC. Below is a link to a zip file containing all of the resource files in this tutorial.
ZIP file contents:
Path | File | Notes |
---|---|---|
/ | TestGround screenshot.png | What the App should look like when running. |
/TestGround/ | about.t.S00 | S00-wrapped about.t SEQ file, PETSCII. |
/TestGround/ | icon.charset.a.P00 | P00-wrapped icon.charset.a PRG file, TMP source-binary. |
/TestGround/ | icon.charset.S00 | S00-wrapped icon.charset SEQ file, 3Icon. |
/TestGround/ | init.a.P00 | P00-wrapped init.a PRG file, TMP source-binary. |
/TestGround/ | init.o.P00 | P00-wrapped init.o PRG file, C64 OS App init binary. |
/TestGround/ | main.a.P00 | P00-wrapped main.a PRG file, TMP source-binary. |
/TestGround/ | main.o.P00 | P00-wrapped main.o PRG file, C64 OS Application binary. |
/TestGround/ | main.l.S00 | S00-wrapped main.l labels file, before exlabel processing. |
/TestGround/ | main.xl.S00 | S00-wrapped main.xl labels file, after exlabel processing. |
/TestGround/ | menu.m.S00 | S00-wrapped menu.m SEQ file, PETSCII, menu definitions. |
/TestGround/ | msg.t.S00 | S00-wrapped msg.t SEQ file, PETSCII, displayed at launch. |
/TestGround/ | sample.t.S00 | S00-wrapped sample.t SEQ file, PETSCII, Application resource. |
/TestGround/ | tkfooter.a.P00 | P00-wrapped tkfooter.a PRG file, TMP source-binary. |
/TestGround/ | tkfooter.r.P00 | P00-wrapped tkfooter.r PRG file, custom Toolkit class. |
/TestGround (raw)/ | about.t | about.t SEQ file, PETSCII. |
/TestGround (raw)/ | icon.charset.a | icon.charset.a PRG file, TMP source-binary. |
/TestGround (raw)/ | icon.charset.a.txt | icon.charset.a Text file, ASCII. |
/TestGround (raw)/ | init.a | init.a PRG file, TMP source-binary. |
/TestGround (raw)/ | init.a.txt | init.a Text file, ASCII. |
/TestGround (raw)/ | main.a | main.a PRG file, TMP source-binary. |
/TestGround (raw)/ | main.a.txt | main.a Text file, ASCII. |
/TestGround (raw)/ | main.l | main.l Text file, ASCII, label export. |
/TestGround (raw)/ | main.xl | main.xl Text file, ASCII, label export, exlabel processed. |
/TestGround (raw)/ | menu.m | menu.m SEQ file, PETSCII, menu definitions. |
/TestGround (raw)/ | sample.t | sample.t Text file, PETSCII, Application resource. |
/TestGround (raw)/ | sample.t.txt | sample.t Text file, ASCII, Application resource. |
/TestGround (raw)/ | tkfooter.a | tkfooter.a PRG file, TMP source-binary. |
/TestGround (raw)/ | tkfooter.a.txt | tkfooter.a Text file, ASCII, TMP source code. |
The "TestGround" directory and its contents are ready to be put on an SD2IEC, (which implicitly unwraps the .S00 and .P00 wrappers,) and run as an Application on C64 OS.
The "TestGround (raw)" directory and its contents have been unwrapped to make it easier to open them in a HexEditor or PETSCII-TextEditor on a Mac or PC. The .a files (unwrapped) can be viewed using tmpview. And the source code files ending with .txt have been pre-converted to ASCII using tmpview, for easily examining in an ASCII-TextEditor.
Next Chapter: Writing a Utility
- Starting around v1.05 STOP+RESTORE became a more reliable way to go back to Homebase. This does not require a message command, so technically the user is not completely trapped in an Application that doesn't support message commands. [↩]
- The shortcuts themselves are configurable using the Configure Tool. [↩]
Table of Contents
This document is subject to revision updates.
Last modified: Jun 28, 2024