Wednesday, 3 December 2008

Whitespace - good & bad


I'm a big fan of using lots of whitespace to make my code more readable. Blank lines, indentation, spaces in comments, spaces between parameters etc. Also I go for more long-winded forms of multiline structures and I prefer not to have functions as parameters.

As an example, consider the following 2 code snippets:
    if x > 100 then put "Over limit" into fld "Result"
    else put "OK" into fld "Result"
    put char 1 to 3 of line lineoffset("Text",fld 3,10) of fld 3 into fld 2
and
    if x > 100 then
        put "Over limit" into fld "Result"
    else
        put "OK" into fld "Result"
    end if

    put lineoffset("Text", fld 3, 10) into tLineNum
    put char 1 to 3 of line tLineNum of fld 3 into tChars
    put tChars into fld 2


Both do exactly the same thing without error.
The second example has 9 lines while the first example only uses 3 lines, but which is easier to understand, debug and error-check? I bench-marked the two scripts and they gave virtually identical times.

However, my passion for whitespace caused a problem last week which took a while to work out. I had an "errorDialog" handler and I wanted to trap for certain error numbers, so I wrote the following script:

on errorDialog pErrorContents
    put "324, 624, 538" into tIgnoredErrors -- move abort, wait abort, script abort
    if item 1 of pErrorContents is among the items of tIgnoredErrors then exit errorDialog

    saveErrorLog pErrorContents

    pass errorDialog
end errorDialog

Then I found that my error log was recording errors with the first number being 624, which it should have been ignoring. I couldn't work this out for a while until I realised that "624 is among the items of tIgnoredErrors" returned false, because I had added spaces between each item. Once I changed the first line of the handler to read:
    put "324,624,538" into tIgnoredErrors   -- move abort, wait abort, script abort

Then it all worked fine. So the moral of the tale is, don't add extra white space to a string if you are doing item comparisons.

Until next time,
Sarah

Wednesday, 19 November 2008

Help for disabled buttons

I like to perform a lot of data evaluation on whatever information users of my applications are allowed to enter. Going along with one of my favourite programming principles:

Never ask for any information that the program can find out for itself.

I try to keep user input at a minimum and I like to provide help for any that is entered. The help involves tool tips, best-guess data entered automatically, and disabling controls that are either not needed or are not valid with the currently entered data.

I do a lot of process control and there is no use trying to ask the program to open a valve if you haven't told it which valve to open yet. In this case, the "Open valve" button would probably be disabled.

However this can be a bit frustrating. A user keeps trying to open the valve, it doesn't work and they don't know why. So I developed a practice of putting another button UNDERNEATH any button that might be disabled. This second button will only be clickable if the button on top of it is disabled and it can provide some explanation about what needs to be done before the button is re-enabled.

Here is an example:

Make a new stack.
Add an option menu called "Valve" and have it's options be None, Valve 1, Valve 2 & Valve 3.
Add a button called "Open valve".

Edit the script of the option menu as follows:
on menuPick pChoice
    if pChoice = "None" then
        disable btn "Open valve"
    else
        enable btn "Open valve"
    end if
end menuPick

Test the operation of this menu button and you should see the "Open valve" button disable & enable as required.

Edit the "Open valve" button script as follows:
on mouseUp
    put the selectedtext of btn "Valve" into tValve
    answer info tValve & " is now open."
end mouseUp

Choose "None" from the option menu and check that the button is disabled.
Click the button - nothing happens.
Chose a valve from the option menu and click the "Open valve" button which should now be enabled. You will get a message indicating the valve has been opened.

Duplicate the "Open valve" button (duplicate so that it is exactly the same style and size).
Rename it to something like "Open valve disabled".
Turn off it's "Show name" property and make sure it is enabled.
Move it so that it is positioned exactly on top of the original button.
Set it's script to:
on mouseUp
    answer warning "Please select a valve to turn on."
end mouseUp

Now send this button to the back so that it is hidden behind the "Open valve" button.

Test by making selections in the option menu and clicking the "Open valve" button.
You should either get a message saying the valve is opened, or a message telling you to select a valve. Either way, you get some feedback as to what is happening or why nothing has happened.

Obviously, in such a simple example this is not really necessary, but for a more complicated layout with multiple reasons why a button did not work, it can be really useful. You could also extend the script in the hidden button to work out how much data is incorrect or missing and present a specific list.

Until next time,
Sarah

