Skip to content

feat: add layout cycling widget and shortcut #345

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

Open
wants to merge 2 commits into
base: v17.0
Choose a base branch
from

Conversation

joj0s
Copy link

@joj0s joj0s commented May 30, 2025

This PR implements feature request #50 , adding a widget which allows users to switch between their defined layouts via keyboard shorcuts.

Possible discussion points:

  • The styling which was implemented is as close as possible to the provided mockup (for a backend dev at least 😅). I used hardcoded hex values and it is not adaptive to user dark/light preference, which I think makes sense given that this is how Gnome's alt-tab widget behaves as well, but it might be preferable to tweak the styling a bit.
  • I tested this with multiple shortcut combinations. Combinations which use the <Super> accelerator were quite iffy (Masking them using Gtk methods and detecting when the user released them) so I resorted to polling for pressed modifiers. If you think there is a better way of doing this feel free to edit this PR!
  • In multiple monitor scenarios, this implementation considers the active monitor - in which to show the widget -, the one with the currently focused window. This logic may also be subject to change

Tested on Gnome 47 and Gnome 48 on Wayland

@domferr
Copy link
Owner

domferr commented May 30, 2025

Oh wow, you made an amazing job! I'm about to review it

export class LayoutSwitcherPopup {
private _popup!: St.Widget;
private _layoutList!: St.BoxLayout;
private _layouts: Layout[];
Copy link
Owner

Choose a reason for hiding this comment

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

It would be better to not save a reference of the layouts outside of the global state. So instead of having this private field, you can always do GlobalState.get().layouts. Now that I see this, I understand that the GlobalState could be better, ensuring it is the only one having the state and having the rights to manage it

Comment on lines +46 to +51
private _getCurrentMonitorIndex(): number {
const focusWindow = global.display.focus_window;
if (focusWindow) return focusWindow.get_monitor();

return Main.layoutManager.primaryIndex;
}
Copy link
Owner

Choose a reason for hiding this comment

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

This is perfect!

import GlobalState from '@utils/globalState';
import Settings from '@settings/settings';
import { St, Clutter, GLib } from '@gi.ext';
import { Gtk } from '@gi.prefs';
Copy link
Owner

Choose a reason for hiding this comment

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

All the imports from @gi.prefs are not allowed in the code driving the extension but only in the code driving the extension's preferences 😢

I put in @gi.prefs everything that can be imported by extension's preferences only and in @gi.ext everything that we are allowed to import in the extension code only. Unfortunately this is a requirement coming from GNOME, they will not publish any update that goes against this (source)

Comment on lines +175 to +198
private _populateLayouts() {
const buttonSize = { width: 142, height: 80 };
const hasGaps = Settings.get_inner_gaps(1).top > 0;
const gapSize = hasGaps ? 2 : 0;

this._layoutButtons = this._layouts.map((layout, ind) => {
const container = new St.Widget({
style_class: 'layout-switcher-item',
});

this._layoutList.add_child(container);

const button = new LayoutButton(
container,
layout,
gapSize,
buttonSize.height,
buttonSize.width,
);

if (ind === this._selectedIndex) button.set_checked(true);
return button;
});
}
Copy link
Owner

Choose a reason for hiding this comment

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

This is perfect!!

Settings.SETTING_CYCLE_LAYOUTS,
);

const switcher = new LayoutSwitcherPopup(keybindings);
Copy link
Owner

Choose a reason for hiding this comment

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

Do you think it is possible to not construct the object every time, but instead, to construct only once and then hide/show when it is needed?

@domferr
Copy link
Owner

domferr commented May 30, 2025

Thank you SO much for your contribution, it is invaluable! I just added comments about very important things, but the overall work is extremely solid, well done!

The styling which was implemented is as close as possible to #50 (comment) (for a backend dev at least 😅). I used hardcoded hex values and it is not adaptive to user dark/light preference, which I think makes sense given that this is how Gnome's alt-tab widget behaves as well, but it might be preferable to tweak the styling a bit.

Yeah, don't worry. I can figure out witch CSS classes they use so we can use them as well. This will immediately enable us to support the GNOME theme the user has!

I tested this with multiple shortcut combinations. Combinations which use the accelerator were quite iffy (Masking them using Gtk methods and detecting when the user released them) so I resorted to polling for pressed modifiers. If you think there is a better way of doing this feel free to edit this PR!

Let me check how GNOME does with their ALT+TAB, I think we can mimic that.

In multiple monitor scenarios, this implementation considers the active monitor - in which to show the widget -, the one with the currently focused window. This logic may also be subject to change

I believe here you picked the best approach!

@domferr domferr changed the base branch from main to v17.0 May 30, 2025 21:19
@domferr
Copy link
Owner

domferr commented May 30, 2025

I see they have class AppSwitcherPopup which extends the class SwitcherPopup. SwitcherPopup basically has everything we need, from keybindings handling to positioning in the middle of the screen

@domferr
Copy link
Owner

domferr commented May 31, 2025

Hello! I made some progress on this, by following your contribution and how GNOME shell does the ALT+TAB one.

Video.del.2025-05-31.17-12-14.webm

I'm going to edit the pull request, so you can have those changes too. Few things:

  • It is possible to go forward with keybinding, as well as forward and backward with arrows keys. I didn't understand how to go backwards with keybinding too
  • I didn't test with GNOME 48 yet

@domferr
Copy link
Owner

domferr commented May 31, 2025

Hmmmm I don't have experience with GitHub and I was not able to edit the PR. I pushed my changes in branch https://github.com/domferr/tilingshell/tree/joj0s/feat/layout-cycling

@joj0s
Copy link
Author

joj0s commented Jun 1, 2025

Hey, first of all no worries, let's pick up from the branch you created. I tested it locally and functionality wise it seems to be working great so far! Just a minor issue where it takes a bit long to detect the release of the keybinding. Would you like me to check out anything in particular and work on that branch?

@domferr
Copy link
Owner

domferr commented Jun 1, 2025

I tested it locally and functionality wise it seems to be working great so far!

Great!

Just a minor issue where it takes a bit long to detect the release of the keybinding.

I confirm that I also have that behaviour. I believe it comes from setting 0 as mask value here. GNOME shell provides the actual mask of the binding (source) by exposing the binding from the keybinding event. However, in my experimentations the binding argument is undefined...

@joj0s
Copy link
Author

joj0s commented Jun 2, 2025

You're right, it does come from the 0 mask value. And yeah, for some reason the binding argument is indeed undefined, but we can get the actual mask value from the Clutter event, like this:

const action = Main.wm.addKeybinding(
            Settings.SETTING_CYCLE_LAYOUTS,
            extensionSettings,
            Meta.KeyBindingFlags.NONE,
            Shell.ActionMode.NORMAL,
            (display: Meta.Display, _, event: Clutter.Event) => {
                const keyBinding = event as any;
                const mask = keyBinding.get_mask();
                this.emit('cycle-layouts', display, action, mask);
            },
        );

I can only push changes to my own fork but feel free to try it out. That seems to solve the release check issue with most keybinding combinations, but again the same problems arise when using combinations with <Super> as the modifier, which is why I originally resorted to polling to check for modifier release.

I am thinking of possibly overriding vfunc_key_release_event to manually detect super key modifiers in this case.

@domferr
Copy link
Owner

domferr commented Jun 3, 2025

Hey @joj0s that's a good catch! I confirm it fixes the issue to me as well, I pushed it in the branch. Can you share some more details about the other problem you are facing?

@joj0s
Copy link
Author

joj0s commented Jun 4, 2025

Huh, nevermind, I had some issues yesterday where if I set up the cycling keybinding to be <Super> + anything like <Super> + 'J', then release check would stop working and I would have to click away from the widget for it to close. Whereas with other keybindings it would work fine.

Re-tested it today, it works and I can't reproduce it anymore. If you also have no issues I'd say it's looking good

@domferr
Copy link
Owner

domferr commented Jun 14, 2025

Hello, sorry for the late reply, I believe this is looking great! I'll close the pull request and merge the branch with our work. Is it ok for you if I cite your github account in release notes?

@joj0s
Copy link
Author

joj0s commented Jun 16, 2025

Happy to hear that!

Is it ok for you if I cite your github account in release notes?

Of course, go right ahead!

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.

2 participants