diff --git a/src/bridge/featureBridge.js b/src/bridge/featureBridge.js index eccccbe..4527ced 100644 --- a/src/bridge/featureBridge.js +++ b/src/bridge/featureBridge.js @@ -96,6 +96,7 @@ module.exports = { ipcMain.handle('listen:startMacosSystemAudio', async () => await listenService.handleStartMacosAudio()); ipcMain.handle('listen:stopMacosSystemAudio', async () => await listenService.handleStopMacosAudio()); ipcMain.handle('update-google-search-setting', async (event, enabled) => await listenService.handleUpdateGoogleSearchSetting(enabled)); + ipcMain.handle('listen:isSessionActive', async () => await listenService.isSessionActive()); ipcMain.handle('listen:changeSession', async (event, listenButtonText) => { console.log('[FeatureBridge] listen:changeSession from mainheader', listenButtonText); try { diff --git a/src/bridge/windowBridge.js b/src/bridge/windowBridge.js index 6555049..0313add 100644 --- a/src/bridge/windowBridge.js +++ b/src/bridge/windowBridge.js @@ -24,7 +24,6 @@ module.exports = { ipcMain.on('header-state-changed', (event, state) => windowManager.handleHeaderStateChanged(state)); ipcMain.on('header-animation-finished', (event, state) => windowManager.handleHeaderAnimationFinished(state)); ipcMain.handle('get-header-position', () => windowManager.getHeaderPosition()); - 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)); }, diff --git a/src/features/listen/listenService.js b/src/features/listen/listenService.js index 33b7cfc..1533588 100644 --- a/src/features/listen/listenService.js +++ b/src/features/listen/listenService.js @@ -54,53 +54,7 @@ class ListenService { } async handleListenRequest(listenButtonText) { - const { windowPool, updateLayout } = require('../../window/windowManager'); - const listenWindow = windowPool.get('listen'); - const header = windowPool.get('header'); - - try { - switch (listenButtonText) { - case 'Listen': - console.log('[ListenService] changeSession to "Listen"'); - listenWindow.show(); - updateLayout(); - listenWindow.webContents.send('window-show-animation'); - await this.initializeSession(); - listenWindow.webContents.send('session-state-changed', { isActive: true }); - break; - - case 'Stop': - console.log('[ListenService] changeSession to "Stop"'); - await this.closeSession(); - listenWindow.webContents.send('session-state-changed', { isActive: false }); - break; - - case 'Done': - console.log('[ListenService] changeSession to "Done"'); - listenWindow.webContents.send('window-hide-animation'); - listenWindow.webContents.send('session-state-changed', { isActive: false }); - break; - - default: - throw new Error(`[ListenService] unknown listenButtonText: ${listenButtonText}`); - } - - header.webContents.send('listen:changeSessionResult', { success: true }); - - } catch (error) { - console.error('[ListenService] error in handleListenRequest:', error); - header.webContents.send('listen:changeSessionResult', { success: false }); - throw error; - } - } - - initialize() { - this.setupIpcHandlers(); - console.log('[ListenService] Initialized and ready.'); - } - - async handleListenRequest(listenButtonText) { - const { windowPool, updateLayout } = require('../../window/windowManager'); + const { windowPool } = require('../../window/windowManager'); const listenWindow = windowPool.get('listen'); const header = windowPool.get('header'); diff --git a/src/features/shortcuts/shortcutsService.js b/src/features/shortcuts/shortcutsService.js index 44ad6e7..ec2cc2c 100644 --- a/src/features/shortcuts/shortcutsService.js +++ b/src/features/shortcuts/shortcutsService.js @@ -8,13 +8,11 @@ class ShortcutsService { constructor() { this.lastVisibleWindows = new Set(['header']); this.mouseEventsIgnored = false; - this.movementManager = null; this.windowPool = null; this.allWindowVisibility = true; } - initialize(movementManager, windowPool) { - this.movementManager = movementManager; + initialize(windowPool) { this.windowPool = windowPool; internalBridge.on('reregister-shortcuts', () => { console.log('[ShortcutsService] Reregistering shortcuts due to header state change.'); @@ -138,7 +136,7 @@ class ShortcutsService { } async registerShortcuts(registerOnlyToggleVisibility = false) { - if (!this.movementManager || !this.windowPool) { + if (!this.windowPool) { console.error('[Shortcuts] Service not initialized. Cannot register shortcuts.'); return; } @@ -179,7 +177,7 @@ class ShortcutsService { if (displays.length > 1) { displays.forEach((display, index) => { const key = `${modifier}+Shift+${index + 1}`; - globalShortcut.register(key, () => this.movementManager.moveToDisplay(display.id)); + globalShortcut.register(key, () => internalBridge.emit('window:moveToDisplay', { displayId: display.id })); }); } @@ -190,7 +188,7 @@ class ShortcutsService { ]; edgeDirections.forEach(({ key, direction }) => { globalShortcut.register(key, () => { - if (header && header.isVisible()) this.movementManager.moveToEdge(direction); + if (header && header.isVisible()) internalBridge.emit('window:moveToEdge', { direction }); }); }); @@ -232,16 +230,16 @@ class ShortcutsService { }; break; case 'moveUp': - callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('up'); }; + callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'up' }); }; break; case 'moveDown': - callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('down'); }; + callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'down' }); }; break; case 'moveLeft': - callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('left'); }; + callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'left' }); }; break; case 'moveRight': - callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('right'); }; + callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'right' }); }; break; case 'toggleClickThrough': callback = () => { diff --git a/src/preload.js b/src/preload.js index 2cd6f9c..6d16963 100644 --- a/src/preload.js +++ b/src/preload.js @@ -290,7 +290,7 @@ contextBridge.exposeInMainWorld('api', { stopMacosSystemAudio: () => ipcRenderer.invoke('listen:stopMacosSystemAudio'), // Session Management - isSessionActive: () => ipcRenderer.invoke('is-session-active'), + isSessionActive: () => ipcRenderer.invoke('listen:isSessionActive'), // Listeners onSystemAudioData: (callback) => ipcRenderer.on('system-audio-data', callback), diff --git a/src/ui/listen/audioCore/listenCapture.js b/src/ui/listen/audioCore/listenCapture.js index 2f52f25..ade889e 100644 --- a/src/ui/listen/audioCore/listenCapture.js +++ b/src/ui/listen/audioCore/listenCapture.js @@ -422,6 +422,12 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu try { if (isMacOS) { + + const sessionActive = await window.api.listenCapture.isSessionActive(); + if (!sessionActive) { + throw new Error('STT sessions not initialized - please wait for initialization to complete'); + } + // On macOS, use SystemAudioDump for audio and getDisplayMedia for screen console.log('Starting macOS capture with SystemAudioDump...'); @@ -466,6 +472,12 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu console.log('macOS screen capture started - audio handled by SystemAudioDump'); } else if (isLinux) { + + const sessionActive = await window.api.listenCapture.isSessionActive(); + if (!sessionActive) { + throw new Error('STT sessions not initialized - please wait for initialization to complete'); + } + // Linux - use display media for screen capture and getUserMedia for microphone mediaStream = await navigator.mediaDevices.getDisplayMedia({ video: { diff --git a/src/ui/settings/ShortCutSettingsView.js b/src/ui/settings/ShortCutSettingsView.js index 691a059..808eab4 100644 --- a/src/ui/settings/ShortCutSettingsView.js +++ b/src/ui/settings/ShortCutSettingsView.js @@ -171,6 +171,7 @@ export class ShortcutSettingsView extends LitElement { async handleSave() { if (!window.api) return; + this.feedback = {}; const result = await window.api.shortcutSettingsView.saveShortcuts(this.shortcuts); if (!result.success) { alert('Failed to save shortcuts: ' + result.error); @@ -179,6 +180,7 @@ export class ShortcutSettingsView extends LitElement { handleClose() { if (!window.api) return; + this.feedback = {}; window.api.shortcutSettingsView.closeShortcutSettingsWindow(); } diff --git a/src/window/smoothMovementManager.js b/src/window/smoothMovementManager.js index c81f0a4..63a8ab8 100644 --- a/src/window/smoothMovementManager.js +++ b/src/window/smoothMovementManager.js @@ -1,11 +1,8 @@ const { screen } = require('electron'); class SmoothMovementManager { - constructor(windowPool, getDisplayById, getCurrentDisplay, updateLayout) { + constructor(windowPool) { this.windowPool = windowPool; - this.getDisplayById = getDisplayById; - this.getCurrentDisplay = getCurrentDisplay; - this.updateLayout = updateLayout; this.stepSize = 80; this.animationDuration = 300; this.headerPosition = { x: 0, y: 0 }; @@ -14,6 +11,8 @@ class SmoothMovementManager { this.lastVisiblePosition = null; this.currentDisplayId = null; this.animationFrameId = null; + + this.animationTimers = new Map(); } /** @@ -22,164 +21,25 @@ class SmoothMovementManager { */ _isWindowValid(win) { if (!win || win.isDestroyed()) { - if (this.isAnimating) { - console.warn('[MovementManager] Window destroyed mid-animation. Halting.'); - this.isAnimating = false; - if (this.animationFrameId) { - clearTimeout(this.animationFrameId); - this.animationFrameId = null; - } + // 해당 창의 타이머가 있으면 정리 + if (this.animationTimers.has(win)) { + clearTimeout(this.animationTimers.get(win)); + this.animationTimers.delete(win); } return false; } return true; } - moveToDisplay(displayId) { - const header = this.windowPool.get('header'); - if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return; - - const targetDisplay = this.getDisplayById(displayId); - if (!targetDisplay) return; - - const currentBounds = header.getBounds(); - const currentDisplay = this.getCurrentDisplay(header); - - if (currentDisplay.id === targetDisplay.id) return; - - const relativeX = (currentBounds.x - currentDisplay.workArea.x) / currentDisplay.workAreaSize.width; - const relativeY = (currentBounds.y - currentDisplay.workArea.y) / currentDisplay.workAreaSize.height; - const targetX = targetDisplay.workArea.x + targetDisplay.workAreaSize.width * relativeX; - const targetY = targetDisplay.workArea.y + targetDisplay.workAreaSize.height * relativeY; - - const finalX = Math.max(targetDisplay.workArea.x, Math.min(targetDisplay.workArea.x + targetDisplay.workAreaSize.width - currentBounds.width, targetX)); - const finalY = Math.max(targetDisplay.workArea.y, Math.min(targetDisplay.workArea.y + targetDisplay.workAreaSize.height - currentBounds.height, targetY)); - - this.headerPosition = { x: currentBounds.x, y: currentBounds.y }; - this.animateToPosition(header, finalX, finalY); - this.currentDisplayId = targetDisplay.id; - } - - hideToEdge(edge, callback, { instant = false } = {}) { - const header = this.windowPool.get('header'); - if (!header || header.isDestroyed()) { - if (typeof callback === 'function') callback(); - return; - } - - const { x, y } = header.getBounds(); - this.lastVisiblePosition = { x, y }; - this.hiddenPosition = { edge }; - - if (instant) { - header.hide(); - if (typeof callback === 'function') callback(); - return; - } - - header.webContents.send('window-hide-animation'); - - setTimeout(() => { - if (!header.isDestroyed()) header.hide(); - if (typeof callback === 'function') callback(); - }, 5); - } - - showFromEdge(callback) { - const header = this.windowPool.get('header'); - if (!header || header.isDestroyed()) { - if (typeof callback === 'function') callback(); - return; - } - - // 숨기기 전에 기억해둔 위치 복구 - if (this.lastVisiblePosition) { - header.setPosition( - this.lastVisiblePosition.x, - this.lastVisiblePosition.y, - false // animate: false - ); - } - - header.show(); - header.webContents.send('window-show-animation'); - - // 내부 상태 초기화 - this.hiddenPosition = null; - this.lastVisiblePosition = null; - - if (typeof callback === 'function') callback(); - } - - moveStep(direction) { - const header = this.windowPool.get('header'); - if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return; - - const currentBounds = header.getBounds(); - this.headerPosition = { x: currentBounds.x, y: currentBounds.y }; - let targetX = this.headerPosition.x; - let targetY = this.headerPosition.y; - - console.log(`[MovementManager] Moving ${direction} from (${targetX}, ${targetY})`); - - const windowSize = { - width: currentBounds.width, - height: currentBounds.height - }; - - switch (direction) { - case 'left': targetX -= this.stepSize; break; - case 'right': targetX += this.stepSize; break; - case 'up': targetY -= this.stepSize; break; - case 'down': targetY += this.stepSize; break; - default: return; - } - - // Find the display that contains or is nearest to the target position - const nearestDisplay = screen.getDisplayNearestPoint({ x: targetX, y: targetY }); - const { x: workAreaX, y: workAreaY, width: workAreaWidth, height: workAreaHeight } = nearestDisplay.workArea; - - // Only clamp if the target position would actually go out of bounds - let clampedX = targetX; - let clampedY = targetY; - - // Check horizontal bounds - if (targetX < workAreaX) { - clampedX = workAreaX; - } else if (targetX + currentBounds.width > workAreaX + workAreaWidth) { - clampedX = workAreaX + workAreaWidth - currentBounds.width; - } - - // Check vertical bounds - if (targetY < workAreaY) { - clampedY = workAreaY; - console.log(`[MovementManager] Clamped Y to top edge: ${clampedY}`); - } else if (targetY + currentBounds.height > workAreaY + workAreaHeight) { - clampedY = workAreaY + workAreaHeight - currentBounds.height; - console.log(`[MovementManager] Clamped Y to bottom edge: ${clampedY}`); - } - - console.log(`[MovementManager] Final position: (${clampedX}, ${clampedY}), Work area: ${workAreaX},${workAreaY} ${workAreaWidth}x${workAreaHeight}`); - - // Only move if there's an actual change in position - if (clampedX === this.headerPosition.x && clampedY === this.headerPosition.y) { - console.log(`[MovementManager] No position change, skipping animation`); - return; - } - - this.animateToPosition(header, clampedX, clampedY, windowSize); - } - /** - * [수정됨] 창을 목표 지점으로 부드럽게 애니메이션합니다. - * 완료 콜백 및 기타 옵션을 지원합니다. - * @param {BrowserWindow} win - 애니메이션할 창 - * @param {number} targetX - 목표 X 좌표 - * @param {number} targetY - 목표 Y 좌표 - * @param {object} [options] - 추가 옵션 - * @param {object} [options.sizeOverride] - 애니메이션 중 사용할 창 크기 - * @param {function} [options.onComplete] - 애니메이션 완료 후 실행할 콜백 - * @param {number} [options.duration] - 애니메이션 지속 시간 (ms) + * + * @param {BrowserWindow} win + * @param {number} targetX + * @param {number} targetY + * @param {object} [options] + * @param {object} [options.sizeOverride] + * @param {function} [options.onComplete] + * @param {number} [options.duration] */ animateWindow(win, targetX, targetY, options = {}) { if (!this._isWindowValid(win)) { @@ -194,7 +54,6 @@ class SmoothMovementManager { const { width, height } = sizeOverride || start; const step = () => { - // 애니메이션 중간에 창이 파괴될 경우 콜백을 실행하고 중단 if (!this._isWindowValid(win)) { if (onComplete) onComplete(); return; @@ -208,112 +67,116 @@ class SmoothMovementManager { win.setBounds({ x: Math.round(x), y: Math.round(y), width, height }); if (p < 1) { - setTimeout(step, 8); // requestAnimationFrame 대신 setTimeout으로 간결하게 처리 + setTimeout(step, 8); } else { - // 애니메이션 종료 - this.updateLayout(); // 레이아웃 재정렬 + this.layoutManager.updateLayout(); if (onComplete) { - onComplete(); // 완료 콜백 실행 + onComplete(); } } }; step(); } - animateToPosition(header, targetX, targetY, windowSize) { - if (!this._isWindowValid(header)) return; - - this.isAnimating = true; - const startX = this.headerPosition.x; - const startY = this.headerPosition.y; + fade(win, { from, to, duration = 250, onComplete }) { + if (!this._isWindowValid(win)) { + if (onComplete) onComplete(); + return; + } + const startOpacity = from ?? win.getOpacity(); const startTime = Date.now(); + + const step = () => { + if (!this._isWindowValid(win)) { + if (onComplete) onComplete(); return; + } + const progress = Math.min(1, (Date.now() - startTime) / duration); + const eased = 1 - Math.pow(1 - progress, 3); + win.setOpacity(startOpacity + (to - startOpacity) * eased); + + if (progress < 1) { + setTimeout(step, 8); + } else { + win.setOpacity(to); + if (onComplete) onComplete(); + } + }; + step(); + } + + animateWindowBounds(win, targetBounds, options = {}) { + if (this.animationTimers.has(win)) { + clearTimeout(this.animationTimers.get(win)); + } - if (!Number.isFinite(targetX) || !Number.isFinite(targetY) || !Number.isFinite(startX) || !Number.isFinite(startY)) { - this.isAnimating = false; + if (!this._isWindowValid(win)) { + if (options.onComplete) options.onComplete(); return; } - const animate = () => { - if (!this._isWindowValid(header)) return; + this.isAnimating = true; - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / this.animationDuration, 1); - const eased = 1 - Math.pow(1 - progress, 3); - const currentX = startX + (targetX - startX) * eased; - const currentY = startY + (targetY - startY) * eased; - - if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) { - this.isAnimating = false; + const startBounds = win.getBounds(); + const startTime = Date.now(); + const duration = options.duration || this.animationDuration; + + const step = () => { + if (!this._isWindowValid(win)) { + if (options.onComplete) options.onComplete(); return; } - - if (!this._isWindowValid(header)) return; - const { width, height } = windowSize || header.getBounds(); - header.setBounds({ - x: Math.round(currentX), - y: Math.round(currentY), - width, - height - }); - + + const progress = Math.min(1, (Date.now() - startTime) / duration); + const eased = 1 - Math.pow(1 - progress, 3); + + const newBounds = { + x: Math.round(startBounds.x + (targetBounds.x - startBounds.x) * eased), + y: Math.round(startBounds.y + (targetBounds.y - startBounds.y) * eased), + width: Math.round(startBounds.width + ((targetBounds.width ?? startBounds.width) - startBounds.width) * eased), + height: Math.round(startBounds.height + ((targetBounds.height ?? startBounds.height) - startBounds.height) * eased), + }; + win.setBounds(newBounds); + if (progress < 1) { - this.animationFrameId = setTimeout(animate, 8); + const timerId = setTimeout(step, 8); + this.animationTimers.set(win, timerId); } else { - this.animationFrameId = null; - this.isAnimating = false; - if (Number.isFinite(targetX) && Number.isFinite(targetY)) { - if (!this._isWindowValid(header)) return; - header.setPosition(Math.round(targetX), Math.round(targetY)); - // Update header position to the actual final position - this.headerPosition = { x: Math.round(targetX), y: Math.round(targetY) }; + win.setBounds(targetBounds); + this.animationTimers.delete(win); + + if (this.animationTimers.size === 0) { + this.isAnimating = false; } - this.updateLayout(); + + if (options.onComplete) options.onComplete(); } }; - animate(); + step(); } - - moveToEdge(direction) { - const header = this.windowPool.get('header'); - if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return; - - const display = this.getCurrentDisplay(header); - const { width, height } = display.workAreaSize; - const { x: workAreaX, y: workAreaY } = display.workArea; - const currentBounds = header.getBounds(); - - const windowSize = { - width: currentBounds.width, - height: currentBounds.height - }; - - let targetX = currentBounds.x; - let targetY = currentBounds.y; - - switch (direction) { - case 'left': - targetX = workAreaX; - break; - case 'right': - targetX = workAreaX + width - windowSize.width; - break; - case 'up': - targetY = workAreaY; - break; - case 'down': - targetY = workAreaY + height - windowSize.height; - break; + + animateWindowPosition(win, targetPosition, options = {}) { + if (!this._isWindowValid(win)) { + if (options.onComplete) options.onComplete(); + return; + } + const currentBounds = win.getBounds(); + const targetBounds = { ...currentBounds, ...targetPosition }; + this.animateWindowBounds(win, targetBounds, options); + } + + animateLayout(layout, animated = true) { + if (!layout) return; + for (const winName in layout) { + const win = this.windowPool.get(winName); + const targetBounds = layout[winName]; + if (win && !win.isDestroyed() && targetBounds) { + if (animated) { + this.animateWindowBounds(win, targetBounds); + } else { + win.setBounds(targetBounds); + } + } } - - header.setBounds({ - x: Math.round(targetX), - y: Math.round(targetY), - width: windowSize.width, - height: windowSize.height - }); - - this.headerPosition = { x: targetX, y: targetY }; - this.updateLayout(); } destroy() { diff --git a/src/window/windowLayoutManager.js b/src/window/windowLayoutManager.js index cdd0ef3..07965bb 100644 --- a/src/window/windowLayoutManager.js +++ b/src/window/windowLayoutManager.js @@ -27,130 +27,15 @@ class WindowLayoutManager { this.PADDING = 80; } - updateLayout() { - if (this.isUpdating) return; - this.isUpdating = true; - - setImmediate(() => { - this.positionWindows(); - this.isUpdating = false; - }); - } - - /** - * - * @param {object} [visibilityOverride] - { listen: true, ask: true } - * @returns {{listen: {x:number, y:number}|null, ask: {x:number, y:number}|null}} - */ - getTargetBoundsForFeatureWindows(visibilityOverride = {}) { + getHeaderPosition = () => { const header = this.windowPool.get('header'); - if (!header?.getBounds) return {}; - - const headerBounds = header.getBounds(); - const display = getCurrentDisplay(header); - const { width: screenWidth, height: screenHeight } = display.workAreaSize; - const { x: workAreaX, y: workAreaY } = display.workArea; - - const ask = this.windowPool.get('ask'); - const listen = this.windowPool.get('listen'); - - const askVis = visibilityOverride.ask !== undefined ? - visibilityOverride.ask : - (ask && ask.isVisible() && !ask.isDestroyed()); - const listenVis = visibilityOverride.listen !== undefined ? - visibilityOverride.listen : - (listen && listen.isVisible() && !listen.isDestroyed()); - - if (!askVis && !listenVis) return {}; - - const PAD = 8; - const headerTopRel = headerBounds.y - workAreaY; - const headerBottomRel = headerTopRel + headerBounds.height; - const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2; - - const relativeX = headerCenterXRel / screenWidth; - const relativeY = (headerBounds.y - workAreaY) / screenHeight; - const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY); - - const askB = ask ? ask.getBounds() : null; - const listenB = listen ? listen.getBounds() : null; - - const result = { listen: null, ask: null }; - - if (askVis && listenVis) { - let askXRel = headerCenterXRel - (askB.width / 2); - let listenXRel = askXRel - listenB.width - PAD; - - if (listenXRel < PAD) { - listenXRel = PAD; - askXRel = listenXRel + listenB.width + PAD; - } - if (askXRel + askB.width > screenWidth - PAD) { - askXRel = screenWidth - PAD - askB.width; - listenXRel = askXRel - listenB.width - PAD; - } - - // [수정] 'above'일 경우 하단 정렬, 'below'일 경우 상단 정렬 - if (strategy.primary === 'above') { - const windowBottomAbs = headerBounds.y - PAD; - const askY = windowBottomAbs - askB.height; - const listenY = windowBottomAbs - listenB.height; - result.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(askY) }; - result.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(listenY) }; - } else { // 'below' - const yPos = headerBottomRel + PAD; - const yAbs = yPos + workAreaY; - result.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(yAbs) }; - result.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(yAbs) }; - } - - } else { // 한 창만 보일 때는 기존 로직 유지 (정상 동작 확인) - const winB = askVis ? askB : listenB; - let xRel = headerCenterXRel - winB.width / 2; - xRel = Math.max(PAD, Math.min(screenWidth - winB.width - PAD, xRel)); - - let yPos; - if (strategy.primary === 'above') { - const windowBottomRel = headerTopRel - PAD; - yPos = windowBottomRel - winB.height; - } else { // 'below' - yPos = headerBottomRel + PAD; - } - - const abs = { x: Math.round(xRel + workAreaX), y: Math.round(yPos + workAreaY) }; - if (askVis) result.ask = abs; - if (listenVis) result.listen = abs; + if (header) { + const [x, y] = header.getPosition(); + return { x, y }; } - return result; - } + return { x: 0, y: 0 }; + }; - positionWindows() { - const header = this.windowPool.get('header'); - if (!header?.getBounds) return; - - const headerBounds = header.getBounds(); - const display = getCurrentDisplay(header); - const { width: screenWidth, height: screenHeight } = display.workAreaSize; - const { x: workAreaX, y: workAreaY } = display.workArea; - - const headerCenterX = headerBounds.x - workAreaX + headerBounds.width / 2; - const headerCenterY = headerBounds.y - workAreaY + headerBounds.height / 2; - - const relativeX = headerCenterX / screenWidth; - const relativeY = headerCenterY / screenHeight; - - const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY); - - this.positionFeatureWindows(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 }); - } - } - } /** * @@ -179,67 +64,6 @@ class WindowLayoutManager { } - positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) { - const ask = this.windowPool.get('ask'); - const listen = this.windowPool.get('listen'); - const askVisible = ask && ask.isVisible() && !ask.isDestroyed(); - const listenVisible = listen && listen.isVisible() && !listen.isDestroyed(); - - if (!askVisible && !listenVisible) return; - - const PAD = 8; - const headerTopRel = headerBounds.y - workAreaY; - const headerBottomRel = headerTopRel + headerBounds.height; - const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2; - - let askBounds = askVisible ? ask.getBounds() : null; - let listenBounds = listenVisible ? listen.getBounds() : null; - - if (askVisible && listenVisible) { - let askXRel = headerCenterXRel - (askBounds.width / 2); - let listenXRel = askXRel - listenBounds.width - PAD; - - if (listenXRel < PAD) { - listenXRel = PAD; - askXRel = listenXRel + listenBounds.width + PAD; - } - if (askXRel + askBounds.width > screenWidth - PAD) { - askXRel = screenWidth - PAD - askBounds.width; - listenXRel = askXRel - listenBounds.width - PAD; - } - - // [수정] 'above'일 경우 하단 정렬, 'below'일 경우 상단 정렬 - if (strategy.primary === 'above') { - const windowBottomAbs = headerBounds.y - PAD; - const askY = windowBottomAbs - askBounds.height; - const listenY = windowBottomAbs - listenBounds.height; - ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(askY), width: askBounds.width, height: askBounds.height }); - listen.setBounds({ x: Math.round(listenXRel + workAreaX), y: Math.round(listenY), width: listenBounds.width, height: listenBounds.height }); - } else { // 'below' - const yPos = headerBottomRel + PAD; - const yAbs = yPos + workAreaY; - ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(yAbs), width: askBounds.width, height: askBounds.height }); - listen.setBounds({ x: Math.round(listenXRel + workAreaX), y: Math.round(yAbs), width: listenBounds.width, height: listenBounds.height }); - } - - } else { // 한 창만 보일 때는 기존 로직 유지 (정상 동작 확인) - const win = askVisible ? ask : listen; - const winBounds = askVisible ? askBounds : listenBounds; - let xRel = headerCenterXRel - winBounds.width / 2; - xRel = Math.max(PAD, Math.min(screenWidth - winBounds.width - PAD, xRel)); - - let yPos; - if (strategy.primary === 'above') { - const windowBottomRel = headerTopRel - PAD; - yPos = windowBottomRel - winBounds.height; - } else { // 'below' - yPos = headerBottomRel + PAD; - } - const yAbs = yPos + workAreaY; - - win.setBounds({ x: Math.round(xRel + workAreaX), y: Math.round(yAbs), width: winBounds.width, height: winBounds.height }); - } - } /** * @returns {{x: number, y: number} | null} @@ -269,26 +93,205 @@ class WindowLayoutManager { return { x: Math.round(clampedX), y: Math.round(clampedY) }; } - positionShortcutSettingsWindow() { + + calculateHeaderResize(header, { width, height }) { + if (!header) return null; + const currentBounds = header.getBounds(); + const centerX = currentBounds.x + currentBounds.width / 2; + const newX = Math.round(centerX - width / 2); + const display = getCurrentDisplay(header); + const { x: workAreaX, width: workAreaWidth } = display.workArea; + const clampedX = Math.max(workAreaX, Math.min(workAreaX + workAreaWidth - width, newX)); + return { x: clampedX, y: currentBounds.y, width, height }; + } + + calculateClampedPosition(header, { x: newX, y: newY }) { + if (!header) return null; + const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY }); + const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea; + const headerBounds = header.getBounds(); + const clampedX = Math.max(workAreaX, Math.min(newX, workAreaX + width - headerBounds.width)); + const clampedY = Math.max(workAreaY, Math.min(newY, workAreaY + height - headerBounds.height)); + return { x: clampedX, y: clampedY }; + } + + calculateWindowHeightAdjustment(senderWindow, targetHeight) { + if (!senderWindow) return null; + const currentBounds = senderWindow.getBounds(); + const minHeight = senderWindow.getMinimumSize()[1]; + const maxHeight = senderWindow.getMaximumSize()[1]; + let adjustedHeight = Math.max(minHeight, targetHeight); + if (maxHeight > 0) { + adjustedHeight = Math.min(maxHeight, adjustedHeight); + } + return { ...currentBounds, height: adjustedHeight }; + } + + // 기존 getTargetBoundsForFeatureWindows를 이 함수로 대체합니다. + calculateFeatureWindowLayout(visibility, headerBoundsOverride = null) { + const header = this.windowPool.get('header'); + const headerBounds = headerBoundsOverride || (header ? header.getBounds() : null); + + if (!headerBounds) return {}; + + let display; + if (headerBoundsOverride) { + const boundsCenter = { + x: headerBounds.x + headerBounds.width / 2, + y: headerBounds.y + headerBounds.height / 2, + }; + display = screen.getDisplayNearestPoint(boundsCenter); + } else { + display = getCurrentDisplay(header); + } + + const { width: screenWidth, height: screenHeight, x: workAreaX, y: workAreaY } = display.workArea; + + const ask = this.windowPool.get('ask'); + const listen = this.windowPool.get('listen'); + + const askVis = visibility.ask && ask && !ask.isDestroyed(); + const listenVis = visibility.listen && listen && !listen.isDestroyed(); + + if (!askVis && !listenVis) return {}; + + const PAD = 8; + const headerTopRel = headerBounds.y - workAreaY; + const headerBottomRel = headerTopRel + headerBounds.height; + const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2; + + const relativeX = headerCenterXRel / screenWidth; + const relativeY = (headerBounds.y - workAreaY) / screenHeight; + const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY); + + const askB = askVis ? ask.getBounds() : null; + const listenB = listenVis ? listen.getBounds() : null; + + if (askVis) { + console.log(`[Layout Debug] Ask Window Bounds: height=${askB.height}, width=${askB.width}`); + } + if (listenVis) { + console.log(`[Layout Debug] Listen Window Bounds: height=${listenB.height}, width=${listenB.width}`); + } + + const layout = {}; + + if (askVis && listenVis) { + let askXRel = headerCenterXRel - (askB.width / 2); + let listenXRel = askXRel - listenB.width - PAD; + + if (listenXRel < PAD) { + listenXRel = PAD; + askXRel = listenXRel + listenB.width + PAD; + } + if (askXRel + askB.width > screenWidth - PAD) { + askXRel = screenWidth - PAD - askB.width; + listenXRel = askXRel - listenB.width - PAD; + } + + if (strategy.primary === 'above') { + const windowBottomAbs = headerBounds.y - PAD; + layout.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(windowBottomAbs - askB.height), width: askB.width, height: askB.height }; + layout.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(windowBottomAbs - listenB.height), width: listenB.width, height: listenB.height }; + } else { // 'below' + const yAbs = headerBounds.y + headerBounds.height + PAD; + layout.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(yAbs), width: askB.width, height: askB.height }; + layout.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(yAbs), width: listenB.width, height: listenB.height }; + } + } else { // Single window + const winName = askVis ? 'ask' : 'listen'; + const winB = askVis ? askB : listenB; + if (!winB) return {}; + + let xRel = headerCenterXRel - winB.width / 2; + xRel = Math.max(PAD, Math.min(screenWidth - winB.width - PAD, xRel)); + + let yPos; + if (strategy.primary === 'above') { + yPos = (headerBounds.y - workAreaY) - PAD - winB.height; + } else { // 'below' + yPos = (headerBounds.y - workAreaY) + headerBounds.height + PAD; + } + + layout[winName] = { x: Math.round(xRel + workAreaX), y: Math.round(yPos + workAreaY), width: winB.width, height: winB.height }; + } + return layout; + } + + calculateShortcutSettingsWindowPosition() { const header = this.windowPool.get('header'); const shortcutSettings = this.windowPool.get('shortcut-settings'); - - if (!header || header.isDestroyed() || !shortcutSettings || shortcutSettings.isDestroyed()) { - return; - } - + if (!header || !shortcutSettings) return null; + const headerBounds = header.getBounds(); const shortcutBounds = shortcutSettings.getBounds(); - const display = getCurrentDisplay(header); - const { workArea } = display; - + const { workArea } = getCurrentDisplay(header); + 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)); + + return { x: newX, y: newY, width: shortcutBounds.width, height: shortcutBounds.height }; + } - shortcutSettings.setBounds({ x: newX, y: newY, width: shortcutBounds.width, height: shortcutBounds.height }); + calculateStepMovePosition(header, direction) { + if (!header) return null; + const currentBounds = header.getBounds(); + const stepSize = 80; // 이동 간격 + let targetX = currentBounds.x; + let targetY = currentBounds.y; + + switch (direction) { + case 'left': targetX -= stepSize; break; + case 'right': targetX += stepSize; break; + case 'up': targetY -= stepSize; break; + case 'down': targetY += stepSize; break; + } + + return this.calculateClampedPosition(header, { x: targetX, y: targetY }); + } + + calculateEdgePosition(header, direction) { + if (!header) return null; + const display = getCurrentDisplay(header); + const { workArea } = display; + const currentBounds = header.getBounds(); + + let targetX = currentBounds.x; + let targetY = currentBounds.y; + + switch (direction) { + case 'left': targetX = workArea.x; break; + case 'right': targetX = workArea.x + workArea.width - currentBounds.width; break; + case 'up': targetY = workArea.y; break; + case 'down': targetY = workArea.y + workArea.height - currentBounds.height; break; + } + return { x: targetX, y: targetY }; + } + + calculateNewPositionForDisplay(window, targetDisplayId) { + if (!window) return null; + + const targetDisplay = screen.getAllDisplays().find(d => d.id === targetDisplayId); + if (!targetDisplay) return null; + + const currentBounds = window.getBounds(); + const currentDisplay = getCurrentDisplay(window); + + if (currentDisplay.id === targetDisplay.id) return { x: currentBounds.x, y: currentBounds.y }; + + const relativeX = (currentBounds.x - currentDisplay.workArea.x) / currentDisplay.workArea.width; + const relativeY = (currentBounds.y - currentDisplay.workArea.y) / currentDisplay.workArea.height; + + const targetX = targetDisplay.workArea.x + targetDisplay.workArea.width * relativeX; + const targetY = targetDisplay.workArea.y + targetDisplay.workArea.height * relativeY; + + const clampedX = Math.max(targetDisplay.workArea.x, Math.min(targetX, targetDisplay.workArea.x + targetDisplay.workArea.width - currentBounds.width)); + const clampedY = Math.max(targetDisplay.workArea.y, Math.min(targetY, targetDisplay.workArea.y + targetDisplay.workArea.height - currentBounds.height)); + + return { x: Math.round(clampedX), y: Math.round(clampedY) }; } /** diff --git a/src/window/windowManager.js b/src/window/windowManager.js index 692199e..5e29ec5 100644 --- a/src/window/windowManager.js +++ b/src/window/windowManager.js @@ -30,8 +30,6 @@ if (shouldUseLiquidGlass) { let isContentProtectionOn = true; let lastVisibleWindows = new Set(['header']); -const HEADER_HEIGHT = 47; -const DEFAULT_WINDOW_WIDTH = 353; let currentHeaderState = 'apikey'; const windowPool = new Map(); @@ -40,50 +38,26 @@ let settingsHideTimer = null; let layoutManager = null; -function updateLayout() { - if (layoutManager) { - layoutManager.updateLayout(); - } -} let movementManager = null; -const FADE_DURATION = 250; -const FADE_FPS = 60; +function updateChildWindowLayouts(animated = true) { + if (movementManager.isAnimating) return; -/** - * 윈도우 투명도를 서서히 변경한다. - * @param {BrowserWindow} win - * @param {number} from - * @param {number} to - * @param {number} duration - * @param {Function=} onComplete - */ -function fadeWindow(win, from, to, duration = FADE_DURATION, onComplete) { - if (!win || win.isDestroyed()) return; - - const steps = Math.max(1, Math.round(duration / (1000 / FADE_FPS))); - let currentStep = 0; - - win.setOpacity(from); - - const timer = setInterval(() => { - if (win.isDestroyed()) { clearInterval(timer); return; } - - currentStep += 1; - const progress = currentStep / steps; - const eased = progress < 1 - ? 1 - Math.pow(1 - progress, 3) - : 1; - - win.setOpacity(from + (to - from) * eased); - - if (currentStep >= steps) { - clearInterval(timer); - win.setOpacity(to); - onComplete && onComplete(); + const visibleWindows = {}; + const listenWin = windowPool.get('listen'); + const askWin = windowPool.get('ask'); + if (listenWin && !listenWin.isDestroyed() && listenWin.isVisible()) { + visibleWindows.listen = true; } - }, 1000 / FADE_FPS); + if (askWin && !askWin.isDestroyed() && askWin.isVisible()) { + visibleWindows.ask = true; + } + + if (Object.keys(visibleWindows).length === 0) return; + + const newLayout = layoutManager.calculateFeatureWindowLayout(visibleWindows); + movementManager.animateLayout(newLayout, animated); } const showSettingsWindow = () => { @@ -98,6 +72,34 @@ const cancelHideSettingsWindow = () => { internalBridge.emit('window:requestVisibility', { name: 'settings', visible: true }); }; +const moveWindowStep = (direction) => { + internalBridge.emit('window:moveStep', { direction }); +}; + +const resizeHeaderWindow = ({ width, height }) => { + internalBridge.emit('window:resizeHeaderWindow', { width, height }); +}; + +const handleHeaderAnimationFinished = (state) => { + internalBridge.emit('window:headerAnimationFinished', state); +}; + +const getHeaderPosition = () => { + return new Promise((resolve) => { + internalBridge.emit('window:getHeaderPosition', (position) => { + resolve(position); + }); + }); +}; + +const moveHeaderTo = (newX, newY) => { + internalBridge.emit('window:moveHeaderTo', { newX, newY }); +}; + +const adjustWindowHeight = (sender, targetHeight) => { + internalBridge.emit('window:adjustWindowHeight', { sender, targetHeight }); +}; + function setupWindowController(windowPool, layoutManager, movementManager) { internalBridge.on('window:requestVisibility', ({ name, visible }) => { @@ -106,6 +108,109 @@ function setupWindowController(windowPool, layoutManager, movementManager) { internalBridge.on('window:requestToggleAllWindowsVisibility', ({ targetVisibility }) => { changeAllWindowsVisibility(windowPool, targetVisibility); }); + internalBridge.on('window:moveToDisplay', ({ displayId }) => { + // movementManager.moveToDisplay(displayId); + const header = windowPool.get('header'); + if (header) { + const newPosition = layoutManager.calculateNewPositionForDisplay(header, displayId); + if (newPosition) { + movementManager.animateWindowPosition(header, newPosition, { + onComplete: () => updateChildWindowLayouts(true) + }); + } + } + }); + internalBridge.on('window:moveToEdge', ({ direction }) => { + const header = windowPool.get('header'); + if (header) { + const newPosition = layoutManager.calculateEdgePosition(header, direction); + movementManager.animateWindowPosition(header, newPosition, { + onComplete: () => updateChildWindowLayouts(true) + }); + } + }); + + internalBridge.on('window:moveStep', ({ direction }) => { + const header = windowPool.get('header'); + if (header) { + const newHeaderPosition = layoutManager.calculateStepMovePosition(header, direction); + if (!newHeaderPosition) return; + + const futureHeaderBounds = { ...header.getBounds(), ...newHeaderPosition }; + const visibleWindows = {}; + const listenWin = windowPool.get('listen'); + const askWin = windowPool.get('ask'); + if (listenWin && !listenWin.isDestroyed() && listenWin.isVisible()) { + visibleWindows.listen = true; + } + if (askWin && !askWin.isDestroyed() && askWin.isVisible()) { + visibleWindows.ask = true; + } + + const newChildLayout = layoutManager.calculateFeatureWindowLayout(visibleWindows, futureHeaderBounds); + + movementManager.animateWindowPosition(header, newHeaderPosition); + movementManager.animateLayout(newChildLayout); + } + }); + + internalBridge.on('window:resizeHeaderWindow', ({ width, height }) => { + const header = windowPool.get('header'); + if (!header || movementManager.isAnimating) return; + + const newHeaderBounds = layoutManager.calculateHeaderResize(header, { width, height }); + + const wasResizable = header.isResizable(); + if (!wasResizable) header.setResizable(true); + + movementManager.animateWindowBounds(header, newHeaderBounds, { + onComplete: () => { + if (!wasResizable) header.setResizable(false); + updateChildWindowLayouts(true); + } + }); + }); + internalBridge.on('window:headerAnimationFinished', (state) => { + const header = windowPool.get('header'); + if (!header || header.isDestroyed()) return; + + if (state === 'hidden') { + header.hide(); + } else if (state === 'visible') { + updateChildWindowLayouts(false); + } + }); + internalBridge.on('window:getHeaderPosition', (reply) => { + const header = windowPool.get('header'); + if (header && !header.isDestroyed()) { + reply(header.getBounds()); + } else { + reply({ x: 0, y: 0, width: 0, height: 0 }); + } + }); + internalBridge.on('window:moveHeaderTo', ({ newX, newY }) => { + const header = windowPool.get('header'); + if (header) { + const newPosition = layoutManager.calculateClampedPosition(header, { x: newX, y: newY }); + header.setPosition(newPosition.x, newPosition.y); + } + }); + internalBridge.on('window:adjustWindowHeight', ({ sender, targetHeight }) => { + const senderWindow = windowPool.get(sender); + if (senderWindow) { + const newBounds = layoutManager.calculateWindowHeightAdjustment(senderWindow, targetHeight); + + const wasResizable = senderWindow.isResizable(); + if (!wasResizable) senderWindow.setResizable(true); + + movementManager.animateWindowBounds(senderWindow, newBounds, { + onComplete: () => { + if (!wasResizable) senderWindow.setResizable(false); + updateChildWindowLayouts(true); + } + }); + } + }); } function changeAllWindowsVisibility(windowPool, targetVisibility) { @@ -220,7 +325,10 @@ async function handleWindowVisibilityRequest(windowPool, layoutManager, movement if (name === 'shortcut-settings') { if (shouldBeVisible) { - layoutManager.positionShortcutSettingsWindow(); + // layoutManager.positionShortcutSettingsWindow(); + const newBounds = layoutManager.calculateShortcutSettingsWindowPosition(); + if (newBounds) win.setBounds(newBounds); + if (process.platform === 'darwin') { win.setAlwaysOnTop(true, 'screen-saver'); } else { @@ -242,91 +350,55 @@ async function handleWindowVisibilityRequest(windowPool, layoutManager, movement } if (name === 'listen' || name === 'ask') { + const win = windowPool.get(name); const otherName = name === 'listen' ? 'ask' : 'listen'; const otherWin = windowPool.get(otherName); const isOtherWinVisible = otherWin && !otherWin.isDestroyed() && otherWin.isVisible(); + + const ANIM_OFFSET_X = 50; + const ANIM_OFFSET_Y = 20; - const ANIM_OFFSET_X = 100; - const ANIM_OFFSET_Y = 20; + const finalVisibility = { + listen: (name === 'listen' && shouldBeVisible) || (otherName === 'listen' && isOtherWinVisible), + ask: (name === 'ask' && shouldBeVisible) || (otherName === 'ask' && isOtherWinVisible), + }; + if (!shouldBeVisible) { + finalVisibility[name] = false; + } + + const targetLayout = layoutManager.calculateFeatureWindowLayout(finalVisibility); if (shouldBeVisible) { + if (!win) return; + const targetBounds = targetLayout[name]; + if (!targetBounds) return; + + const startPos = { ...targetBounds }; + if (name === 'listen') startPos.x -= ANIM_OFFSET_X; + else if (name === 'ask') startPos.y -= ANIM_OFFSET_Y; + win.setOpacity(0); + win.setBounds(startPos); + win.show(); - if (name === 'listen') { - if (!isOtherWinVisible) { - const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: false }); - if (!targets.listen) return; + movementManager.fade(win, { to: 1 }); + movementManager.animateLayout(targetLayout); - 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); + if (!win || !win.isVisible()) return; - const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: false }); - if (targets.listen) { - movementManager.animateWindow(otherWin, targets.listen.x, targets.listen.y); - } - } - } + const currentBounds = win.getBounds(); + const targetPos = { ...currentBounds }; + if (name === 'listen') targetPos.x -= ANIM_OFFSET_X; + else if (name === 'ask') targetPos.y -= ANIM_OFFSET_Y; + + movementManager.fade(win, { to: 0, onComplete: () => win.hide() }); + movementManager.animateWindowPosition(win, targetPos); + + // 다른 창들도 새 레이아웃으로 애니메이션 + const otherWindowsLayout = { ...targetLayout }; + delete otherWindowsLayout[name]; + movementManager.animateLayout(otherWindowsLayout); } } } @@ -350,52 +422,6 @@ const toggleContentProtection = () => { return newStatus; }; -const resizeHeaderWindow = ({ width, height }) => { - const header = windowPool.get('header'); - if (header) { - console.log(`[WindowManager] Resize request: ${width}x${height}`); - - if (movementManager && movementManager.isAnimating) { - console.log('[WindowManager] Skipping resize during animation'); - return { success: false, error: 'Cannot resize during animation' }; - } - - const currentBounds = header.getBounds(); - console.log(`[WindowManager] Current bounds: ${currentBounds.width}x${currentBounds.height} at (${currentBounds.x}, ${currentBounds.y})`); - - if (currentBounds.width === width && currentBounds.height === height) { - console.log('[WindowManager] Already at target size, skipping resize'); - return { success: true }; - } - - const wasResizable = header.isResizable(); - if (!wasResizable) { - header.setResizable(true); - } - - const centerX = currentBounds.x + currentBounds.width / 2; - const newX = Math.round(centerX - width / 2); - - const display = getCurrentDisplay(header); - const { x: workAreaX, width: workAreaWidth } = display.workArea; - - const clampedX = Math.max(workAreaX, Math.min(workAreaX + workAreaWidth - width, newX)); - - header.setBounds({ x: clampedX, y: currentBounds.y, width, height }); - - if (!wasResizable) { - header.setResizable(false); - } - - if (updateLayout) { - updateLayout(); - } - - return { success: true }; - } - return { success: false, error: 'Header window not found' }; -}; - const openLoginPage = () => { const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000'; @@ -404,12 +430,6 @@ const openLoginPage = () => { console.log('Opening personalization page:', personalizeUrl); }; -const moveWindowStep = (direction) => { - if (movementManager) { - movementManager.moveStep(direction); - } -}; - function createFeatureWindows(header, namesToCreate) { // if (windowPool.has('listen')) return; @@ -423,7 +443,7 @@ function createFeatureWindows(header, namesToCreate) { hasShadow: false, skipTaskbar: true, hiddenInMissionControl: true, - resizable: true, + resizable: false, webPreferences: { nodeIntegration: false, contextIsolation: true, @@ -614,24 +634,18 @@ function getCurrentDisplay(window) { return screen.getDisplayNearestPoint(windowCenter); } -function getDisplayById(displayId) { - const displays = screen.getAllDisplays(); - return displays.find(d => d.id === displayId) || screen.getPrimaryDisplay(); -} - - - - function createWindows() { + const HEADER_HEIGHT = 47; + const DEFAULT_WINDOW_WIDTH = 353; + const primaryDisplay = screen.getPrimaryDisplay(); const { y: workAreaY, width: screenWidth } = primaryDisplay.workArea; const initialX = Math.round((screenWidth - DEFAULT_WINDOW_WIDTH) / 2); const initialY = workAreaY + 21; - movementManager = new SmoothMovementManager(windowPool, getDisplayById, getCurrentDisplay, updateLayout); - + const header = new BrowserWindow({ width: DEFAULT_WINDOW_WIDTH, height: HEADER_HEIGHT, @@ -681,15 +695,23 @@ function createWindows() { }); } windowPool.set('header', header); - header.on('moved', updateLayout); layoutManager = new WindowLayoutManager(windowPool); + movementManager = new SmoothMovementManager(windowPool); + + + header.on('moved', () => { + if (movementManager.isAnimating) { + return; + } + updateChildWindowLayouts(false); + }); header.webContents.once('dom-ready', () => { - shortcutsService.initialize(movementManager, windowPool); + shortcutsService.initialize(windowPool); shortcutsService.registerShortcuts(); }); - setupIpcHandlers(movementManager); + setupIpcHandlers(windowPool, layoutManager); setupWindowController(windowPool, layoutManager, movementManager); if (currentHeaderState === 'main') { @@ -721,16 +743,13 @@ function createWindows() { } }); - header.on('resize', () => { - console.log('[WindowManager] Header resize event triggered'); - updateLayout(); - }); + header.on('resize', () => updateChildWindowLayouts(false)); return windowPool; } -function setupIpcHandlers(movementManager) { - // quit-application handler moved to windowBridge.js to avoid duplication + +function setupIpcHandlers(windowPool, layoutManager) { screen.on('display-added', (event, newDisplay) => { console.log('[Display] New display added:', newDisplay.id); }); @@ -738,18 +757,25 @@ function setupIpcHandlers(movementManager) { screen.on('display-removed', (event, oldDisplay) => { console.log('[Display] Display removed:', oldDisplay.id); const header = windowPool.get('header'); + if (header && getCurrentDisplay(header).id === oldDisplay.id) { const primaryDisplay = screen.getPrimaryDisplay(); - movementManager.moveToDisplay(primaryDisplay.id); + const newPosition = layoutManager.calculateNewPositionForDisplay(header, primaryDisplay.id); + if (newPosition) { + // 복구 상황이므로 애니메이션 없이 즉시 이동 + header.setPosition(newPosition.x, newPosition.y, false); + updateChildWindowLayouts(false); + } } }); screen.on('display-metrics-changed', (event, display, changedMetrics) => { - // console.log('[Display] Display metrics changed:', display.id, changedMetrics); - updateLayout(); + // 레이아웃 업데이트 함수를 새 버전으로 호출 + updateChildWindowLayouts(false); }); } + const handleHeaderStateChanged = (state) => { console.log(`[WindowManager] Header state changed to: ${state}`); currentHeaderState = state; @@ -762,96 +788,8 @@ const handleHeaderStateChanged = (state) => { internalBridge.emit('reregister-shortcuts'); }; -const handleHeaderAnimationFinished = (state) => { - const header = windowPool.get('header'); - if (!header || header.isDestroyed()) return; - - if (state === 'hidden') { - header.hide(); - console.log('[WindowManager] Header hidden after animation.'); - } else if (state === 'visible') { - console.log('[WindowManager] Header shown after animation.'); - updateLayout(); - } -}; - -const getHeaderPosition = () => { - const header = windowPool.get('header'); - if (header) { - const [x, y] = header.getPosition(); - return { x, y }; - } - return { x: 0, y: 0 }; -}; - -const moveHeader = (newX, newY) => { - const header = windowPool.get('header'); - if (header) { - const currentY = newY !== undefined ? newY : header.getBounds().y; - header.setPosition(newX, currentY, false); - updateLayout(); - } -}; - -const moveHeaderTo = (newX, newY) => { - const header = windowPool.get('header'); - if (header) { - const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY }); - const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea; - const headerBounds = header.getBounds(); - - let clampedX = newX; - let clampedY = newY; - - if (newX < workAreaX) { - clampedX = workAreaX; - } else if (newX + headerBounds.width > workAreaX + width) { - clampedX = workAreaX + width - headerBounds.width; - } - - if (newY < workAreaY) { - clampedY = workAreaY; - } else if (newY + headerBounds.height > workAreaY + height) { - clampedY = workAreaY + height - headerBounds.height; - } - - header.setPosition(clampedX, clampedY, false); - updateLayout(); - } -}; - -const adjustWindowHeight = (sender, targetHeight) => { - const senderWindow = BrowserWindow.fromWebContents(sender); - if (senderWindow) { - const wasResizable = senderWindow.isResizable(); - if (!wasResizable) { - senderWindow.setResizable(true); - } - - const currentBounds = senderWindow.getBounds(); - const minHeight = senderWindow.getMinimumSize()[1]; - const maxHeight = senderWindow.getMaximumSize()[1]; - - let adjustedHeight; - if (maxHeight === 0) { - adjustedHeight = Math.max(minHeight, targetHeight); - } else { - adjustedHeight = Math.max(minHeight, Math.min(maxHeight, targetHeight)); - } - - senderWindow.setSize(currentBounds.width, adjustedHeight, false); - - if (!wasResizable) { - senderWindow.setResizable(false); - } - - updateLayout(); - } -}; - module.exports = { - updateLayout, createWindows, windowPool, toggleContentProtection, @@ -865,7 +803,6 @@ module.exports = { handleHeaderStateChanged, handleHeaderAnimationFinished, getHeaderPosition, - moveHeader, moveHeaderTo, adjustWindowHeight, }; \ No newline at end of file