Skip to content

Commit 04519de

Browse files
committed
feat(reader): improve Windows controls and add fit shortcuts
1 parent f4da6ea commit 04519de

9 files changed

Lines changed: 91 additions & 5 deletions

File tree

‎doc/preview-windows.png‎

-24.3 KB
Loading

‎electron/layout-settings-smoke.mjs‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import assert from 'node:assert/strict';
2+
import {app} from 'electron';
3+
export async function verifyLayoutSettings(window){
4+
const evaluate=code=>window.webContents.executeJavaScript(code);
5+
const wait=async code=>{for(let n=0;n<200;n++){try{if(await evaluate(code))return;}catch{}await new Promise(resolve=>setTimeout(resolve,50));}throw Error('Layout settings timed out: '+code);};
6+
const open=async()=>{window.show();window.focus();await new Promise(resolve=>setTimeout(resolve,150));await wait('window.previewReady===true');await evaluate(`if(!document.querySelector('.settings'))document.querySelector('.toolbar-actions fluent-button:last-child').click()`);await wait(`!!document.querySelector('.settings .mac-mode-control')`);};
7+
await wait('window.previewReady===true');
8+
await evaluate(`window.previewPreferences.save({engine:'pdf_math_precise',translationMode:'reading',interactionMode:'comparison',sourceLanguage:'Japanese',pageConcurrency:7,reduceMotion:true,kernelAdvancedOptions:{pdf_math_fast:{},pdf_math_precise:{custom_system_prompt:'Keep mathematics unchanged'}}})`);
9+
await new Promise(resolve=>{window.webContents.once('did-finish-load',resolve);window.webContents.reload();});
10+
await wait(`window.previewReady===true`);
11+
await new Promise(resolve=>setTimeout(resolve,600));
12+
const expected=await evaluate('window.previewPreferences.load()');
13+
for(const direction of ['horizontal','vertical']){
14+
await evaluate(`(async()=>{const menu=await window.previewWindow.menu();const find=items=>{for(const item of items){if(item.id==='layout-${direction}')return item.path;const path=item.submenu&&find(item.submenu);if(path)return path;}};await window.previewWindow.menuAction(find(menu));})()`);
15+
await wait(`(async()=> (await window.previewPreferences.load()).direction==='${direction}')()`);
16+
await open();
17+
await new Promise(resolve=>setTimeout(resolve,300));
18+
const actual=await evaluate('window.previewPreferences.load()');
19+
for(const key of ['engine','translationMode','interactionMode','sourceLanguage','pageConcurrency','reduceMotion','kernelAdvancedOptions'])assert.deepEqual(actual[key],expected[key],key+' survives layout and settings remount');
20+
await evaluate(`document.querySelector('.settings-heading fluent-button').click()`);
21+
await new Promise(resolve=>setTimeout(resolve,250));
22+
}
23+
await open();
24+
await wait(`!document.querySelector('.mac-mode-control').disabled`);
25+
await evaluate(`document.querySelector('.mac-mode-control fluent-tab[data-segment-value="pdf_math_fast"]').click()`);
26+
await wait(`(async()=> (await window.previewPreferences.load()).engine==='pdf_math_fast')()`);
27+
await wait(`!!document.querySelector('.mac-mode-control')&&!document.querySelector('.mac-mode-control').disabled`);
28+
await evaluate(`document.querySelector('.mac-mode-control fluent-tab[data-segment-value="pdf_math_fast"]').focus()`);
29+
window.webContents.sendInputEvent({type:'keyDown',keyCode:'Right'});window.webContents.sendInputEvent({type:'keyUp',keyCode:'Right'});
30+
await wait(`(async()=> (await window.previewPreferences.load()).engine==='pdf_math_precise')()`);
31+
console.log('Layout regression passed: saved kernel, modes and advanced settings survive remount; mouse and keyboard kernel selection still work.');
32+
app.quit();
33+
}

