pdf-synctex-controls.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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. import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
  15. const GoToCodeButton = memo(function GoToCodeButton({
  16. syncToCode,
  17. syncToCodeInFlight,
  18. isDetachLayout,
  19. }: {
  20. syncToCode: ({ visualOffset }: { visualOffset: number }) => void
  21. syncToCodeInFlight: boolean
  22. isDetachLayout?: boolean
  23. }) {
  24. const { t } = useTranslation()
  25. useCommandProvider(
  26. () => [
  27. {
  28. id: 'synctex-sync-to-code',
  29. handler: () => {
  30. sendMB('jump-to-location', {
  31. method: 'command',
  32. direction: 'pdf-location-in-code',
  33. })
  34. syncToCode({ visualOffset: 72 })
  35. },
  36. disabled: syncToCodeInFlight,
  37. label: t('go_to_pdf_location_in_code_action'),
  38. },
  39. ],
  40. [t, syncToCode, syncToCodeInFlight]
  41. )
  42. const buttonClasses = classNames('synctex-control', {
  43. 'detach-synctex-control': !!isDetachLayout,
  44. })
  45. let buttonIcon = null
  46. if (syncToCodeInFlight) {
  47. buttonIcon = <OLSpinner size="sm" />
  48. } else if (!isDetachLayout) {
  49. buttonIcon = (
  50. <MaterialIcon type="arrow_left_alt" className="synctex-control-icon" />
  51. )
  52. }
  53. const syncToCodeWithButton = useCallback(() => {
  54. sendMB('jump-to-location', {
  55. method: 'arrow',
  56. direction: 'pdf-location-in-code',
  57. })
  58. syncToCode({ visualOffset: 72 })
  59. }, [syncToCode])
  60. const overlayProps = useMemo(
  61. () => ({
  62. placement: (isDetachLayout ? 'bottom' : 'right') as Placement,
  63. }),
  64. [isDetachLayout]
  65. )
  66. return (
  67. <OLTooltip
  68. id="sync-to-code"
  69. description={t('go_to_pdf_location_in_code')}
  70. overlayProps={overlayProps}
  71. >
  72. <span>
  73. <OLButton
  74. variant="secondary"
  75. size="sm"
  76. onClick={syncToCodeWithButton}
  77. disabled={syncToCodeInFlight}
  78. className={buttonClasses}
  79. aria-label={t('go_to_pdf_location_in_code')}
  80. >
  81. {buttonIcon}
  82. {isDetachLayout ? <span>&nbsp;{t('show_in_code')}</span> : ''}
  83. </OLButton>
  84. </span>
  85. </OLTooltip>
  86. )
  87. })
  88. const GoToPdfButton = memo(function GoToPdfButton({
  89. syncToPdf,
  90. syncToPdfInFlight,
  91. isDetachLayout,
  92. canSyncToPdf,
  93. }: {
  94. syncToPdf: () => void
  95. syncToPdfInFlight: boolean
  96. canSyncToPdf: boolean
  97. isDetachLayout?: boolean
  98. }) {
  99. const { t } = useTranslation()
  100. const tooltipPlacement = isDetachLayout ? 'bottom' : 'right'
  101. const buttonClasses = classNames('synctex-control', {
  102. 'detach-synctex-control': !!isDetachLayout,
  103. })
  104. const handleSyncToPdf = useCallback(() => {
  105. sendMB('jump-to-location', {
  106. method: 'arrow',
  107. direction: 'code-location-in-pdf',
  108. })
  109. syncToPdf()
  110. }, [syncToPdf])
  111. useCommandProvider(
  112. () => [
  113. {
  114. id: 'synctex-sync-to-pdf',
  115. handler: () => {
  116. sendMB('jump-to-location', {
  117. method: 'command',
  118. direction: 'code-location-in-pdf',
  119. })
  120. syncToPdf()
  121. },
  122. label: t('go_to_code_location_in_pdf'),
  123. disabled: syncToPdfInFlight || !canSyncToPdf,
  124. },
  125. ],
  126. [t, syncToPdf, syncToPdfInFlight, canSyncToPdf]
  127. )
  128. let buttonIcon = null
  129. if (syncToPdfInFlight) {
  130. buttonIcon = <OLSpinner size="sm" />
  131. } else if (!isDetachLayout) {
  132. buttonIcon = (
  133. <MaterialIcon type="arrow_right_alt" className="synctex-control-icon" />
  134. )
  135. }
  136. return (
  137. <OLTooltip
  138. id="sync-to-pdf"
  139. description={t('go_to_code_location_in_pdf')}
  140. overlayProps={{ placement: tooltipPlacement }}
  141. >
  142. <span>
  143. <OLButton
  144. variant="secondary"
  145. size="sm"
  146. onClick={handleSyncToPdf}
  147. disabled={syncToPdfInFlight || !canSyncToPdf}
  148. className={buttonClasses}
  149. aria-label={t('go_to_code_location_in_pdf')}
  150. >
  151. {buttonIcon}
  152. {isDetachLayout ? <span>&nbsp;{t('show_in_pdf')}</span> : ''}
  153. </OLButton>
  154. </span>
  155. </OLTooltip>
  156. )
  157. })
  158. function PdfSynctexControls() {
  159. const { detachRole } = useLayoutContext()
  160. const { pdfUrl, pdfViewer, position } = useCompileContext()
  161. const {
  162. syncToCode,
  163. syncToPdf,
  164. syncToCodeInFlight,
  165. syncToPdfInFlight,
  166. canSyncToPdf,
  167. } = useSynctex()
  168. const visualPreviewEnabled = useFeatureFlag('visual-preview')
  169. if (visualPreviewEnabled) {
  170. return null
  171. }
  172. if (!position) {
  173. return null
  174. }
  175. if (!pdfUrl || pdfViewer === 'native') {
  176. return null
  177. }
  178. if (detachRole === 'detacher') {
  179. return (
  180. <GoToPdfButton
  181. syncToPdf={syncToPdf}
  182. syncToPdfInFlight={syncToPdfInFlight}
  183. isDetachLayout
  184. canSyncToPdf={canSyncToPdf}
  185. />
  186. )
  187. } else if (detachRole === 'detached') {
  188. return (
  189. <GoToCodeButton
  190. syncToCode={syncToCode}
  191. syncToCodeInFlight={syncToCodeInFlight}
  192. isDetachLayout
  193. />
  194. )
  195. } else {
  196. return (
  197. <>
  198. <GoToPdfButton
  199. syncToPdf={syncToPdf}
  200. syncToPdfInFlight={syncToPdfInFlight}
  201. canSyncToPdf={canSyncToPdf}
  202. />
  203. <GoToCodeButton
  204. syncToCode={syncToCode}
  205. syncToCodeInFlight={syncToCodeInFlight}
  206. />
  207. </>
  208. )
  209. }
  210. }
  211. export default memo(PdfSynctexControls)