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

Add focus stealing prevention to MinimalWindowManager #3693

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

tarek-y-ismail
Copy link
Contributor

@tarek-y-ismail tarek-y-ismail commented Dec 4, 2024

Closes #2586.

Adds a new constructor to FloatingWindowManagerPolicy to control focus stealing prevention. This in a nutshell stops new windows from being focused and raised. When used with xdg-activation-v1, this improves security as external actors can't just steal focus by opening a new window (in addition to the niceties xdg-activation-v1 adds to usability).

TODO:

Copy link
Collaborator

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not what I expected. I expected a miral API to "opt in" to "focus stealing prevention" [FSP].

This demonstrates that there's already a way to "opt in" to FSP by writing a customise window management policy, but isn't what I expected (a new constructor to MinimalWindowManager)

examples/example-server-lib/floating_window_manager.h Outdated Show resolved Hide resolved
examples/example-server-lib/floating_window_manager.cpp Outdated Show resolved Hide resolved
Copy link
Collaborator

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's more like what I was expecting. One question and several nits.

On a API design note: bool parameters tend to make code hard to read:

    // unreadable
    foo(..., false, ...);

Consider using an enum class:

    // readable
    foo(..., FocusStealing::allow, ...);

That is written assuming something like

    enum class FocusStealing
    {
        allow,
        prevent
    };

src/miral/minimal_window_manager.cpp Outdated Show resolved Hide resolved
include/miral/miral/minimal_window_manager.h Outdated Show resolved Hide resolved
examples/miral-shell/shell_main.cpp Outdated Show resolved Hide resolved
src/miral/minimal_window_manager.cpp Outdated Show resolved Hide resolved
src/miral/basic_window_manager.cpp Outdated Show resolved Hide resolved
@tarek-y-ismail tarek-y-ismail changed the title Add focus stealing prevention to FloatingWindowManagerPolicy Add focus stealing prevention to MinimalWindowManager Dec 5, 2024
Copy link
Collaborator

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API looks good. But I'm not sure the implementation is quite there

Comment on lines 302 to 315
// If focus stealing prevention is on, swap the old focused window (now in
// the back) with the new window in the front.
// If it's a legitimate window, it'll be focused and raised via
// xdg-activation.
if ((self->focus_stealing == FocusStealing::prevent) && tools.active_window())
tools.swap_tree_order(tools.active_window(), window_info.window());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced that this is exactly right.

  1. Consider there being a fullscreen window with focus when a notification window is created. I don't expect the notification to be activated (as it shouldn't get focus), but it should be above the fullscreen window. Trying to push it "below" seems wrong (even if it ultimately fails because of an implementation detail: the windows are not in the same layer);
  2. Consider an application that creates a new window, e.g. a menu. This should go on top without needing activation. Here the call fails with log_error("Unable to swap tree order due to overlap"); because BasicWindowManager checks for an overlap between the trees. Spamming the log with errors is a bad idea;
  3. In addition to the SurfaceStack, application_selector is maintaining a stack of windows. I don't see this swap being notified (it will still have the new window on top). Having inconsistencies between the two stacks will surely lead to problems.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On 1., what is "a notification window"? How would we distinguish it from any other surface?

My original bug report about stealing did mention notifications, because that's how it affected me. But notifications should never be toplevels, and we can't be expected to treat them differently to any other toplevel.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can't be expected to treat them differently to any other toplevel

This code is handling ALL windows, not just toplevels

Copy link
Contributor Author

@tarek-y-ismail tarek-y-ismail Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For 1., looking at window types:

    mir_window_type_normal,       /**< AKA "regular"                       */
    mir_window_type_utility,      /**< AKA "floating"                      */
    mir_window_type_dialog,
    mir_window_type_gloss,
    mir_window_type_freestyle,
    mir_window_type_menu,
    mir_window_type_inputmethod,  /**< AKA "OSK" or handwriting etc.       */
    mir_window_type_satellite,    /**< AKA "toolbox"/"toolbar"             */
    mir_window_type_tip,          /**< AKA "tooltip"                       */
    mir_window_type_decoration,

