sublimehq / sublime_text

Issue tracker for Sublime Text
https://www.sublimetext.com
807 stars 39 forks source link

(De)-activation APIs don't track when focus is lost/regained to/from the status bar #5623

Closed eugenesvk closed 1 year ago

eugenesvk commented 1 year ago

Description of the bug

The on_deactivated/on_activated APIs and their async friends don't track loss/gain of focus when you click on a menu in the status bar even though the input focus is lost — typing gets redirected to said status bar menu

Steps to reproduce

  1. Start ST in safe mode
  2. Save this snippet in Packages/User (in the safe mode folder)
    import sublime_plugin, os
    class TestInputFocusEvents(sublime_plugin.ViewEventListener):
    def on_deactivated_async(self):
    print('deactivated')
  3. Open ST's console
  4. Open any status bar menu and look at console

Expected behavior

(De)-activated APIs should track all input focus loss/gain events, so opening a status bar menu, which moves input focus from current view, should print deactivated

Actual behavior

No deactivated printed

Sublime Text build number

4138

Operating system & version

macOS 10.15

(Linux) Desktop environment and/or window manager

No response

Additional information

No response

OpenGL context information

No response

BenjaminSchaaf commented 1 year ago

Context menus - including those on the status bar - don't steal focus. Note how when exiting the menu the text area is still focused. This is working as intended.

eugenesvk commented 1 year ago

don't steal focus

of course they do! The issue is not which area is focused AFTER the menu is closed (why would it matter?), but when it's OPEN, the whole point of input focus per wiki

Text entered at the keyboard or pasted from a clipboard is sent to the component which has the focus.

That's exactly what's going on — opening the status bar menu steals focus from the buffer, so the text input is sent to the menu, not the buffer! Yes, after the menu is closed, the buffer receives the text input again, but that's called regaining a focus (and should be tracked by the on_activated API), it doesn't invalidate the fact that input focus was lost! The only difference the menus have vs. some command palette is that they don't move the cursor indicator (which, come to think of it, is also a bug — visible text cursor implies the ability to input text at that position, but it's not possible since input focus is lost and you can't input any text at the cursor position!)

This leads to various issues like inability to efficiently track that tab/line ending settings were changed from the status bar menu (on_activated doesn't track input focus properly, so you'd have to resort to APIs that fire on every keystroke)

BenjaminSchaaf commented 1 year ago

Context menus are special and always have been. Take for instance this input field on github, it shows a caret and a blue outline but only when focused. Notice how it doesn't go away when you right-click on the input field? This is how all GUI software works.

eugenesvk commented 1 year ago

how all GUI software works

Oh, c'mon, your own app doesn't work in such a simplistic fashion

Yes, if I have a non-blinking caret, I lose the last indicator that differentiates between active input focus and lack thereof — but that's simply an oversight, not some indicator of how GUI menus are special, you could still dim the cursor to indicate loss of focus and maintain all the other special behavior

But it's also besides the point: my main issue here is not the behavior of the GUI part, but of the programming API, so I'd like to be able to detect what you're detecting when you make the cursor stop blinking — that the current cursor has lost input focus!

Regardless of how the GUI indicates it, this is a simple fact that your activate/deactivate don't track activation/deactivation of input focus despite the fact that your docs state otherwise

on_activated(view: View) Called when a view gains input focus.

BenjaminSchaaf commented 1 year ago

why is that? Is it maybe to indicate to the user that he can't input text any more where he could do that a moment ago?

This is behavior specific to macOS; it doesn't pass animation events to modal parents. The cursor does continue to blink on Linux for instance.

But it's also besides the point: my main issue here is not the behavior of the GUI part, but of the programming API, so I'd like to be able to detect what you're detecting when you make the cursor stop blinking — that the current cursor has lost input focus!

[NSWindowDelegate windowDidResignKey] - the cocoa callback for when the window loses focus - isn't called when a context menu is opened. Deactivate isn't called because it isn't deactivated. This is how GUI applications work.

If you want a callback for when a context menu is opened I'd suggest filing a separate feature request. Although I question the utility, if it did exist it would have to be a separate callback.

