mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
Feat/task widget global shortcut (#7099)
* feat(electron): add global shortcut for task widget toggle Adds a configurable global shortcut (`globalToggleTaskWidget`) that shows/hides the task widget without changing the persisted enabled/disabled preference. The shortcut is a no-op while the task widget feature is disabled. When the user reveals the widget via the shortcut while the main window is visible, a sticky "user-forced visible" flag keeps it up (like always-show) instead of letting the next focus/show event immediately hide it. The flag clears when the user toggles the widget off, opens the app from the widget, or the feature is disabled. Rebased onto master to drop the now-superseded tray-indicator refactor; the only main-window.ts change is the additive user-forced-visible gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(electron): handle task widget shortcut races --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
72538c18d0
commit
e0be3a1a16
10 changed files with 449 additions and 24 deletions
|
|
@ -2,6 +2,7 @@ import { globalShortcut, ipcMain } from 'electron';
|
|||
import { IPC } from '../shared-with-frontend/ipc-events.const';
|
||||
import { KeyboardConfig } from '../../src/app/features/config/keyboard-config.model';
|
||||
import { getWin, setWasMaximizedBeforeHide } from '../main-window';
|
||||
import { toggleTaskWidgetVisibility } from '../task-widget/task-widget';
|
||||
import { showOrFocus } from '../various-shared';
|
||||
import { ensureIndicator } from '../indicator';
|
||||
import { getIsMinimizeToTray } from '../shared-state';
|
||||
|
|
@ -22,6 +23,7 @@ const registerShowAppShortCuts = (cfg: KeyboardConfig): void => {
|
|||
'globalToggleTaskStart',
|
||||
'globalAddNote',
|
||||
'globalAddTask',
|
||||
'globalToggleTaskWidget',
|
||||
];
|
||||
|
||||
if (cfg) {
|
||||
|
|
@ -82,6 +84,10 @@ const registerShowAppShortCuts = (cfg: KeyboardConfig): void => {
|
|||
};
|
||||
break;
|
||||
|
||||
case 'globalToggleTaskWidget':
|
||||
actionFn = toggleTaskWidgetVisibility;
|
||||
break;
|
||||
|
||||
default:
|
||||
actionFn = () => undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { IS_MAC, IS_GNOME_DESKTOP } from './common.const';
|
|||
import {
|
||||
destroyTaskWidget,
|
||||
getIsTaskWidgetAlwaysShow,
|
||||
getIsTaskWidgetUserForcedVisible,
|
||||
hideTaskWidget,
|
||||
showTaskWidget,
|
||||
} from './task-widget/task-widget';
|
||||
|
|
@ -452,21 +453,27 @@ function initWinEventListeners(app: Electron.App): void {
|
|||
appCloseHandler(app);
|
||||
appMinimizeHandler(app);
|
||||
|
||||
// Handle restore and show events to hide task widget
|
||||
// Handle restore and show events to hide task widget. `getIsTaskWidgetUserForcedVisible()`
|
||||
// keeps the widget up when the user explicitly revealed it via the global shortcut.
|
||||
mainWin.on('restore', () => {
|
||||
if (!getIsTaskWidgetAlwaysShow()) {
|
||||
if (!getIsTaskWidgetAlwaysShow() && !getIsTaskWidgetUserForcedVisible()) {
|
||||
hideTaskWidget();
|
||||
}
|
||||
});
|
||||
|
||||
mainWin.on('show', () => {
|
||||
if (!getIsTaskWidgetAlwaysShow()) {
|
||||
if (!getIsTaskWidgetAlwaysShow() && !getIsTaskWidgetUserForcedVisible()) {
|
||||
hideTaskWidget();
|
||||
}
|
||||
});
|
||||
|
||||
mainWin.on('focus', () => {
|
||||
if (mainWin.isVisible() && !mainWin.isMinimized() && !getIsTaskWidgetAlwaysShow()) {
|
||||
if (
|
||||
mainWin.isVisible() &&
|
||||
!mainWin.isMinimized() &&
|
||||
!getIsTaskWidgetAlwaysShow() &&
|
||||
!getIsTaskWidgetUserForcedVisible()
|
||||
) {
|
||||
hideTaskWidget();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
297
electron/task-widget.test.cjs
Normal file
297
electron/task-widget.test.cjs
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const Module = require('node:module');
|
||||
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const originalModuleLoad = Module._load;
|
||||
const taskWidgetModulePath = path.resolve(__dirname, 'task-widget/task-widget.ts');
|
||||
|
||||
let createdWindows = [];
|
||||
let loadSimpleStoreAllImpl;
|
||||
|
||||
const createDeferred = () => {
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((promiseResolve, promiseReject) => {
|
||||
resolve = promiseResolve;
|
||||
reject = promiseReject;
|
||||
});
|
||||
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
class FakeWebContents {
|
||||
on() {}
|
||||
send() {}
|
||||
focus() {}
|
||||
isDestroyed() {
|
||||
return false;
|
||||
}
|
||||
removeAllListeners() {}
|
||||
}
|
||||
|
||||
class FakeBrowserWindow {
|
||||
constructor() {
|
||||
this._visible = false;
|
||||
this.showCount = 0;
|
||||
this.showInactiveCount = 0;
|
||||
this.hideCount = 0;
|
||||
this._handlers = new Map();
|
||||
this.webContents = new FakeWebContents();
|
||||
createdWindows.push(this);
|
||||
}
|
||||
|
||||
static getAllWindows() {
|
||||
return createdWindows.slice();
|
||||
}
|
||||
|
||||
loadFile() {}
|
||||
setVisibleOnAllWorkspaces() {}
|
||||
setOpacity() {}
|
||||
setClosable() {}
|
||||
removeAllListeners() {}
|
||||
destroy() {}
|
||||
on(eventName, handler) {
|
||||
this._handlers.set(eventName, handler);
|
||||
}
|
||||
emit(eventName) {
|
||||
const handler = this._handlers.get(eventName);
|
||||
if (handler) handler();
|
||||
}
|
||||
getBounds() {
|
||||
return { width: 300, height: 80, x: 0, y: 0 };
|
||||
}
|
||||
isDestroyed() {
|
||||
return false;
|
||||
}
|
||||
isVisible() {
|
||||
return this._visible;
|
||||
}
|
||||
show() {
|
||||
this._visible = true;
|
||||
this.showCount += 1;
|
||||
}
|
||||
showInactive() {
|
||||
this._visible = true;
|
||||
this.showInactiveCount += 1;
|
||||
}
|
||||
hide() {
|
||||
this._visible = false;
|
||||
this.hideCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const installMocks = () => {
|
||||
Module._load = function patchedLoad(request, parent, isMain) {
|
||||
if (request === 'electron') {
|
||||
return {
|
||||
BrowserWindow: FakeBrowserWindow,
|
||||
ipcMain: { on: () => {}, removeAllListeners: () => {} },
|
||||
screen: {
|
||||
getPrimaryDisplay: () => ({ workAreaSize: { width: 1920, height: 1080 } }),
|
||||
getDisplayMatching: () => ({
|
||||
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (request === 'electron-log/main') {
|
||||
return { info: () => {} };
|
||||
}
|
||||
if (request.endsWith('simple-store')) {
|
||||
return {
|
||||
loadSimpleStoreAll: () => loadSimpleStoreAllImpl(),
|
||||
saveSimpleStore: () => {},
|
||||
};
|
||||
}
|
||||
if (request.endsWith('common.const')) {
|
||||
return { IS_MAC: false };
|
||||
}
|
||||
return originalModuleLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
};
|
||||
|
||||
const loadModule = () => {
|
||||
delete require.cache[taskWidgetModulePath];
|
||||
return require(taskWidgetModulePath);
|
||||
};
|
||||
|
||||
const flush = () => new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
test.beforeEach(() => {
|
||||
createdWindows = [];
|
||||
loadSimpleStoreAllImpl = async () => ({});
|
||||
installMocks();
|
||||
});
|
||||
|
||||
test.afterEach(() => {
|
||||
Module._load = originalModuleLoad;
|
||||
});
|
||||
|
||||
test('toggleTaskWidgetVisibility is a no-op while the task widget feature is disabled', () => {
|
||||
const mod = loadModule();
|
||||
mod.toggleTaskWidgetVisibility();
|
||||
assert.equal(createdWindows.length, 0, 'no window should be created when disabled');
|
||||
});
|
||||
|
||||
test('toggleTaskWidgetVisibility shows the widget when it is enabled but hidden', async () => {
|
||||
const mod = loadModule();
|
||||
mod.updateTaskWidgetEnabled(true);
|
||||
await flush();
|
||||
|
||||
assert.equal(createdWindows.length, 1, 'enabling should create the widget window');
|
||||
const win = createdWindows[0];
|
||||
assert.equal(win.isVisible(), false, 'widget starts hidden');
|
||||
|
||||
mod.toggleTaskWidgetVisibility();
|
||||
assert.equal(win.isVisible(), true, 'toggle should show the hidden widget');
|
||||
assert.equal(win.showInactiveCount, 1);
|
||||
});
|
||||
|
||||
test('toggleTaskWidgetVisibility hides the widget when it is enabled and visible', async () => {
|
||||
const mod = loadModule();
|
||||
mod.updateTaskWidgetEnabled(true);
|
||||
await flush();
|
||||
|
||||
const win = createdWindows[0];
|
||||
mod.showTaskWidget();
|
||||
assert.equal(win.isVisible(), true, 'widget should be visible before toggling');
|
||||
|
||||
mod.toggleTaskWidgetVisibility();
|
||||
assert.equal(win.isVisible(), false, 'toggle should hide the visible widget');
|
||||
assert.equal(win.hideCount, 1);
|
||||
});
|
||||
|
||||
test('forcing the widget visible via the shortcut sets a sticky user-forced flag', async () => {
|
||||
const mod = loadModule();
|
||||
mod.updateTaskWidgetEnabled(true);
|
||||
await flush();
|
||||
|
||||
assert.equal(mod.getIsTaskWidgetUserForcedVisible(), false, 'flag starts cleared');
|
||||
|
||||
mod.toggleTaskWidgetVisibility();
|
||||
assert.equal(
|
||||
mod.getIsTaskWidgetUserForcedVisible(),
|
||||
true,
|
||||
'showing via the shortcut sets the sticky flag',
|
||||
);
|
||||
|
||||
mod.toggleTaskWidgetVisibility();
|
||||
assert.equal(
|
||||
mod.getIsTaskWidgetUserForcedVisible(),
|
||||
false,
|
||||
'hiding via the shortcut clears the sticky flag',
|
||||
);
|
||||
});
|
||||
|
||||
test('disabling the widget clears the sticky user-forced flag', async () => {
|
||||
const mod = loadModule();
|
||||
mod.updateTaskWidgetEnabled(true);
|
||||
await flush();
|
||||
|
||||
mod.toggleTaskWidgetVisibility();
|
||||
assert.equal(mod.getIsTaskWidgetUserForcedVisible(), true);
|
||||
|
||||
mod.updateTaskWidgetEnabled(false);
|
||||
assert.equal(
|
||||
mod.getIsTaskWidgetUserForcedVisible(),
|
||||
false,
|
||||
'disabling the feature resets the sticky flag',
|
||||
);
|
||||
});
|
||||
|
||||
test('disabling clears the sticky flag even when the widget window is absent', () => {
|
||||
const mod = loadModule();
|
||||
|
||||
// Enable but do not flush: createTaskWidgetWindow() is mid-flight, so
|
||||
// taskWidgetWin is still null (the "absent window" / async re-create gap).
|
||||
mod.updateTaskWidgetEnabled(true);
|
||||
|
||||
// User hits the shortcut during that gap — the flag is set without a window.
|
||||
mod.toggleTaskWidgetVisibility();
|
||||
assert.equal(createdWindows.length, 0, 'no window exists yet');
|
||||
assert.equal(
|
||||
mod.getIsTaskWidgetUserForcedVisible(),
|
||||
true,
|
||||
'shortcut sets the sticky flag even without a window',
|
||||
);
|
||||
|
||||
// Disabling now must clear the flag even though destroyTaskWidget() is
|
||||
// skipped (its guard requires an existing window), or it would leak into
|
||||
// the next enable.
|
||||
mod.updateTaskWidgetEnabled(false);
|
||||
assert.equal(
|
||||
mod.getIsTaskWidgetUserForcedVisible(),
|
||||
false,
|
||||
'disabling clears the flag regardless of whether the window exists',
|
||||
);
|
||||
});
|
||||
|
||||
test('disabling while async creation is pending prevents the widget window from being created', async () => {
|
||||
const storeLoad = createDeferred();
|
||||
loadSimpleStoreAllImpl = () => storeLoad.promise;
|
||||
const mod = loadModule();
|
||||
|
||||
mod.updateTaskWidgetEnabled(true);
|
||||
mod.updateTaskWidgetEnabled(false);
|
||||
|
||||
storeLoad.resolve({});
|
||||
await flush();
|
||||
|
||||
assert.equal(
|
||||
createdWindows.length,
|
||||
0,
|
||||
'disabling before persisted bounds load resolves should cancel window creation',
|
||||
);
|
||||
});
|
||||
|
||||
test('shortcut reveal while async creation is pending shows the widget after creation completes', async () => {
|
||||
const storeLoad = createDeferred();
|
||||
loadSimpleStoreAllImpl = () => storeLoad.promise;
|
||||
const mod = loadModule();
|
||||
|
||||
mod.updateTaskWidgetEnabled(true);
|
||||
mod.toggleTaskWidgetVisibility();
|
||||
|
||||
storeLoad.resolve({});
|
||||
await flush();
|
||||
|
||||
assert.equal(createdWindows.length, 1, 'only the initial in-flight creation is reused');
|
||||
assert.equal(
|
||||
createdWindows[0].isVisible(),
|
||||
true,
|
||||
'pending shortcut reveal should show the window once it exists',
|
||||
);
|
||||
});
|
||||
|
||||
test('shortcut reveal uses showInactive so the current app keeps focus', async () => {
|
||||
const mod = loadModule();
|
||||
mod.updateTaskWidgetEnabled(true);
|
||||
await flush();
|
||||
|
||||
const win = createdWindows[0];
|
||||
mod.toggleTaskWidgetVisibility();
|
||||
|
||||
assert.equal(win.showInactiveCount, 1);
|
||||
assert.equal(win.showCount, 0);
|
||||
assert.equal(win.isVisible(), true, 'widget should still become visible');
|
||||
});
|
||||
|
||||
test('the closed event clears the sticky flag so it does not outlive the window', async () => {
|
||||
const mod = loadModule();
|
||||
mod.updateTaskWidgetEnabled(true);
|
||||
await flush();
|
||||
|
||||
const win = createdWindows[0];
|
||||
mod.toggleTaskWidgetVisibility();
|
||||
assert.equal(mod.getIsTaskWidgetUserForcedVisible(), true);
|
||||
|
||||
win.emit('closed');
|
||||
assert.equal(
|
||||
mod.getIsTaskWidgetUserForcedVisible(),
|
||||
false,
|
||||
'closing the window clears the sticky flag',
|
||||
);
|
||||
});
|
||||
|
|
@ -10,6 +10,14 @@ import { IS_MAC } from '../common.const';
|
|||
let taskWidgetWin: BrowserWindow | null = null;
|
||||
let isTaskWidgetEnabled = false;
|
||||
let isAlwaysShow = false;
|
||||
// Set when the user explicitly reveals the widget via the global shortcut
|
||||
// (`globalToggleTaskWidget`) while the main window is visible. Like
|
||||
// `isAlwaysShow`, it suppresses the automatic "hide the widget when the main
|
||||
// window is shown/focused" behavior — but only until the user hides the widget
|
||||
// again (toggles off) or opens the app from the widget. This gives the shortcut
|
||||
// a sticky "user-forced visible" effect instead of being immediately undone by
|
||||
// the next focus event.
|
||||
let isUserForcedVisible = false;
|
||||
let currentTask: TaskCopy | null = null;
|
||||
let isPomodoroEnabled = false;
|
||||
let currentPomodoroSessionTime = 0;
|
||||
|
|
@ -18,16 +26,28 @@ let currentFocusSessionTime = 0;
|
|||
let initTimeoutId: NodeJS.Timeout | null = null;
|
||||
let currentOpacity = 95;
|
||||
let listenersRegistered = false;
|
||||
let isCreatingWindow = false;
|
||||
let taskWidgetCreationPromise: Promise<void> | null = null;
|
||||
let taskWidgetCreationGeneration = 0;
|
||||
let pendingShowAfterCreate = false;
|
||||
let pendingShowAfterCreateInactive = false;
|
||||
|
||||
const TASK_WIDGET_BOUNDS_KEY = 'taskWidgetBounds';
|
||||
const LEGACY_BOUNDS_KEY = 'overlayBounds';
|
||||
let boundsDebounceTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
type ShowTaskWidgetOptions = Readonly<{
|
||||
inactive?: boolean;
|
||||
}>;
|
||||
|
||||
export const updateTaskWidgetEnabled = (isEnabled: boolean): void => {
|
||||
isTaskWidgetEnabled = isEnabled;
|
||||
|
||||
if (isEnabled && !taskWidgetWin && !isCreatingWindow) {
|
||||
if (!isEnabled) {
|
||||
destroyTaskWidget();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!taskWidgetWin && !taskWidgetCreationPromise) {
|
||||
initListeners();
|
||||
createTaskWidgetWindow().then(() => {
|
||||
// Window creation is async; re-apply the cached opacity here because
|
||||
|
|
@ -44,11 +64,16 @@ export const updateTaskWidgetEnabled = (isEnabled: boolean): void => {
|
|||
mainWindow.webContents.send(IPC.REQUEST_CURRENT_TASK_FOR_TASK_WIDGET);
|
||||
}
|
||||
});
|
||||
} else if (!isEnabled && taskWidgetWin) {
|
||||
destroyTaskWidget();
|
||||
}
|
||||
};
|
||||
|
||||
const clearPendingTaskWidgetCreation = (): void => {
|
||||
taskWidgetCreationGeneration += 1;
|
||||
taskWidgetCreationPromise = null;
|
||||
pendingShowAfterCreate = false;
|
||||
pendingShowAfterCreateInactive = false;
|
||||
};
|
||||
|
||||
export const destroyTaskWidget = (): void => {
|
||||
// Clear any pending timeouts
|
||||
if (initTimeoutId) {
|
||||
|
|
@ -64,7 +89,8 @@ export const destroyTaskWidget = (): void => {
|
|||
|
||||
// Disable task widget to prevent close event prevention
|
||||
isTaskWidgetEnabled = false;
|
||||
isCreatingWindow = false;
|
||||
isUserForcedVisible = false;
|
||||
clearPendingTaskWidgetCreation();
|
||||
|
||||
// Remove IPC listeners
|
||||
ipcMain.removeAllListeners('task-widget-show-main-window');
|
||||
|
|
@ -97,11 +123,34 @@ export const destroyTaskWidget = (): void => {
|
|||
}
|
||||
};
|
||||
|
||||
const createTaskWidgetWindow = async (): Promise<void> => {
|
||||
if (taskWidgetWin || isCreatingWindow) {
|
||||
const createTaskWidgetWindow = (): Promise<void> => {
|
||||
if (taskWidgetWin) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (taskWidgetCreationPromise) {
|
||||
return taskWidgetCreationPromise;
|
||||
}
|
||||
|
||||
const creationGeneration = taskWidgetCreationGeneration;
|
||||
const nextCreationPromise = createTaskWidgetWindowForGeneration(
|
||||
creationGeneration,
|
||||
).finally(() => {
|
||||
if (taskWidgetCreationPromise === nextCreationPromise) {
|
||||
taskWidgetCreationPromise = null;
|
||||
}
|
||||
});
|
||||
|
||||
taskWidgetCreationPromise = nextCreationPromise;
|
||||
return nextCreationPromise;
|
||||
};
|
||||
|
||||
const createTaskWidgetWindowForGeneration = async (
|
||||
creationGeneration: number,
|
||||
): Promise<void> => {
|
||||
if (taskWidgetWin) {
|
||||
return;
|
||||
}
|
||||
isCreatingWindow = true;
|
||||
|
||||
const primaryDisplay = screen.getPrimaryDisplay();
|
||||
const { width: screenWidth } = primaryDisplay.workAreaSize;
|
||||
|
|
@ -143,7 +192,14 @@ const createTaskWidgetWindow = async (): Promise<void> => {
|
|||
// Use defaults (file may not exist on first run)
|
||||
}
|
||||
|
||||
isCreatingWindow = false;
|
||||
if (
|
||||
taskWidgetWin ||
|
||||
!isTaskWidgetEnabled ||
|
||||
creationGeneration !== taskWidgetCreationGeneration
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On macOS, transparent + frameless windows do not support native window
|
||||
// dragging or edge resizing (see Electron's BrowserWindow docs: "Transparent
|
||||
// windows are not resizable. Setting `resizable` to `true` may make a
|
||||
|
|
@ -190,6 +246,10 @@ const createTaskWidgetWindow = async (): Promise<void> => {
|
|||
|
||||
taskWidgetWin.on('closed', () => {
|
||||
taskWidgetWin = null;
|
||||
// Tie "user-forced visible" to the window's lifetime: once the window is
|
||||
// gone the sticky flag has no widget to keep visible, so don't let it
|
||||
// linger into a future re-create.
|
||||
isUserForcedVisible = false;
|
||||
});
|
||||
|
||||
taskWidgetWin.on('ready-to-show', () => {
|
||||
|
|
@ -232,9 +292,30 @@ const createTaskWidgetWindow = async (): Promise<void> => {
|
|||
|
||||
// Update initial state
|
||||
updateTaskWidgetContent();
|
||||
|
||||
updateTaskWidgetOpacity(currentOpacity);
|
||||
|
||||
if (pendingShowAfterCreate) {
|
||||
const showInactive = pendingShowAfterCreateInactive;
|
||||
pendingShowAfterCreate = false;
|
||||
pendingShowAfterCreateInactive = false;
|
||||
showTaskWidgetWindow({ inactive: showInactive });
|
||||
}
|
||||
};
|
||||
|
||||
export const showTaskWidget = (): void => {
|
||||
const showTaskWidgetWindow = (options: ShowTaskWidgetOptions = {}): void => {
|
||||
if (!taskWidgetWin || taskWidgetWin.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.inactive) {
|
||||
taskWidgetWin.showInactive();
|
||||
} else {
|
||||
taskWidgetWin.show();
|
||||
}
|
||||
};
|
||||
|
||||
export const showTaskWidget = (options: ShowTaskWidgetOptions = {}): void => {
|
||||
if (!isTaskWidgetEnabled) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -242,12 +323,9 @@ export const showTaskWidget = (): void => {
|
|||
// Recreate task widget if it was accidentally closed
|
||||
if (!taskWidgetWin) {
|
||||
info('Task widget window was destroyed, recreating');
|
||||
createTaskWidgetWindow().then(() => {
|
||||
if (taskWidgetWin && !taskWidgetWin.isDestroyed()) {
|
||||
updateTaskWidgetOpacity(currentOpacity);
|
||||
taskWidgetWin.show();
|
||||
}
|
||||
});
|
||||
pendingShowAfterCreate = true;
|
||||
pendingShowAfterCreateInactive = pendingShowAfterCreateInactive || !!options.inactive;
|
||||
createTaskWidgetWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +336,7 @@ export const showTaskWidget = (): void => {
|
|||
// Only show if not already visible
|
||||
if (!taskWidgetWin.isVisible()) {
|
||||
info('Showing task widget');
|
||||
taskWidgetWin.show();
|
||||
showTaskWidgetWindow(options);
|
||||
} else {
|
||||
info('Task widget already visible');
|
||||
}
|
||||
|
|
@ -284,6 +362,27 @@ export const hideTaskWidget = (): void => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the task widget's visibility. Intended for the global shortcut
|
||||
* (`globalToggleTaskWidget`): it only acts when the task widget feature is
|
||||
* enabled in settings and never changes that persisted enabled/disabled
|
||||
* preference — it just shows or hides the existing widget.
|
||||
*/
|
||||
export const toggleTaskWidgetVisibility = (): void => {
|
||||
if (!isTaskWidgetEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (taskWidgetWin && !taskWidgetWin.isDestroyed() && taskWidgetWin.isVisible()) {
|
||||
isUserForcedVisible = false;
|
||||
hideTaskWidget();
|
||||
return;
|
||||
}
|
||||
|
||||
isUserForcedVisible = true;
|
||||
showTaskWidget({ inactive: true });
|
||||
};
|
||||
|
||||
const initListeners = (): void => {
|
||||
if (listenersRegistered) {
|
||||
return;
|
||||
|
|
@ -299,6 +398,10 @@ const initListeners = (): void => {
|
|||
// event.preventDefault() on 'minimize' has no effect).
|
||||
mainWindow.restore();
|
||||
mainWindow.show();
|
||||
// Opening the app from the widget is an explicit "I'm going to the app"
|
||||
// gesture, so clear any sticky user-forced visibility and let the widget
|
||||
// follow the normal companion behavior again.
|
||||
isUserForcedVisible = false;
|
||||
if (!isAlwaysShow) {
|
||||
hideTaskWidget();
|
||||
}
|
||||
|
|
@ -374,6 +477,8 @@ export const updateTaskWidgetAlwaysShow = (alwaysShow: boolean): void => {
|
|||
|
||||
export const getIsTaskWidgetAlwaysShow = (): boolean => isAlwaysShow;
|
||||
|
||||
export const getIsTaskWidgetUserForcedVisible = (): boolean => isUserForcedVisible;
|
||||
|
||||
export const updateTaskWidgetOpacity = (opacity: number): void => {
|
||||
currentOpacity = opacity;
|
||||
if (!taskWidgetWin || taskWidgetWin.isDestroyed()) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { app, BrowserWindow } from 'electron';
|
||||
import { info } from 'electron-log/main';
|
||||
import { getWin, getWasMaximizedBeforeHide } from './main-window';
|
||||
import { getIsTaskWidgetAlwaysShow, hideTaskWidget } from './task-widget/task-widget';
|
||||
import {
|
||||
getIsTaskWidgetAlwaysShow,
|
||||
getIsTaskWidgetUserForcedVisible,
|
||||
hideTaskWidget,
|
||||
} from './task-widget/task-widget';
|
||||
import { setIsQuiting } from './shared-state';
|
||||
|
||||
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
||||
|
|
@ -36,8 +40,9 @@ export function showOrFocus(passedWin: BrowserWindow): void {
|
|||
if (getWasMaximizedBeforeHide()) win.maximize();
|
||||
}
|
||||
|
||||
// Hide task widget when main window is shown
|
||||
if (!getIsTaskWidgetAlwaysShow()) {
|
||||
// Hide task widget when main window is shown, unless the user explicitly
|
||||
// pinned it visible via the global shortcut.
|
||||
if (!getIsTaskWidgetAlwaysShow() && !getIsTaskWidgetUserForcedVisible()) {
|
||||
hideTaskWidget();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
|
|||
globalToggleTaskStart: null,
|
||||
globalAddNote: null,
|
||||
globalAddTask: null,
|
||||
globalToggleTaskWidget: null,
|
||||
addNewTask: 'Shift+A',
|
||||
addNewProject: 'Shift+P',
|
||||
addNewNote: 'Alt+N',
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection<KeyboardConfig> = {
|
|||
kbField('globalToggleTaskStart', T.GCF.KEYBOARD.GLOBAL_TOGGLE_TASK_START),
|
||||
kbField('globalAddNote', T.GCF.KEYBOARD.GLOBAL_ADD_NOTE),
|
||||
kbField('globalAddTask', T.GCF.KEYBOARD.GLOBAL_ADD_TASK),
|
||||
kbField('globalToggleTaskWidget', T.GCF.KEYBOARD.GLOBAL_TOGGLE_TASK_WIDGET),
|
||||
]
|
||||
: []),
|
||||
// APP WIDE
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export type KeyboardConfig = Readonly<{
|
|||
globalShowHide?: string | null;
|
||||
globalAddNote?: string | null;
|
||||
globalAddTask?: string | null;
|
||||
globalToggleTaskWidget?: string | null;
|
||||
toggleBacklog?: string | null;
|
||||
goToFocusMode?: string | null;
|
||||
goToWorkView?: string | null;
|
||||
|
|
|
|||
|
|
@ -2403,6 +2403,7 @@ const T = {
|
|||
GLOBAL_ADD_TASK: 'GCF.KEYBOARD.GLOBAL_ADD_TASK',
|
||||
GLOBAL_SHOW_HIDE: 'GCF.KEYBOARD.GLOBAL_SHOW_HIDE',
|
||||
GLOBAL_TOGGLE_TASK_START: 'GCF.KEYBOARD.GLOBAL_TOGGLE_TASK_START',
|
||||
GLOBAL_TOGGLE_TASK_WIDGET: 'GCF.KEYBOARD.GLOBAL_TOGGLE_TASK_WIDGET',
|
||||
GO_TO_DAILY_AGENDA: 'GCF.KEYBOARD.GO_TO_DAILY_AGENDA',
|
||||
GO_TO_FOCUS_MODE: 'GCF.KEYBOARD.GO_TO_FOCUS_MODE',
|
||||
GO_TO_SCHEDULE: 'GCF.KEYBOARD.GO_TO_SCHEDULE',
|
||||
|
|
|
|||
|
|
@ -2346,6 +2346,7 @@
|
|||
"GLOBAL_ADD_TASK": "Add new task",
|
||||
"GLOBAL_SHOW_HIDE": "Show/hide Super Productivity",
|
||||
"GLOBAL_TOGGLE_TASK_START": "Start/stop time tracking for last active task",
|
||||
"GLOBAL_TOGGLE_TASK_WIDGET": "Show/hide task widget",
|
||||
"GO_TO_DAILY_AGENDA": "Go to agenda",
|
||||
"GO_TO_FOCUS_MODE": "Enter focus mode",
|
||||
"GO_TO_SCHEDULE": "Go to Today",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue