Skip to content

Commit

Permalink
Floating view panel
Browse files Browse the repository at this point in the history
Back with main
  • Loading branch information
cmdcolin committed Nov 19, 2024
1 parent 9a83551 commit 73dd391
Show file tree
Hide file tree
Showing 19 changed files with 502 additions and 88 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"@types/rbush": "^3.0.0",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.0",
"@types/react-resizable": "^3.0.7",
"@types/react-virtualized-auto-sizer": "^1.0.0",
"@types/set-value": "^4.0.1",
"@types/string-template": "^1.0.2",
Expand Down
63 changes: 63 additions & 0 deletions packages/app-core/src/ui/App/FloatingViewPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useEffect, useState } from 'react'
import { ResizableBox } from 'react-resizable'
import { observer } from 'mobx-react'
import { AbstractViewModel, SessionWithDrawerWidgets } from '@jbrowse/core/util'
import { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel'
import { MenuItem as JBMenuItem } from '@jbrowse/core/ui/Menu'
import DraggableDialog from '@jbrowse/core/ui/DraggableDialog'

// locals
import StaticViewPanel from './StaticViewPanel'
import './test.css'
import useMeasure from '@jbrowse/core/util/useMeasure'

type AppSession = SessionWithDrawerWidgets & {
savedSessionNames: string[]
menus: {
label: string
menuItems: JBMenuItem[]
}[]
snackbarMessages: SnackbarMessage[]
renameCurrentSession: (arg: string) => void
popSnackbarMessage: () => unknown
}

const FloatingViewPanel = observer(function ({
view,
session,
}: {
view: AbstractViewModel
session: AppSession
}) {
const zIndex = session.focusedViewId === view.id ? 101 : 100
const [ref, { height }] = useMeasure()
const [mode, setMode] = useState<'se' | 'e'>('se')
const [size, setSize] = useState<{ width: number; height: number }>()
const h = size !== undefined ? size.height : undefined

useEffect(() => {
if (h !== undefined && height !== undefined && height - h > 50) {
setMode('e')
}
}, [h, height])

return (
<DraggableDialog zIndex={zIndex}>
<ResizableBox
className="box"
height={(height || 100) + 20}
resizeHandles={[mode]}
onResize={(_event, { size }) => {
setSize(size)
}}
width={1000}
>
<div ref={ref}>
<StaticViewPanel view={view} session={session} />
</div>
</ResizableBox>
</DraggableDialog>
)
})

export default FloatingViewPanel
65 changes: 65 additions & 0 deletions packages/app-core/src/ui/App/StaticViewPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { Suspense } from 'react'
import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary'
import { observer } from 'mobx-react'

// locals
import {
getEnv,
AbstractViewModel,
SessionWithDrawerWidgets,
} from '@jbrowse/core/util'
import { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel'

// ui elements
import ErrorMessage from '@jbrowse/core/ui/ErrorMessage'
import LoadingEllipses from '@jbrowse/core/ui/LoadingEllipses'
import { MenuItem as JBMenuItem } from '@jbrowse/core/ui/Menu'

// locals
import ViewContainer from './ViewContainer'

type AppSession = SessionWithDrawerWidgets & {
savedSessionNames: string[]
menus: { label: string; menuItems: JBMenuItem[] }[]
snackbarMessages: SnackbarMessage[]
renameCurrentSession: (arg: string) => void
popSnackbarMessage: () => unknown
}

const StaticViewPanel = observer(function StaticViewPanel2({
view,
session,
}: {
view: AbstractViewModel
session: AppSession
}) {
const { pluginManager } = getEnv(session)
const viewType = pluginManager.getViewType(view.type)
if (!viewType) {
throw new Error(`unknown view type ${view.type}`)
}
const { ReactComponent } = viewType
return (
<ViewContainer
view={view}
onClose={() => {
session.removeView(view)
}}
onMinimize={() => {
view.setMinimized(!view.minimized)
}}
>
{!view.minimized ? (
<ErrorBoundary
FallbackComponent={({ error }) => <ErrorMessage error={error} />}
>
<Suspense fallback={<LoadingEllipses variant="h6" />}>
<ReactComponent model={view} session={session} />
</Suspense>
</ErrorBoundary>
) : null}
</ViewContainer>
)
})

export default StaticViewPanel
19 changes: 19 additions & 0 deletions packages/app-core/src/ui/App/StaticViewWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { useEffect, useRef } from 'react'
import { observer } from 'mobx-react'

const StaticViewWrapper = observer(function ({
children,
}: {
children: React.ReactNode
}) {
const scrollRef = useRef<HTMLDivElement>(null)

// scroll the view into view when first mounted. note: this effect will run
// only once, because of the empty array second param
useEffect(() => {
scrollRef.current?.scrollIntoView({ block: 'center' })
}, [])
return <div>{children}</div>
})

export default StaticViewWrapper
18 changes: 13 additions & 5 deletions packages/app-core/src/ui/App/ViewContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util'

// locals
import ViewHeader from './ViewHeader'
import StaticViewWrapper from './StaticViewWrapper'

const useStyles = makeStyles()(theme => ({
viewContainer: {
Expand All @@ -23,7 +24,7 @@ const useStyles = makeStyles()(theme => ({
},
}))

const ViewContainer = observer(function ({
const ViewContainer = observer(function ViewContainer2({
view,
onClose,
onMinimize,
Expand All @@ -38,6 +39,7 @@ const ViewContainer = observer(function ({
const ref = useWidthSetter(view, theme.spacing(1))
const { classes, cx } = useStyles()
const session = getSession(view) as SessionWithFocusedViewAndDrawerWidgets
const { focusedViewId } = session

useEffect(() => {
function handleSelectView(e: Event) {
Expand All @@ -60,12 +62,18 @@ const ViewContainer = observer(function ({
elevation={12}
className={cx(
classes.viewContainer,
session.focusedViewId === view.id
? classes.focusedView
: classes.unfocusedView,
focusedViewId === view.id ? classes.focusedView : classes.unfocusedView,
)}
>
<ViewHeader view={view} onClose={onClose} onMinimize={onMinimize} />
{view.isFloating ? (
<div style={{ cursor: 'all-scroll' }}>
<ViewHeader view={view} onClose={onClose} onMinimize={onMinimize} />
</div>
) : (
<StaticViewWrapper>
<ViewHeader view={view} onClose={onClose} onMinimize={onMinimize} />
</StaticViewWrapper>
)}
<Paper>{children}</Paper>
</Paper>
)
Expand Down
1 change: 1 addition & 0 deletions packages/app-core/src/ui/App/ViewContainerTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const useStyles = makeStyles()(theme => ({
backgroundColor: theme.palette.secondary.light,
},
}))

const ViewContainerTitle = observer(function ({
view,
}: {
Expand Down
43 changes: 9 additions & 34 deletions packages/app-core/src/ui/App/ViewHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import React, { useEffect, useRef } from 'react'
import { IconButton } from '@mui/material'
import { makeStyles } from 'tss-react/mui'
import { observer } from 'mobx-react'
import { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes/models'
import { getSession } from '@jbrowse/core/util'

// icons
import CloseIcon from '@mui/icons-material/Close'
import MinimizeIcon from '@mui/icons-material/Minimize'
import AddIcon from '@mui/icons-material/Add'
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'

// locals
import ViewMenu from './ViewMenu'
import ViewContainerTitle from './ViewContainerTitle'
import { getSession } from '@jbrowse/core/util'
import ViewHeaderButtons from './ViewHeaderButtons'

const useStyles = makeStyles()(theme => ({
icon: {
Expand All @@ -31,32 +28,6 @@ const useStyles = makeStyles()(theme => ({
},
}))

const ViewButtons = observer(function ({
view,
onClose,
onMinimize,
}: {
view: IBaseViewModel
onClose: () => void
onMinimize: () => void
}) {
const { classes } = useStyles()
return (
<>
<IconButton data-testid="minimize_view" onClick={onMinimize}>
{view.minimized ? (
<AddIcon className={classes.icon} fontSize="small" />
) : (
<MinimizeIcon className={classes.icon} fontSize="small" />
)}
</IconButton>
<IconButton data-testid="close_view" onClick={onClose}>
<CloseIcon className={classes.icon} fontSize="small" />
</IconButton>
</>
)
})

const ViewHeader = observer(function ({
view,
onClose,
Expand All @@ -66,7 +37,7 @@ const ViewHeader = observer(function ({
onClose: () => void
onMinimize: () => void
}) {
const { classes } = useStyles()
const { classes, cx } = useStyles()
const scrollRef = useRef<HTMLDivElement>(null)
const session = getSession(view)

Expand All @@ -78,7 +49,7 @@ const ViewHeader = observer(function ({
}
}, [])
return (
<div ref={scrollRef} className={classes.viewHeader}>
<div ref={scrollRef} className={cx('viewHeader', classes.viewHeader)}>
<ViewMenu model={view} IconProps={{ className: classes.icon }} />
<div className={classes.grow} />
<div className={classes.viewTitle}>
Expand All @@ -88,7 +59,11 @@ const ViewHeader = observer(function ({
<ViewContainerTitle view={view} />
</div>
<div className={classes.grow} />
<ViewButtons onClose={onClose} onMinimize={onMinimize} view={view} />
<ViewHeaderButtons
onClose={onClose}
onMinimize={onMinimize}
view={view}
/>
</div>
)
})
Expand Down
53 changes: 53 additions & 0 deletions packages/app-core/src/ui/App/ViewHeaderButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react'
import { IconButton } from '@mui/material'
import { makeStyles } from 'tss-react/mui'
import { observer } from 'mobx-react'
import { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes/models'

// icons
import CloseIcon from '@mui/icons-material/Close'
import MinimizeIcon from '@mui/icons-material/Minimize'
import AddIcon from '@mui/icons-material/Add'
import OpenInNew from '@mui/icons-material/OpenInNew'

const useStyles = makeStyles()(theme => ({
icon: {
color: theme.palette.secondary.contrastText,
},
}))

const ViewHeaderButtons = observer(function ({
view,
onClose,
onMinimize,
}: {
view: IBaseViewModel
onClose: () => void
onMinimize: () => void
}) {
const { classes } = useStyles()
return (
<>
<IconButton
data-testid="open_in_new"
onClick={() => {
view.setIsFloating(!view.isFloating)
}}
>
<OpenInNew className={classes.icon} fontSize="small" />
</IconButton>
<IconButton data-testid="minimize_view" onClick={onMinimize}>
{view.minimized ? (
<AddIcon className={classes.icon} fontSize="small" />
) : (
<MinimizeIcon className={classes.icon} fontSize="small" />
)}
</IconButton>
<IconButton data-testid="close_view" onClick={onClose}>
<CloseIcon className={classes.icon} fontSize="small" />
</IconButton>
</>
)
})

export default ViewHeaderButtons
7 changes: 5 additions & 2 deletions packages/app-core/src/ui/App/ViewPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Suspense } from 'react'
import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary'
import { ErrorBoundary } from 'react-error-boundary'
import { observer } from 'mobx-react'

// locals
Expand All @@ -20,7 +20,10 @@ import ViewContainer from './ViewContainer'

type AppSession = SessionWithDrawerWidgets & {
savedSessionNames: string[]
menus: { label: string; menuItems: JBMenuItem[] }[]
menus: {
label: string
menuItems: JBMenuItem[]
}[]
snackbarMessages: SnackbarMessage[]
renameCurrentSession: (arg: string) => void
popSnackbarMessage: () => unknown
Expand Down
Loading

0 comments on commit 73dd391

Please sign in to comment.