Page 1 of 1

Field in a group receiving focus without having focus... ?

Posted: Wed Apr 03, 2024 3:06 am
by stam
Hi all, this is a weird one I decided to repost here because it will probably get lost in in translation where this is.

In short: I created a group widget that functions as a combobox, but with added features like placeholder text and keyboard navigation.
https://forums.livecode.com/viewtopic.php?f=9&t=39005

The group consists of an SVG widget icon and 2 fields (a normal 1-line field and a listField). When using the group, the 1-line field receives the focus.
All works quite well. Using the up/down arrows or scroll, I can navigate the dropdown without having to use the mouse etc. The entire script is in the group script and of course arrow keys and scroll are trapped in the group's rawKeyDown handler.

Now the weird part which I can't explain:
When clicking out of the field and removing the focus ring from the 1-line field and the group, the rawKeyDown handler still traps the arrow keys!!!
If a click out of the group a 2nd time, that stops happening!!!
If I put a line in the rawKeyDown to put the focusedObject, then it returns the 1-line field even though that visually has lost the focus!!!

I know that's a lot of exclamation marks, but this broke my mind a bit ;)
I was able to address this by adding focus on nothing to closeField and exitField handlers.


But this seems wrong to me... Is this expected behaviour or a bug?

Why should a group's rawKeyDown handler fire even though it's not in focus?
Or more accurately, why does the group report that the focusedObject is the group's text entry field after clicking out of said field and group and the field no longer displays a cursor and has lost it's focus ring?

The entire group script is below:

Code: Select all

#< ----------------------  CONSTANTS --------------------------- >
constant kReturn = 65293
constant kEnter = 65421
constant kEscape = 65307
constant kTab = 65289
constant kBackspace = 65288
constant kDelete = 65535
constant kArrowDown = 65364
constant kArrowUp = 65362
constant kArrowRight = 65363
constant kArrowLeft = 65361
constant kScrollDown = 65309
constant kScrollUp = 65308
#</

#< -----------------------  HANDLERS ---------------------------- >
local sFieldID, sDiscloseID, sDropdownID

private function getFoundLine
    local tData, tLine, tFilter
    checkForIDs
    if fieldIsEmpty() then return empty
    return lineOffset(the text of control id sFieldID, control id sDropdownID )
end getFoundLine

private function fieldIsEmpty
    checkForIDs
    try
        return the text of control id sFieldID = the placeholderText of me or the text of control id sFieldID is empty
    end try
end fieldIsEmpty

private command setStyle
    checkForIDs
    set the textColor of control id sDropdownID to the normalColor of me
    if fieldIsEmpty() then 
        set the textColor of control id sFieldID to the placeholderColor of me
        set the textStyle of control id sFieldID to the placeholderStyle of me
        set the text of control id sFieldID to the placeholderText of me
        select before control id sFieldID
    else
        set the textColor of control id sFieldID to the normalColor of me
        set the textStyle of control id sFieldID to the normalStyle of me
        select after control id sFieldID
    end if
    centerVertically
end setStyle

command showDropdown
    checkForIDs
    local tRect
    show control id sDropdownID
    
    put item 1 of the rect of control id sFieldID - 4 into item 1 of tRect
    put item 2 of the rect of control id sFieldID- 4 into item 2 of tRect
    put item 3 of the rect of control id sFieldID + 4 into item 3 of tRect
    put item 4 of the rect of control id sDropdownID + 4 into item 4 of tRect
    set the rect of me to tRect
end showDropdown

command hideDropdown
    checkForIDs
    local tRect
    hide control id sDropdownID
    
    put item 1 of the rect of control id sFieldID - 4 into item 1 of tRect
    put item 2 of the rect of control id sFieldID - 4 into item 2 of tRect
    put item 3 of the rect of control id sFieldID + 4 into item 3 of tRect
    put item 4 of the rect of control id sFieldID + 4 into item 4 of tRect
    set the rect of me to tRect
end hideDropdown

command checkForIDs
        if sFieldID is empty or sDropdownID is empty or sDiscloseID is empty then openControl
    end checkForIDs
    
command centerVertically
    local tFieldY, tFormatRect, tFormatheight, tFormatHalfHeight, tCurrFormatTop, tCenterField_To_TopTextDiff
    put item 2 of the loc of control id sFieldID into tfieldY
    put the formattedRect of line 1 to - 1 of control id sFieldID into tFormatRect
    put item 4 of tFormatRect - item 2 of tFormatRect into tFormatHeight
    put tFormatHeight div 2 into tFormatHalfHeight
    put item 2 of tFormatRect into tCurrFormatTop
    put tfieldY - tCurrFormatTop into tCenterField_To_TopTextDiff
    set the topMargin of control id sFieldID to the topMargin of control id sFieldID + tCenterField_To_TopTextDiff - tFormatHalfHeight
