@@ -15,12 +15,15 @@ import { defineMessage, useIntl } from "react-intl";
15
15
16
16
import { set_account_table } from "@cocalc/frontend/account/util" ;
17
17
import { useRedux } from "@cocalc/frontend/app-framework" ;
18
- import { Icon } from "@cocalc/frontend/components" ;
18
+ import { HelpIcon , Icon } from "@cocalc/frontend/components" ;
19
19
import {
20
+ BUILD_ON_SAVE_ICON_DISABLED ,
21
+ BUILD_ON_SAVE_ICON_ENABLED ,
22
+ BUILD_ON_SAVE_LABEL ,
20
23
ZOOM_MESSAGES ,
21
24
ZOOM_PERCENTAGES ,
22
25
} from "@cocalc/frontend/frame-editors/frame-tree/commands/generic-commands" ;
23
- import { labels } from "@cocalc/frontend/i18n" ;
26
+ import { editor , labels } from "@cocalc/frontend/i18n" ;
24
27
import { COLORS } from "@cocalc/util/theme" ;
25
28
import { Actions } from "./actions" ;
26
29
@@ -43,14 +46,38 @@ const CONTROL_PAGE_STYLE = {
43
46
color : COLORS . GRAY_M ,
44
47
} as const ;
45
48
46
- const autoSyncTooltipMessage = defineMessage ( {
49
+ export const AUTO_SYNC_TOOLTIP_MSG = defineMessage ( {
47
50
id : "editor.latex.pdf_controls.auto_sync.tooltip" ,
48
51
defaultMessage :
49
52
"Auto-sync between source and PDF: cursor moves follow PDF scrolling, PDF scrolls to cursor position" ,
50
53
description :
51
54
"Tooltip explaining bidirectional auto-sync functionality in LaTeX PDF controls" ,
52
55
} ) ;
53
56
57
+ export const SYNC_HELP_MSG = {
58
+ title : defineMessage ( {
59
+ id : "editor.latex.pdf_controls.sync_help.title" ,
60
+ defaultMessage : "LaTeX Sync Help" ,
61
+ description : "Title for LaTeX sync help popup" ,
62
+ } ) ,
63
+ content : defineMessage ( {
64
+ id : "editor.latex.pdf_controls.sync_help.content" ,
65
+ defaultMessage : `<p><strong>Manual Mode:</strong></p>
66
+ <ul>
67
+ <li>Use ALT+Return in source document to jump to corresponding PDF location</li>
68
+ <li>Double-click in PDF for inverse search to source</li>
69
+ </ul>
70
+ <p><strong>Automatic Mode:</strong></p>
71
+ <ul>
72
+ <li>Syncs automatically from cursor changes in source to PDF</li>
73
+ <li>Moving the PDF viewport moves the cursor in source</li>
74
+ </ul>
75
+ <p>This functionality uses SyncTeX to coordinate between LaTeX source and PDF output.</p>` ,
76
+ description :
77
+ "Complete explanation of LaTeX sync functionality including manual and automatic modes" ,
78
+ } ) ,
79
+ } ;
80
+
54
81
interface PDFControlsProps {
55
82
actions : Actions ;
56
83
id : string ;
@@ -62,6 +89,7 @@ interface PDFControlsProps {
62
89
y : number ;
63
90
} | null ;
64
91
onClearViewportInfo ?: ( ) => void ;
92
+ pageDimensions ?: { width : number ; height : number } [ ] ;
65
93
}
66
94
67
95
export function PDFControls ( {
@@ -71,6 +99,7 @@ export function PDFControls({
71
99
currentPage = 1 ,
72
100
viewportInfo,
73
101
onClearViewportInfo,
102
+ pageDimensions = [ ] ,
74
103
} : PDFControlsProps ) {
75
104
const intl = useIntl ( ) ;
76
105
@@ -91,6 +120,7 @@ export function PDFControls({
91
120
const storedAutoSyncEnabled =
92
121
useRedux ( [ actions . name , "local_view_state" , id , "autoSyncEnabled" ] ) ??
93
122
false ; // Default to false
123
+
94
124
const [ localAutoSyncEnabled , setLocalAutoSyncEnabled ] = useState (
95
125
storedAutoSyncEnabled ,
96
126
) ;
@@ -122,6 +152,23 @@ export function PDFControls({
122
152
[ actions , autoSyncInProgress , onClearViewportInfo ] ,
123
153
) ;
124
154
155
+ // Handle manual sync from middle of current page
156
+ const handleManualSync = useCallback ( ( ) => {
157
+ if (
158
+ pageDimensions . length === 0 ||
159
+ storedCurrentPage < 1 ||
160
+ storedCurrentPage > pageDimensions . length
161
+ ) {
162
+ return ; // No page dimensions available or invalid page
163
+ }
164
+ const pageDim = pageDimensions [ storedCurrentPage - 1 ] ; // pages are 1-indexed
165
+ handleViewportSync (
166
+ storedCurrentPage ,
167
+ pageDim . width / 2 ,
168
+ pageDim . height / 2 ,
169
+ ) ;
170
+ } , [ handleViewportSync , storedCurrentPage , pageDimensions ] ) ;
171
+
125
172
// Sync state with stored values when they change
126
173
useEffect ( ( ) => {
127
174
setLocalAutoSyncEnabled ( storedAutoSyncEnabled ) ;
@@ -267,8 +314,16 @@ export function PDFControls({
267
314
key : "auto-build" ,
268
315
label : (
269
316
< div style = { { display : "flex" , alignItems : "center" , gap : "8px" } } >
270
- < Icon name = { buildOnSave ? "check-square" : "square" } />
271
- Auto Build
317
+ < Icon
318
+ name = {
319
+ buildOnSave
320
+ ? BUILD_ON_SAVE_ICON_ENABLED
321
+ : BUILD_ON_SAVE_ICON_DISABLED
322
+ }
323
+ />
324
+ { intl . formatMessage ( BUILD_ON_SAVE_LABEL , {
325
+ enabled : buildOnSave ,
326
+ } ) }
272
327
</ div >
273
328
) ,
274
329
onClick : toggleBuildOnSave ,
@@ -279,7 +334,15 @@ export function PDFControls({
279
334
{
280
335
key : "custom-zoom" ,
281
336
label : (
282
- < div style = { { display : "flex" , alignItems : "center" , gap : "8px" } } >
337
+ < div
338
+ style = { { display : "flex" , alignItems : "center" , gap : "8px" } }
339
+ onClick = { ( e ) => {
340
+ e . stopPropagation ( ) ;
341
+ } }
342
+ onMouseDown = { ( e ) => {
343
+ e . stopPropagation ( ) ;
344
+ } }
345
+ >
283
346
< InputNumber
284
347
size = "small"
285
348
style = { { width : "80px" } }
@@ -337,8 +400,8 @@ export function PDFControls({
337
400
return (
338
401
< div style = { CONTROL_STYLE } >
339
402
{ /* Left side controls */ }
340
- < div style = { { display : "flex" , gap : "10px" , alignItems : "center" } } >
341
- { /* Build Controls */ }
403
+ { /* Build Controls */ }
404
+ < div style = { { display : "flex" , alignItems : "center" , gap : "5px" } } >
342
405
< Dropdown . Button
343
406
type = "primary"
344
407
size = "small"
@@ -348,24 +411,40 @@ export function PDFControls({
348
411
onClick = { handleBuild }
349
412
>
350
413
< Icon name = "play-circle" />
351
- Build
414
+ { intl . formatMessage ( editor . build_control_and_log_title_short ) }
352
415
</ Dropdown . Button >
416
+ </ div >
353
417
354
- { /* Auto-Sync Control */ }
355
- < Tooltip title = { intl . formatMessage ( autoSyncTooltipMessage ) } >
356
- < div style = { { display : "flex" , alignItems : "center" , gap : "6px" } } >
357
- < Switch
358
- size = "small"
359
- checked = { localAutoSyncEnabled }
360
- onChange = { handleAutoSyncChange }
361
- />
362
- < Icon name = "exchange" />
363
- < span style = { { fontSize : "13px" } } > Sync </ span >
364
- </ div >
418
+ { /* Auto-Sync Control */ }
419
+ < div style = { { display : "flex" , alignItems : "center" , gap : "5px" } } >
420
+ < Icon name = "exchange" / >
421
+ < Tooltip title = { intl . formatMessage ( AUTO_SYNC_TOOLTIP_MSG ) } >
422
+ < Switch
423
+ size = "small"
424
+ checked = { localAutoSyncEnabled }
425
+ onChange = { handleAutoSyncChange }
426
+ checkedChildren = { intl . formatMessage ( labels . on ) }
427
+ unCheckedChildren = { intl . formatMessage ( labels . off ) }
428
+ / >
365
429
</ Tooltip >
430
+ < Button
431
+ type = "text"
432
+ size = "small"
433
+ style = { { fontSize : "13px" , padding : "0 4px" , height : "auto" } }
434
+ onClick = { handleManualSync }
435
+ disabled = { pageDimensions . length === 0 }
436
+ >
437
+ Sync
438
+ </ Button >
439
+ < HelpIcon
440
+ title = { intl . formatMessage ( SYNC_HELP_MSG . title ) }
441
+ placement = "bottomLeft"
442
+ >
443
+ { intl . formatMessage ( SYNC_HELP_MSG . content ) }
444
+ </ HelpIcon >
366
445
</ div >
367
446
368
- { /* Right side page navigation */ }
447
+ { /* middle: page navigation */ }
369
448
{ totalPages > 0 && (
370
449
< div style = { CONTROL_PAGE_STYLE } >
371
450
< InputNumber
0 commit comments