2008-04-21

Click Click

Wouldn’t it be great if Woopsi supported double-clicks? No? Well, that’s just too bad.

Double-clicks are sent through the hierarchy as single clicks. When a gadget receives a click, it checks to see where and when the last click occurred. If it occurred within a reasonable proximity to the current click (within a 20 pixel bounding box and within the last 15 frames) it is processed as a double-click. Note that the first click is still processed as a single click, so if you want to delay processing the first click in anticipation of a second, your gadgets will need to do some more work.

I’ve added double-click support so that the new ListBox gadget I’ve added can perform a different function when an item within it is double-clicked. I’m thinking of a file requester in which double-clicking an entry automatically selects the entry and closes the requester. It would have been possible to have supported this without system-wide double-click support, but it’s a lot of work to have to repeat it for every time it’s needed.

The ListBox itself isn’t finished yet, but it’s mostly working. All I need to sort out is scrolling, which I’ll probably achieve by inheriting from the ScrollingPanel. List items can be selected (single click), unselected (click again) and double-clicked.

I’ve gone for a very simple list box class, more like the ContextMenu than Jeff’s List/ListSource classes. I don’t think I need anything too complex in this case as it has a specific purpose (showing a list of text-based options).

Other changes include the removal of the WiFi icon, which can apparently cause problems - as it’s a stock PALib icon I’m happy to see it go - and moving the ROM icon to a different directory to prevent it being bundled into the ROM twice.

Comments

Jeff on 2008-04-21 at 20:57 said:

The thing about double-clicking in a list specifically is that you can detect it quite easily without the framework’s help, since the first click pretty much needs to make the list entry ‘selected’. Thus, if you ever click the same selection twice, that would count as a double-click.

Purists argue that if the clicks are in the same selection, but far enough apart, they should count as two seperate clicks rather than one - personally, I disagree specifically because of the lack of precision of the stylus (mine, anyway)