end centerVertically
#</


#< -------------------------  EVENTS ------------------------------- >
on openControl
    put the id of field "field" of me into sFieldID
    put the id of field "dropdown" of me into sDropdownID
    put the id of widget "disclose" of me into sDiscloseID
    if the text of field "field" of me is empty then setStyle
end openControl

on resizeControl
    checkForIDs
    local tRect, tFieldRect, tDropdownRect, tExpanded
    put the visible of control id sDropdownID into tExpanded
    put the rect of me into tRect 
    // field "field"
    if tExpanded then
        put item 1 of tRect  + 4 into item 1 of tFieldRect
        put item 2 of tRect +4 into item 2 of tFieldRect
        put item 3 of tRect - 4 into item 3 of tFieldRect
        put item 2 of tFieldRect + the height of control id sFieldID into item 4 of tFieldRect
    else
        put item 1 of tRect  + 4 into item 1 of tFieldRect
        put item 2 of tRect +4 into item 2 of tFieldRect
        put item 3 of tRect - 4 into item 3 of tFieldRect
        put item 4 of tRect - 4 into item 4 of tFieldRect
        centerVertically
    end if
    set the rect of control id sFieldID to tFieldRect
    
    // widget "disclose"
    set the height of control id sDiscloseID to the height of control id sFieldID - 4
    set the loc of control id sDiscloseID to the loc of control id sFieldID
    set the right of control id sDiscloseID to the right of control id sFieldID - 5
    
    // field "dropdown"
    if tExpanded then
        put tFieldRect into tDropdownRect
        put item 4 of tFieldRect + 2 into item 2 of tDropdownRect
        put item 4 of tRect - 4 into item 4 of tDropdownRect
        set the rect of control id sDropdownID to tDropdownRect
    end if
    
    if  the formattedHeight of control id sDropdownID > the height of control id sDropdownID + 2 then
        set the vScrollbar of control id sDropdownID to true
    else
        set the vScrollbar of control id sDropdownID to false
    end if
    
    set the dgColumnWidth["item"] of control id sDropdownID to the width of control id sDropdownID
end resizeControl

on rawKeyDown pKeyNum
    checkForIDs
    if the long id of control id sFieldID <> the focusedObject then pass rawKeyDown
    local tData, tRow, tNumRows
    switch pKeyNum
        case kReturn
        case kEnter
            // show dropdown if hidden
            if not the visible of control id sDropdownID then 
                showDropdown
                set the hilitedLine of control id sDropdownID to getFoundLine()
                // if dropdown visible but no selection, enter/retun commits the text in field
            else if the visible of control id sDropdownID and the hilitedLine of control id sDropdownID is empty then
                hideDropdown
                set the textContent of me to the text of control id sFieldID
                // if dropdown is visible and an option selected, select & commit
            else if the visible of control id sDropdownID and the hilitedLine of control id sDropdownID is not empty then
                set the textContent of me to line( the hilitedLine of control id sDropdownID) of control id sDropdownID
                hideDropdown
            end if
            break
            
        case kArrowDown
        case kScrollDown
            showDropdown
            put the hilitedLine of control id sDropdownID into tRow
            put the number of lines of control id sDropdownID into tNumRows
            if tRow is empty or tRow = tNumRows then
                set the hilitedLine of control id sDropdownID to 1
            else
                set the hilitedLine of control id sDropdownID to tRow + 1
            end if
            break
            
        case kArrowUp
        case kScrollUp
            showDropdown
            if the hilitedLine of control id sDropdownID = 1 or the hilitedLine of control id sDropdownID is empty then 
                set the hilitedLine of control id sDropdownID to the number of lines of control id sDropdownID
            else
                set the hilitedLine of control id sDropdownID to the hilitedLine of control id sDropdownID - 1
            end if
            break
            
            
        case kEscape
            if the visible of control id sDropdownID then
                if the hilitedLines of control id sDropdownID is empty then
                    hideDropdown
                else
                    set the  hilitedLines of control id sDropdownID to empty  
                end if
            end if
            break
            
        default
            if fieldIsEmpty() then set the text of control id sFieldID to empty
            pass rawKeyDown
    end switch
end rawKeyDown

on rawKeyUp pKeyNum
    checkForIDs
    if pKeyNum is in (kArrowDown && kArrowUp && kScrollDown && kScrollUp && kEnter && kReturn && kEscape) then pass rawKeyUp
    setStyle
    set the hilitedLines of control id sDropdownID to getFoundLine()
    if the hilitedLines of control id sDropdownID is not empty then showDropdown
    
    # required for select-all.  cmd-c/cmd-v/cmd-x all work as is, but not cmd-a (a = 97)
    if the commandKey is down and pKeyNum = 97 then select char 1 to -1 of control id sFieldID
    pass rawKeyUp
