pdf-synctex-controls.tsx 4.7 KB

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