Maybe we can only swap mir_window_type_{normal,utility}? This would also address point 2 since menus won't get swapped automatically then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For 3., the first thing that comes to mind is overloading advise_new_window to sync the two stacks together

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I proposes that the windows that should be swapped are mir_window_type_normal, in the "application" layer, and parentless; anything else can be left on top

Copy link
Contributor Author

@tarek-y-ismail tarek-y-ismail Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like some wayland applications use mir_window_type_freestyle as well.

Edit: A lot of wayland applications. Including firefox, gnome-terminal, weston-terminal, qterminal, vlc

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK: in the "application" layer, and parentless

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I relaxed the condition and tried around with different applications and things seem to work as expected.

examples/miral-shell/shell_main.cpp Outdated Show resolved Hide resolved
@tarek-y-ismail
Copy link
Contributor Author

tarek-y-ismail commented Dec 5, 2024

@Saviq @AlanGriffiths Window offsetting might be a bit more involved?

The simplest thing I have in mind is making sure that when a window is placed, it shouldn't be completely occluding the windows beneath it in the surface stack. In the case of too many windows underneath, or just a really big window being opened, we can give up and occlude them. This would require some changes to SurfaceStack though.

What do you think? Should we spin it off into its own thing?

@Saviq
Copy link
Collaborator

Saviq commented Dec 5, 2024

What do you think? Should we spin it off into its own thing?

Yes.

There's something that dawned on me… does e.g. Firefox activate its own new windows on e.g. Ctrl+N, or do we need an exception here for new windows of the currently active client? Which would also address any children opening?

Add constructor to `FloatingWindowManagerPolicy` and
`MinimalWindowManager` to control focus stealing prevention.

For properly behaving applications, xdg-activation-v1 should take care
of focusing and raising any subsequent children.
Only applies to windows in the application layer that don't have a
parent.
@tarek-y-ismail tarek-y-ismail force-pushed the control-new-window-placement-in-policies branch from 2f48609 to 9002657 Compare December 6, 2024 08:50
@tarek-y-ismail
Copy link
Contributor Author

Firefox activate its own new windows on e.g. Ctrl+N

Nope. Only the first (mother?) instance activates. Ctrl+N opens a new window, but in the background.

Copy link
Collaborator

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks reasonable. Not done much experimenting with it as I'm not entirely sure what the expected behaviour is. Will leave merging to @Saviq

@Saviq
Copy link
Collaborator

Saviq commented Dec 6, 2024

Nope. Only the first (mother?) instance activates. Ctrl+N opens a new window, but in the background.

That makes sense. And means the stealing prevention logic should only act when the new surface is from a client different than the active one.

@AlanGriffiths
Copy link
Collaborator

the stealing prevention logic should only act when the new surface is from a client different than the active one

+1

@tarek-y-ismail
Copy link
Contributor Author

tarek-y-ismail commented Dec 6, 2024

Some bugs I found while trying to fix alt+tab/grave:

  1. qterminal (or Mir) seems to have a bug where Window::application is not the same between two different instances? This isn't the case with other terminals/applications. So it's likely to be a qterminal bug.

  2. There's a bug where if you spawn an app from a terminal, switch to the newly spawned app, and press alt+grave, instead of doing nothing because you only have one instance of the app, you'll switch to the terminal again. This happens with (gnome-terminal + xeyes, gnome-terminal+weston-terminal, qterminal+vlc)

    Debugging showed that it->application() and (*originally_selected_it).application() both have the same value, even with one instance of two different apps.

    if (it->application() == (*originally_selected_it).application() && tools.can_select_window(*it))

As for the alt+tab hackaround, this was just to validate my hypothesis of what caused the bug (FSP'd surfaces not being pushed onto mru_active_windows because they're not immediately activated), I'm not sure how to improve on it but I'll let it simmer in the background during the weekend

Copy link
Collaborator

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qterminal (or Mir) seems to have a bug where Window::application is not the same between two different instances? This isn't the case with other terminals/applications. So it's likely to be a qterminal bug.

I don't think it is a bug: if you start two qterminal instances, you get two programs running each of which independently connects to the server, so the server sees two different applications.

If you start two gnome-terminal instances you get one /usr/bin/gnome-terminal.real program running which connect to the server once and opens two windows. So the server sees one application. (cat $(which gnome-terminal) for details)