‎electron/main.mjs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ else {
188188
{id:'app-menu',label:smoke?app.name:'PDFMathReader',submenu:[{id:'app-about',role:'about'},command('Settings…',accelerator(','),'settings','app-settings'),{type:'separator'},{id:'app-quit',role:'quit'}]},
189189
{id:'file-menu',label:'File',submenu:[{id:'file-new-window',label:'New Window',accelerator:accelerator('N'),click:()=>{void createWindow().catch(handleBackendFailure);}},command('Open PDF…',accelerator('O'),'open','file-open'),command('Open recents...',undefined,'recents','file-recents'),command('Preference',undefined,'preferences','file-preferences'),{type:'separator'},command('Close Document',accelerator('W'),'close-document','file-close-document'),{id:'file-close-window',label:'Close Window',accelerator:closeWindowAccelerator(process.platform),click:(_item,target)=>(target||focusedWindow())?.close()}]},
190190
{id:'edit-menu',role:'editMenu'},
191-
{id:'view-menu',label:'View',submenu:[command('Find…',accelerator('F'),'search','view-search'),command('Show Original / Translation',accelerator('R'),'translation','view-translation'),command('Zoom In',accelerator('='),'zoom-in','view-zoom-in'),command('Zoom Out',accelerator('-'),'zoom-out','view-zoom-out'),command('Fit Width',undefined,'fit-width','view-fit-width'),command('Fit Height',undefined,'fit-height','view-fit-height'),command('Toggle Sidebar',accelerator('B'),'sidebar','view-sidebar'),{type:'separator'},{id:'view-layout',label:'Layout',submenu:['vertical','horizontal'].map(direction=>({id:'layout-'+direction,label:direction==='vertical'?'Vertical':'Horizontal',type:'radio',checked:direction==='vertical',click:(_item,target)=>(target||focusedWindow())?.webContents.send('reader:action','layout:'+direction)}))},{label:'Pages per Row',id:'layout-columns',submenu:[1,2,4].map(columns=>({id:'columns-'+columns,label:({1:'One Side',2:'Two Sides',4:'Quad Side'})[columns],accelerator:accelerator(columns===4?3:columns),type:'radio',checked:columns===1,click:(_item,target)=>(target||focusedWindow())?.webContents.send('reader:action','columns:'+columns)}))},{type:'separator'},{id:'view-fullscreen',role:'togglefullscreen'}]},
191+
{id:'view-menu',label:'View',submenu:[command('Find…',accelerator('F'),'search','view-search'),command('Show Original / Translation',accelerator('R'),'translation','view-translation'),command('Zoom In',accelerator('='),'zoom-in','view-zoom-in'),command('Zoom Out',accelerator('-'),'zoom-out','view-zoom-out'),command('Fit Width',accelerator('0'),'fit-width','view-fit-width'),command('Fit Height',accelerator('9'),'fit-height','view-fit-height'),command('Toggle Sidebar',accelerator('B'),'sidebar','view-sidebar'),{type:'separator'},{id:'view-layout',label:'Layout',submenu:['vertical','horizontal'].map(direction=>({id:'layout-'+direction,label:direction==='vertical'?'Vertical':'Horizontal',type:'radio',checked:direction==='vertical',click:(_item,target)=>(target||focusedWindow())?.webContents.send('reader:action','layout:'+direction)}))},{label:'Pages per Row',id:'layout-columns',submenu:[1,2,4].map(columns=>({id:'columns-'+columns,label:({1:'One Side',2:'Two Sides',4:'Quad Side'})[columns],accelerator:accelerator(columns===4?3:columns),type:'radio',checked:columns===1,click:(_item,target)=>(target||focusedWindow())?.webContents.send('reader:action','columns:'+columns)}))},{type:'separator'},{id:'view-fullscreen',role:'togglefullscreen'}]},
192192
{id:'go-menu',label:'Go',submenu:[command('Previous Page',undefined,'page-previous','go-previous'),command('Next Page',undefined,'page-next','go-next'),{type:'separator'},...Array.from({length:9},(_,i)=>command(`Go to ${(i+1)*10}%`,accelerator('Shift+'+(i+1)),`percent:${(i+1)*10}`,`go-percent-${(i+1)*10}`)),command('Go to 100%',accelerator('Shift+0'),'percent:100','go-percent-100')]},
193193
{id:'translation-menu',label:'Translation',submenu:[command('Choose Language…',accelerator('L'),'language','translation-language'),command('Choose Kernel…',accelerator('K'),'kernel','translation-kernel')]},...(process.platform==='win32'?[]:[{id:'window-menu',role:'windowMenu'}])];const locale=preferences?.load?.().uiLanguage||'en';const localize=items=>items.map(item=>({...item,...item.label?{label:menuLabel(item.label,locale)}:{},...Array.isArray(item.submenu)?{submenu:localize(item.submenu)}:{}}));const localized=localize(template),actions=new Map(),indexActions=items=>{for(const item of items){if(item.id&&typeof item.click==='function')actions.set(item.id,item.click);if(Array.isArray(item.submenu))indexActions(item.submenu);}};indexActions(localized);menuActions=actions;applicationMenu=Menu.buildFromTemplate(localized);Menu.setApplicationMenu(process.platform==='win32'?null:applicationMenu);if(process.platform==='win32')for(const target of windows.keys())hideNativeMenuBar(target);};rebuildMenu();
194194
const credentialOptions={path:join(app.getPath('userData'),'openai-key.enc'),safeStorage};
@@ -297,7 +297,7 @@ else {
297297
ipcMain.handle('documents:open',async(event,value)=>{const target=trustedWindow(event);if(typeof value?.path==='string'){await validateSystemPDF(value.path);await openDocumentWindow(value.path,target);}else{if(!value||typeof value.name!=='string'||!(value.bytes instanceof Uint8Array)||value.bytes.byteLength>50*1024*1024)throw Error('Invalid PDF.');await openDocumentWindow({name:value.name,bytes:value.bytes},target);}return true;});
298298
ipcMain.handle('window:new',async event=>{trustedWindow(event);await createWindow();});
299299
window=await createWindow(pendingFiles.shift());backend=windows.get(window).backend;documentsReady=true;await deliverPendingFiles();
300-
if(smoke){const checks=await import('./smoke.mjs');if(smoke==='windows-settings')await (await import('./windows-settings-smoke.mjs')).verifyWindowsSettings(window);else if(smoke==='locales')await (await import('./locales-smoke.mjs')).verifyLocales(window);else if(smoke==='advanced')await (await import('./advanced-smoke.mjs')).verifyAdvanced(window);else if(smoke==='fluent')await (await import('./fluent-smoke.mjs')).verifyFluent(window);else if(smoke==='search')await (await import('./search-smoke.mjs')).verifySearch(window);else if(smoke==='multi-window')await (await import('./multi-window-smoke.mjs')).verifyMultiWindow(window,windows,createWindow);else if(smoke==='fit-width')await (await import('./fit-width-smoke.mjs')).verifyFitWidth(window,recents);else if(smoke==='reading-view')await (await import('./reading-view-smoke.mjs')).verifyReadingView(window,recents);else if(smoke==='performance')await (await import('./performance-smoke.mjs')).verifyPerformance(window);else if(smoke==='benchmark')await (await import('./benchmark-smoke.mjs')).verifyBenchmark(window);else if(smoke==='coverage')await (await import('./coverage-smoke.mjs')).verifyCoverage(window,backend,token);else if(smoke==='resize'){await (await import('./recents-smoke.mjs')).verifyRecents(window,recents);await (await import('./resize-smoke.mjs')).verifyResize(window);}else if(smoke==='animation')await (await import('./animation-smoke.mjs')).verifyAnimation(window);else if(smoke==='ux')await (await import('./ux-smoke.mjs')).verifyUX(window,recents);else if(smoke==='layout-region')await (await import('./layout-region-smoke.mjs')).verifyLayoutRegion(window);else if(smoke==='kernel-choice')await (await import('./kernel-choice-smoke.mjs')).verifyKernelChoice(window);else if(smoke==='kernels')await (await import('./kernel-smoke.mjs')).verifyKernelUI(window);else if(smoke==='file-open')await checks.verifySystemOpen(window);else await checks.verify(window,backend,token,smoke,credentials);}
300+
if(smoke){const checks=await import('./smoke.mjs');if(smoke==='layout-settings')await (await import('./layout-settings-smoke.mjs')).verifyLayoutSettings(window);else if(smoke==='windows-settings')await (await import('./windows-settings-smoke.mjs')).verifyWindowsSettings(window);else if(smoke==='locales')await (await import('./locales-smoke.mjs')).verifyLocales(window);else if(smoke==='advanced')await (await import('./advanced-smoke.mjs')).verifyAdvanced(window);else if(smoke==='fluent')await (await import('./fluent-smoke.mjs')).verifyFluent(window);else if(smoke==='search')await (await import('./search-smoke.mjs')).verifySearch(window);else if(smoke==='multi-window')await (await import('./multi-window-smoke.mjs')).verifyMultiWindow(window,windows,createWindow);else if(smoke==='fit-width')await (await import('./fit-width-smoke.mjs')).verifyFitWidth(window,recents);else if(smoke==='reading-view')await (await import('./reading-view-smoke.mjs')).verifyReadingView(window,recents);else if(smoke==='performance')await (await import('./performance-smoke.mjs')).verifyPerformance(window);else if(smoke==='benchmark')await (await import('./benchmark-smoke.mjs')).verifyBenchmark(window);else if(smoke==='coverage')await (await import('./coverage-smoke.mjs')).verifyCoverage(window,backend,token);else if(smoke==='resize'){await (await import('./recents-smoke.mjs')).verifyRecents(window,recents);await (await import('./resize-smoke.mjs')).verifyResize(window);}else if(smoke==='animation')await (await import('./animation-smoke.mjs')).verifyAnimation(window);else if(smoke==='ux')await (await import('./ux-smoke.mjs')).verifyUX(window,recents);else if(smoke==='layout-region')await (await import('./layout-region-smoke.mjs')).verifyLayoutRegion(window);else if(smoke==='kernel-choice')await (await import('./kernel-choice-smoke.mjs')).verifyKernelChoice(window);else if(smoke==='kernels')await (await import('./kernel-smoke.mjs')).verifyKernelUI(window);else if(smoke==='file-open')await checks.verifySystemOpen(window);else await checks.verify(window,backend,token,smoke,credentials);}
301301
}).catch(async error=>{if(smoke){await backend?.close();console.error('Desktop smoke failed:',error.stack||error.message);if(smoke==='file-open')await writeFile('/tmp/preview-system-open-result.json',JSON.stringify({passed:false,error:error.message}));app.exit(1);return;}dialog.showErrorBox('PDFMathReader',`The local reader could not start.\n\n${error?.message||'The backend utility process did not become ready.'}\n\nQuit and reopen PDFMathReader.`);app.quit();});
302302
app.on('before-quit',event=>{
303303
if(!backend || quitting)return;

‎electron/window-chrome.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,6 @@ export function shortcutAction(platform,input){
9595
if(!input.control||input.meta||input.alt)return null;
9696
}
9797
const digit=/^Digit[0-9]$/.test(input.code||'')?input.code.slice(-1):/^\d$/.test(key)?key:null;
98-
if(digit!==null)return input.shift?'percent:'+(digit==='0'?100:Number(digit)*10):({'1':'columns:1','2':'columns:2','3':'columns:4'}[digit]||null);
98+
if(digit!==null)return input.shift?'percent:'+(digit==='0'?100:Number(digit)*10):({'0':'fit-width','9':'fit-height','1':'columns:1','2':'columns:2','3':'columns:4'}[digit]||null);
9999
return SHORTCUT_ACTIONS[key]||null;
100100
}

‎server/window-chrome.test.mjs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,19 @@ test('accelerators and before-input shortcuts follow each platform',()=>{
4747

4848
test('page navigation supports dedicated keys and shift arrows on every platform',()=>{for(const platform of ['darwin','win32','linux'])for(const [key,shift,expected] of [['PageUp',false,'page-previous'],['PageDown',false,'page-next'],['ArrowUp',true,'page-previous'],['ArrowLeft',true,'page-previous'],['ArrowDown',true,'page-next'],['ArrowRight',true,'page-next']])assert.equal(shortcutAction(platform,{type:'keyDown',key,shift}),expected);assert.equal(shortcutAction('darwin',{type:'keyDown',key:'ArrowDown'}),null);});
4949

50+
test('fit shortcuts preserve shifted percentage navigation on every platform',()=>{
51+
for(const platform of ['darwin','win32','linux']){
52+
const modifier=platform==='darwin'?{meta:true}:{control:true};
53+
for(const [digit,action] of [['0','fit-width'],['9','fit-height']]){
54+
const input={type:'keyDown',key:digit,code:'Digit'+digit,...modifier};
55+
assert.equal(shortcutAction(platform,input),action);
56+
assert.equal(shortcutAction(platform,{...input,shift:true}),'percent:'+(digit==='0'?100:90));
57+
assert.equal(shortcutAction(platform,{...input,alt:true}),null);
58+
assert.equal(shortcutAction(platform,{type:'keyDown',key:digit}),null);
59+
}
60+
}
61+
});
62+
5063
test('serialized menu entries expose numeric paths and preserve nested metadata',()=>{
5164
const open={id:'file-open',label:'Open PDF…',type:'normal',enabled:true,checked:false,accelerator:'Ctrl+O'};
5265
const menu={items:[{id:'file-menu',label:'File',type:'submenu',enabled:true,checked:false,accelerator:null,submenu:{items:[{type:'separator'},open]}}]};

‎src/App.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ watch([pageInput,zoomInput,()=>pages.value.length,navigatorVisible],()=>nextTick
307307
async function openSettings(target){selectedParagraph.value=null;settings.value=true;await nextTick();if(target==='language'){languageInput.value?.focus();languageMenuOpen.value=true;}else if(target==='kernel'){kernelFocusPending=engineBusy.value;kernelInput.value?.el?.querySelector('.macvue-segment[data-state="on"]')?.focus();}}
308308
function chooseKernel(id){kernelFocusPending=!!kernelInput.value?.el?.contains(document.activeElement);engine.value=id;}
309309
function kernelKeys(e){const current=kernelOptions.findIndex(k=>k.id===engine.value);const index=e.key==='ArrowRight'?Math.min(2,current+1):e.key==='ArrowLeft'?Math.max(0,current-1):e.key==='Home'?0:e.key==='End'?2:null;if(index===null)return;e.preventDefault();if(engineBusy.value)return;kernelFocusPending=true;engine.value=kernelOptions[index].id;nextTick(()=>kernelInput.value?.el?.querySelector('.macvue-segment[data-state="on"]')?.focus());}
310-
function readerAction(action){if(action.startsWith('layout:'))direction.value=action.split(':')[1];else if(action.startsWith('columns:'))columns.value=Number(action.split(':')[1]);else if(action==='close-document')void closeDocument(true);else if(action==='recents'){if((pages.value.length||loading.value)&&window.previewWindow?.new)void window.previewWindow.new();else void closeDocument();}else if(action==='preferences')openSettings();else if(action==='copy-paragraph')void copyHoveredParagraph();else if(action==='search')void openSearch();else if(action==='open')fileInput.value?.click();else if(action==='translation'){if(!translationTaskProgress.value.busy)showTranslations.value=!showTranslations.value;}else if(action==='zoom-in')changeZoom(.1);else if(action==='zoom-out')changeZoom(-.1);else if(action==='page-previous'){if(pages.value.length)go(active.value-1);}else if(action==='page-next'){if(pages.value.length)go(active.value+1);}else if(action==='sidebar'){if(pages.value.length)toggleSidebar();}else if(action==='settings')settings.value?settings.value=false:openSettings();else if(action==='language'||action==='kernel')openSettings(action);else if(action==='fit-width')chooseFit('width');else if(action==='fit-height')chooseFit('height');else if(action.startsWith('percent:'))go(Math.max(1,Math.ceil(pages.value.length*Number(action.split(':')[1])/100)));}
310+
function readerAction(action){if(action.startsWith('layout:'))direction.value=action.split(':')[1];else if(action.startsWith('columns:'))columns.value=Number(action.split(':')[1]);else if(action==='close-document')void closeDocument(true);else if(action==='recents'){if((pages.value.length||loading.value)&&window.previewWindow?.new)void window.previewWindow.new();else void closeDocument();}else if(action==='preferences')openSettings();else if(action==='copy-paragraph')void copyHoveredParagraph();else if(action==='search')void openSearch();else if(action==='open')fileInput.value?.click();else if(action==='translation'){if(!translationTaskProgress.value.busy)showTranslations.value=!showTranslations.value;}else if(action==='zoom-in')changeZoom(.1);else if(action==='zoom-out')changeZoom(-.1);else if(action==='page-previous'){if(pages.value.length)go(active.value-1);}else if(action==='page-next'){if(pages.value.length)go(active.value+1);}else if(action==='sidebar'){if(pages.value.length)toggleSidebar();}else if(action==='settings')settings.value?settings.value=false:openSettings();else if(action==='language'||action==='kernel')openSettings(action);else if(action==='fit-width'||action==='fit-height'){if(pages.value.length){chooseFit(action==='fit-width'?'width':'height');notifyCopy(menuLabel(action==='fit-width'?'Fit Width':'Fit Height',uiLanguage.value));}}else if(action.startsWith('percent:'))go(Math.max(1,Math.ceil(pages.value.length*Number(action.split(':')[1])/100)));}
311311
function dismissPopovers(){windowsMenu.value?.close();settings.value=false;selectedParagraph.value=null;kernelFocusPending=false;error.value='';}
312312
function outsidePopover(e){if(e.type==="focusin"&&kernelFocusPending&&engineBusy.value)return;if(e.target instanceof Element&&e.target.closest('.document-search,[data-popover-trigger],.settings,.error-banner,.macvue-pop-up-button-content'))return;dismissPopovers();}
313313
function popoverFocusOut(e){if(kernelFocusPending&&engineBusy.value)return;if(e.relatedTarget&&!e.currentTarget.contains(e.relatedTarget)&&!e.relatedTarget.closest?.('.macvue-pop-up-button-content'))dismissPopovers();}

0 commit comments

Comments
 (0)