Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cooperation between focus components #6390

Open
FloVanGH opened this issue Sep 30, 2024 · 12 comments
Open

Cooperation between focus components #6390

FloVanGH opened this issue Sep 30, 2024 · 12 comments
Labels
rfc Request for comments: proposals for changes

Comments

@FloVanGH
Copy link
Member

Currently it is really hard to implement something like an autocomplete box. Where you need an element e.g. LineEdit to enter text to filter your list and to check for down arrow and up arrow key events to navigate between the items in your list.

Example

import { LineEdit, StandardListView, VerticalBox } from "std-widgets.slint";

export component Demo {
    forward-focus: search;

    in property <[StandardListViewItem]> model: [
        { text: "Item 1" },
        { text: "Item 2" },
        { text: "Item 3" },
    ];

    callback filter_model(/* filter_text */ string);

    FocusScope {
        VerticalBox {
            search := LineEdit {
                placeholder-text: "search";

                edited(text) => {
                    root.filter_model(text);
                }
            }

            list := StandardListView {
                current-item: 0;
                model: root.model;
            }
        }

        key-pressed(event) => {
            if event.text == Key.UpArrow {
                list.current-item = max(list.current-item - 1, 0);
                return accept;
            }
            if event.text == Key.DownArrow {
                list.current-item = min(list.current-item + 1, list.model.length - 1);
                return accept;
            }
            reject
        }
    }

The problem is if LineEdit has focus then FocuScope does not have focus and you cannot check if the arrow keys are pressed.

We should start to think and define how a solution for this problem could looks like.

One thing that could be possible is something like Routed Events in WPF, with bubbling events from source to leafs and after that from the leafs back to source. An eventhandler that is part of this path can decided if it wants to handle (accept) the event or to reject it, what means the next event handler has the opportunity to handle the event instead.

@FloVanGH FloVanGH added the rfc Request for comments: proposals for changes label Sep 30, 2024
@tronical
Copy link
Member

As a side-note: The reason why the above code doesn't work is because TextInput (and LineEdit by extension) accepts up/down keys even if it's single line input, instead of ignoring - in which case it would bubble up. UpArrow and DownArrow correspond to moving the cursor to the previous or next line, or start of text / end of text for single line text - as a standard keyboard shortcut.

@ogoffart
Copy link
Member

For the specific case of up and down in a LineEdit, Qt doesn't go to the beginning or end of the line with up or down. Most application have their own function (like up and down in a preselect) Although a <input type="text"> does go to the beinning/end of line.
=>We can continue the discussion in #6393

Also related: #4337

I didn't fully understood routed event. How would it translate in Slint?

Should FocusScope gain new callback such as filter-key-pressed or something like that that is received before children, as suggested in #4337 (comment) ?

@FloVanGH
Copy link
Member Author

FloVanGH commented Sep 30, 2024

I didn't fully understood routed event. How would itranslate in Slint?

The event system in WPF is really complex but powerful. You can work with predefined like key events and mouse events or you could also implement custom events. Then you can trigger events from a node e.g. from a custom controls, that bubbles from this node along the tree to the leafs and tunnels back to the origin node until the event is handled (accepted).

Example

export component EventDemo {
     FocusScope { 
            preview-key-pressed(event) => {
                 if event.text == Key.DownArrow {
                    // return accept to stop bubbling and to not handle additional arrow up in LineEdit
                   return accept;
                }
               // return reject to allow rest events to continue bubbleling
               reject
            }

           key-pressed => {
                // will not reached by events handled by the LineEdit
          }

           LineEdit {}
    }
}

In these example the key pressed event stares to bubble from the window.

@Enyium
Copy link
Contributor

Enyium commented Oct 1, 2024

Let me just quickly reference event capturing in browsers, since nobody seems to have done that yet: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture.

@tronical
Copy link
Member

tronical commented Oct 1, 2024

I think the web's model is likely one of legacy, but oddly the combination of capturing and bubbling still makes sense and would for us, too.

So how would that look like in practice?

  • There are two phases: In the capturing phase, the event travels from the window to the target element. If the target element is not handling the event, it bubbles back up.
  • The travel in both directions happens along the same path.
  • When the target element is not known, the path is determined on-the-fly during the capturing phase (and used in reverse during the bubbling phase).
  • When the target is known (for example dispatch to a focused element), the path is determined by traversing from parent to parent.
  • FocusScope could have capture-key-* callbacks. I think they should be invoked if a boolean capture property is true.
  • TouchArea could have a capture-pointer-event (this resembles our internal input_event_filter_before_children).

@ogoffart
Copy link
Member

ogoffart commented Oct 1, 2024

The capture concept is a bit strange.
For the pointer, we have the concept of "grab" but for the keyboard there is no such concept.

Right now, for example, the line edit intercept the key press event, but not the key release event, as can be shown in this example
And it seems people have used this trick to have shortcut on key release even though something was capturing the events.

For pointer event it is easier to grab, but for the keyboard, there are repetition and many keys. Should there be a grab for each keys or for the whole keyboard?

The travel in both directions happens along the same path

That's hard to maintain as what happens if the focus changes between the two passes (because the "capture" event changes the focus, or changes the tree. Should that be forbidden?

So in other word, I'm not sure capture is the right term.
I like filter- a bit better.

@tronical
Copy link
Member

tronical commented Oct 1, 2024

Note that grab != capture. Capturing is just the name of the phase used in the DOM, it's not related to grabbing.

@tronical
Copy link
Member

tronical commented Oct 1, 2024

The travel in both directions happens along the same path

That's hard to maintain as what happens if the focus changes between the two passes (because the "capture" event changes the focus, or changes the tree. Should that be forbidden?

If the focus event changes the focus somewhere along the way, the entire remaining part of capturing (or bubbling) is aborted anyway, isn't it?

(unless the focus changes but the filter claims that it didn't handle it)

So in other word, I'm not sure capture is the right term. I like filter- a bit better.

We could call it filter in the callbacks, but we need to give a name of this phase or way of propagating events to the target element in our documentation.

@tronical
Copy link
Member

tronical commented Oct 1, 2024

(FWIW, I'm not entirely sold on this, I just wanted to outline how one possible solution could look like for Slint in a manner that we could explain/document and that resembles the web then)

@ogoffart
Copy link
Member

ogoffart commented Oct 1, 2024

What I suggest is:

When a key is pressed, the event first triggers the filter-key-pressed callback, starting from the Window and traversing down through the parent-child hierarchy to the element that has focus. If none of the filter-key-pressed callbacks return accept, the key-pressed callback is invoked, starting from the focused element and going back up the hierarchy to the Window. The process stops as soon as any callback returns accept, and only FocusScope's along the direct parent-child chain of the focused element are involved.

In retrospect, we probably should have had pressed (and released) event processed from parent to child only, from the beginning. But it is too late to change.
We could introduce a new element name (FocusScope2 or FocusScopeV2) but IMHO adding a new callback is as easy (although it takes some more memory, but that the compiler could anyway choose between one of the two FocusScope)

Other name suggestion

  • filter-key-pressed
  • pre-key-pressed
  • capture-key-pressed
  • intercept-key-pressed
  • key-pressed-intercept

@Enyium
Copy link
Contributor

Enyium commented Oct 1, 2024

  • I think "intercept" describes something more out of the ordinary of the system than "capture", like something the system wasn't made for, at least not for regular use.
  • "capture" is well known from the browser framework.
  • When people are tempted to just perform their whole logic (like key-pressed logic) in this early-stage callback (instead of using both the early- and late-stage callback), filter-... may be less fitting than capture-....
    • However, when adhering to best practices of using both the early- and late-stage callback (if wanting more than filtering), filter-... could be preferrable. It could then even be desirable to have the context of the filtering reflected in the name: filter-inner-.... This would make it clear that the element's own late-stage callback isn't affected by the filter.

@tronical
Copy link
Member

tronical commented Oct 2, 2024

I'm also not a fan of the term capture and prefer intercept. Capturing implies the result, while interception leaves it open (as it should be). I think what Olivier describes is pretty much what I had in mind as well (in different words).

In terms of traversal, I think we should determine the path upfront, i.e. from the target focus element go back up forwards (parent, parent.parent, etc.) and collect all the possible interceptors, and then start with the outermost interceptor candidate, and work our way back towards the target focus element.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rfc Request for comments: proposals for changes
Projects
None yet
Development

No branches or pull requests

4 participants