Many-to-many checkboxes revisited, part 4: Wrapping up the UI code
by Unknown user
First, some housecleaning. Mike Hanson pointed out that I needed a couple of additional lines of code (5 and 6) in my TakeAccepted handler:
CML_UI_ListCheckbox.TakeMouseClick procedure!,byte code if keycode() = MouseLeft | and self.ListFEQ{PROPLIST:MouseUpZone} = LISTZONE:icon | ! Contributed by Mike Hanson and self.ListFEQ{PROPLIST:MouseDownZone} = LISTZONE:icon | ! Contributed by Mike Hanson and self.ListFEQ{PROPLIST:MouseUpRow} = self.ListFEQ{PROPLIST:MouseDownRow} | and self.ListFEQ{PROPLIST:MouseUpField} = self.ListFEQ{PROPLIST:MouseDownField} | and self.ListFEQ{PROPLIST:MouseDownField} = self.ListQCheckboxFieldNumber get(self.ListQ,choice(self.ListFEQ)) self.ToggleCurrentCheckbox() put(self.ListQ) end return Level:Benign
Those two lines of code restrict the click operation to just the icon area; without them clicking anywhere in the cell that contains the icon would change the icon.
But there's another problem with the original method - I'd neglected to return a value, yet the Register function docs specifically say you must return one of three values from the function:
Level:Benign | Calls any other handlers and the ACCEPT loop, if available. |
Level:Notify | Doesn't call other handlers or the ACCEPT loop. This is like executing CYCLE when processing the event in an ACCEPT loop. |
Level:Fatal | Doesn't call other handlers or the ACCEPT loop. This is like executing BREAK when processing the event in an ACCEPT loop. |
I always want processing to continue normally, so I'm returning Level:Benign.
I also had a suggestion from Greg for a toggle capability, so I added a ToggleAll method. In an effort to reuse as much code as possible, I had ToggleAll call the SetAll method with an equate indicating all values should be toggled:
TrueValue equate(1) FalseValue equate(2) ToggleValue equate(3) ... CML_UI_ListCheckbox.ToggleAll procedure code self.SetAll(ToggleValue)
The SetAll method now looks like this:
CML_UI_ListCheckbox.SetAll procedure(bool flag) x long code loop x = 1 to records(self.ListQ) get(self.ListQ,x) if flag = ToggleValue self.ToggleCurrentCheckbox() else self.ListQIconField = flag end put(self.ListQ) end
There's a call to a new ToggleCurrentCheckbox method, which is marked private because it relies on the correct queue record being in memory, something that's under the class's control:
CML_UI_ListCheckbox.ToggleCurrentCheckbox procedure code self.ListQIconField = choose(self.ListQIconField=TrueValue,FalseValue,TrueValue)
This method is now called from TakeMouseClick, so the Choose logic exists in just one place. It doesn't really save any lines of code, but it does improve reuse and that's always something worth pursuing as it enhances maintainability.
And finally I added something that may only be practical in its current form for simple list boxes (and not ABC browses): the ability to press the space bar to toggle an entry and then advance to the next item.
I added a Bool property (see Equates.clw - Bool equates to Signed which equates to Short) called ToggleAndAdvanceWithSpaceKey, and in the Initialize method I added the following code which sets SpaceKey as an Alrt attribute on the list box and then registers an event handler, similar to the way I registered a handler for icon field selection. (Oh, and I updated the handler call by adding the list box FEQ as the last parameter, so the handler is only called for the list box not for all controls.)
if self.ToggleAndAdvanceWithSpaceKey self.ListFEQ{PROP:Alrt} = SpaceKey register(EVENT:AlertKey,address(self.TakeSpaceKey),address(self),,self.ListFEQ) end
Here's the handler:
CML_UI_ListCheckbox.TakeSpaceKey procedure!,byte code if keycode() = SpaceKey get(self.ListQ,choice(self.ListFEQ)) self.ToggleCurrentCheckbox() put(self.ListQ) if pointer(self.ListQ) < records(self.ListQ) select(self.ListFEQ,choice(self.ListFEQ)+1) end end return Level:Benign
And here's the current demo with the Toggle All button:
As long as I don't get distracted with adding any more bells and whistles to the UI component, it's just about time to start thinking about data persistence. How can I save and load checkbox data?
As I've said before, my usual approach with non-UI code is to start with a unit test, but in this case I think applying this class to an ABC browse will help to illustrate some of the potential issues I need to consider.
ABCTest.app
I've created a simple app with a single ABC browse (included in the source zip).
It has these lines in the AfterGlobalIncludes global embed point:
include('CML_UI_ListCheckbox.inc'),once include('CML_System_Diagnostics_Logger.inc'),once
All the classes included in the ClarionMag Library follow the same convention: by default, any app that uses these classes will expect to find them in a DLL. But I'm doing testing and development, and in those circumstances I usually compile the classes along with whatever app is using them, whether it's an executable or a unit test DLL. And that means adding
_Compile_CML_Class_Source_=>1
to the Conditional Compile Symbols in the project properties:
This probably isn't something you'd do with your own classes; in fact there's a good chance that when you write classes you'll compile them for each app. That's how I started using classes also, but eventually I ran into problems when sharing classes between apps (usually DLLs) in a large, multi-DLL system. You can read more about the ClarionMag Library compiling approach in Look Ma! No project defines!
The ABC browse procedure has one declaration in the data embed:
ListCheckbox CML_UI_ListCheckbox
And one line of code at the end of the ThisWindow.Init method:
ListCheckbox.Initialize(Queue:Browse:1,Queue:Browse:1.Checked_Icon,?Browse:1)
But how did I know what parameters to use? I had to go looking in the generated source, which is a bit funky. This would be an excellent job for a template and would avoid potential typos.
The first parameter, as you'll recall, is the queue that provides data to the list box.
The second is the Long field following the field that will be used as a checkbox. So I did have to go into the list box formatter for the browse and add a local variable and configure it as having a checkbox:
In my simple hand coded test I defined the queue myself and added the extra Long field. In the ABC browse, enabling the Icon property caused the templates to generate that extra field.
Within the browse procedure I only had to add two lines of code to get the checkbox effect.
The first time I ran the app, however, I got an odd effect. No icons showed at all until I clicked on the icon's location:
I found the cause in the browse's SetQueueRecord method:
The available icons are determined by the class's Initialize method, using PROP:IconList assignments. A value of zero indicates no icon at all.
I solved this by added
SELF.Q.Checked_Icon = 2
in the last embed so the unchecked icons display by default.
With the modification the browse works as expected, with one proviso.
This is a page-loaded browse, and the queue that holds the data in the list box at any one time is only the data you see on screen. So if you check a checkbox and then scroll so the checked line is off screen, when you scroll back to the line again the checkbox has been cleared. That makes total sense, since the class is only tracking the items that are currently displayed.
You can solve this by creating a file loaded browse where all records are in the queue, but that isn't ideal for every situation. A better solution is something that can handle both file and page loaded list boxes.
And that sets the stage for persisting data...
Download the source