Clicking an item to unselect it is going to confuse the Windows/OSX types where you need to control/command-click to unselect (via the selection-extension) mechanism. I am guessing that you are thinking about the ListBox solely from a ‘one-row-only’ selection model, whereas I’m definitely working towards my List being able to do multiple-discrete-selection (think about ‘select multiple files, delete’ or ‘select multiple addresses, sync to server’, etc. There is no reason they need to be contiguous in the list.

From the perspective of implementing a context menu, or even a standard menu bar, I don’t think double-clicks are appropriate either, are they? Noone expects to be able to double-click a menu (at least, not after the first time)

As to the file requester problem, I don’t see the problem. The file requester can just run its own event loop which listens to an instance variable that it sets if you ever want to return.


char *FileSelector::select_existing(
        const char *prompt )
{
        FileSelector *fs;
        FontBase *f = new couriernew14();
        fs = new FileSelector(f);
        if (fs) {
                fs->RunLoop();
/// in here you check if fs thinks a file was selected ///
                delete fs;
        }
        return NULL;
}

// pump the message loop - handleClick() will reset _looping if/when a button is
// pushed...
void FileSelector::RunLoop()
{
        _looping = true;
        while (_looping) {
                ((WoopsiApplication*)woopsiApplication)->ProcessOneVBL();
        }
}

bool FileSelector::handleEvent(const EventArgs& e)
{
        switch (e.type) {
        default:
                break;

        case EVENT_RELEASE:
                // ignore unless stylus is inside button
                if (!e.gadget->checkCollision(e.eventX,e.eventY)) break;
                _looping = false;
                return true;

        case EVENT_KEY_RELEASE:
                switch (e.keyCode) {
                default:
                        break;

                case KEY_CODE_A:
                        Debug::printf("A: sel=%d",_list->selected());
                        _looping = false;
                        break;

                case KEY_CODE_B:
                        _looping = false;
                        break;
                }
        }

        return(false);
}

ant on 2008-04-21 at 21:57 said:

The new double click code treats clicks that are a distance apart as separate clicks. It’s quite generous in its error margin because the screen isn’t amazingly accurate.

The potential’s there for making the list support both single and multiple selections. It’s single only at the moment mainly because I’ve just started on it and decided to go for the easy option first.

The context menu won’t be affected by the double click code as it closes as soon as the stylus is released the first time. As for other gadgets, I was considering putting a “doubleClickable” flag in the gadget class.

I’d be interested in your thoughts on double clicking, actually. I remember that you’ve requested it in the past, but is it really suitable for the DS? Is there any situation in which double clicking is a desired behaviour? Should Woopsi support it anyway, and not presume to force UI conventions on people?

I read a post over at Coding Horror recently, which makes me ask. Jeff Attwood’s suggestion was that double-clicking, as a non-obvious hidden behaviour, wasn’t a good UI convention to have. He suggested doing away with it entirely, although in a bizarre twist, he suggested this after noting that people tend to hate it when double clicking is taken away from them. A good example is KDE - one of the many things I loathe about KDE is its default single-click icon launching. Cant’t stand it.

Woopsi is in a different situation. Although it’s mimicking a desktop UI it isn’t a desktop UI, so we are free (in a limited way) to define our own conventions.

(Typing on an iPod is hard work…)

ant on 2008-04-21 at 22:08 said:

Modal dialogues - I haven’t even considered this, to be honest. The invisible screen is one approach that would work quite well (like the password box that Ubuntu pops up). It would be possible to have a pointer in Woopsi to a modal gadget and block all other interaction until the pointer is null. It’d have to allow interaction with all of the modal gadget’s ancestors and children, though, which would cause complications.

I’m not even sure how the Amiga handled it. It might be easiest if a modal window disables the window that created it, then enables it again when the modal window closes. That way you preserve the multitasking illusion and modal windows only lock up one “application”.

Jeff on 2008-04-21 at 22:45 said:

Of course, the above snippet only really works if your FileSelector class obscures the entire screen, since it doesn’t catch attempts to switch away (to another window/screen).

In reality this is a problem with any kind of modal dialog you attempt - I’m not sure what the best shaped fix for this is, if the dialog isn’t full-screen (with no depth-change gadgets, etc) - and it sucks if the debug panel pops up as well…

I think this is one of the reasons the Alert class has sort of waited in the wings, hasn’t it? Because it has no easy way to lock the users attention on itself…

Jeff on 2008-04-22 at 00:09 said:

As it stands, I’m not sure what other widgets might want to support double-clicking, other than lists (where a double-click implies the ‘ok’ button somewhere else on the same window) and text-entry fields (where a double-click implies ‘select-word’, neither of which really exists at present.

In fact, double-clicking a button is something that I’d be working hard to prevent - users jittering the mouse^H^H^H^H^Hstylus being interpreted as a double-click is downright annoying, especially if you queue up events and try to deliver the second click to an Ok button which has since disappeared.

Its no big deal if the base gadget provides double-click detection. But pretty much all of the standard gadgets need to ensure that they treat double-click as two single-clicks unless they have some special semantic defined. Otherwise, the quick user (say, someone tapping on a Keyboard gadget) can’t tap out the word ‘subbookkeeper’ without cursing you four times.

So, to clarify my position, I don’t think double-click support is a bad thing - just that it introduces subtleties that need to be considered on a gadget by gadget basis, and that sensible ‘default behaviour’ may be difficult to achieve.

The issue of what double-click means in a list that allows multiple-selection, that has multiple rows selected already, is mind-numbing, especially since there is no ‘shift’ key on the DS, so its difficult to implement the same semantics as the other platforms. At the moment, I believe there’s a pseudo-standard that says “A=Select, B=Back” and not much else.

I’m thinking that having any shoulder-button pressed should count as ‘shift’. In a text widget, then, I click to move the cursor around, and shift-click-drag to extend the selection. In a list, I click to change the selection, and shift-click to extend the selection. Double-click, without shift, technically means change the selection to just the current row, then process the selection. Double-click-with-shift means include the current row in the selection, then process the selection.

From a usability perspective, I think my hands would be happy with this approach, whereas having any of the other buttons (the x pad, or X/Y) as shift modifier would be uncomfortable. But thats very subjective.

(The X-pad is handled in my list already anyway. Up/Down are no brainers. Left/Right I treat as page-up/down respectively, which matches the behaviour of the R4 game menu)

Jeff on 2008-04-22 at 00:19 said:

The big problem with modal dialogs, and magic globals, is that you quite often want to stack them.

Consider a file selection dialog - the app wants this to be modal because it can’t continue till that question is replied to. However, its quite common for the [OK] button to allow the caller to reject the selection before closing. Perhaps its the ‘select a file for output’ box where the user selects a directory by tree-crawling through a list, but also types in a filename. If the filename exists or contains invalid characters, the selector wants to display a “Bad Stimpy, No Biscuit” dialog over the top of the selector, then continue with the selection, rather than returning to the caller who checks for a problem, displays the error then loops.

Or even something just goes wrong in the file selector and it wants to display an error message.

The natural fit is to display one modal over the top of another, which could technically go as deep as you like. If you have a single global, this falls apart. You need a stack (which can just be implemented by having each modal window save/restore the previous value)

Your multi-tasking illusion could be maintained on a screen-by-screen basis, perhaps though that complicates things because you need to allow clicks on the front window or the SCREEN decorations but nothing else. I’ve just spent a bunch of time at work trying to work through Windows and its brain-dead idea of “TaskModal” vs “ApplModal” vs “SystemModal” and haven’t found any of them that really works the way the human expects. Really, if you are being prompted for a filename in one function, its hard to see where its sensible to be able to switch to another screen and potentially delete the directory that the front window is currently displaying.

As it stands, the “event loop” is currently in Woopsi:: and it works out which gadgets it should dispatch events to. Pretty much the same logic could be run inside Screen:: or Window:: couldn’t it? If the click is outside that gadget, then its just going to be discarded, and if its inside then it will propagate down to sub-gadgets properly. A Modal Window then just runs the event loop (which becomes window->play();waitForVBL();) until it thinks it is finished.

ant on 2008-04-22 at 07:44 said:

That’s true. The Gadget class could implement a “runModal()” method that uses some of the code currently in the Woopsi class to run its own VBL system. That would probably solve the whole issue without too much effort.

ant on 2008-04-22 at 08:53 said:

…Except it will get very, very messy very quickly. I’m tempted to say that a window on an invisible screen should be the “proper” way to handle modal windows.

ant on 2008-04-22 at 20:26 said:

It’s pretty easy to put together. If you inherit from the Screen class and just override the draw(Rect clipRect) method with this, you get an invisible screen:

void ModalScreen::draw(Rect clipRect) { _flags.erased = false; erase(); }

In fact, just pull down the SVN code again. There’s a new ModalScreen class. Open one of these, and add a window to it - instant modal window. The demo has an example added, but as the “OK” button in the alert requester doesn’t close the ModalScreen, it blocks everything out permanently. You’d need to receive gadget events (probably in a subclass of ModalScreen) and close the screen when the child’s close event fires.

Jeff on 2008-04-22 at 21:00 said:

Can you add a demo that shows the use of an ‘invisible’ screen then? My file selector currently uses the whole screen so its no big deal, but I’m at the point where I need an ErrorMessageDisplay that would be the same sort of modal…

Jeff on 2008-04-23 at 08:33 said:

Thanks for that, I’ll see how it all fits together.

One other thing thats probably tedious but worth doing is the conflict between draw() and draw(Rect*)

I’d suggest renaming draw() to eliminate the stupid compiler problem…

Perhaps draw() really means refresh() anyway?