diff --git a/src/main/index.ts b/src/main/index.ts index 305b2c0..4a19b06 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1895,6 +1895,7 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro autoHideMenuBar: true, title: `Prism ${SCOPE_LABELS[kind]}`, alwaysOnTop: getWindowStateStore().getPopoutAlwaysOnTop(kind), + ...(process.platform === 'darwin' ? { acceptFirstMouse: true } : {}), show: false, ...getStaticWindowIconOptions(), webPreferences: { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 5d7b567..66ab258 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -20,6 +20,7 @@ import { getRendererWindowCapabilities } from './windowCapabilities' export default function App(): JSX.Element { const [toolbarVisible, setToolbarVisible] = useState(false) + const [measurementActive, setMeasurementActive] = useState(false) const [settingsPanelHeight, setSettingsPanelHeight] = useState(0) const [bottomBarHeight, setBottomBarHeight] = useState(0) const [trayReady, setTrayReady] = useState(false) @@ -302,13 +303,13 @@ export default function App(): JSX.Element { onMouseUp={useNativeDragRegions ? undefined : handleAltDragEnd} >
- +
diff --git a/src/renderer/components/ScopeModule.tsx b/src/renderer/components/ScopeModule.tsx index b16bacd..7001397 100644 --- a/src/renderer/components/ScopeModule.tsx +++ b/src/renderer/components/ScopeModule.tsx @@ -55,6 +55,7 @@ interface ScopeModuleProps { theme?: ScopeModuleTheme settings?: ScopeSettings[ScopeKind] frameScheduler?: FrameScheduler + onMeasurementActiveChange?: (active: boolean) => void dataSource?: | SpectrumAnalyzerDataSource | OscilloscopeDataSource @@ -400,6 +401,7 @@ export default function ScopeModule({ theme, settings, frameScheduler, + onMeasurementActiveChange, dataSource, }: ScopeModuleProps): JSX.Element { const containerRef = useRef(null) @@ -447,6 +449,7 @@ export default function ScopeModule({ ? visualizer as ScopeMeasurementSource : null }, + onActiveChange: onMeasurementActiveChange, }) useEffect(() => { diff --git a/src/renderer/components/Strip.tsx b/src/renderer/components/Strip.tsx index 0c00b45..5650435 100644 --- a/src/renderer/components/Strip.tsx +++ b/src/renderer/components/Strip.tsx @@ -9,7 +9,11 @@ import { usePerformanceStore } from '../stores/performanceStore' import { FrameScheduler } from '../visualizers/frameScheduler' import { getRendererWindowCapabilities } from '../windowCapabilities' -export default function Strip(): JSX.Element { +interface StripProps { + onMeasurementActiveChange?: (active: boolean) => void +} + +export default function Strip({ onMeasurementActiveChange }: StripProps): JSX.Element { const scopeOrder = useSettingsStore((s) => s.scopeOrder) const hiddenScopes = useSettingsStore((s) => s.hiddenScopes) const scopePopouts = useSettingsStore((s) => s.scopePopouts) @@ -268,6 +272,7 @@ export default function Strip(): JSX.Element { ))} diff --git a/src/renderer/popouts/ScopePopoutWindow.tsx b/src/renderer/popouts/ScopePopoutWindow.tsx index f63fb72..de7f85c 100644 --- a/src/renderer/popouts/ScopePopoutWindow.tsx +++ b/src/renderer/popouts/ScopePopoutWindow.tsx @@ -62,6 +62,7 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps) const [snapshot, setSnapshot] = useState | null>(null) const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(false) const [cursorInsideWindow, setCursorInsideWindow] = useState(false) + const [measurementActive, setMeasurementActive] = useState(false) const prevMiniSettingsOpenRef = useRef(false) const frameTarget = usePerformanceStore((s) => s.frameTarget) const miniSettingsOpen = useUiStore((s) => s.settingsOpen) @@ -230,6 +231,7 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps) 'scope-popout__chrome', miniSettingsOpen ? 'is-expanded' : '', cursorInsideWindow ? 'is-cursor-inside' : '', + measurementActive ? 'is-measuring' : '', ].join(' ').trim()} >
@@ -294,6 +296,7 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps) settings={effectiveSettings} frameScheduler={frameScheduler} dataSource={dataSource} + onMeasurementActiveChange={setMeasurementActive} /> diff --git a/src/renderer/styles/globals.css b/src/renderer/styles/globals.css index 5613de8..a5870d6 100644 --- a/src/renderer/styles/globals.css +++ b/src/renderer/styles/globals.css @@ -2284,7 +2284,7 @@ button.toolbar__version:hover { .scope-popout__viewport:hover .scope-popout__chrome, .scope-popout__chrome:hover, -.scope-popout__chrome:focus-within, +.scope-popout__chrome:has(:focus-visible), .scope-popout__chrome.is-cursor-inside, .scope-popout__chrome.is-expanded { max-height: 58px; @@ -2293,6 +2293,13 @@ button.toolbar__version:hover { transform: translateY(0); } +.scope-popout__viewport .scope-popout__chrome.is-measuring { + max-height: 0; + opacity: 0; + pointer-events: none; + transform: translateY(-8px); +} + .scope-popout__header { display: flex; align-items: center; diff --git a/test/renderer-helpers.test.ts b/test/renderer-helpers.test.ts index a70e952..f4520e8 100644 --- a/test/renderer-helpers.test.ts +++ b/test/renderer-helpers.test.ts @@ -4212,6 +4212,38 @@ test('main and detached windows keep frameless Prism chrome while enabling snap- assert.doesNotMatch(nowPlayingSource, /WindowResizeOverlay/) }) +test('detached scope interactions accept the first macOS click and hide chrome during measurement', async () => { + const mainSource = await readFile(join(process.cwd(), 'src', 'main', 'index.ts'), 'utf8') + const scopeModuleSource = await readFile(join(process.cwd(), 'src', 'renderer', 'components', 'ScopeModule.tsx'), 'utf8') + const popoutSource = await readFile(join(process.cwd(), 'src', 'renderer', 'popouts', 'ScopePopoutWindow.tsx'), 'utf8') + const stylesSource = await readFile(join(process.cwd(), 'src', 'renderer', 'styles', 'globals.css'), 'utf8') + const mainWindowFactorySource = mainSource.slice( + mainSource.indexOf('function createMainWindow'), + mainSource.indexOf('function createScopePopoutWindow'), + ) + + assert.match(mainSource, /function createScopePopoutWindow\([\s\S]*?process\.platform === 'darwin' \? \{ acceptFirstMouse: true \} : \{\}/) + assert.doesNotMatch(mainWindowFactorySource, /acceptFirstMouse/) + assert.match(scopeModuleSource, /onMeasurementActiveChange\?: \(active: boolean\) => void/) + assert.match(scopeModuleSource, /onActiveChange: onMeasurementActiveChange/) + assert.match(popoutSource, /measurementActive \? 'is-measuring' : ''/) + assert.match(popoutSource, /onMeasurementActiveChange=\{setMeasurementActive\}/) + assert.match(stylesSource, /\.scope-popout__chrome:has\(:focus-visible\)/) + assert.doesNotMatch(stylesSource, /\.scope-popout__chrome:focus-within/) + assert.match(stylesSource, /\.scope-popout__viewport \.scope-popout__chrome\.is-measuring \{[\s\S]*?max-height: 0;[\s\S]*?opacity: 0;[\s\S]*?pointer-events: none;[\s\S]*?transform: translateY\(-8px\);[\s\S]*?\}/) +}) + +test('main window hides its toolbar while a docked scope measurement is active', async () => { + const appSource = await readFile(join(process.cwd(), 'src', 'renderer', 'App.tsx'), 'utf8') + const stripSource = await readFile(join(process.cwd(), 'src', 'renderer', 'components', 'Strip.tsx'), 'utf8') + + assert.match(stripSource, /onMeasurementActiveChange\?: \(active: boolean\) => void/) + assert.match(stripSource, /onMeasurementActiveChange=\{onMeasurementActiveChange\}/) + assert.match(appSource, /const \[measurementActive, setMeasurementActive\] = useState\(false\)/) + assert.match(appSource, /toolbarVisible && !measurementActive \? 'is-visible' : ''/) + assert.match(appSource, //) +}) + test('programmatic top/bottom reposition flushes fresh bounds through persistence channels', async () => { const mainSource = await readFile(join(process.cwd(), 'src', 'main', 'index.ts'), 'utf8')