pdf-synctex-controls.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import classNames from 'classnames'
  2. import { memo, useCallback, useMemo } from 'react'
  3. import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
  4. import { useLayoutContext } from '../../../shared/context/layout-context'
  5. import { useTranslation } from 'react-i18next'
  6. import OLTooltip from '@/shared/components/ol/ol-tooltip'
  7. import OLButton from '@/shared/components/ol/ol-button'
  8. import MaterialIcon from '@/shared/components/material-icon'
  9. import { Placement } from 'react-bootstrap/types'
  10. import useSynctex from '../hooks/use-synctex'
  11. import { useFeatureFlag } from '@/shared/context/split-test-context'
  12. import OLSpinner from '@/shared/components/ol/ol-spinner'
  13. import { sendMB } from '@/infrastructure/event-tracking'
  14. const GoToCodeButton = memo(function GoToCodeButton({
  15. syncToCode,
  16. syncToCodeInFlight,
  17. isDetachLayout,
  18. }: {
  19. syncToCode: ({ visualOffset }: { visualOffset: number }) => void
  20. syncToCodeInFlight: boolean
  21. isDetachLayout?: boolean
  22. }) {
  23. const { t } = useTranslation()
  24. const buttonClasses = classNames('synctex-control', {
  25. 'detach-synctex-control': !!isDetachLayout,
  26. })
  27. let buttonIcon = null
  28. if (syncToCodeInFlight) {
  29. buttonIcon = <OLSpinner size="sm" />
  30. } else if (!isDetachLayout) {
  31. buttonIcon = (
  32. <MaterialIcon type="arrow_left_alt" className="synctex-control-icon" />
  33. )
  34. }
  35. const syncToCodeWithButton = useCallback(() => {
  36. sendMB('jump-to-location', {
  37. method: 'arrow',
  38. direction: 'pdf-location-in-code',
  39. })
  40. syncToCode({ visualOffset: 72 })
  41. }, [syncToCode])
  42. const overlayProps = useMemo(
  43. () => ({
  44. placement: (isDetachLayout ? 'bottom' : 'right') as Placement,
  45. }),
  46. [isDetachLayout]
  47. )
  48. return (
  49. <OLTooltip
  50. id="sync-to-code"
  51. description={t('go_to_pdf_location_in_code')}
  52. overlayProps={overlayProps}
  53. >
  54. <span>
  55. <OLButton
  56. variant="secondary"
  57. size="sm"
  58. onClick={syncToCodeWithButton}
  59. disabled={syncToCodeInFlight}
  60. className={buttonClasses}
  61. aria-label={t('go_to_pdf_location_in_code')}
  62. >
  63. {buttonIcon}
  64. {isDetachLayout ? <span>&nbsp;{t('show_in_code')}</span> : ''}
  65. </OLButton>
  66. </span>
  67. </OLTooltip>
  68. )
  69. })
  70. const GoToPdfButton = memo(function GoToPdfButton({
  71. syncToPdf,
  72. syncToPdfInFlight,
  73. isDetachLayout,
  74. canSyncToPdf,
  75. }: {
  76. syncToPdf: () => void
  77. syncToPdfInFlight: boolean
  78. canSyncToPdf: boolean
  79. isDetachLayout?: boolean
  80. }) {
  81. const { t } = useTranslation()
  82. const tooltipPlacement = isDetachLayout ? 'bottom' : 'right'
  83. const buttonClasses = classNames('synctex-control', {
  84. 'detach-synctex-control': !!isDetachLayout,
  85. })
  86. const handleSyncToPdf = useCallback(() => {
  87. sendMB('jump-to-location', {
  88. method: 'arrow',
  89. direction: 'code-location-in-pdf',
  90. })
  91. syncToPdf()
  92. }, [syncToPdf])
  93. let buttonIcon = null
  94. if (syncToPdfInFlight) {
  95. buttonIcon = <OLSpinner size="sm" />
  96. } else if (!isDetachLayout) {
  97. buttonIcon = (
  98. <MaterialIcon type="arrow_right_alt" className="synctex-control-icon" />
  99. )
  100. }
  101. return (
  102. <OLTooltip
  103. id="sync-to-pdf"
  104. description={t('go_to_code_location_in_pdf')}
  105. overlayProps={{ placement: tooltipPlacement }}
  106. >
  107. <span>
  108. <OLButton
  109. variant="secondary"
  110. size="sm"
  111. onClick={handleSyncToPdf}
  112. disabled={syncToPdfInFlight || !canSyncToPdf}
  113. className={buttonClasses}
  114. aria-label={t('go_to_code_location_in_pdf')}
  115. >
  116. {buttonIcon}
  117. {isDetachLayout ? <span>&nbsp;{t('show_in_pdf')}</span> : ''}
  118. </OLButton>
  119. </span>
  120. </OLTooltip>
  121. )
  122. })
  123. function PdfSynctexControls() {
  124. const { detachRole } = useLayoutContext()
  125. const { pdfUrl, pdfViewer, position } = useCompileContext()
  126. const {
  127. syncToCode,
  128. syncToPdf,
  129. syncToCodeInFlight,
  130. syncToPdfInFlight,
  131. canSyncToPdf,
  132. } = useSynctex()
  133. const visualPreviewEnabled = useFeatureFlag('visual-preview')
  134. if (visualPreviewEnabled) {
  135. return null
  136. }
  137. if (!position) {
  138. return null
  139. }
  140. if (!pdfUrl || pdfViewer === 'native') {
  141. return null
  142. }
  143. if (detachRole === 'detacher') {
  144. return (
  145. <GoToPdfButton
  146. syncToPdf={syncToPdf}
  147. syncToPdfInFlight={syncToPdfInFlight}
  148. isDetachLayout
  149. canSyncToPdf={canSyncToPdf}
  150. />
  151. )
  152. } else if (detachRole === 'detached') {
  153. return (
  154. <GoToCodeButton
  155. syncToCode={syncToCode}
  156. syncToCodeInFlight={syncToCodeInFlight}
  157. isDetachLayout
  158. />
  159. )
  160. } else {
  161. return (
  162. <>
  163. <GoToPdfButton
  164. syncToPdf={syncToPdf}
  165. syncToPdfInFlight={syncToPdfInFlight}
  166. canSyncToPdf={canSyncToPdf}
  167. />
  168. <GoToCodeButton
  169. syncToCode={syncToCode}
  170. syncToCodeInFlight={syncToCodeInFlight}
  171. />
  172. </>
  173. )
  174. }
  175. }
  176. export default memo(PdfSynctexControls)