126 lines
5.1 KiB
TypeScript
126 lines
5.1 KiB
TypeScript
// src/main/windows/suggestion-overlay-presenter.ts
|
|
// 입력 제안 오버레이 표시 정책의 단일 정본 (RecordingTipPresenter 와 같은 형태).
|
|
//
|
|
// 예전엔 bootstrap 의 이벤트 클로저 하나에 표시 여부 판단, 텔레메트리 표시 플래그 동기화, show/update/hide
|
|
// 선택, payload 매핑(show 와 update 가 같은 7개 필드를 따로 나열), 예외 삼키기가 섞여 있었다. bootstrap 은
|
|
// electron·서비스를 최상위에서 import 해 이 정책을 테스트할 수 없었고, 필드를 추가할 때마다 두 매핑을 따로
|
|
// 고쳐야 했다(누락돼도 컴파일러가 못 잡았다). 이제 매핑은 함수 하나이고, 창은 뷰 포트로 주입한다.
|
|
|
|
import type { AnchorKind, PhysicalRect, SuggestionState } from '@d3ro/core/input-intelligence'
|
|
import { decideSuggestionOverlayAction } from '../suggestion-overlay-policy'
|
|
import {
|
|
hideSuggestionOverlay,
|
|
isSuggestionOverlayVisible,
|
|
showSuggestionOverlay,
|
|
updateSuggestionOverlay,
|
|
} from './WindowManager'
|
|
|
|
/** 오버레이 내용 — show 와 update 가 같이 쓴다 */
|
|
export type SuggestionOverlayContentPayload = Pick<
|
|
SuggestionState,
|
|
'candidates' | 'activeIndex' | 'targetTotal' | 'generating' | 'warmingUp' | 'partialText' | 'provenance'
|
|
>
|
|
|
|
/** 새로 띄울 때만 필요한 배치 정보를 더한 payload */
|
|
export interface SuggestionOverlayShowPayload extends SuggestionOverlayContentPayload {
|
|
anchor: PhysicalRect | null
|
|
anchorKind: AnchorKind
|
|
appName: string | null
|
|
}
|
|
|
|
/** 상태 → 오버레이 내용 (단일 매핑) */
|
|
export function toSuggestionOverlayContent(state: SuggestionState): SuggestionOverlayContentPayload {
|
|
return {
|
|
candidates: state.candidates,
|
|
activeIndex: state.activeIndex,
|
|
targetTotal: state.targetTotal,
|
|
generating: state.generating,
|
|
warmingUp: state.warmingUp,
|
|
partialText: state.partialText,
|
|
provenance: state.provenance,
|
|
}
|
|
}
|
|
|
|
/** 상태 → 새로 띄울 때의 payload (내용 + 앵커·앱 이름) */
|
|
export function toSuggestionOverlayShowPayload(state: SuggestionState): SuggestionOverlayShowPayload {
|
|
return {
|
|
...toSuggestionOverlayContent(state),
|
|
anchor: state.anchor,
|
|
anchorKind: state.anchorKind,
|
|
appName: state.appName,
|
|
}
|
|
}
|
|
|
|
/** 보여 줄 것(후보·생성 중·준비 중·부분 텍스트)이 있고 붙을 앵커가 있으면 표시한다. */
|
|
export function shouldPresentSuggestion(state: SuggestionState): boolean {
|
|
const hasSomethingToShow =
|
|
state.candidates.length > 0 || state.generating || state.warmingUp || !!state.partialText
|
|
return hasSomethingToShow && !!state.anchor
|
|
}
|
|
|
|
/** 오버레이 창 포트 — 테스트에서는 가짜로 바꾼다 */
|
|
export interface SuggestionOverlayView {
|
|
show(payload: SuggestionOverlayShowPayload): void
|
|
update(payload: SuggestionOverlayContentPayload): void
|
|
hide(): void
|
|
isVisible(): boolean
|
|
}
|
|
|
|
export interface SuggestionOverlayPresenterDeps {
|
|
view: SuggestionOverlayView
|
|
/** 텔레메트리에 "제안을 보여 주는 중" 을 알린다(입력 가로채기 판단용) */
|
|
setPresentationActive(active: boolean): void
|
|
warn(message: string): void
|
|
}
|
|
|
|
function describe(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error)
|
|
}
|
|
|
|
export class SuggestionOverlayPresenter {
|
|
constructor(private readonly _deps: SuggestionOverlayPresenterDeps) {}
|
|
|
|
/**
|
|
* 제안 상태가 바뀌었다. 오버레이·창 호출이 던져도 삼킨다 — UI 문제로 서비스의 진행 플래그가
|
|
* 굳어 기능 전체가 멈추면 안 된다. (텔레메트리 동기화는 오버레이 호출보다 먼저, 같은 보호 안에서)
|
|
*/
|
|
onUpdated(state: SuggestionState): void {
|
|
try {
|
|
const shouldPresent = shouldPresentSuggestion(state)
|
|
this._deps.setPresentationActive(shouldPresent)
|
|
const view = this._deps.view
|
|
const action = decideSuggestionOverlayAction(view.isVisible(), shouldPresent)
|
|
if (action === 'hide') {
|
|
view.hide()
|
|
} else if (action === 'update') {
|
|
// 이미 떠 있으면 내용만 갱신한다 — setBounds/present 를 매 스트리밍 청크마다
|
|
// 다시 부르면 X 클릭이 재present 에 가로채이고 패널이 튄다(실측).
|
|
view.update(toSuggestionOverlayContent(state))
|
|
} else {
|
|
view.show(toSuggestionOverlayShowPayload(state))
|
|
}
|
|
} catch (error) {
|
|
this._deps.warn(`제안 오버레이 표시 실패: ${describe(error)}`)
|
|
}
|
|
}
|
|
|
|
/** 제안이 닫혔다. 텔레메트리 플래그는 항상 내리고, 숨기기 실패만 삼킨다. */
|
|
onCleared(): void {
|
|
this._deps.setPresentationActive(false)
|
|
try {
|
|
this._deps.view.hide()
|
|
} catch (error) {
|
|
this._deps.warn(`제안 오버레이 숨기기 실패: ${describe(error)}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
/** WindowManager 의 제안 오버레이 창을 뷰 포트로 감싼다 */
|
|
export function createWindowSuggestionOverlayView(): SuggestionOverlayView {
|
|
return {
|
|
show: (payload) => showSuggestionOverlay(payload),
|
|
update: (payload) => updateSuggestionOverlay(payload),
|
|
hide: () => hideSuggestionOverlay(),
|
|
isVisible: () => isSuggestionOverlayVisible(),
|
|
}
|
|
}
|