src/miral/application_selector.cpp Outdated Show resolved Hide resolved
src/miral/mru_window_list.h Outdated Show resolved Hide resolved
examples/miral-shell/shell_main.cpp Outdated Show resolved Hide resolved
@@ -204,6 +213,7 @@ auto ApplicationSelector::advance(bool reverse, bool within_app) -> Window
auto it = find(selected);

std::optional<Window> next_window = std::nullopt;
auto should_continue = true;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty much the same logic, but I think this is a bit clearer. Not sure if I should revert or keep these changes..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the logic could be clearer, but I don't think having to understand both next_window.operator bool() and should_continue is an improvement.

Also, is there an intentional change to the logic? Or should it be the same? ("Pretty much the same logic" doesn't make that clear)

Comment on lines 199 to 208

// Need to push onto the MRU list for alt+tab to work correctly.
//
// Originally, applications were pushed only when selected, but due to
// focus stealing prevention, they were not activated and thus weren't
// selected nor added to mru_active_windows, breaking alt+tab
auto window = info_for(surface).window();
auto const top = mru_active_windows.top();
if(top && top != window)
mru_active_windows.push_unfocused(window);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure this is a tad too hacky. Suggestions welcome :)

src/miral/minimal_window_manager.cpp Outdated Show resolved Hide resolved
src/miral/minimal_window_manager.cpp Outdated Show resolved Hide resolved
@tarek-y-ismail tarek-y-ismail marked this pull request as ready for review December 11, 2024 17:04
@tarek-y-ismail tarek-y-ismail requested a review from a team as a code owner December 11, 2024 17:04
Copy link
Collaborator

@Saviq Saviq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure what the expected behaviour is.

  1. miral-shell --focus-stealing-prevention 1
  2. run gnome-terminal
  3. gedit &
  4. mate-terminal
  5. go back to the terminal
  6. gedit

Because gedit supports activation, it should be activated each time (as described by #3639). But because mate-terminal doesn't, it doesn't get activated.

So +1 from me on the behaviour.

Just a nit on the command line option.

examples/miral-shell/shell_main.cpp Outdated Show resolved Hide resolved
@tarek-y-ismail tarek-y-ismail force-pushed the control-new-window-placement-in-policies branch from fb7c7fa to 40c93fe Compare December 18, 2024 14:22
Copy link
Collaborator

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO list doesn't look "ready", does it need updating?

@@ -204,6 +213,7 @@ auto ApplicationSelector::advance(bool reverse, bool within_app) -> Window
auto it = find(selected);

std::optional<Window> next_window = std::nullopt;
auto should_continue = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the logic could be clearer, but I don't think having to understand both next_window.operator bool() and should_continue is an improvement.

Also, is there an intentional change to the logic? Or should it be the same? ("Pretty much the same logic" doesn't make that clear)

Comment on lines 305 to 307
auto in_background = self->prevent_focus_stealing(window_info);

self->application_selector.advise_new_window(window_info, in_background);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. It looks odd to call a function on self to get a value passed to a function on self (shouldn't all this logic be in self?); and,
  2. in_background isn't the clearest name (as evidenced by the long comment explaining it) and could be const

bool miral::MinimalWindowManager::Impl::prevent_focus_stealing(miral::WindowInfo const& info)
{
auto const normal_app_without_parent = (info.depth_layer() == mir_depth_layer_application && !info.parent());
auto const decoration_surface = info.type() == mir_window_type_decoration;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we checking for decorations? Won't that mean that decorations will be raised and focussed regardless of parents and layer? (Which seems wrong)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far, I only see decorations attached to parent windows, so we don't need to check for that. But your comment about the layer is true.

@Saviq
Copy link
Collaborator

Saviq commented Dec 18, 2024

@tarek-y-ismail don't add the launch_app_env here, make a separate PR based on this one, instead.

@tarek-y-ismail tarek-y-ismail force-pushed the control-new-window-placement-in-policies branch from aef6758 to 10ef673 Compare December 18, 2024 17:34
@tarek-y-ismail tarek-y-ismail force-pushed the control-new-window-placement-in-policies branch from 10ef673 to 6d7de20 Compare December 18, 2024 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement focus stealing prevention
3 participants