Phase 3.5 구현: 커서 위치 히스토리 팝업 (D3RO 고유 기능)

- HistoryPopup: Vanilla JS, 다크 카드(#242427), 앰버 악센트(#f25b29)
- Ctrl+Shift+V → 커서 위치에 최근 10건 히스토리 팝업
- Arrow↑↓ 선택, Enter 붙여넣기, 1-9 직접 선택, ESC 닫기
- focusable: false → 활성 앱 포커스 유지
- 2-phase 리사이즈, 등장/퇴장 애니메이션
- WindowManager: HistoryPopup 관리 + 프리로딩
- Bootstrap: globalShortcut 등록 + IPC 연동
This commit is contained in:
Yun Chan 2026-04-05 02:37:35 +09:00
parent 291e2a29d0
commit f131b581a9
7 changed files with 459 additions and 5 deletions

View file

@ -16,6 +16,7 @@ const logger = getLogger('WindowManager')
let mainWindow: BrowserWindow | null = null
let recordingTipWindow: BrowserWindow | null = null
let resultPopupWindow: BrowserWindow | null = null
let historyPopupWindow: BrowserWindow | null = null
// ── 메인 윈도우 ───────────────────────────────────────
@ -262,11 +263,106 @@ export function hideResultPopup(): void {
}
}
// ── HistoryPopup 팝업 ─────────────────────────────────
function createHistoryPopupWindow(): BrowserWindow {
const win = new BrowserWindow({
width: 360,
height: 400,
show: false,
frame: false,
transparent: true,
resizable: false,
alwaysOnTop: true,
skipTaskbar: true,
focusable: false,
webPreferences: {
preload: join(__dirname, '../preload/popup.js'),
sandbox: false,
contextIsolation: true,
nodeIntegration: false
}
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
win.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/popups/history-popup/index.html`)
} else {
win.loadFile(join(__dirname, '../renderer/popups/history-popup/index.html'))
}
win.on('closed', () => {
historyPopupWindow = null
})
return win
}
export function getHistoryPopupWindow(): BrowserWindow {
if (!historyPopupWindow || historyPopupWindow.isDestroyed()) {
historyPopupWindow = createHistoryPopupWindow()
logger.info('HistoryPopup window created')
}
return historyPopupWindow
}
export function showHistoryPopup(entries: Array<Record<string, unknown>>): void {
const win = getHistoryPopupWindow()
win.webContents.send('history:showItems', { entries })
const handler = (_event: Electron.IpcMainEvent, data: { width: number; height: number }) => {
ipcMain.removeListener('history:popupMeasured', handler)
const cursorPos = screen.getCursorScreenPoint()
const display = screen.getDisplayNearestPoint(cursorPos)
let x = cursorPos.x - Math.round(data.width / 2)
let y = cursorPos.y - data.height - 20
x = Math.max(display.workArea.x, Math.min(x, display.workArea.x + display.workArea.width - data.width))
if (y < display.workArea.y) {
y = cursorPos.y + 20
}
win.setBounds({ x, y, width: data.width, height: data.height })
if (!win.isVisible()) {
win.showInactive()
}
win.webContents.send('history:show', {})
}
ipcMain.on('history:popupMeasured', handler)
}
export function hideHistoryPopup(): void {
if (historyPopupWindow && !historyPopupWindow.isDestroyed()) {
historyPopupWindow.webContents.send('history:hide', {})
setTimeout(() => {
if (historyPopupWindow && !historyPopupWindow.isDestroyed()) {
historyPopupWindow.hide()
}
}, 150)
}
}
export function sendKeyToHistoryPopup(key: string): void {
if (historyPopupWindow && !historyPopupWindow.isDestroyed() && historyPopupWindow.isVisible()) {
historyPopupWindow.webContents.send('history:keyEvent', { key })
}
}
export function isHistoryPopupVisible(): boolean {
return !!(historyPopupWindow && !historyPopupWindow.isDestroyed() && historyPopupWindow.isVisible())
}
// ── 프리로딩 ──────────────────────────────────────────
export function preloadPopupWindows(): void {
getRecordingTipWindow()
getResultPopupWindow()
getHistoryPopupWindow()
logger.info('Popup windows preloaded')
}