Tuesday, 14 October 2008

Adding a menu shortcut to Rev

I like to keep my fingers on the keyboard when scripting, so it has frustrated me that the menu item to bring up the Application Browser does not have a menu shortcut. So here is how to add one for yourself.

Since this involves changing the Rev IDE, first we want to make a backup of the relevant stack. Go to the folder that contains your Revolution application. Look in the Toolset folder and find the "revmenubar.rev" file. Make a backup copy of this, so if anything goes horribly wrong, you can always put it back to the way it was.

Now start up Revolution. In the View menu, make sure "Revolution UI Elements in Lists" is checked. This makes all the stacks in the Revolution IDE appear in the Application Browser. Now open the Application Browser using the item in the Tools menu. You will see a long list of stacks, mostly starting with "rev".

Scroll down until you find "revMenuBar" and click it's disclosure triangle to show it's card. Click on the card reference (card id 1002) and in the list on the right, you will see all the objects on that card.

Click the "Layer" column header to make sure the objects are sorted by layer, then look for "revMenuBar" which is layer 3 on my system. In the revMenuBar, look for the Tools button (layer 6). Right-click on it's name and choose "Property Inspector" from the popup menu. If this is not showing the "Basic Properties" tab, then choose it from the popup at the top of the Property Inspector window.

Now we need to decide what shortcut to assign to the Application Browser. It can't be either A or B because they are already used. I chose Command/Control-6 since Command/Control 1 - 5 are used for navigating between cards. If you choose something else, then just substitute that character wherever I use 6.

Scroll down through the list of menu items until you see "Application Browser". Edit this line to show:
Application Browser/6

Click outside that field to make the change take effect. Have a look at the Rev Tools menu and you should now see your new shortcut in the menu.

Go back to the Application Browser, make sure "revMenuBar" is selected, right-click it and choose "Save". And that's it. You now have a new keyboard shortcut for the Application menu. Un-check "Revolution UI Elements in Lists" in the View menu, to un-clutter the Application Browser's display again.

Because most of the Rev IDE is open, this technique can be applied to any menu item. It can even be used to alter existing shortcuts of you prefer something else. The only problem is that if you upgrade to a newer version of Revolution, you will have to re-apply this fix.

Until next time,
Sarah

Monday, 8 September 2008

Spell checking in Revolution - final

Now for the wrap-up: what scripts do you need, where should they go and how do you specify which fields to check.

Here is a download link for the final demo stack.



In it, I have assembled all the required scripts into the stack script, and set up two fields, only one of which will get checked. This requires checking to see if the selectedObject or the clickField is valid before trying to process a rawKeyUp or mouseDown event, but this is worth the effort since it makes the scripts universal and avoids having to duplicate them in every field that needs checking.

With the scripts all in the stack script, this stack could be made into a library stack and activated with "start using". The complete stack script could also be copied into a button in your stack and made into a frontScript or a backScript using "insert script".

For each field, the script checks for a custom property called "cSpellCheck". If this property exists and is set to "true", then the field will be checked. This allows you to limit the spell checker so you can stop it looking at web sites, email addresses and other non-standard text fields.

The script contains all the handlers discussed in previous posts, but with the extra checks for the "cSpellCheck" property.

I hope people find this useful - I have certainly enjoyed creating it and am finding the results to be very good.

Until next time,
Sarah

Tuesday, 29 July 2008

Spell checking in Revolution - part 4

Now it is time to work out how to correct the spelling mistakes that have been detected and marked. The conventional way is to allow the user to right-click on any incorrect word and offer a popup button with a list of valid suggestions, so that's what I'm going to do.

Since the list of suggested words will be different each time, the menu has to be constructed after the user has clicked. The XSpell AppleScript extension has a command to guess the correct spelling of any word. This was discussed in part 1 of this series. So what we have to do is detect a right-mouse click in the text field, find the word being clicked, use AppleScript to get a list of possibilities and assemble them into a popup. If a new word is chosen from the popup, then that word must be placed into the text and the error markings removed.

To detect a right-mouse click, I use the parameter which is supplied with every mouseUp or mouseDown message and which gives the button number. For the right mouse button, the button number is 3. Don't forget to "pass" the mouseDown message if you are not using it.

on mouseDown pMouseBtnNum
    if pMouseBtnNum = 3 then
        checkWord
    else
        pass mouseDown
    end if
end mouseDown

This calls a routine called "checkWord" which does the work. It stores the location of clicked word in a script local called sWordToFix, so that the fixing handler knows where to put the replacement.
local sWordToFix

command checkWord
    -- find the clicked word & see if it is marked as a spelling error
    put the clicktext into tWord
    put the clickchunk into tChunk
    if tChunk is empty then exit checkWord -- no word being clicked on
    if the textstyle of tChunk contains "underline" is false then exit checkWord -- not marked as a mispelt word

    -- store the chunk of the word we're looking it so it can be fixed
    put tChunk into sWordToFix

    -- get suggestions for this word
    put "guess spelling of word " & quote & tWord & quote into tScript
    do tScript as AppleScript
    put the result into tGuess

    if tGuess is empty then
        put "(No suggestions" into tGuess
    else
        -- {"Sample", "maple", "Staple"}
        replace quote & ", " & quote with cr in tGuess
        replace quote with "" in tGuess
        delete first char of tGuess
        delete last char of tGuess
    end if

    -- now show popup button with this list of guesses, first making sure the popup button exists
    checkForPopup
    put tGuess into btn "SpellCheckGuesses"
    popup btn "SpellCheckGuesses" at the mouseloc
end checkWord

The "checkForPopup" handler creates the popup button is required and sets it's script:

command checkForPopup
    if there is not a btn "SpellCheckGuesses" then
        create invisible btn "SpellCheckGuesses"
        set the style of btn "SpellCheckGuesses" to "menu"
        set the menumode of btn "SpellCheckGuesses" to "popup"

        -- set the script of the new popup button
        put "on menuPick pGuess" & cr & "fixWord pGuess" & cr & "end menuPick" into tScript
        set the script of btn "SpellCheckGuesses" to tScript
    end if
end checkForPopup

This sets the script of the popup button, so that it will call the fixWord handler, sending it the newly chosen word.

command fixWord pCorrect
    -- store current text style
    put the textstyle of sWordToFix into tStyle

    -- replace the mispelt word
    put "put " & quote & pCorrect & quote & " into " & sWordToFix into tCmd
    do tCmd

    -- allow for change in word length when resetting previous text style
    put the number of chars in pCorrect into tNewLength
    put word 2 of sWordToFix + tNewLength - 1 into word 4 of sWordToFix

    -- remove the underline style from the text
    replace "underline" with "" in tStyle
    if char 1 of tStyle = comma then delete char 1 of tStyle
    if char -1 of tStyle = comma then delete char -1 of tStyle
    set the textstyle of sWordToFix to tStyle
    set the textcolor of sWordToFix to ""

    select after sWordToFix
end fixWord

fixWord replaces the incorrect word, inserts the newly chosen word into the same place and removes the text formatting that was showing the word as incorrect.

So now we have a working system. We can check spelling, mark incorrect words and now correct them.

Next time, I'll go into more details about where to put all these handlers and how to arrange it that multiple fields can be checked without any script duplication.

Until next time,
Sarah

Tuesday, 22 July 2008

Spell checking in Revolution - part 3

In the first 2 parts of this thread, I discussed how to spell check using AppleScript and then how to transfer that to Revolution. This time, I want to talk about the design of the spell checker inside Revolution. The main questions I felt that I had to answer were:
  • How should the user be notified of spelling errors?
  • Should checking happen all the time or only when the user specifically asks for it?
With regard to notification, the standard these days seems to be a red line of some sort under the incorrect words. Microsoft uses a wavy line, Apple uses a dotted line. However neither of these is easily accessible in Revolution, so I decided to use a text style and colour to indicate spelling errors. The key is to choose a style and a colour that you are unlikely to use normally. Otherwise, spelling errors may just look like normal text.

I experimented with several different options but in the end I went for red underlined text to indicate errors. This will be a matter of choice and will depend on the sort of application you are creating, but this is what works for me.

Here is a screen shot of a portion of my application, showing several words marked in this way.


Before I move on to scripting this, the next decision needs to be made: when to check?

Again, this is a matter for opinion and I guess it should really be made an option in your application's preferences. However I prefer checking as I type. Obviously, there is no point in checking after every keystroke as that would make incomplete words appear as errors. So I decided to trigger the spell check using a rawKeyUp handler. A rawKeyUp or rawKeyDown event handler has a single parameter which is a numeric code for the pressed key. If you want to test out some keys, I suggest you try my KeyCoder stack which will tell you the code for any key you press.

