Skip to content

Commit 66715a6

Browse files
committed
feat: add bidirectional quick return links for search results
1 parent a27f14f commit 66715a6

9 files changed

Lines changed: 86 additions & 7 deletions

File tree

‎electron/export-symbols.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import AppKit
22
import Foundation
33
let output=CommandLine.arguments[1]
44
try FileManager.default.createDirectory(atPath:output,withIntermediateDirectories:true)
5-
for name in ["magnifyingglass","sidebar.left","minus.magnifyingglass","plus.magnifyingglass","gearshape","doc.badge.plus","xmark","character.book.closed","doc.text","checkmark.circle","trash","arrow.left.and.right","arrow.up.and.down","chevron.up","chevron.down","text.bubble"] {
5+
for name in ["link","magnifyingglass","sidebar.left","minus.magnifyingglass","plus.magnifyingglass","gearshape","doc.badge.plus","xmark","character.book.closed","doc.text","checkmark.circle","trash","arrow.left.and.right","arrow.up.and.down","chevron.up","chevron.down","text.bubble"] {
66
guard let image=NSImage(systemSymbolName:name,accessibilityDescription:nil)?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize:22,weight:.regular)) else { fatalError("Missing system symbol: \(name)") }
77
let canvas=NSImage(size:NSSize(width:28,height:28))
88
canvas.lockFocus()