rchl commented 1 year ago

This leads to various issues like inability to efficiently track that tab/line ending settings were changed from the status bar menu (on_activated doesn't track input focus properly, so you'd have to resort to APIs that fire on every keystroke)

This sounds like it would be a very inefficient, error-prone and "brute-forcy" way of solving your need anyway. Maybe you should be asking for a way to get notified on view settings changing instead so that it can be handled in a comprehensive and efficient way. (I'm not sure if the current settings change listener can handle view settings...)

jfcherng commented 1 year ago

I'm not sure if the current settings change listener can handle view settings...

Yes, it does.

eugenesvk commented 1 year ago

This is behavior specific to macOS; it doesn't pass animation events to modal parents. The cursor does continue to blink on Linux for instance.

Ok, so once again Mac 1, Linux 0 on the GUI design front? :)

[NSWindowDelegate windowDidResignKey] - the cocoa callback for when the window loses focus - isn't called when a context menu is opened. Deactivate isn't called because it isn't deactivated. This is how GUI applications work.

Hm, but the thing that loses input text focus is the input text field (let's call it iTextField) part of the window, not the window itself, no? Are you saying that there within both the cocoa/SwiftUI API systems you:

If you want a callback for when a context menu is opened I'd suggest filing a separate feature request. Although I question the utility, if it did exist it would have to be a separate callback.

Not really, I also don't know the utility of tracking them, it's just that when I was trying to track input focus to update information about the newly focused view I've read your docs, chose the on_activated API and then noticed that it doesn't track all activations when the context menus steal input focus So if you match the input focus definiton to your docs, this is the appropriate issue. If you insist otherwise, please at least update your docs to say that not all input focus gains/losses are tracked

P.S. I've also had some other issues with the lack of an ability to track input focus in the past, but it was too long ago I don't remember what they were :)

eugenesvk commented 1 year ago

This sounds like it would be a very inefficient, error-prone and "brute-forcy" way of solving your need anyway. Maybe you should be asking for a way to get notified on view settings changing instead so that it can be handled in a comprehensive and efficient way. (I'm not sure if the current settings change listener can handle view settings...)

But the "settings change listener" is exactly that API :):

While the focus gains are much more rare, so it's conceptually more efficient (though maybe not practically); and the only error I've noticed so far is when the docs don't match the API behavior on input focus, hence this bug report :)

P.S. Though the most recent B of the AB problem would best be solved if the status bar supported advanced templating, then I wouldn't even need to calculate any values myself, only do a few string replaces to change the text/format the values I'd take from what ST already provides :) Though my guess is we're unlikely to ever reach that advanced level of customization, so hacking some reformatting in python will always win

rchl commented 1 year ago

But the "settings change listener" is exactly that API :):

* inefficient: multiple-times-per-every-keystroke

I suppose it's not really a discussion for this issue but if you had multiple view settings change notifications on each keystroke then it sounds like you have some plugin that does trigger that and it could likely be optimized. It shouldn't happen by default.

eugenesvk commented 1 year ago

I suppose it's not really a discussion for this issue but if you had multiple view settings change notifications on each keystroke then it sounds like you have some plugin that does trigger that and it could likely be optimized. It shouldn't happen by default.

Good point! Unfortunately, it add a dependency on the world being efficient and of course it's not :( For example, I've dived into this a bit and found the culprit — and of course this is one of the most popular (top25) plugins! And of course there is already a 5 years old issue opened there! And of course I like some functionality from the plugin (e.g., highlighting quotes and changing highlight style; otherwise I'd just remove it and use the default ST's functionality)

ST's safe mode seems to behave ok, it only indulges in the per-every-keystroke sin when you type in the first line of a new buffer (this changes the tab name and the tab name is part of the settings). (then the only remaining issue would be the buffer properties that aren't view settings)

BenjaminSchaaf commented 1 year ago

