mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-12 05:10:51 +02:00
fix popout scopes issues
This commit is contained in:
+20
-5
@@ -26,6 +26,7 @@ import { resolveNativeThemeSource } from '../shared/themeState'
|
|||||||
import { resolveWindowCapabilities } from '../shared/windowCapabilities'
|
import { resolveWindowCapabilities } from '../shared/windowCapabilities'
|
||||||
import {
|
import {
|
||||||
clampDraggedMainWindowBounds,
|
clampDraggedMainWindowBounds,
|
||||||
|
clampRestoredWindowBounds,
|
||||||
raiseWindowAboveNormalPopouts,
|
raiseWindowAboveNormalPopouts,
|
||||||
resolveExpandedMainWindowBounds,
|
resolveExpandedMainWindowBounds,
|
||||||
} from '../shared/windowGeometry'
|
} from '../shared/windowGeometry'
|
||||||
@@ -98,6 +99,7 @@ const NOW_PLAYING_CONFIG_DEFAULTS = {
|
|||||||
const STATIC_APP_ICON_FILENAME = 'icon.png'
|
const STATIC_APP_ICON_FILENAME = 'icon.png'
|
||||||
const MAIN_WINDOW_SYNC_SUPPRESSION_MS = 180
|
const MAIN_WINDOW_SYNC_SUPPRESSION_MS = 180
|
||||||
const MAIN_WINDOW_VISIBLE_GRAB_MARGIN = 64
|
const MAIN_WINDOW_VISIBLE_GRAB_MARGIN = 64
|
||||||
|
const RESTORED_WINDOW_VISIBLE_MARGIN = 64
|
||||||
const runtimeWindowCapabilities = resolveWindowCapabilities({
|
const runtimeWindowCapabilities = resolveWindowCapabilities({
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
argv: process.argv,
|
argv: process.argv,
|
||||||
@@ -582,7 +584,11 @@ function syncMainWindowLogicalBounds(window: BrowserWindow, bounds = window.getB
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyMainWindowLogicalBounds(window: BrowserWindow, bounds: WindowBounds): void {
|
function applyMainWindowLogicalBounds(window: BrowserWindow, bounds: WindowBounds): void {
|
||||||
const logicalBounds = normalizeMainWindowBounds(bounds)
|
const logicalBounds = clampRestoredWindowBounds(
|
||||||
|
normalizeMainWindowBounds(bounds),
|
||||||
|
getDisplayWorkAreas(),
|
||||||
|
RESTORED_WINDOW_VISIBLE_MARGIN,
|
||||||
|
)
|
||||||
mainWindowLogicalBounds = logicalBounds
|
mainWindowLogicalBounds = logicalBounds
|
||||||
suppressMainWindowSync()
|
suppressMainWindowSync()
|
||||||
const expandedBounds = resolveExpandedMainWindowBounds(logicalBounds, getSettingsHeight(window), getDisplayWorkAreas())
|
const expandedBounds = resolveExpandedMainWindowBounds(logicalBounds, getSettingsHeight(window), getDisplayWorkAreas())
|
||||||
@@ -605,10 +611,12 @@ function applyLogicalBounds(window: BrowserWindow, bounds: WindowBounds): void {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setBounds({
|
const nextBounds = clampRestoredWindowBounds({
|
||||||
...bounds,
|
...bounds,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
})
|
}, getDisplayWorkAreas(), RESTORED_WINDOW_VISIBLE_MARGIN)
|
||||||
|
|
||||||
|
window.setBounds(nextBounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWindowHeight(window: BrowserWindow, bounds: WindowBounds, height: number, y = bounds.y): void {
|
function setWindowHeight(window: BrowserWindow, bounds: WindowBounds, height: number, y = bounds.y): void {
|
||||||
@@ -1121,9 +1129,12 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro
|
|||||||
width: POPOUT_DEFAULTS.width,
|
width: POPOUT_DEFAULTS.width,
|
||||||
height: POPOUT_DEFAULTS.height,
|
height: POPOUT_DEFAULTS.height,
|
||||||
}
|
}
|
||||||
const bounds = shouldRestoreGeometry
|
const normalizedBounds = shouldRestoreGeometry
|
||||||
? normalizeBounds(rawBounds, fallbackBounds)
|
? normalizeBounds(rawBounds, fallbackBounds)
|
||||||
: fallbackBounds
|
: fallbackBounds
|
||||||
|
const bounds = shouldRestoreGeometry
|
||||||
|
? clampRestoredWindowBounds(normalizedBounds, getDisplayWorkAreas(), RESTORED_WINDOW_VISIBLE_MARGIN)
|
||||||
|
: normalizedBounds
|
||||||
suppressNextPopoutBoundsEvents.add(kind)
|
suppressNextPopoutBoundsEvents.add(kind)
|
||||||
|
|
||||||
const options: BrowserWindowConstructorOptions = {
|
const options: BrowserWindowConstructorOptions = {
|
||||||
@@ -1212,7 +1223,11 @@ function syncScopePopouts(nextState: ScopePopoutSyncStateMap): void {
|
|||||||
|
|
||||||
if (supportsGeometryPersistence() && desired.bounds) {
|
if (supportsGeometryPersistence() && desired.bounds) {
|
||||||
const currentBounds = popoutWindow.getBounds()
|
const currentBounds = popoutWindow.getBounds()
|
||||||
const nextBounds = normalizeBounds(desired.bounds, currentBounds)
|
const nextBounds = clampRestoredWindowBounds(
|
||||||
|
normalizeBounds(desired.bounds, currentBounds),
|
||||||
|
getDisplayWorkAreas(),
|
||||||
|
RESTORED_WINDOW_VISIBLE_MARGIN,
|
||||||
|
)
|
||||||
const hasBoundsDelta =
|
const hasBoundsDelta =
|
||||||
currentBounds.x !== nextBounds.x
|
currentBounds.x !== nextBounds.x
|
||||||
|| currentBounds.y !== nextBounds.y
|
|| currentBounds.y !== nextBounds.y
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const defaultTheme = resolveTheme(createDefaultTheme())
|
|||||||
export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps): JSX.Element {
|
export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps): JSX.Element {
|
||||||
const [snapshot, setSnapshot] = useState<ScopePopoutSnapshot<ScopeKind> | null>(null)
|
const [snapshot, setSnapshot] = useState<ScopePopoutSnapshot<ScopeKind> | null>(null)
|
||||||
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(false)
|
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(false)
|
||||||
|
const [cursorInsideWindow, setCursorInsideWindow] = useState(false)
|
||||||
const prevMiniSettingsOpenRef = useRef(false)
|
const prevMiniSettingsOpenRef = useRef(false)
|
||||||
const frameTarget = usePerformanceStore((s) => s.frameTarget)
|
const frameTarget = usePerformanceStore((s) => s.frameTarget)
|
||||||
const miniSettingsOpen = useUiStore((s) => s.settingsOpen)
|
const miniSettingsOpen = useUiStore((s) => s.settingsOpen)
|
||||||
@@ -78,6 +79,30 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps)
|
|||||||
frameScheduler.setFrameTarget(frameTarget)
|
frameScheduler.setFrameTarget(frameTarget)
|
||||||
}, [frameScheduler, frameTarget])
|
}, [frameScheduler, frameTarget])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isDisposed = false
|
||||||
|
|
||||||
|
const syncCursorInsideWindow = (): void => {
|
||||||
|
void window.electronAPI.isCursorInsideWindow()
|
||||||
|
.then((isInside) => {
|
||||||
|
if (!isDisposed) {
|
||||||
|
setCursorInsideWindow(isInside)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Renderer pointer events still update the chrome when cursor polling is unavailable.
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
syncCursorInsideWindow()
|
||||||
|
const interval = setInterval(syncCursorInsideWindow, 120)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isDisposed = true
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribeSnapshot = window.electronAPI.onScopePopoutSnapshot((nextSnapshot) => {
|
const unsubscribeSnapshot = window.electronAPI.onScopePopoutSnapshot((nextSnapshot) => {
|
||||||
if (nextSnapshot.kind !== scopeKind) return
|
if (nextSnapshot.kind !== scopeKind) return
|
||||||
@@ -182,6 +207,9 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps)
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="scope-popout"
|
className="scope-popout"
|
||||||
|
onMouseEnter={() => setCursorInsideWindow(true)}
|
||||||
|
onMouseMove={() => setCursorInsideWindow(true)}
|
||||||
|
onMouseLeave={() => setCursorInsideWindow(false)}
|
||||||
onMouseDown={useNativeDragRegions ? undefined : handleAltDragStart}
|
onMouseDown={useNativeDragRegions ? undefined : handleAltDragStart}
|
||||||
onMouseUp={useNativeDragRegions ? undefined : handleAltDragEnd}
|
onMouseUp={useNativeDragRegions ? undefined : handleAltDragEnd}
|
||||||
>
|
>
|
||||||
@@ -193,6 +221,7 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps)
|
|||||||
className={[
|
className={[
|
||||||
'scope-popout__chrome',
|
'scope-popout__chrome',
|
||||||
miniSettingsOpen ? 'is-expanded' : '',
|
miniSettingsOpen ? 'is-expanded' : '',
|
||||||
|
cursorInsideWindow ? 'is-cursor-inside' : '',
|
||||||
].join(' ').trim()}
|
].join(' ').trim()}
|
||||||
>
|
>
|
||||||
<header className={`scope-popout__header ${useNativeDragRegions ? 'is-native-drag' : ''}`.trim()}>
|
<header className={`scope-popout__header ${useNativeDragRegions ? 'is-native-drag' : ''}`.trim()}>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const ACTIVE_PROFILE_KEY = 'prism:activeProfile'
|
|||||||
const PROFILE_GEOMETRY_SYNC_WINDOW_MS = 800
|
const PROFILE_GEOMETRY_SYNC_WINDOW_MS = 800
|
||||||
|
|
||||||
interface PersistedSettingsState {
|
interface PersistedSettingsState {
|
||||||
|
activeProfileId?: string | null
|
||||||
|
profileBaselineSignature?: string
|
||||||
scopeOrder: ScopeKind[]
|
scopeOrder: ScopeKind[]
|
||||||
hiddenScopes: ScopeKind[]
|
hiddenScopes: ScopeKind[]
|
||||||
widthWeights: Record<ScopeKind, number>
|
widthWeights: Record<ScopeKind, number>
|
||||||
@@ -112,6 +114,29 @@ function buildPersistedScopePopouts(scopePopouts: ScopePopoutStateMap): ScopePop
|
|||||||
: stripScopePopoutBounds(normalizedScopePopouts)
|
: stripScopePopoutBounds(normalizedScopePopouts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildProfileBaselineSignature(profile: Profile | null): string | undefined {
|
||||||
|
if (!profile) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedProfile = normalizeProfile(profile, profile.name)
|
||||||
|
const scopePopouts = SCOPE_KINDS.reduce((acc, kind) => {
|
||||||
|
acc[kind] = {
|
||||||
|
poppedOut: normalizedProfile.scopePopouts[kind]?.poppedOut === true,
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {} as Record<ScopeKind, { poppedOut: boolean }>)
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
name: normalizedProfile.name,
|
||||||
|
scopeOrder: normalizeScopeOrder(normalizedProfile.scopeOrder),
|
||||||
|
hiddenScopes: normalizeHiddenScopes(normalizedProfile.hiddenScopes),
|
||||||
|
widthWeights: normalizeWidthWeights(normalizedProfile.widthWeights),
|
||||||
|
scopeSettings: mergeScopeSettings(normalizedProfile.scopeSettings),
|
||||||
|
scopePopouts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function restoreBaselineScopePopoutBounds(
|
function restoreBaselineScopePopoutBounds(
|
||||||
scopePopouts: ScopePopoutStateMap,
|
scopePopouts: ScopePopoutStateMap,
|
||||||
baseline: Profile | null,
|
baseline: Profile | null,
|
||||||
@@ -144,6 +169,30 @@ function restoreBaselineGeometry(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restoreBaselinePopoutOpenState(
|
||||||
|
state: Pick<SettingsState, 'savedProfileBaseline'>,
|
||||||
|
workingState: WorkingSettingsState,
|
||||||
|
): WorkingSettingsState {
|
||||||
|
const baseline = state.savedProfileBaseline
|
||||||
|
if (!baseline) {
|
||||||
|
return workingState
|
||||||
|
}
|
||||||
|
|
||||||
|
const baselinePopouts = normalizeScopePopouts(baseline.scopePopouts)
|
||||||
|
return {
|
||||||
|
...workingState,
|
||||||
|
scopePopouts: SCOPE_KINDS.reduce((acc, kind) => {
|
||||||
|
acc[kind] = {
|
||||||
|
...workingState.scopePopouts[kind],
|
||||||
|
poppedOut: baselinePopouts[kind]?.poppedOut === true,
|
||||||
|
windowBounds: workingState.scopePopouts[kind]?.windowBounds
|
||||||
|
?? baselinePopouts[kind]?.windowBounds,
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {} as ScopePopoutStateMap),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function loadFromStorage(): Partial<PersistedSettingsState> {
|
function loadFromStorage(): Partial<PersistedSettingsState> {
|
||||||
if (!canUseBrowserStorage()) {
|
if (!canUseBrowserStorage()) {
|
||||||
return {}
|
return {}
|
||||||
@@ -165,7 +214,9 @@ function loadFromStorage(): Partial<PersistedSettingsState> {
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveToStorage(state: WorkingSettingsState): void {
|
function saveToStorage(
|
||||||
|
state: WorkingSettingsState & Pick<SettingsState, 'activeProfileId' | 'savedProfileBaseline'>,
|
||||||
|
): void {
|
||||||
if (!canUseBrowserStorage()) {
|
if (!canUseBrowserStorage()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -177,6 +228,8 @@ function saveToStorage(state: WorkingSettingsState): void {
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||||
|
activeProfileId: state.activeProfileId,
|
||||||
|
profileBaselineSignature: buildProfileBaselineSignature(state.savedProfileBaseline),
|
||||||
scopeOrder: state.scopeOrder,
|
scopeOrder: state.scopeOrder,
|
||||||
hiddenScopes: Array.from(state.hiddenScopes),
|
hiddenScopes: Array.from(state.hiddenScopes),
|
||||||
widthWeights: state.widthWeights,
|
widthWeights: state.widthWeights,
|
||||||
@@ -189,7 +242,9 @@ function saveToStorage(state: WorkingSettingsState): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistWorkingState(state: WorkingSettingsState): void {
|
function persistWorkingState(
|
||||||
|
state: WorkingSettingsState & Pick<SettingsState, 'activeProfileId' | 'savedProfileBaseline'>,
|
||||||
|
): void {
|
||||||
saveToStorage(state)
|
saveToStorage(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +257,26 @@ function hasPersistedWorkingState(state: Partial<PersistedSettingsState>): boole
|
|||||||
|| 'windowBounds' in state
|
|| 'windowBounds' in state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canRestorePersistedWorkingState(
|
||||||
|
state: Partial<PersistedSettingsState>,
|
||||||
|
snapshot: ProfileLibrarySnapshot,
|
||||||
|
): boolean {
|
||||||
|
if (!hasPersistedWorkingState(state)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.activeProfileId || state.activeProfileId !== snapshot.activeProfileId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeProfile = snapshot.profiles[snapshot.activeProfileId]
|
||||||
|
if (!activeProfile || typeof state.profileBaselineSignature !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.profileBaselineSignature === buildProfileBaselineSignature(activeProfile)
|
||||||
|
}
|
||||||
|
|
||||||
function loadLegacyProfileMigrationPayload(): LegacyProfileMigrationPayload | null {
|
function loadLegacyProfileMigrationPayload(): LegacyProfileMigrationPayload | null {
|
||||||
try {
|
try {
|
||||||
const rawProfiles = localStorage.getItem(PROFILES_STORAGE_KEY)
|
const rawProfiles = localStorage.getItem(PROFILES_STORAGE_KEY)
|
||||||
@@ -558,19 +633,18 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If localStorage has a working state from a previous session, preserve it so the
|
// Preserve a previous working draft only when it belongs to the current active
|
||||||
// user picks up exactly where they left off (dirty or not). The saved profile becomes
|
// profile baseline. Legacy or stale state should not override saved popout state.
|
||||||
// the baseline for dirty-state comparison but the working values are left as-is.
|
|
||||||
// On first launch (no stored state) we load the profile normally.
|
|
||||||
const storedWorkingState = loadFromStorage()
|
const storedWorkingState = loadFromStorage()
|
||||||
const hasStoredWorkingState = hasPersistedWorkingState(storedWorkingState)
|
const shouldRestoreWorkingState = canRestorePersistedWorkingState(storedWorkingState, snapshot)
|
||||||
applyProfileSnapshot(set, snapshot, { loadActiveProfile: !hasStoredWorkingState })
|
applyProfileSnapshot(set, snapshot, { loadActiveProfile: !shouldRestoreWorkingState })
|
||||||
if (hasStoredWorkingState) {
|
if (shouldRestoreWorkingState) {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const nextWorkingState = restoreBaselineGeometry(
|
const persistedWorkingState = restoreBaselineGeometry(
|
||||||
state,
|
state,
|
||||||
createWorkingStateFromPersistedState(storedWorkingState),
|
createWorkingStateFromPersistedState(storedWorkingState),
|
||||||
)
|
)
|
||||||
|
const nextWorkingState = restoreBaselinePopoutOpenState(state, persistedWorkingState)
|
||||||
return commitWorkingState(state, nextWorkingState, state.savedProfileBaseline)
|
return commitWorkingState(state, nextWorkingState, state.savedProfileBaseline)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1977,6 +1977,8 @@ button.toolbar__version:hover {
|
|||||||
|
|
||||||
.scope-popout__viewport:hover .scope-popout__chrome,
|
.scope-popout__viewport:hover .scope-popout__chrome,
|
||||||
.scope-popout__chrome:hover,
|
.scope-popout__chrome:hover,
|
||||||
|
.scope-popout__chrome:focus-within,
|
||||||
|
.scope-popout__chrome.is-cursor-inside,
|
||||||
.scope-popout__chrome.is-expanded {
|
.scope-popout__chrome.is-expanded {
|
||||||
max-height: 58px;
|
max-height: 58px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@@ -145,6 +145,15 @@ export function clampDraggedMainWindowBounds(
|
|||||||
return clampBoundsWithVisibleMargin(actualBounds, envelope, visibleMargin)
|
return clampBoundsWithVisibleMargin(actualBounds, envelope, visibleMargin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clampRestoredWindowBounds(
|
||||||
|
bounds: WindowBounds,
|
||||||
|
workAreas: readonly WorkAreaBounds[],
|
||||||
|
visibleMargin: number,
|
||||||
|
): WindowBounds {
|
||||||
|
const envelope = buildDisplayEnvelope(bounds, bounds, workAreas)
|
||||||
|
return clampBoundsWithVisibleMargin(bounds, envelope, visibleMargin)
|
||||||
|
}
|
||||||
|
|
||||||
export function raiseWindowAboveNormalPopouts(
|
export function raiseWindowAboveNormalPopouts(
|
||||||
mainWindow: StackableWindowLike | null,
|
mainWindow: StackableWindowLike | null,
|
||||||
popouts: Iterable<StackableWindowLike>,
|
popouts: Iterable<StackableWindowLike>,
|
||||||
|
|||||||
+223
-18
@@ -31,6 +31,7 @@ import {
|
|||||||
import { resolveWindowCapabilities } from '../src/shared/windowCapabilities'
|
import { resolveWindowCapabilities } from '../src/shared/windowCapabilities'
|
||||||
import {
|
import {
|
||||||
clampDraggedMainWindowBounds,
|
clampDraggedMainWindowBounds,
|
||||||
|
clampRestoredWindowBounds,
|
||||||
raiseWindowAboveNormalPopouts,
|
raiseWindowAboveNormalPopouts,
|
||||||
resolveExpandedMainWindowBounds,
|
resolveExpandedMainWindowBounds,
|
||||||
} from '../src/shared/windowGeometry'
|
} from '../src/shared/windowGeometry'
|
||||||
@@ -2906,6 +2907,19 @@ test('dragged main-window bounds keep a visible grab margin without sticking at
|
|||||||
assert.equal(clamped.y, 120)
|
assert.equal(clamped.y, 120)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('restored window bounds clamp back to a visible display margin', () => {
|
||||||
|
const clamped = clampRestoredWindowBounds(
|
||||||
|
{ x: 1800, y: 1400, width: 420, height: 240 },
|
||||||
|
[{ x: 0, y: 0, width: 800, height: 600 }],
|
||||||
|
64,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(clamped.x, 736)
|
||||||
|
assert.equal(clamped.y, 536)
|
||||||
|
assert.equal(clamped.width, 420)
|
||||||
|
assert.equal(clamped.height, 240)
|
||||||
|
})
|
||||||
|
|
||||||
test('raiseWindowAboveNormalPopouts raises the main window when an unpinned popout exists', () => {
|
test('raiseWindowAboveNormalPopouts raises the main window when an unpinned popout exists', () => {
|
||||||
const main = createFakeStackableWindow()
|
const main = createFakeStackableWindow()
|
||||||
const normalPopout = createFakeStackableWindow(false)
|
const normalPopout = createFakeStackableWindow(false)
|
||||||
@@ -2989,19 +3003,16 @@ test('initializeProfiles restores persisted dirty window bounds while keeping th
|
|||||||
const previousSettingsState = useSettingsStore.getState()
|
const previousSettingsState = useSettingsStore.getState()
|
||||||
const fakeStorage = installFakeLocalStorage()
|
const fakeStorage = installFakeLocalStorage()
|
||||||
const restoredBounds: WindowBounds[] = []
|
const restoredBounds: WindowBounds[] = []
|
||||||
|
const profile = createDefaultProfile(DEFAULT_PROFILE_NAME)
|
||||||
|
profile.windowBounds = { x: 10, y: 20, width: 900, height: 180 }
|
||||||
const dirtyBounds = { x: 44, y: 55, width: 900, height: 180 }
|
const dirtyBounds = { x: 44, y: 55, width: 900, height: 180 }
|
||||||
const fakeWindow = installFakeElectronWindow({
|
const fakeWindow = installFakeElectronWindow({
|
||||||
getProfileSnapshot: async () => {
|
getProfileSnapshot: async () => ({
|
||||||
const profile = createDefaultProfile(DEFAULT_PROFILE_NAME)
|
activeProfileId: DEFAULT_PROFILE_ID,
|
||||||
profile.windowBounds = { x: 10, y: 20, width: 900, height: 180 }
|
profiles: {
|
||||||
|
[DEFAULT_PROFILE_ID]: profile,
|
||||||
return {
|
},
|
||||||
activeProfileId: DEFAULT_PROFILE_ID,
|
}),
|
||||||
profiles: {
|
|
||||||
[DEFAULT_PROFILE_ID]: profile,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getWindowBounds: async () => dirtyBounds,
|
getWindowBounds: async () => dirtyBounds,
|
||||||
setWindowBounds: (bounds: WindowBounds) => {
|
setWindowBounds: (bounds: WindowBounds) => {
|
||||||
restoredBounds.push(bounds)
|
restoredBounds.push(bounds)
|
||||||
@@ -3009,17 +3020,24 @@ test('initializeProfiles restores persisted dirty window bounds while keeping th
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const profile = createDefaultProfile(DEFAULT_PROFILE_NAME)
|
useSettingsStore.getState().applyExternalProfileSnapshot({
|
||||||
profile.windowBounds = { x: 10, y: 20, width: 900, height: 180 }
|
activeProfileId: DEFAULT_PROFILE_ID,
|
||||||
|
profiles: {
|
||||||
|
[DEFAULT_PROFILE_ID]: profile,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Promise.resolve()
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
|
const rawStored = fakeStorage.getItem('prism:settings')
|
||||||
|
assert.ok(rawStored)
|
||||||
|
const stored = JSON.parse(rawStored) as Record<string, unknown>
|
||||||
fakeStorage.setItem('prism:settings', JSON.stringify({
|
fakeStorage.setItem('prism:settings', JSON.stringify({
|
||||||
scopeOrder: profile.scopeOrder,
|
...stored,
|
||||||
hiddenScopes: profile.hiddenScopes,
|
|
||||||
widthWeights: profile.widthWeights,
|
|
||||||
scopeSettings: profile.scopeSettings,
|
|
||||||
scopePopouts: profile.scopePopouts,
|
|
||||||
windowBounds: dirtyBounds,
|
windowBounds: dirtyBounds,
|
||||||
}))
|
}))
|
||||||
|
restoredBounds.length = 0
|
||||||
|
useSettingsStore.setState(previousSettingsState)
|
||||||
|
|
||||||
await useSettingsStore.getState().initializeProfiles()
|
await useSettingsStore.getState().initializeProfiles()
|
||||||
|
|
||||||
@@ -3154,6 +3172,193 @@ test('initializeProfiles ignores stale persisted theme-only state and loads the
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('initializeProfiles ignores legacy persisted inline popouts and restores active profile popouts', async () => {
|
||||||
|
const previousSettingsState = useSettingsStore.getState()
|
||||||
|
const fakeStorage = installFakeLocalStorage()
|
||||||
|
const savedPopoutBounds = { x: 140, y: 60, width: 420, height: 240 }
|
||||||
|
const fakeWindow = installFakeElectronWindow({
|
||||||
|
getProfileSnapshot: async () => {
|
||||||
|
const profile = createDefaultProfile(DEFAULT_PROFILE_NAME)
|
||||||
|
profile.scopePopouts.spectrum = {
|
||||||
|
poppedOut: true,
|
||||||
|
windowBounds: savedPopoutBounds,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeProfileId: DEFAULT_PROFILE_ID,
|
||||||
|
profiles: {
|
||||||
|
[DEFAULT_PROFILE_ID]: profile,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getWindowBounds: async () => ({ x: 10, y: 20, width: 900, height: 180 }),
|
||||||
|
setWindowBounds: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const staleProfile = createDefaultProfile(DEFAULT_PROFILE_NAME)
|
||||||
|
staleProfile.scopePopouts.spectrum = {
|
||||||
|
poppedOut: false,
|
||||||
|
windowBounds: savedPopoutBounds,
|
||||||
|
}
|
||||||
|
fakeStorage.setItem('prism:settings', JSON.stringify({
|
||||||
|
scopeOrder: staleProfile.scopeOrder,
|
||||||
|
hiddenScopes: staleProfile.hiddenScopes,
|
||||||
|
widthWeights: staleProfile.widthWeights,
|
||||||
|
scopeSettings: staleProfile.scopeSettings,
|
||||||
|
scopePopouts: staleProfile.scopePopouts,
|
||||||
|
}))
|
||||||
|
|
||||||
|
await useSettingsStore.getState().initializeProfiles()
|
||||||
|
|
||||||
|
const state = useSettingsStore.getState()
|
||||||
|
assert.equal(state.scopePopouts.spectrum.poppedOut, true)
|
||||||
|
assert.deepEqual(state.scopePopouts.spectrum.windowBounds, savedPopoutBounds)
|
||||||
|
assert.equal(state.hasUnsavedProfileChanges, false)
|
||||||
|
} finally {
|
||||||
|
useSettingsStore.setState(previousSettingsState)
|
||||||
|
fakeWindow.restore()
|
||||||
|
fakeStorage.restore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('initializeProfiles restores saved popout open state while preserving matching dirty draft values', async () => {
|
||||||
|
const previousSettingsState = useSettingsStore.getState()
|
||||||
|
const fakeStorage = installFakeLocalStorage()
|
||||||
|
const savedPopoutBounds = { x: 140, y: 60, width: 420, height: 240 }
|
||||||
|
const dirtyPopoutBounds = { x: 180, y: 72, width: 440, height: 260 }
|
||||||
|
const profile = createDefaultProfile(DEFAULT_PROFILE_NAME)
|
||||||
|
profile.scopePopouts.spectrum = {
|
||||||
|
poppedOut: true,
|
||||||
|
windowBounds: savedPopoutBounds,
|
||||||
|
}
|
||||||
|
const fakeWindow = installFakeElectronWindow({
|
||||||
|
getProfileSnapshot: async () => ({
|
||||||
|
activeProfileId: DEFAULT_PROFILE_ID,
|
||||||
|
profiles: {
|
||||||
|
[DEFAULT_PROFILE_ID]: profile,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getWindowBounds: async () => ({ x: 10, y: 20, width: 900, height: 180 }),
|
||||||
|
setWindowBounds: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
useSettingsStore.getState().applyExternalProfileSnapshot({
|
||||||
|
activeProfileId: DEFAULT_PROFILE_ID,
|
||||||
|
profiles: {
|
||||||
|
[DEFAULT_PROFILE_ID]: profile,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Promise.resolve()
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
|
const rawStored = fakeStorage.getItem('prism:settings')
|
||||||
|
assert.ok(rawStored)
|
||||||
|
const stored = JSON.parse(rawStored) as {
|
||||||
|
activeProfileId?: string | null
|
||||||
|
profileBaselineSignature?: string
|
||||||
|
scopePopouts: ScopePopoutStateMap
|
||||||
|
}
|
||||||
|
assert.equal(stored.activeProfileId, DEFAULT_PROFILE_ID)
|
||||||
|
assert.equal(typeof stored.profileBaselineSignature, 'string')
|
||||||
|
|
||||||
|
fakeStorage.setItem('prism:settings', JSON.stringify({
|
||||||
|
...stored,
|
||||||
|
scopeSettings: {
|
||||||
|
...profile.scopeSettings,
|
||||||
|
spectrum: {
|
||||||
|
...profile.scopeSettings.spectrum,
|
||||||
|
smoothing: 0.42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scopePopouts: {
|
||||||
|
...stored.scopePopouts,
|
||||||
|
spectrum: {
|
||||||
|
poppedOut: false,
|
||||||
|
windowBounds: dirtyPopoutBounds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
useSettingsStore.setState(previousSettingsState)
|
||||||
|
|
||||||
|
await useSettingsStore.getState().initializeProfiles()
|
||||||
|
|
||||||
|
const state = useSettingsStore.getState()
|
||||||
|
assert.equal(state.scopePopouts.spectrum.poppedOut, true)
|
||||||
|
assert.deepEqual(state.scopePopouts.spectrum.windowBounds, dirtyPopoutBounds)
|
||||||
|
assert.equal(state.scopeSettings.spectrum.smoothing, 0.42)
|
||||||
|
assert.equal(state.hasUnsavedProfileChanges, true)
|
||||||
|
} finally {
|
||||||
|
useSettingsStore.setState(previousSettingsState)
|
||||||
|
fakeWindow.restore()
|
||||||
|
fakeStorage.restore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('initializeProfiles discards persisted popout state when the saved profile baseline changed', async () => {
|
||||||
|
const previousSettingsState = useSettingsStore.getState()
|
||||||
|
const fakeStorage = installFakeLocalStorage()
|
||||||
|
const savedPopoutBounds = { x: 140, y: 60, width: 420, height: 240 }
|
||||||
|
const originalProfile = createDefaultProfile(DEFAULT_PROFILE_NAME)
|
||||||
|
const changedProfile = createDefaultProfile(DEFAULT_PROFILE_NAME)
|
||||||
|
changedProfile.scopePopouts.spectrum = {
|
||||||
|
poppedOut: true,
|
||||||
|
windowBounds: savedPopoutBounds,
|
||||||
|
}
|
||||||
|
let snapshotProfile = originalProfile
|
||||||
|
const fakeWindow = installFakeElectronWindow({
|
||||||
|
getProfileSnapshot: async () => ({
|
||||||
|
activeProfileId: DEFAULT_PROFILE_ID,
|
||||||
|
profiles: {
|
||||||
|
[DEFAULT_PROFILE_ID]: snapshotProfile,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getWindowBounds: async () => ({ x: 10, y: 20, width: 900, height: 180 }),
|
||||||
|
setWindowBounds: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
useSettingsStore.getState().applyExternalProfileSnapshot({
|
||||||
|
activeProfileId: DEFAULT_PROFILE_ID,
|
||||||
|
profiles: {
|
||||||
|
[DEFAULT_PROFILE_ID]: originalProfile,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Promise.resolve()
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
|
const rawStored = fakeStorage.getItem('prism:settings')
|
||||||
|
assert.ok(rawStored)
|
||||||
|
const stored = JSON.parse(rawStored) as {
|
||||||
|
scopePopouts: ScopePopoutStateMap
|
||||||
|
}
|
||||||
|
fakeStorage.setItem('prism:settings', JSON.stringify({
|
||||||
|
...stored,
|
||||||
|
scopePopouts: {
|
||||||
|
...stored.scopePopouts,
|
||||||
|
spectrum: {
|
||||||
|
poppedOut: false,
|
||||||
|
windowBounds: { x: 180, y: 72, width: 440, height: 260 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
snapshotProfile = changedProfile
|
||||||
|
useSettingsStore.setState(previousSettingsState)
|
||||||
|
|
||||||
|
await useSettingsStore.getState().initializeProfiles()
|
||||||
|
|
||||||
|
const state = useSettingsStore.getState()
|
||||||
|
assert.equal(state.scopePopouts.spectrum.poppedOut, true)
|
||||||
|
assert.deepEqual(state.scopePopouts.spectrum.windowBounds, savedPopoutBounds)
|
||||||
|
assert.equal(state.hasUnsavedProfileChanges, false)
|
||||||
|
} finally {
|
||||||
|
useSettingsStore.setState(previousSettingsState)
|
||||||
|
fakeWindow.restore()
|
||||||
|
fakeStorage.restore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('profiles without saved window bounds mark the first user move dirty after load sync completes', async () => {
|
test('profiles without saved window bounds mark the first user move dirty after load sync completes', async () => {
|
||||||
const previousSettingsState = useSettingsStore.getState()
|
const previousSettingsState = useSettingsStore.getState()
|
||||||
const currentBounds = { x: 10, y: 20, width: 900, height: 180 }
|
const currentBounds = { x: 10, y: 20, width: 900, height: 180 }
|
||||||
|
|||||||
Reference in New Issue
Block a user