‎electron/main.mjs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {createQuickLinkStore} from './quick-links.mjs';
12
import {app,BrowserWindow,dialog,Menu,ShareMenu,ipcMain,shell,clipboard,safeStorage,systemPreferences,nativeTheme} from 'electron';
23
import {randomBytes} from 'node:crypto';
34
import {execFile} from 'node:child_process';
@@ -272,6 +273,9 @@ else {
272273
});
273274
});
274275
ipcMain.handle('previewAnnotations:markDeleteHint',async event=>{trustedWindow(event);return annotations.markDeleteHint();});
276+
const quickLinks=createQuickLinkStore(join(app.getPath('userData'),'quick-links'));
277+
ipcMain.handle('quickLinks:load',(event,key)=>{trustedWindow(event);return quickLinks.load(key);});
278+
ipcMain.handle('quickLinks:save',(event,{key,links})=>{trustedWindow(event);return quickLinks.save(key,links);});
275279
ipcMain.handle('previewAnnotations:loadState',async(event,key)=>{trustedWindow(event);return annotations.loadState(key);});
276280
ipcMain.handle('previewAnnotations:load',async(event,key)=>{const target=trustedWindow(event);return annotations.load(key);});
277281
ipcMain.handle('previewAnnotations:save',async(event,value)=>{const target=trustedWindow(event),state=windows.get(target);return annotations.save(value,annotationSourceForWindow(state,value?.key));});
@@ -414,7 +418,7 @@ else {
414418
ipcMain.handle('window:new',async event=>{trustedWindow(event);await createWindow();});
415419
if(!smoke&&preferences.load().restoreDocuments){for(const document of documentSession.restore()){try{await validateSystemPDF(document.path);}catch{continue;}if(!pendingFiles.includes(document.path))await createWindow(document.path,document.view);}}
416420
window=[...windows.keys()][0]||await createWindow(pendingFiles.shift());backend=windows.get(window).backend;documentsReady=true;await deliverPendingFiles();
417-
if(smoke){const checks=await import('./smoke.mjs');if(smoke==='translation-prefetch')await (await import('./translation-prefetch-smoke.mjs')).verifyTranslationPrefetch(window,recents);else if(smoke==='pdf-navigation')await (await import('./pdf-navigation-smoke.mjs')).verifyPDFNavigation(window,recents);else if(smoke==='sidebar')await (await import('./sidebar-smoke.mjs')).verifySidebar(window,recents);else if(smoke==='advanced-cache')await (await import('./advanced-cache-smoke.mjs')).verifyAdvancedCache(window);else if(smoke==='recent-menu')await (await import('./recent-menu-smoke.mjs')).verifyRecentMenu(window,recents);else if(smoke==='startup')await (await import('./startup-smoke.mjs')).verifyStartup(window);else if(smoke==='session')await (await import('./session-smoke.mjs')).verifySession(window,windows,createWindow);else 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==='annotations')await (await import('./annotations-smoke.mjs')).verifyAnnotations(window,recents);else if(smoke==='file-open')await checks.verifySystemOpen(window);else await checks.verify(window,backend,token,smoke,credentials);}
421+
if(smoke){const checks=await import('./smoke.mjs');if(smoke==='quick-links')await (await import('./quick-links-smoke.mjs')).verifyQuickLinks(window);else if(smoke==='translation-prefetch')await (await import('./translation-prefetch-smoke.mjs')).verifyTranslationPrefetch(window,recents);else if(smoke==='pdf-navigation')await (await import('./pdf-navigation-smoke.mjs')).verifyPDFNavigation(window,recents);else if(smoke==='sidebar')await (await import('./sidebar-smoke.mjs')).verifySidebar(window,recents);else if(smoke==='advanced-cache')await (await import('./advanced-cache-smoke.mjs')).verifyAdvancedCache(window);else if(smoke==='recent-menu')await (await import('./recent-menu-smoke.mjs')).verifyRecentMenu(window,recents);else if(smoke==='startup')await (await import('./startup-smoke.mjs')).verifyStartup(window);else if(smoke==='session')await (await import('./session-smoke.mjs')).verifySession(window,windows,createWindow);else 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==='annotations')await (await import('./annotations-smoke.mjs')).verifyAnnotations(window,recents);else if(smoke==='file-open')await checks.verifySystemOpen(window);else await checks.verify(window,backend,token,smoke,credentials);}
418422
}).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();});
419423
app.on('before-quit',event=>{
420424
if(!backend || quitting)return;

‎electron/preload.cjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,5 @@ contextBridge.exposeInMainWorld('previewResize',Object.freeze({
7979
contextBridge.exposeInMainWorld('previewClipboard',Object.freeze({writeText:text=>ipcRenderer.invoke('clipboard:write-text',text)}));
8080

8181
contextBridge.exposeInMainWorld('previewHaptics',Object.freeze({tick:()=>ipcRenderer.invoke('haptics:tick')}));
82+
83+
contextBridge.exposeInMainWorld('previewQuickLinks',Object.freeze({load:key=>ipcRenderer.invoke('quickLinks:load',key),save:(key,links)=>ipcRenderer.invoke('quickLinks:save',{key,links})}));

‎electron/quick-links-smoke.mjs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import assert from 'node:assert/strict';
2+
export async function verifyQuickLinks(window){
3+
const evaluate=code=>window.webContents.executeJavaScript(code),pause=ms=>new Promise(r=>setTimeout(r,ms));
4+
async function wait(code){for(let i=0;i<400;i++){if(await evaluate(code))return;await pause(50);}throw Error('Quick links timeout: '+code);}
5+
await wait(`!![...document.querySelectorAll('button')].find(b=>b.textContent==='Try a sample document')`);
6+
await evaluate(`[...document.querySelectorAll('button')].find(b=>b.textContent==='Try a sample document').click()`);
7+
await wait(`!![...document.querySelectorAll('.paragraph')].find(b=>b.getAttribute('aria-label')?.includes('Mock translated paragraph'))`);
8+
await evaluate(`document.querySelector('[aria-label="Search document"]').click()`);
9+
await wait(`!!document.querySelector('[aria-label="Search document text"]')`);
10+
await evaluate(`(()=>{const input=document.querySelector('[aria-label="Search document text"]');input.value='Mock translated';input.dispatchEvent(new Event('input',{bubbles:true}));})()`);
11+
await wait(`!!document.querySelector('.quick-return-create')`);
12+
await evaluate(`document.querySelector('.quick-return-create').click()`);
13+
await wait(`!document.querySelector('.document-search')&&document.querySelectorAll('.paragraph-quick-link').length===2`);
14+
for(const side of ['origin','result']){await evaluate(`document.querySelector('[data-link-side="${side}"]').click()`);await pause(250);assert.equal(await evaluate(`document.querySelectorAll('.paragraph-quick-link [data-symbol="link"]').length`),2);}
15+
await evaluate(`document.querySelector('.translation-toggle').click()`);await pause(250);
16+
assert.equal(await evaluate(`document.querySelectorAll('.paragraph-quick-link').length`),2);
17+
await new Promise(resolve=>{window.webContents.once('did-finish-load',resolve);window.webContents.reload();});
18+
await wait(`!![...document.querySelectorAll('button')].find(b=>b.textContent==='Try a sample document')`);
19+
await evaluate(`[...document.querySelectorAll('button')].find(b=>b.textContent==='Try a sample document').click()`);
20+
await wait(`document.querySelectorAll('.paragraph-quick-link').length===2`);
21+
console.log('Quick links passed: creation, paired SF Symbols, both directions, original/translation switching, persistence across reload.');window.close();
22+
}

‎electron/quick-links.mjs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {createHash} from 'node:crypto';
2+
import {mkdir,readFile,writeFile,rename} from 'node:fs/promises';
3+
import {join} from 'node:path';
4+
export function createQuickLinkStore(root){
5+
const path=key=>{if(typeof key!=='string'||!key||key.length>1024)throw Error('Invalid document key');return join(root,createHash('sha256').update(key).digest('hex')+'.json');};
6+
let writes=Promise.resolve();
7+
return {
8+
async load(key){await writes;try{return JSON.parse(await readFile(path(key),'utf8'));}catch(e){if(e.code==='ENOENT')return [];throw e;}},
9+
save(key,links){const target=path(key);if(!Array.isArray(links)||links.length>10000||links.some(l=>typeof l.id!=='string'||['origin','result'].some(side=>!Number.isInteger(l[side]?.page)||l[side].page<1||!['x','y','width','height'].every(k=>Number.isFinite(l[side].box?.[k])))))throw Error('Invalid quick links');const snapshot=JSON.stringify(links);const operation=writes.then(async()=>{await mkdir(root,{recursive:true});await writeFile(target+'.tmp',snapshot);await rename(target+'.tmp',target);});writes=operation.catch(()=>{});return operation;}
10+
};
11+
}

‎public/symbols/link.png‎

4.48 KB
Loading

0 commit comments

Comments
 (0)