Hm, but the thing that loses input text focus is the input text field (let's call it iTextField) part of the window, not the window itself, no?

That's not generally how it works, no. A context menu is a new modal window, modal meaning that it effectively blocks the main application while showing (Context menus are again special and actually block the entire window manager, not just the window). As such the primary window doesn't lose focus, instead focus is effectively layered on top of the application.

eugenesvk commented 1 year ago

As such the primary window doesn't lose focus, instead focus is effectively layered on top of the application

That's exactly the loss of input focus! It's impossible (by definition)

Context menus are special and always have been. Take for instance this input field on github, it shows a caret and a blue outline but only when focused. Notice how it doesn't go away when you right-click on the input field? This is how all GUI software works.

...am glad I found that this broken-by-definition behavior is not how all GUI software works Started with a comparison of some basic apps :)

↓App / Open→ right click menu drop-down top menu window menu²
WordPad Main c−¹    c−    c+ b−   
WordPad Find c+¹ b− h+ c− h− c−³    h−
LibreOffice Main c−¹    c−    c+ b−   
LibreOffice Find c−    c−    c+ b−   
SublimeText Main c+ b+   c+ b+    c+ b+   
SublimeText Find c+ b+   c+ b+    c+ b+   

¹ WordPad/LibreOffice's right click also moves he cursor in the Main window, and its invisible position is indicated by the closest-to-text corner of the context menu box. However, in a Find menu it doesn't move the cursor, so it makes sense to keep showing the caret (non-blinking) to indicate the location of a potential paste chosen from the menu (though LibreOffice still hides it) ² Window's header's menu that contains restore/move/size etc window commands ³ but this shifts input focus to the main window, where caret becomes visible, though non blinking

So some GUI software is actually not broken re. tracking of input focus and doesn't confuse users with a blinking cursor in a place where not keyboard input is possible, and some even hides the cursor altogether on loss of input focus!

Don't know about all the APIs in the zoo of all the GUI frameworks and whether/how they allow an app to track all this, but at least this quick test seems to invalidate your argument that everything is broken :)

P.S.

This is behavior specific to macOS; it doesn't pass animation events to modal parents. The cursor does continue to blink on Linux for instance.

Hm, but right click context menu isn't a modal since it doesn't block the main app window Also, Mac does pass animation events to the browser, in a Safari/Chromium empty box the cursor continues to blink with a right click context menu opened (in a non-empty box right click forces text selection, so there is no cursor)

BenjaminSchaaf commented 1 year ago

That's exactly the loss of input focus! It's impossible (by definition)

It's not impossible. It's generally how widgets work. If you have a text area inside of a window and you focus the text area the window is still focused, but not all events are passed up to the window. It's of course more complicated than that, but input focus is never just "this one specific thing has focus and nothing else".

...am glad I found that this broken-by-definition behavior is not how all GUI software works

I wasn't referring to the specific caret behavior, but that the context menu nests input focus instead of stealing it. The specific caret behavior is an observable behavior that proves that the window still has focus. It's more subtle but most window managers slightly gray windows when they're not focused and you'll see that that doesn't happen when you open a context menu. Because the window still has focus.

Hm, but right click context menu isn't a modal since it doesn't block the main app window

It is modal. The way this is usually implemented is through a nested (modal) event loop, where some events are still passed to the parent window. Some UI frameworks don't nest modal loops anymore instead faking the same behavior through other methods. They still call it modal. The simple fact remains that the parent window is still focused.

eugenesvk commented 1 year ago

It's not impossible. It's generally how widgets work.

I think you bringing up window APIs is what's let us astray — since there are input focus changes:

That's what I meant when I was trying to come up with an explanation for the Mac API behavior you mentioned

"Hm, but the thing that loses input text focus is the input text field (let's call it iTextField) part of the window, not the window itself, no?"

Which is the same as "you focus the text area [inside of a window] the window is still focused"

But if you insist on a strict separation, then yes, it is impossible for the window to have input focus if it doesn't receive any input, it is the definition of input focus

So

input focus is never just "this one specific thing has focus and nothing else".

is not what I said, it's not about "nothig else", it's about some specific thing that does NOT receive input, which then by definiton has no input focus. If some specific thing does receive input, and then it has focus even if some other thing also receives focus

I wasn't referring to the specific caret behavior, but that the context menu nests input focus instead of stealing it.

