Cross-platform windows.onFocusChanged issues - How to check if no windows have focus? How to detect a minimize event?

According to the windows.onFocusChanged event documentation, it returns either the focused window’s ID, or -1 (windows.WINDOW_ID_NONE).

Will be windows.WINDOW_ID_NONE if all browser windows have lost focus.

But…

In Windows and some Linux window managers, WINDOW_ID_NONE will always be sent immediately preceding a switch from one browser window to another.

meaning on some (most) OSes (e.g. Windows), a focus switch between windows fires this event twice, one returning -1 and the other a windowId. While on others (e.g. MacOS), only one is fired, returning windowId. And I suppose all OSes fire just one, returning -1, when no window has focus.

Given the need to check for these twin simultaneous events, what is the best way to detect each of these conditions on any platform?

  • A browser window is minimized
  • No browser window has focus
  • Both at once

Maybe you could ignore the callback values and query fresh data from the API.
I use something like this, but for a different use-case.

const allWindowns = (await browser.windows.getAll()).filter(w => w.type === browser.windows.WindowType.NORMAL);
const minimized = allWindowns.filter(w => w.state === browser.windows.WindowState.MINIMIZED);

Thanks @juraj.masiar for that idea. I’ve come up with something using windows.getLastFocused() and manual tracking of focus instead:

(For brevity, browser.windows.WindowState.MINIMIZED = 'minimized' and browser.windows.WINDOW_ID_NONE = -1)

// Manually track which window has focus.
// This is how we find out which window was defocused and whether it was/is minimized.
const FocusedWindowId = {
    set: focusedWindowId => browser.storage.session.set({ focusedWindowId }),
    get: async () => (await browser.storage.session.get('focusedWindowId')).focusedWindowId,
}

function handleDefocusedWindow(window) {
  console.log(`windowId ${window.id} defocused ${window.state === 'minimized' ? 'and minimized' : ''}`);
}

browser.windows.onFocusChanged.addListener(async focusedWindowId => {

    // focusedWindowId is -1 when a window loses focus in Windows and some Linux window managers, or only when no window has focus in MacOS and others.
    // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/windows/onFocusChanged
    // Hence when a window switch happens, this event is fired twice in Windows et al: from the defocused window (-1) and from the focused window.
    if (focusedWindowId === -1) {
        const lastFocusedWindow = await browser.windows.getLastFocused();
        if (lastFocusedWindow.focused) {
            // We are in Windows et al - the counterpart event capture will handle the focused window separately
        } else {
            handleDefocusedWindow(lastFocusedWindow);
            FocusedWindowId.set(null);
            console.log('No window is focused');
        }
        return;
    }

    const defocusedWindowId = await FocusedWindowId.get();
    const defocusedWindow = defocusedWindowId && await browser.windows.get(defocusedWindowId).catch(() => null);
    if (defocusedWindow)
        handleDefocusedWindow(defocusedWindow);

    FocusedWindowId.set(focusedWindowId);
});

If you see any issues let me know!

1 Like

Looks interesting :slight_smile:.
The only issue I see is the persistent storage browser.storage.local. Why not store it in memory to avoid IO operation?

If you are targeting Firefox 115 and above (Firefox ESR 115 is out in 2 weeks), you can use browser.storage.session:

I didn’t know storage.session is now available! Thanks!