diff --git a/src/bridge/featureBridge.js b/src/bridge/featureBridge.js index 4dde899..be6d348 100644 --- a/src/bridge/featureBridge.js +++ b/src/bridge/featureBridge.js @@ -29,9 +29,12 @@ module.exports = { ipcMain.handle('settings:shutdown-ollama', async () => await settingsService.shutdownOllama()); // Shortcuts - ipcMain.handle('get-current-shortcuts', async () => await shortcutsService.loadKeybinds()); - ipcMain.handle('get-default-shortcuts', async () => await shortcutsService.handleRestoreDefaults()); - ipcMain.handle('save-shortcuts', async (event, newKeybinds) => await shortcutsService.handleSaveShortcuts(newKeybinds)); + ipcMain.handle('settings:getCurrentShortcuts', async () => await shortcutsService.loadKeybinds()); + ipcMain.handle('shortcut:getDefaultShortcuts', async () => await shortcutsService.handleRestoreDefaults()); + ipcMain.handle('shortcut:closeShortcutSettingsWindow', async () => await shortcutsService.closeShortcutSettingsWindow()); + ipcMain.handle('shortcut:openShortcutSettingsWindow', async () => await shortcutsService.openShortcutSettingsWindow()); + ipcMain.handle('shortcut:saveShortcuts', async (event, newKeybinds) => await shortcutsService.handleSaveShortcuts(newKeybinds)); + ipcMain.handle('shortcut:toggleAllWindowsVisibility', async () => await shortcutsService.toggleAllWindowsVisibility()); // Permissions ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions()); diff --git a/src/bridge/windowBridge.js b/src/bridge/windowBridge.js index 6382158..313273c 100644 --- a/src/bridge/windowBridge.js +++ b/src/bridge/windowBridge.js @@ -7,14 +7,13 @@ module.exports = { ipcMain.handle('toggle-content-protection', () => windowManager.toggleContentProtection()); ipcMain.handle('resize-header-window', (event, args) => windowManager.resizeHeaderWindow(args)); ipcMain.handle('get-content-protection-status', () => windowManager.getContentProtectionStatus()); - ipcMain.handle('open-shortcut-editor', () => windowManager.openShortcutEditor()); - ipcMain.on('show-settings-window', (event, bounds) => windowManager.showSettingsWindow(bounds)); + ipcMain.on('show-settings-window', () => windowManager.showSettingsWindow()); ipcMain.on('hide-settings-window', () => windowManager.hideSettingsWindow()); ipcMain.on('cancel-hide-settings-window', () => windowManager.cancelHideSettingsWindow()); + ipcMain.handle('open-login-page', () => windowManager.openLoginPage()); ipcMain.handle('open-personalize-page', () => windowManager.openLoginPage()); ipcMain.handle('move-window-step', (event, direction) => windowManager.moveWindowStep(direction)); - ipcMain.on('close-shortcut-editor', () => windowManager.closeWindow('shortcut-settings')); ipcMain.handle('open-external', (event, url) => shell.openExternal(url)); // Newly moved handlers from windowManager @@ -24,9 +23,6 @@ module.exports = { ipcMain.handle('move-header', (event, newX, newY) => windowManager.moveHeader(newX, newY)); ipcMain.handle('move-header-to', (event, newX, newY) => windowManager.moveHeaderTo(newX, newY)); ipcMain.handle('adjust-window-height', (event, targetHeight) => windowManager.adjustWindowHeight(event.sender, targetHeight)); - ipcMain.handle('toggle-all-windows-visibility', () => windowManager.toggleAllWindowsVisibility()); - // ipcMain.on('animation-finished', (event) => windowManager.handleAnimationFinished(event.sender)); - // ipcMain.handle('ask:closeAskWindow', () => windowManager.closeAskWindow()); }, notifyFocusChange(win, isFocused) { diff --git a/src/features/ask/askService.js b/src/features/ask/askService.js index 78ab69f..4274583 100644 --- a/src/features/ask/askService.js +++ b/src/features/ask/askService.js @@ -3,7 +3,6 @@ const { createStreamingLLM } = require('../common/ai/factory'); // Lazy require helper to avoid circular dependency issues const getWindowManager = () => require('../../window/windowManager'); const internalBridge = require('../../bridge/internalBridge'); -const { EVENTS } = internalBridge; const getWindowPool = () => { try { @@ -162,11 +161,11 @@ class AskService { this._broadcastState(); } else { if (askWindow && askWindow.isVisible()) { - internalBridge.emit('request-window-visibility', { name: 'ask', visible: false }); + internalBridge.emit('window:requestVisibility', { name: 'ask', visible: false }); this.state.isVisible = false; } else { console.log('[AskService] Showing hidden Ask window'); - internalBridge.emit('request-window-visibility', { name: 'ask', visible: true }); + internalBridge.emit('window:requestVisibility', { name: 'ask', visible: true }); this.state.isVisible = true; } if (this.state.isVisible) { @@ -192,7 +191,7 @@ class AskService { }; this._broadcastState(); - internalBridge.emit('request-window-visibility', { name: 'ask', visible: false }); + internalBridge.emit('window:requestVisibility', { name: 'ask', visible: false }); return { success: true }; } @@ -217,7 +216,16 @@ class AskService { * @returns {Promise<{success: boolean, response?: string, error?: string}>} */ async sendMessage(userPrompt, conversationHistoryRaw=[]) { - // ensureAskWindowVisible(); + internalBridge.emit('window:requestVisibility', { name: 'ask', visible: true }); + this.state = { + ...this.state, + isLoading: true, + isStreaming: false, + currentQuestion: userPrompt, + currentResponse: '', + showTextInput: false, + }; + this._broadcastState(); if (this.abortController) { this.abortController.abort('New request received.'); @@ -226,26 +234,10 @@ class AskService { const { signal } = this.abortController; - // if (!userPrompt || userPrompt.trim().length === 0) { - // console.warn('[AskService] Cannot process empty message'); - // return { success: false, error: 'Empty message' }; - // } - - let sessionId; try { console.log(`[AskService] 🤖 Processing message: ${userPrompt.substring(0, 50)}...`); - - this.state = { - ...this.state, - isLoading: true, - isStreaming: false, - currentQuestion: userPrompt, - currentResponse: '', - showTextInput: false, - }; - this._broadcastState(); sessionId = await sessionRepository.getOrCreateActive('ask'); await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() }); diff --git a/src/features/listen/listenService.js b/src/features/listen/listenService.js index 924e9b7..d879d60 100644 --- a/src/features/listen/listenService.js +++ b/src/features/listen/listenService.js @@ -5,7 +5,6 @@ const authService = require('../common/services/authService'); const sessionRepository = require('../common/repositories/session'); const sttRepository = require('./stt/repositories'); const internalBridge = require('../../bridge/internalBridge'); -const { EVENTS } = internalBridge; class ListenService { constructor() { @@ -109,7 +108,7 @@ class ListenService { switch (listenButtonText) { case 'Listen': console.log('[ListenService] changeSession to "Listen"'); - internalBridge.emit('request-window-visibility', { name: 'listen', visible: true }); + internalBridge.emit('window:requestVisibility', { name: 'listen', visible: true }); await this.initializeSession(); listenWindow.webContents.send('session-state-changed', { isActive: true }); break; @@ -122,7 +121,7 @@ class ListenService { case 'Done': console.log('[ListenService] changeSession to "Done"'); - internalBridge.emit('request-window-visibility', { name: 'listen', visible: false }); + internalBridge.emit('window:requestVisibility', { name: 'listen', visible: false }); listenWindow.webContents.send('session-state-changed', { isActive: false }); break; diff --git a/src/features/shortcuts/shortcutsService.js b/src/features/shortcuts/shortcutsService.js index 78e5af2..44ad6e7 100644 --- a/src/features/shortcuts/shortcutsService.js +++ b/src/features/shortcuts/shortcutsService.js @@ -10,6 +10,7 @@ class ShortcutsService { this.mouseEventsIgnored = false; this.movementManager = null; this.windowPool = null; + this.allWindowVisibility = true; } initialize(movementManager, windowPool) { @@ -22,6 +23,41 @@ class ShortcutsService { console.log('[ShortcutsService] Initialized with dependencies and event listener.'); } + async openShortcutSettingsWindow () { + const keybinds = await this.loadKeybinds(); + const shortcutWin = this.windowPool.get('shortcut-settings'); + shortcutWin.webContents.send('shortcut:loadShortcuts', keybinds); + + globalShortcut.unregisterAll(); + internalBridge.emit('window:requestVisibility', { name: 'shortcut-settings', visible: true }); + console.log('[ShortcutsService] Shortcut settings window opened.'); + return { success: true }; + } + + async closeShortcutSettingsWindow () { + await this.registerShortcuts(); + internalBridge.emit('window:requestVisibility', { name: 'shortcut-settings', visible: false }); + console.log('[ShortcutsService] Shortcut settings window closed.'); + return { success: true }; + } + + async handleSaveShortcuts(newKeybinds) { + try { + await this.saveKeybinds(newKeybinds); + await this.closeShortcutSettingsWindow(); + return { success: true }; + } catch (error) { + console.error("Failed to save shortcuts:", error); + await this.closeShortcutSettingsWindow(); + return { success: false, error: error.message }; + } + } + + async handleRestoreDefaults() { + const defaults = this.getDefaultKeybinds(); + return defaults; + } + getDefaultKeybinds() { const isMac = process.platform === 'darwin'; return { @@ -72,32 +108,6 @@ class ShortcutsService { return keybinds; } - async handleSaveShortcuts(newKeybinds) { - try { - await this.saveKeybinds(newKeybinds); - const shortcutEditor = this.windowPool.get('shortcut-settings'); - if (shortcutEditor && !shortcutEditor.isDestroyed()) { - shortcutEditor.close(); // This will trigger re-registration on 'closed' event in windowManager - } else { - // If editor wasn't open, re-register immediately - await this.registerShortcuts(); - } - return { success: true }; - } catch (error) { - console.error("Failed to save shortcuts:", error); - // On failure, re-register old shortcuts to be safe - await this.registerShortcuts(); - return { success: false, error: error.message }; - } - } - - async handleRestoreDefaults() { - const defaults = this.getDefaultKeybinds(); - await this.saveKeybinds(defaults); - await this.registerShortcuts(); - return defaults; - } - async saveKeybinds(newKeybinds) { const keybindsToSave = []; for (const action in newKeybinds) { @@ -112,38 +122,22 @@ class ShortcutsService { console.log(`[Shortcuts] Saved keybinds.`); } - toggleAllWindowsVisibility(windowPool) { - const header = windowPool.get('header'); - if (!header) return; - - if (header.isVisible()) { - this.lastVisibleWindows.clear(); - - windowPool.forEach((win, name) => { - if (win && !win.isDestroyed() && win.isVisible()) { - this.lastVisibleWindows.add(name); - } - }); - - this.lastVisibleWindows.forEach(name => { - if (name === 'header') return; - const win = windowPool.get(name); - if (win && !win.isDestroyed()) win.hide(); - }); - header.hide(); - - return; - } - - this.lastVisibleWindows.forEach(name => { - const win = windowPool.get(name); - if (win && !win.isDestroyed()) { - win.show(); - } + async toggleAllWindowsVisibility() { + const targetVisibility = !this.allWindowVisibility; + internalBridge.emit('window:requestToggleAllWindowsVisibility', { + targetVisibility: targetVisibility }); + + if (this.allWindowVisibility) { + await this.registerShortcuts(true); + } else { + await this.registerShortcuts(); + } + + this.allWindowVisibility = !this.allWindowVisibility; } - async registerShortcuts() { + async registerShortcuts(registerOnlyToggleVisibility = false) { if (!this.movementManager || !this.windowPool) { console.error('[Shortcuts] Service not initialized. Cannot register shortcuts.'); return; @@ -168,6 +162,14 @@ class ShortcutsService { sendToRenderer('shortcuts-updated', keybinds); + if (registerOnlyToggleVisibility) { + if (keybinds.toggleVisibility) { + globalShortcut.register(keybinds.toggleVisibility, () => this.toggleAllWindowsVisibility()); + } + console.log('[Shortcuts] registerOnlyToggleVisibility, only toggleVisibility shortcut is registered.'); + return; + } + // --- Hardcoded shortcuts --- const isMac = process.platform === 'darwin'; const modifier = isMac ? 'Cmd' : 'Ctrl'; @@ -195,7 +197,7 @@ class ShortcutsService { // --- User-configurable shortcuts --- if (header?.currentHeaderState === 'apikey') { if (keybinds.toggleVisibility) { - globalShortcut.register(keybinds.toggleVisibility, () => this.toggleAllWindowsVisibility(this.windowPool)); + globalShortcut.register(keybinds.toggleVisibility, () => this.toggleAllWindowsVisibility()); } console.log('[Shortcuts] ApiKeyHeader is active, only toggleVisibility shortcut is registered.'); return; @@ -208,7 +210,7 @@ class ShortcutsService { let callback; switch(action) { case 'toggleVisibility': - callback = () => this.toggleAllWindowsVisibility(this.windowPool); + callback = () => this.toggleAllWindowsVisibility(); break; case 'nextStep': callback = () => askService.toggleAskButton(true); @@ -282,4 +284,7 @@ class ShortcutsService { } } -module.exports = new ShortcutsService(); \ No newline at end of file + +const shortcutsService = new ShortcutsService(); + +module.exports = shortcutsService; \ No newline at end of file diff --git a/src/preload.js b/src/preload.js index 6121c84..04b1046 100644 --- a/src/preload.js +++ b/src/preload.js @@ -95,11 +95,14 @@ contextBridge.exposeInMainWorld('api', { // Settings Window Management cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'), - showSettingsWindow: (bounds) => ipcRenderer.send('show-settings-window', bounds), + showSettingsWindow: () => ipcRenderer.send('show-settings-window'), hideSettingsWindow: () => ipcRenderer.send('hide-settings-window'), // Generic invoke (for dynamic channel names) - invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), + // invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), + sendListenButtonClick: (listenButtonText) => ipcRenderer.invoke('listen:changeSession', listenButtonText), + sendAskButtonClick: () => ipcRenderer.invoke('ask:toggleAskButton'), + sendToggleAllWindowsVisibility: () => ipcRenderer.invoke('shortcut:toggleAllWindowsVisibility'), // Listeners onListenChangeSessionResult: (callback) => ipcRenderer.on('listen:changeSessionResult', callback), @@ -213,8 +216,8 @@ contextBridge.exposeInMainWorld('api', { setAutoUpdate: (isEnabled) => ipcRenderer.invoke('settings:set-auto-update', isEnabled), getContentProtectionStatus: () => ipcRenderer.invoke('get-content-protection-status'), toggleContentProtection: () => ipcRenderer.invoke('toggle-content-protection'), - getCurrentShortcuts: () => ipcRenderer.invoke('get-current-shortcuts'), - openShortcutEditor: () => ipcRenderer.invoke('open-shortcut-editor'), + getCurrentShortcuts: () => ipcRenderer.invoke('settings:getCurrentShortcuts'), + openShortcutSettingsWindow: () => ipcRenderer.invoke('shortcut:openShortcutSettingsWindow'), // Window Management moveWindowStep: (direction) => ipcRenderer.invoke('move-window-step', direction), @@ -245,20 +248,17 @@ contextBridge.exposeInMainWorld('api', { // src/ui/settings/ShortCutSettingsView.js shortcutSettingsView: { // Shortcut Management - saveShortcuts: (shortcuts) => ipcRenderer.invoke('save-shortcuts', shortcuts), - getDefaultShortcuts: () => ipcRenderer.invoke('get-default-shortcuts'), - closeShortcutEditor: () => ipcRenderer.send('close-shortcut-editor'), + saveShortcuts: (shortcuts) => ipcRenderer.invoke('shortcut:saveShortcuts', shortcuts), + getDefaultShortcuts: () => ipcRenderer.invoke('shortcut:getDefaultShortcuts'), + closeShortcutSettingsWindow: () => ipcRenderer.invoke('shortcut:closeShortcutSettingsWindow'), // Listeners - onLoadShortcuts: (callback) => ipcRenderer.on('load-shortcuts', callback), - removeOnLoadShortcuts: (callback) => ipcRenderer.removeListener('load-shortcuts', callback) + onLoadShortcuts: (callback) => ipcRenderer.on('shortcut:loadShortcuts', callback), + removeOnLoadShortcuts: (callback) => ipcRenderer.removeListener('shortcut:loadShortcuts', callback) }, // src/ui/app/content.html inline scripts content: { - // Animation Management - // sendAnimationFinished: () => ipcRenderer.send('animation-finished'), - // Listeners onSettingsWindowHideAnimation: (callback) => ipcRenderer.on('settings-window-hide-animation', callback), removeOnSettingsWindowHideAnimation: (callback) => ipcRenderer.removeListener('settings-window-hide-animation', callback), diff --git a/src/ui/app/MainHeader.js b/src/ui/app/MainHeader.js index 1ace9ea..175c71d 100644 --- a/src/ui/app/MainHeader.js +++ b/src/ui/app/MainHeader.js @@ -2,7 +2,6 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js'; export class MainHeader extends LitElement { static properties = { - // isSessionActive: { type: Boolean, state: true }, isTogglingSession: { type: Boolean, state: true }, shortcuts: { type: Object, state: true }, listenSessionStatus: { type: String, state: true }, @@ -515,30 +514,12 @@ export class MainHeader extends LitElement { } } - invoke(channel, ...args) { - if (this.wasJustDragged) return; - if (window.api) { - window.api.mainHeader.invoke(channel, ...args); - } - // return Promise.resolve(); - } - showSettingsWindow(element) { if (this.wasJustDragged) return; if (window.api) { console.log(`[MainHeader] showSettingsWindow called at ${Date.now()}`); - - window.api.mainHeader.cancelHideSettingsWindow(); + window.api.mainHeader.showSettingsWindow(); - if (element) { - const { left, top, width, height } = element.getBoundingClientRect(); - window.api.mainHeader.showSettingsWindow({ - x: left, - y: top, - width, - height, - }); - } } } @@ -559,9 +540,10 @@ export class MainHeader extends LitElement { this.isTogglingSession = true; try { - const channel = 'listen:changeSession'; const listenButtonText = this._getListenButtonText(this.listenSessionStatus); - await this.invoke(channel, listenButtonText); + if (window.api) { + await window.api.mainHeader.sendListenButtonClick(listenButtonText); + } } catch (error) { console.error('IPC invoke for session change failed:', error); this.isTogglingSession = false; @@ -572,13 +554,26 @@ export class MainHeader extends LitElement { if (this.wasJustDragged) return; try { - const channel = 'ask:toggleAskButton'; - await this.invoke(channel); + if (window.api) { + await window.api.mainHeader.sendAskButtonClick(); + } } catch (error) { console.error('IPC invoke for ask button failed:', error); } } + async _handleToggleAllWindowsVisibility() { + if (this.wasJustDragged) return; + + try { + if (window.api) { + await window.api.mainHeader.sendToggleAllWindowsVisibility(); + } + } catch (error) { + console.error('IPC invoke for all windows visibility button failed:', error); + } + } + renderShortcut(accelerator) { if (!accelerator) return html``; @@ -656,7 +651,7 @@ export class MainHeader extends LitElement { -
this.invoke('toggle-all-windows-visibility')}> +
this._handleToggleAllWindowsVisibility()}>
Show/Hide
diff --git a/src/ui/settings/SettingsView.js b/src/ui/settings/SettingsView.js index 8c74a4e..5d3bacb 100644 --- a/src/ui/settings/SettingsView.js +++ b/src/ui/settings/SettingsView.js @@ -879,7 +879,7 @@ export class SettingsView extends LitElement { //////// after_modelStateService //////// openShortcutEditor() { - window.api.settingsView.openShortcutEditor(); + window.api.settingsView.openShortcutSettingsWindow(); } connectedCallback() { @@ -1019,13 +1019,7 @@ export class SettingsView extends LitElement { window.api.settingsView.hideSettingsWindow(); } - // getMainShortcuts() { - // return [ - // { name: 'Show / Hide', key: '\\' }, - // { name: 'Ask Anything', key: '↵' }, - // { name: 'Scroll AI Response', key: '↕' } - // ]; - // } + getMainShortcuts() { return [ { name: 'Show / Hide', accelerator: this.shortcuts.toggleVisibility }, diff --git a/src/ui/settings/ShortCutSettingsView.js b/src/ui/settings/ShortCutSettingsView.js index ab5e345..691a059 100644 --- a/src/ui/settings/ShortCutSettingsView.js +++ b/src/ui/settings/ShortCutSettingsView.js @@ -179,7 +179,7 @@ export class ShortcutSettingsView extends LitElement { handleClose() { if (!window.api) return; - window.api.shortcutSettingsView.closeShortcutEditor(); + window.api.shortcutSettingsView.closeShortcutSettingsWindow(); } async handleResetToDefault() { diff --git a/src/window/windowLayoutManager.js b/src/window/windowLayoutManager.js index 56559e6..cdd0ef3 100644 --- a/src/window/windowLayoutManager.js +++ b/src/window/windowLayoutManager.js @@ -142,7 +142,14 @@ class WindowLayoutManager { const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY); this.positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY); - this.positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY); + const settings = this.windowPool.get('settings'); + if (settings && !settings.isDestroyed() && settings.isVisible()) { + const settingPos = this.calculateSettingsWindowPosition(); + if (settingPos) { + const { width, height } = settings.getBounds(); + settings.setBounds({ x: settingPos.x, y: settingPos.y, width, height }); + } + } } /** @@ -234,58 +241,54 @@ class WindowLayoutManager { } } - positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) { + /** + * @returns {{x: number, y: number} | null} + */ + calculateSettingsWindowPosition() { + const header = this.windowPool.get('header'); const settings = this.windowPool.get('settings'); - if (!settings?.getBounds || !settings.isVisible()) return; - if (settings.__lockedByButton) { - const headerDisplay = getCurrentDisplay(this.windowPool.get('header')); - const settingsDisplay = getCurrentDisplay(settings); - if (headerDisplay.id !== settingsDisplay.id) { - settings.__lockedByButton = false; - } else { - return; - } + if (!header || header.isDestroyed() || !settings || settings.isDestroyed()) { + return null; } + const headerBounds = header.getBounds(); const settingsBounds = settings.getBounds(); + const display = getCurrentDisplay(header); + const { x: workAreaX, y: workAreaY, width: screenWidth, height: screenHeight } = display.workArea; + const PAD = 5; - const buttonPadding = 17; - let x = headerBounds.x + headerBounds.width - settingsBounds.width - buttonPadding; - let y = headerBounds.y + headerBounds.height + PAD; + const buttonPadding = 170; - const otherVisibleWindows = []; - ['listen', 'ask'].forEach(name => { - const win = this.windowPool.get(name); - if (win && win.isVisible() && !win.isDestroyed()) { - otherVisibleWindows.push({ name, bounds: win.getBounds() }); - } - }); + const x = headerBounds.x + headerBounds.width - settingsBounds.width + buttonPadding; + const y = headerBounds.y + headerBounds.height + PAD; - const settingsNewBounds = { x, y, width: settingsBounds.width, height: settingsBounds.height }; - let hasOverlap = otherVisibleWindows.some(otherWin => this.boundsOverlap(settingsNewBounds, otherWin.bounds)); + const clampedX = Math.max(workAreaX + 10, Math.min(workAreaX + screenWidth - settingsBounds.width - 10, x)); + const clampedY = Math.max(workAreaY + 10, Math.min(workAreaY + screenHeight - settingsBounds.height - 10, y)); - if (hasOverlap) { - x = headerBounds.x + headerBounds.width + PAD; - y = headerBounds.y; - if (x + settingsBounds.width > screenWidth - 10) { - x = headerBounds.x - settingsBounds.width - PAD; - } - if (x < 10) { - x = headerBounds.x + headerBounds.width - settingsBounds.width - buttonPadding; - y = headerBounds.y - settingsBounds.height - PAD; - if (y < 10) { - x = headerBounds.x + headerBounds.width - settingsBounds.width; - y = headerBounds.y + headerBounds.height + PAD; - } - } + return { x: Math.round(clampedX), y: Math.round(clampedY) }; + } + + positionShortcutSettingsWindow() { + const header = this.windowPool.get('header'); + const shortcutSettings = this.windowPool.get('shortcut-settings'); + + if (!header || header.isDestroyed() || !shortcutSettings || shortcutSettings.isDestroyed()) { + return; } - x = Math.max(workAreaX + 10, Math.min(workAreaX + screenWidth - settingsBounds.width - 10, x)); - y = Math.max(workAreaY + 10, Math.min(workAreaY + screenHeight - settingsBounds.height - 10, y)); + const headerBounds = header.getBounds(); + const shortcutBounds = shortcutSettings.getBounds(); + const display = getCurrentDisplay(header); + const { workArea } = display; - settings.setBounds({ x: Math.round(x), y: Math.round(y) }); - settings.moveTop(); + let newX = Math.round(headerBounds.x + (headerBounds.width / 2) - (shortcutBounds.width / 2)); + let newY = Math.round(headerBounds.y); + + newX = Math.max(workArea.x, Math.min(newX, workArea.x + workArea.width - shortcutBounds.width)); + newY = Math.max(workArea.y, Math.min(newY, workArea.y + workArea.height - shortcutBounds.height)); + + shortcutSettings.setBounds({ x: newX, y: newY, width: shortcutBounds.width, height: shortcutBounds.height }); } /** diff --git a/src/window/windowManager.js b/src/window/windowManager.js index 3aa64d9..692199e 100644 --- a/src/window/windowManager.js +++ b/src/window/windowManager.js @@ -5,7 +5,6 @@ const path = require('node:path'); const os = require('os'); const shortcutsService = require('../features/shortcuts/shortcutsService'); const internalBridge = require('../bridge/internalBridge'); -const { EVENTS } = internalBridge; const permissionRepository = require('../features/common/repositories/permission'); /* ────────────────[ GLASS BYPASS ]─────────────── */ @@ -30,9 +29,6 @@ if (shouldUseLiquidGlass) { /* ────────────────[ GLASS BYPASS ]─────────────── */ let isContentProtectionOn = true; -let currentDisplayId = null; - -let mouseEventsIgnored = false; let lastVisibleWindows = new Set(['header']); const HEADER_HEIGHT = 47; const DEFAULT_WINDOW_WIDTH = 353; @@ -42,9 +38,7 @@ const windowPool = new Map(); let settingsHideTimer = null; -let selectedCaptureSourceId = null; -// let shortcutEditorWindow = null; let layoutManager = null; function updateLayout() { if (layoutManager) { @@ -92,20 +86,69 @@ function fadeWindow(win, from, to, duration = FADE_DURATION, onComplete) { }, 1000 / FADE_FPS); } +const showSettingsWindow = () => { + internalBridge.emit('window:requestVisibility', { name: 'settings', visible: true }); +}; -function setupAnimationController(windowPool, layoutManager, movementManager) { - internalBridge.on('request-window-visibility', ({ name, visible }) => { +const hideSettingsWindow = () => { + internalBridge.emit('window:requestVisibility', { name: 'settings', visible: false }); +}; + +const cancelHideSettingsWindow = () => { + internalBridge.emit('window:requestVisibility', { name: 'settings', visible: true }); +}; + + +function setupWindowController(windowPool, layoutManager, movementManager) { + internalBridge.on('window:requestVisibility', ({ name, visible }) => { handleWindowVisibilityRequest(windowPool, layoutManager, movementManager, name, visible); }); + internalBridge.on('window:requestToggleAllWindowsVisibility', ({ targetVisibility }) => { + changeAllWindowsVisibility(windowPool, targetVisibility); + }); } +function changeAllWindowsVisibility(windowPool, targetVisibility) { + const header = windowPool.get('header'); + if (!header) return; + + if (typeof targetVisibility === 'boolean' && + header.isVisible() === targetVisibility) { + return; + } + + if (header.isVisible()) { + lastVisibleWindows.clear(); + + windowPool.forEach((win, name) => { + if (win && !win.isDestroyed() && win.isVisible()) { + lastVisibleWindows.add(name); + } + }); + + lastVisibleWindows.forEach(name => { + if (name === 'header') return; + const win = windowPool.get(name); + if (win && !win.isDestroyed()) win.hide(); + }); + header.hide(); + + return; + } + + lastVisibleWindows.forEach(name => { + const win = windowPool.get(name); + if (win && !win.isDestroyed()) + win.show(); + }); + } /** * * @param {Map} windowPool * @param {WindowLayoutManager} layoutManager * @param {SmoothMovementManager} movementManager - * @param {'listen' | 'ask'} name + * @param {'listen' | 'ask' | 'settings' | 'shortcut-settings'} name * @param {boolean} shouldBeVisible */ async function handleWindowVisibilityRequest(windowPool, layoutManager, movementManager, name, shouldBeVisible) { @@ -117,94 +160,171 @@ async function handleWindowVisibilityRequest(windowPool, layoutManager, movement return; } - const isCurrentlyVisible = win.isVisible(); - if (isCurrentlyVisible === shouldBeVisible) { - console.log(`[WindowManager] Window '${name}' is already in the desired state.`); + if (name !== 'settings') { + const isCurrentlyVisible = win.isVisible(); + if (isCurrentlyVisible === shouldBeVisible) { + console.log(`[WindowManager] Window '${name}' is already in the desired state.`); + return; + } + } + + const disableClicks = (selectedWindow) => { + for (const [name, win] of windowPool) { + if (win !== selectedWindow && !win.isDestroyed()) { + win.setIgnoreMouseEvents(true, { forward: true }); + } + } + }; + + const restoreClicks = () => { + for (const [, win] of windowPool) { + if (!win.isDestroyed()) win.setIgnoreMouseEvents(false); + } + }; + + if (name === 'settings') { + if (shouldBeVisible) { + // Cancel any pending hide operations + if (settingsHideTimer) { + clearTimeout(settingsHideTimer); + settingsHideTimer = null; + } + const position = layoutManager.calculateSettingsWindowPosition(); + if (position) { + win.setBounds(position); + win.__lockedByButton = true; + win.show(); + win.moveTop(); + win.setAlwaysOnTop(true); + } else { + console.warn('[WindowManager] Could not calculate settings window position.'); + } + } else { + // Hide after a delay + if (settingsHideTimer) { + clearTimeout(settingsHideTimer); + } + settingsHideTimer = setTimeout(() => { + if (win && !win.isDestroyed()) { + win.setAlwaysOnTop(false); + win.hide(); + } + settingsHideTimer = null; + }, 200); + + win.__lockedByButton = false; + } return; } - const otherName = name === 'listen' ? 'ask' : 'listen'; - const otherWin = windowPool.get(otherName); - const isOtherWinVisible = otherWin && !otherWin.isDestroyed() && otherWin.isVisible(); - - const ANIM_OFFSET_X = 100; - const ANIM_OFFSET_Y = 20; - - if (shouldBeVisible) { - win.setOpacity(0); - - if (name === 'listen') { - if (!isOtherWinVisible) { - const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: false }); - if (!targets.listen) return; - - const startPos = { x: targets.listen.x - ANIM_OFFSET_X, y: targets.listen.y }; - win.setBounds(startPos); - win.show(); - fadeWindow(win, 0, 1); - movementManager.animateWindow(win, targets.listen.x, targets.listen.y); + if (name === 'shortcut-settings') { + if (shouldBeVisible) { + layoutManager.positionShortcutSettingsWindow(); + if (process.platform === 'darwin') { + win.setAlwaysOnTop(true, 'screen-saver'); } else { - const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: true }); - if (!targets.listen || !targets.ask) return; - - const startListenPos = { x: targets.listen.x - ANIM_OFFSET_X, y: targets.listen.y }; - win.setBounds(startListenPos); - - win.show(); - fadeWindow(win, 0, 1); - movementManager.animateWindow(otherWin, targets.ask.x, targets.ask.y); - movementManager.animateWindow(win, targets.listen.x, targets.listen.y); + win.setAlwaysOnTop(true); } - } else if (name === 'ask') { - if (!isOtherWinVisible) { - const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: false, ask: true }); - if (!targets.ask) return; - - const startPos = { x: targets.ask.x, y: targets.ask.y - ANIM_OFFSET_Y }; - win.setBounds(startPos); - win.show(); - fadeWindow(win, 0, 1); - movementManager.animateWindow(win, targets.ask.x, targets.ask.y); - + // globalShortcut.unregisterAll(); + disableClicks(win); + win.show(); + } else { + if (process.platform === 'darwin') { + win.setAlwaysOnTop(false, 'screen-saver'); } else { - const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: true }); - if (!targets.listen || !targets.ask) return; - - const startAskPos = { x: targets.ask.x, y: targets.ask.y - ANIM_OFFSET_Y }; - win.setBounds(startAskPos); - - win.show(); - fadeWindow(win, 0, 1); - movementManager.animateWindow(otherWin, targets.listen.x, targets.listen.y); - movementManager.animateWindow(win, targets.ask.x, targets.ask.y); + win.setAlwaysOnTop(false); } + restoreClicks(); + win.hide(); } - } else { - const currentBounds = win.getBounds(); - fadeWindow( - win, 1, 0, FADE_DURATION, - () => win.hide() - ); - if (name === 'listen') { - if (!isOtherWinVisible) { - const targetX = currentBounds.x - ANIM_OFFSET_X; - movementManager.animateWindow(win, targetX, currentBounds.y); - } else { - const targetX = currentBounds.x - currentBounds.width; - movementManager.animateWindow(win, targetX, currentBounds.y); - } - } else if (name === 'ask') { - if (!isOtherWinVisible) { - const targetY = currentBounds.y - ANIM_OFFSET_Y; - movementManager.animateWindow(win, currentBounds.x, targetY); - } else { - const targetAskY = currentBounds.y - ANIM_OFFSET_Y; - movementManager.animateWindow(win, currentBounds.x, targetAskY); + return; + } - const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: false }); - if (targets.listen) { + if (name === 'listen' || name === 'ask') { + const otherName = name === 'listen' ? 'ask' : 'listen'; + const otherWin = windowPool.get(otherName); + const isOtherWinVisible = otherWin && !otherWin.isDestroyed() && otherWin.isVisible(); + + const ANIM_OFFSET_X = 100; + const ANIM_OFFSET_Y = 20; + + if (shouldBeVisible) { + win.setOpacity(0); + + if (name === 'listen') { + if (!isOtherWinVisible) { + const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: false }); + if (!targets.listen) return; + + const startPos = { x: targets.listen.x - ANIM_OFFSET_X, y: targets.listen.y }; + win.setBounds(startPos); + win.show(); + fadeWindow(win, 0, 1); + movementManager.animateWindow(win, targets.listen.x, targets.listen.y); + + } else { + const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: true }); + if (!targets.listen || !targets.ask) return; + + const startListenPos = { x: targets.listen.x - ANIM_OFFSET_X, y: targets.listen.y }; + win.setBounds(startListenPos); + + win.show(); + fadeWindow(win, 0, 1); + movementManager.animateWindow(otherWin, targets.ask.x, targets.ask.y); + movementManager.animateWindow(win, targets.listen.x, targets.listen.y); + } + } else if (name === 'ask') { + if (!isOtherWinVisible) { + const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: false, ask: true }); + if (!targets.ask) return; + + const startPos = { x: targets.ask.x, y: targets.ask.y - ANIM_OFFSET_Y }; + win.setBounds(startPos); + win.show(); + fadeWindow(win, 0, 1); + movementManager.animateWindow(win, targets.ask.x, targets.ask.y); + + } else { + const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: true }); + if (!targets.listen || !targets.ask) return; + + const startAskPos = { x: targets.ask.x, y: targets.ask.y - ANIM_OFFSET_Y }; + win.setBounds(startAskPos); + + win.show(); + fadeWindow(win, 0, 1); movementManager.animateWindow(otherWin, targets.listen.x, targets.listen.y); + movementManager.animateWindow(win, targets.ask.x, targets.ask.y); + } + } + } else { + const currentBounds = win.getBounds(); + fadeWindow( + win, 1, 0, FADE_DURATION, + () => win.hide() + ); + if (name === 'listen') { + if (!isOtherWinVisible) { + const targetX = currentBounds.x - ANIM_OFFSET_X; + movementManager.animateWindow(win, targetX, currentBounds.y); + } else { + const targetX = currentBounds.x - currentBounds.width; + movementManager.animateWindow(win, targetX, currentBounds.y); + } + } else if (name === 'ask') { + if (!isOtherWinVisible) { + const targetY = currentBounds.y - ANIM_OFFSET_Y; + movementManager.animateWindow(win, currentBounds.x, targetY); + } else { + const targetAskY = currentBounds.y - ANIM_OFFSET_Y; + movementManager.animateWindow(win, currentBounds.x, targetAskY); + + const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: false }); + if (targets.listen) { + movementManager.animateWindow(otherWin, targets.listen.x, targets.listen.y); + } } } } @@ -276,62 +396,6 @@ const resizeHeaderWindow = ({ width, height }) => { return { success: false, error: 'Header window not found' }; }; -const openShortcutEditor = () => { - const header = windowPool.get('header'); - if (!header) return; - globalShortcut.unregisterAll(); - createFeatureWindows(header, 'shortcut-settings'); -}; - -const showSettingsWindow = (bounds) => { - if (!bounds) return; - const win = windowPool.get('settings'); - if (win && !win.isDestroyed()) { - if (settingsHideTimer) { - clearTimeout(settingsHideTimer); - settingsHideTimer = null; - } - const header = windowPool.get('header'); - const headerBounds = header?.getBounds() ?? { x: 0, y: 0 }; - const settingsBounds = win.getBounds(); - const disp = getCurrentDisplay(header); - const { x: waX, y: waY, width: waW, height: waH } = disp.workArea; - let x = Math.round(headerBounds.x + (bounds?.x ?? 0) + (bounds?.width ?? 0) / 2 - settingsBounds.width / 2); - let y = Math.round(headerBounds.y + (bounds?.y ?? 0) + (bounds?.height ?? 0) + 31); - x = Math.max(waX + 10, Math.min(waX + waW - settingsBounds.width - 10, x)); - y = Math.max(waY + 10, Math.min(waY + waH - settingsBounds.height - 10, y)); - win.setBounds({ x, y }); - win.__lockedByButton = true; - win.show(); - win.moveTop(); - win.setAlwaysOnTop(true); - } -}; - -const hideSettingsWindow = () => { - const window = windowPool.get("settings"); - if (window && !window.isDestroyed()) { - if (settingsHideTimer) { - clearTimeout(settingsHideTimer); - } - settingsHideTimer = setTimeout(() => { - if (window && !window.isDestroyed()) { - window.setAlwaysOnTop(false); - window.hide(); - } - settingsHideTimer = null; - }, 200); - - window.__lockedByButton = false; - } -}; - -const cancelHideSettingsWindow = () => { - if (settingsHideTimer) { - clearTimeout(settingsHideTimer); - settingsHideTimer = null; - } -}; const openLoginPage = () => { const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000'; @@ -474,7 +538,7 @@ function createFeatureWindows(header, namesToCreate) { case 'shortcut-settings': { const shortcutEditor = new BrowserWindow({ ...commonChildOptions, - width: 420, + width: 353, height: 720, modal: false, parent: undefined, @@ -482,36 +546,11 @@ function createFeatureWindows(header, namesToCreate) { titleBarOverlay: false, }); + shortcutEditor.setContentProtection(isContentProtectionOn); + shortcutEditor.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); if (process.platform === 'darwin') { - shortcutEditor.setAlwaysOnTop(true, 'screen-saver'); - } else { - shortcutEditor.setAlwaysOnTop(true); + shortcutEditor.setWindowButtonVisibility(false); } - - /* ──────────[ ① 다른 창 클릭 차단 ]────────── */ - const disableClicks = () => { - for (const [name, win] of windowPool) { - if (win !== shortcutEditor && !win.isDestroyed()) { - win.setIgnoreMouseEvents(true, { forward: true }); - } - } - }; - const restoreClicks = () => { - for (const [, win] of windowPool) { - if (!win.isDestroyed()) win.setIgnoreMouseEvents(false); - } - }; - - const header = windowPool.get('header'); - if (header && !header.isDestroyed()) { - const { x, y, width } = header.getBounds(); - shortcutEditor.setBounds({ x, y, width }); - } - - shortcutEditor.once('ready-to-show', () => { - disableClicks(); - shortcutEditor.show(); - }); const loadOptions = { query: { view: 'shortcut-settings' } }; if (!shouldUseLiquidGlass) { @@ -526,23 +565,11 @@ function createFeatureWindows(header, namesToCreate) { } }); } - - shortcutEditor.on('closed', () => { - restoreClicks(); - windowPool.delete('shortcut-settings'); - console.log('[Shortcuts] Re-enabled after editing.'); - shortcutsService.registerShortcuts(); - }); - - shortcutEditor.webContents.once('dom-ready', async () => { - const keybinds = await shortcutsService.loadKeybinds(); - shortcutEditor.webContents.send('load-shortcuts', keybinds); - }); + windowPool.set('shortcut-settings', shortcutEditor); if (!app.isPackaged) { shortcutEditor.webContents.openDevTools({ mode: 'detach' }); } - windowPool.set('shortcut-settings', shortcutEditor); break; } } @@ -556,6 +583,7 @@ function createFeatureWindows(header, namesToCreate) { createFeatureWindow('listen'); createFeatureWindow('ask'); createFeatureWindow('settings'); + createFeatureWindow('shortcut-settings'); } } @@ -593,35 +621,7 @@ function getDisplayById(displayId) { -function toggleAllWindowsVisibility() { - const header = windowPool.get('header'); - if (!header) return; - - if (header.isVisible()) { - lastVisibleWindows.clear(); - - windowPool.forEach((win, name) => { - if (win && !win.isDestroyed() && win.isVisible()) { - lastVisibleWindows.add(name); - } - }); - - lastVisibleWindows.forEach(name => { - if (name === 'header') return; - const win = windowPool.get(name); - if (win && !win.isDestroyed()) win.hide(); - }); - header.hide(); - - return; - } - - lastVisibleWindows.forEach(name => { - const win = windowPool.get(name); - if (win && !win.isDestroyed()) - win.show(); - }); - } + function createWindows() { @@ -690,7 +690,7 @@ function createWindows() { }); setupIpcHandlers(movementManager); - setupAnimationController(windowPool, layoutManager, movementManager); + setupWindowController(windowPool, layoutManager, movementManager); if (currentHeaderState === 'main') { createFeatureWindows(header, ['listen', 'ask', 'settings', 'shortcut-settings']); @@ -850,13 +850,6 @@ const adjustWindowHeight = (sender, targetHeight) => { }; -const closeWindow = (windowName) => { - const win = windowPool.get(windowName); - if (win && !win.isDestroyed()) { - win.close(); - } -}; - module.exports = { updateLayout, createWindows, @@ -864,14 +857,11 @@ module.exports = { toggleContentProtection, resizeHeaderWindow, getContentProtectionStatus, - openShortcutEditor, showSettingsWindow, hideSettingsWindow, cancelHideSettingsWindow, openLoginPage, moveWindowStep, - closeWindow, - toggleAllWindowsVisibility, handleHeaderStateChanged, handleHeaderAnimationFinished, getHeaderPosition,