end rawKeyUp

on openField
    checkForIDs
    try
        if fieldIsEmpty() then
            select before control id sFieldID
        else
            select after control id sFieldID
        end if
    end try
end openField

on closeField
    checkForIDs
    if (the mouseloc is within the rect of control id sFieldID) or (the optionKey is down) then pass closeField
    if the visible of control id sDropdownID is false then
        set the textContent of me to the text of control id sFieldID
    else
        if the mouseloc is not within the rect of control id sDropdownID then
            set the textContent of me to the text of control id sFieldID
            hideDropdown
        end if
    end if
    focus on nothing
end closeField

on exitField
    checkForIDs
    if (the mouseloc is within the rect of control id sFieldID) or (the optionKey is down) then pass exitField
    if the visible of control id sDropdownID is false then
        set the textContent of me to the text of control id sFieldID
    else
        if the mouseloc is not within the rect of control id sDropdownID then
            set the textContent of me to the text of control id sFieldID
            hideDropdown
        end if
    end if
end exitField

on mouseDown
    checkForIDs
    if the short name of the target is "disclose" then
        set the hilite of control id sDiscloseID to true
        focus on control id sFieldID
    end if
end mouseDown

on mouseRelease
    set the hilite of control id sDiscloseID to false
end mouseRelease

on mouseUp
    if the the short name of the target is "disclose" then
        set the hilite of control id sDiscloseID to false
        if the visible of control id sDropdownID then
            hideDropdown
        else
            showDropdown
        end if
        pass mouseUp
    end if
    
    local tData
    if the clickloc is within the rect of control id sDropdownID then
        if the hilitedLine of control id sDropdownID is not empty then
            put line (the hilitedLine of control id sDropdownID) of control id sDropdownID into tData
            set the text of control id sFieldID to tData
            select after control id sFieldID
            setStyle
            hideDropdown
            set the textContent of me to the text of control id sFieldID
            select after control id sFieldID
        end if
    end if 
end mouseUp
#</


#< ----------------------  PROPERTIES ---------------------------- >
command resetProps
    set the placeholderText of me to empty
    set the placeholderText of me to the placeholderText of me
    set the placeholderColor of me to empty
    set the placeholderColor of me to the placeholderColor of me
    set the placeholderStyle of me to empty
    set the placeholderStyle of me to the placeholderStyle of me
    set the normalColor of me to empty
    set the normalColor of me to the normalColor of me
    set the normalStyle of me to empty
    set the normalStyle of me to the normalStyle of me
    set the groupColor of me to empty
    set the groupColor of me to the groupColor of me
    set the groupHIliteColor of me to empty
    set the groupHiliteColor of me to the groupHiliteColor of me
    setStyle
end resetProps

getProp placeholderText
    if the placeholderText of me is empty then set the placeholderText of me to "Type or Select..."
    return the placeholderText of me
end placeholderText

setProp placeholderText pText
    checkForIDs
    local tText
    put the placeholderText of me into tText
    set the placeholderText of me to pText
    if the text of control id sFieldID is tText then set the text of control id sFieldID to pText
    setStyle
end placeholderText

getProp textContent
    checkForIDs
    if fieldIsEmpty() then
        return empty
    else
        return the text of control id sFieldID
    end if
end textContent

setProp textContent pText
    checkForIDs
    if fieldIsEmpty() then
        set the textContent of me to empty
    else
        set the textContent of me to pText
    end if
    
    if pText is empty then
        set the text of control id sFieldID to the  placeholderText of me
    else
        set the text of control id sFieldID to pText
    end if
    setStyle
    dispatch "comboAction" with the long id of me, the textContent of me
end textContent

getProp listContent
    checkForIDs
    if the listContent of me is empty then set the listcontent of me to "Choice 1" & return & "Choice 2" & return & "Choice 3"
    return the text of control id sDropdownID
end listContent

setProp listContent pList
    checkForIDs
    set the text of control id sDropdownID to pList
end listContent

getProp placeholderColor
    if the placeholderColor of me is empty then set the placeholderColor of me to "180,180,180"
    return the placeholderColor of me 
end placeholderColor

setProp placeholderColor pColor
    set the placeholderColor of me to pColor
    setStyle
end placeholderColor

getProp normalColor
    if the normalColor of me is empty then set the normalColor of me to "66,66,66"
    return the normalColor of me 
end normalColor

setProp normalColor pColor
    set the normalColor of me to pColor
    setStyle
end normalColor

getProp placeholderStyle
    if the placeholderStyle of me is empty then set the placeholderStyle of me to "italic"
    return the placeholderStyle of me
end placeholderStyle

setProp placeholderStyle pStyle
    set the placeholderStyle of me to pStyle
    setStyle
end placeholderStyle

getProp normalStyle
    if the normalStyle of me is empty then set the normalStyle of me to "Plain"
    return the normalStyle of me
end normalStyle

setProp normalStyle pStyle
    set the normalStyle of me to pStyle
    setStyle
end normalStyle

getProp groupColor 
    if the groupColor of me is empty then set the groupColor of me to "255,255,255"
    return the groupColor of me
end groupColor

setProp groupColor pColor
    checkForIDs
    set the backgroundColor of control id sFieldID to pColor
    set the backgroundColor of control id sDropdownID to pColor
    set the groupColor of me to pColor
end groupColor

getProp groupHiliteColor
    if the groupHiliteColor of me is empty then set the groupHiliteColor of me to "173,204,250"
    return the groupHiliteColor of me
end groupHiliteColor

setProp groupHiliteColor pColor
    checkForIDs
    set the hiliteColor of control id sFieldID to pColor
    set the hiliteColor of control id sDropdownID to pColor
    set the groupHiliteColor of me to pColor
end groupHiliteColor

getProp fieldTextSize
    if the fieldTextSize of me is empty then set the fieldTextSize of me to 13
    return the fieldTextSize of me
end fieldTextSize

setProp fieldTextSize pSize
    checkForIDs
    set the textSize of control id sFieldID to pSize
    set the textSize of control id sDropdownID to pSize -1
    set the fieldTextSize of me to pSize
end fieldTextSize

getProp skComboBox
    return true
end skComboBox
#</
Many thanks
Stam

Re: Field in a group receiving focus without having focus... ?

Posted: Wed Apr 03, 2024 6:20 pm
by jacque
Does the group have backgroundBehavior set to true? Also what's the status of navigationArrows?

Re: Field in a group receiving focus without having focus... ?

Posted: Wed Apr 03, 2024 6:51 pm
by stam
jacque wrote:
Wed Apr 03, 2024 6:20 pm
Does the group have backgroundBehavior set to true? Also what's the status of navigationArrows?
Hi Jacque

BackgroundBehavior: no the group isn't a background

NavigationArrows: as far as I know, these navigate from card to card. Which is of no relevance here (the up and down arrows move the hilitedLine in a listField)

The issue here is that even after clicking on the card to remove focus from the field in the group, the group's rawKeyDown handler still reports the focused object is said field, even though it has no active cursor and no focus ring.
And since the field is still somehow the focusedObject, the rawKeyDown is still processed (i.e. the listField's hiltedLine moves up/down, and hitting enter put's it's value in a 1-line field, which always has he focus when the group is active).

Clicking a 2nd time on the card stops this weird behavior.
In the end I handled it by explicitly focusing on nothing in the group's exitField and closeField handlers.

But in my mind this should not have happened at all and I don't understand it.
Seems like a bug... but it's so weird it's hard to be sure.
I mean he script is fairly complex and it's possible I've done something that triggers this - but in that case why does it go away on click the card a 2nd time?

If you wish to test yourself, the last 2 downloads available on the linked thread (URL above) show the issue and then the fix with focus on nothing

Re: Field in a group receiving focus without having focus... ?

Posted: Thu Apr 04, 2024 11:02 pm
by jacque
Okay, I took a look. It gets pretty snarly in there and the message flow is hard to track.

The focused object updates on either exitField or closeField. When a selection is made in the dropdown and text is placed into the prompt field, the dropdown field closes and the prompt field triggers an openField message. Clicking outside the group sends an exitField from the prompt field, but the exitField handler sets the text of the prompt field (the textContent) and selects the text (setStyle), so focus goes back to the group.

I could fix that by changing the second command in the exitField handler to this:

Code: Select all

if (the mouseloc is within the rect of control id sFieldID) or (the optionKey is down) \
        or (the visible of control id sDropdownID is false) then pass exitField
Passing exitField before any other text changes happen seems to bypass the problem.There are probably other ways to fix it, maybe locking messages when updating the prompt field would work. Or don't select the prompt field when putting text into it, since a selection creates focus. Or just keep the "focus on nothing" as-is, which avoids the whole thing entirely.

Re: Field in a group receiving focus without having focus... ?

Posted: Thu Apr 04, 2024 11:07 pm
by stam
Thanks Jacque
I'll have to have a look at the message flow when I'm more awake, but you are undoubtedly correct.
Having said that, it's fixed with just adding 'focus on nothing', so I'll leave that be for now.
If the error is my code (as usual), then at least there's no weird bug ;)