I decided to check for Space, Return, Enter, Tab, Backspace, Delete, Period & Comma. After any of these were clicked, I would then initiate a spell check.

This worked well, but as there got to be more & more data in my field, it started to slow things down, so that even my rather pedestrian typing became sluggish. Each check was first removing any existing markers (text style & colour) and then searching for new errors and applying the markers again. This is quite a lot of work for a field object. I suppose I could have tried to make the spell checking more efficient and only checked the currently selected word, or words close to it, but cutting & pasting could have caused havoc, so I preferred to check the complete field at once.

My solution was to queue a spell check so that it waited until there was a delay in the typing and then checked all at once.

Here is the script that I placed in the field that I wanted to check:

local sLastKeystroke

on rawKeyUp pKey
    -- only spell check if there have been no keystokes for 1000 millisecs
    if sLastKeystroke is empty or sLastKeystroke is not a number then put 0 into sLastKeystroke
    if the milliseconds - sLastKeystroke > 1000 then
        -- only check spellingafter space, tab, delete, backspace, return, enter, full stop, comma
        -- use KeyCoder to get these numbers
        put "32,65289,65293,65421,65288,65535,46,44" into tSpellKeys
        if pKey is among the items of tSpellKeys then
            updateSpelling
        end if
    else
        -- still typing, queue a spell check & try again in half a second
        -- this check will be cancelled and a new check scheduled if I keep typing fast
        cancelMessageName "updateSpelling"
        send "updateSpelling" to me in 500 millisecs
    end if
    put the milliseconds into sLastKeystroke

    pass rawKeyUp
end rawKeyUp


on cancelMessageName pHandlerName
    put the pendingmessages into pMess
    repeat for each line p in pMess
        if item 3 of p contains pHandlerName then cancel item 1 of p
    end repeat
end cancelMessageName

It uses a script local variable to store the time in milliseconds of the last key stroke. If you have stopped typing for at least one second, then press space, spelling will be checked immediately. However if you keep typing, then a "send in time" will be used to tell the system to check spelling in 500 milliseconds. Every time you press another key, this scheduled check gets pushed further into the future, but when you finally stop typing for more than half a second, the spell checker gets it's turn.

The timings of this might need to be adjusted for a more expert typist than me, but this suits me very well.

So now we have the "updateSpelling" handler being triggered appropriately, I guess it's time to work out what it should do.

The first thing is to clear any existing spell checker marks.

on clearSpellingMarkers pFieldName
    lock screen
    repeat with c = 1 to the number of chars in the text of pFieldName
        put the textstyle of char c of pFieldName into tStyle
        if tStyle contains "underline" then
            replace "underline" with "" in tStyle
            if char 1 of tStyle = comma then delete char 1 of tStyle
            if char -1 of tStyle = comma then delete char -1 of tStyle
            set the textstyle of char c of pFieldName to tStyle
        end if
    end repeat

    set the textcolor of char 1 to -1 of pFieldName to ""
end clearSpellingMarkers

This handlers get sent the name of the field to be cleared and then removes any underlines and sets the textColor back to the default. This means that ALL colours will be removed, not just those that indicate incorrect spelling. While this does not worry me, if it is a problem, the spell checking can be changed so it uses a text style notification only, and doesn't change any colours. Text styles other than underlines will be preserved.

Now I can use one of the functions from last time to find the words in the field that need to be marked up. And this handler puts it all together and displays the incorrect words.

on showBadWords pFieldName
    lock screen
    put the text of pFieldName into tText
    put listBadWords(tText) into tBadCharNumbers
    if tBadCharNumbers is empty then exit to top -- no incorrect spellings

    repeat for each line L in tBadCharNumbers
        put item 1 of L into tStartChar
        put item 2 of L into tEndChar
        if tStartChar is not a number or tEndChar is not a number then next repeat

        -- don't mark single characters
        if tEndChar - tStartChar < 2 then next repeat

        put the textstyle of char tStartChar to tEndChar of pFieldName into tOld

        if tOld is empty then
            put "underline" into tNew
        else
            put tOld & ",underline" into tNew
        end if

        set the textstyle of char tStartChar to tEndChar of pFieldName to tNew
        set the textcolor of char tStartChar to tEndChar of pFieldName to "red"
    end repeat
end showBadWords

This uses the listBadWords() function from the last post and then marks up the incorrect words as discussed.