That's a similar of underspecification, if we make it just a bit more specific:

The specific caret behavior is an observable behavior that proves that the window still has focus.

But the topic of this issue is not a Window, but a View, i.e. the main text area, where the proof is the opposite — said text area does lose input focus, which leads to a caret indication changing to reflect this significant event to the user (although not in all the GUI frameworks/apps)

Similarly,

It is modal.

Not by the very definiton you provided (which is correct, the key differentiating feature of a modal window vs a palette window is that the former blocks the main window while the latter doesn't). If you blur this definitional divide, then yeah, you can call a window that does not block anything a modal window

BenjaminSchaaf commented 1 year ago

is not what I said, it's not about "nothig else", it's about some specific thing that does NOT receive input, which then by definiton has no input focus. If some specific thing does receive input, and then it has focus even if some other thing also receives focus

What you're calling input focus is based on the behavior you're seeing, not based on how GUIs actually work. The text view is in a "focused" state, because it's in the hierarchy of focused elements, so it has input focus. The distinction is mostly irrelevant for someone using software, but not for writing it (or writing plugins for it).

But the topic of this issue is not a Window, but a View, i.e. the main text area, where the proof is the opposite

Your "proof" at best shows that there is a state where an widget has input focus, but isn't the last element in the focus hierarchy. Which is trivially true given that a focus hierarchy exists.

It is modal. Not by the very definiton you provided

I said "effectively blocking". This has already taken up enough of my time and I don't see a point continuing this discussion.

eugenesvk commented 1 year ago

What you're calling input focus is based on the behavior you're seeing, not based on how GUIs actually work

For a Graphical UI "seeing" is about actually working Also, if it's not based on how GUIs actually work, how come one of the official Windows GUI frameworks (WPF) is actually working the way I'm calling it?

Check out this tiny sample app from a C# WPF input focus repo or this quick demonstration video: I'm using MS input focus API (appropriately named GotKeyboardFocus/LostKeyboardFocus) like so https://github.com/eugenesvk/WPF_InputFocus/blob/main/MainWindow.xaml#L29 and get callbacks when the text box loses input focus to a right-click context menu or the top window menu (and when it gains input focus back when either the context menu or the window menu is closed) (in the video, yellow background means keyboard focus orange borders mean only logical focus (keyboard focus seems to also override the borders to blue, so when an text box has both, you don't see the orange border) at the bottom of the top text box you see the status of both logical and input focus which is updated if either of these two change)

That's exactly what I expect from an input focus tracking API — whenever an element gains/loses input focus, you get a callback! So the GUI "actually" working matches the concept based on the behavior I'm seeing.

More details on what MS understand by (keyboard) input focus in here, by the way

There can be only one element on the whole desktop that has keyboard focus

so much for

input focus is never just "this one specific thing has focus and nothing else".

The text view is in a "focused" state...

I'm worried that this is again an issue of you using imprecise terminology. Per the WPF example above the text view only has keyboard input focus when it receives keyboard input.

, because it's in the hierarchy of focused elements

Maybe this WPF API IsKeyboardFocusWithin is closer to what you mean?

Your "proof" at best shows that there is a state where an widget has input focus, but isn't the last element in the focus hierarchy. Which is trivially true given that a focus hierarchy exists.

Nope, at best it shows what I've demonstrated in the WPF example above — that a widget has no input focus as indicated by the its false IsKeyboardFocused property on the API side and a non-blinking caret behavior on the visual side. In WPF there is also logical focus that tracks the latest element that had input focus within a focus group, but that's a different concept

I said "effectively blocking".

How can it be effectively blocking when the effect is that the main window is NOT blocked and can react to input without dismissing the "modal"???

This has already taken up enough of my time and I don't see a point continuing this discussion.

The point is still the same — fixing a bug where a text editor doesn't allow you to track input focus changes of its main text area despite having an API that is documented as doing just that. Unfortunately, that requires clearing up some misconceptions about "how all GUI software works"

eugenesvk commented 1 year ago

@BenjaminSchaaf have you had a chance to look at a simple code example illustrating that not all GUI manage input focus in the unintuitive way you've described?