Now the demo stack from last time has been altered. Instead of having a separate field to show the incorrect words, it just has a single field that will mark up mistakes as you type.
You can download the stack here: SpellCheckDemo2.rev
The handlers are all in the field or in the stack script.

All that's left is to work out how to correct spelling errors and how to apply this to a stack with multiple edit fields, without duplicating the scripts in each field.

Until next time,
Sarah

Tuesday, 15 July 2008

Spell checking in Revolution - part 2

Last time, I discussed using the XSpell AppleScript scripting addition to access the OS X spell checker through AppleScript. Now I'll move on to show how I have done this from inside Revolution.

For testing purposes, make a new stack with a field for the text to be checked, a list field for showing the incorrect words and a second list field for showing the possible corrections. Add a button to trigger the spell check.
Here is the script for the "Check Spelling" button.
on mouseUp
    put fld "Text" into tText
    put empty into fld "Errors"
    put empty into fld "Suggestions"

    put listBadWords(tText) into tBadCharNumbers
    if tBadCharNumbers is empty then exit to top -- no incorrect spellings

    put empty into tBadWords
    repeat for each line L in tBadCharNumbers
        put item 1 of L into tStartChar
        put item 2 of L into tEndChar
        if tStartChar is not a number or tEndChar is not a number then next repeat

        put char tStartChar to tEndChar of tText into tNewWord
        put tNewWord & cr after tBadWords
    end repeat
    delete last char of tBadWords

    put tBadWords into fld "Errors"
end mouseUp


-- return a list of all characters that have been spelt incorrectly in the supplied text
-- the list is one word per line, with starting character, ending character
--
function listBadWords pText
    replace quote with "'" in pText
    put "check spelling in text " & quote & pText & quote into tScript
    do tScript as AppleScript
    put the result into tErrorList

    -- check to see if the XSpell addition is installed OK
    if tErrorList = "compiler error" then
        answer error "Please install the XSpell scripting addition first!"
        exit to top
    end if

    if tErrorList is empty then return empty -- no errors

    -- gives this sort of return - linefeeds added by me
    -- {{word:"smaple", starting at:14, ending at:19, guesses:{"sample", "maple", "staple"}},
    -- {word:"shuld", starting at:327, ending at:331, guesses:{"should", "shelled", "shilled"}}}

    -- now process the AppleScript error list into something easier for Rev to handle
    replace "}, {" with cr in tErrorList
    delete first char of tErrorList
    delete last char of tErrorList

    -- retrieve the word number for each mistake.
    put empty into tBadCharNumbers
    repeat for each line L in tErrorList
        -- get the character where the mis-spelled word starts & ends
        set the itemdel to comma
        put item 2 of L into tStart
        put item 3 of L into tEnd
        set the itemdel to ":"
        put last item of tStart into tStart
        put last item of tEnd into tEnd

        put tStart & comma & tEnd & cr after tBadCharNumbers
    end repeat

    delete last char of tBadCharNumbers
    return tBadCharNumbers
end listBadWords

As you can see, it uses a function that calls the AppleScript command, then it parses the result into a list of start & end character numbers for each misspelled word. The mouseUp handler then extracts the words in these locations and lists them in the Errors field.

Now to work out the suggested corrections for each of the errors. Put the following script into the "Errors" field:
on mouseUp
    put the selectedtext of me into tWord
    if tWord is empty then exit to top

    -- get suggestions for this word
    put "guess spelling of word " & quote & tWord & quote into tScript
    do tScript as AppleScript
    put the result into tGuess

    -- no need to check if XSpell is already installed, as this field will be empty unless it has already worked

    if tGuess is empty then
        put "No suggestions" into tGuess
    else
        -- format the returned list into one word per line
        -- {"Sample", "maple", "Staple"}
        replace quote & ", " & quote with cr in tGuess
        replace quote with "" in tGuess
        delete first char of tGuess
        delete last char of tGuess
    end if

    put tGuess into fld "Suggestions"
end mouseUp

Now clicking on any line on this field will produce a list of suggestions in the third field.

Obviously this is not a very useful way to check spelling, but I wanted to demonstrate how it could work, without getting confused by any fancy displays or messages.

If you want a copy of this test stack, you can download it here: SpellCheckDemo.rev

In future posts, I will discuss possible ways to display spelling mistakes,  how to trigger a check and how to allow mistakes to be corrected.

Until next time,
Sarah