diff --git a/frontend/src/lib/components/SortableList/sortableList.tsx b/frontend/src/lib/components/SortableList/sortableList.tsx index e65e133f0..097de9007 100644 --- a/frontend/src/lib/components/SortableList/sortableList.tsx +++ b/frontend/src/lib/components/SortableList/sortableList.tsx @@ -86,7 +86,8 @@ type HoveredItemIdAndArea = { area: HoveredArea; }; -const ELEMENT_TOP_AND_CENTER_AREA_SIZE_IN_PERCENT = 50; +const ITEM_TOP_AND_CENTER_AREA_SIZE_IN_PERCENT = 50; +const GROUP_TOP_AND_CENTER_AREA_SIZE_IN_PERCENT = 30; export type SortableListProps = { contentWhenEmpty?: React.ReactNode; @@ -280,15 +281,19 @@ export function SortableList(props: SortableListProps): React.ReactNode { } function getHoveredAreaOfItem(item: HTMLElement, e: PointerEvent): HoveredArea { + let factor = ITEM_TOP_AND_CENTER_AREA_SIZE_IN_PERCENT / 100; + if (getItemType(item) === ItemType.CONTAINER) { + factor = GROUP_TOP_AND_CENTER_AREA_SIZE_IN_PERCENT / 100; + } const rect = item.getBoundingClientRect(); const topAreaTop = rect.top; - const topAreaBottom = rect.top + (ELEMENT_TOP_AND_CENTER_AREA_SIZE_IN_PERCENT / 100) * rect.height; + const topAreaBottom = rect.top + factor * rect.height; if (e.clientY >= topAreaTop && e.clientY <= topAreaBottom) { return HoveredArea.TOP; } - const bottomAreaTop = rect.bottom - (ELEMENT_TOP_AND_CENTER_AREA_SIZE_IN_PERCENT / 100) * rect.height; + const bottomAreaTop = rect.bottom - factor * rect.height; const bottomAreaBottom = rect.bottom; if (e.clientY >= bottomAreaTop && e.clientY <= bottomAreaBottom) { diff --git a/frontend/src/lib/components/SortableList/sortableListGroup.tsx b/frontend/src/lib/components/SortableList/sortableListGroup.tsx index 5b9ea242a..c6d271bd6 100644 --- a/frontend/src/lib/components/SortableList/sortableListGroup.tsx +++ b/frontend/src/lib/components/SortableList/sortableListGroup.tsx @@ -11,19 +11,19 @@ import { SortableListItemProps } from "./sortableListItem"; export type SortableListGroupProps = { id: string; - title: string; + title: React.ReactNode; initiallyExpanded?: boolean; startAdornment?: React.ReactNode; endAdornment?: React.ReactNode; contentWhenEmpty?: React.ReactNode; - children: React.ReactElement[]; + children?: React.ReactElement[]; }; /** * * @param {SortableListGroupProps} props Object of properties for the SortableListGroup component (see below for details). * @param {string} props.id ID that is unique among all components inside the sortable list. - * @param {string} props.title Title of the list item. + * @param {React.ReactNode} props.title Title of the list item. * @param {boolean} props.initiallyExpanded Whether the group should be expanded by default. * @param {React.ReactNode} props.startAdornment Start adornment to display to the left of the title. * @param {React.ReactNode} props.endAdornment End adornment to display to the right of the title. @@ -88,7 +88,9 @@ export function SortableListGroup(props: SortableListGroupProps): React.ReactNod } )} > - {props.children.length === 0 ? props.contentWhenEmpty : props.children} + {props.children === undefined || props.children.length === 0 + ? props.contentWhenEmpty + : props.children} {isHovered && sortableListContext.hoveredArea === HoveredArea.BOTTOM && } @@ -97,7 +99,7 @@ export function SortableListGroup(props: SortableListGroupProps): React.ReactNod } type HeaderProps = { - title: string; + title: React.ReactNode; expanded: boolean; onToggleExpanded?: () => void; icon?: React.ReactNode; @@ -118,7 +120,7 @@ function Header(props: HeaderProps): React.ReactNode { > {props.expanded ? : } -
+
{props.startAdornment}
{props.title}
{props.endAdornment} diff --git a/frontend/src/lib/components/SortableList/sortableListItem.tsx b/frontend/src/lib/components/SortableList/sortableListItem.tsx index 123ce1219..1dc1de5b8 100644 --- a/frontend/src/lib/components/SortableList/sortableListItem.tsx +++ b/frontend/src/lib/components/SortableList/sortableListItem.tsx @@ -3,14 +3,15 @@ import React from "react"; import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect"; import { createPortal } from "@lib/utils/createPortal"; import { resolveClassNames } from "@lib/utils/resolveClassNames"; -import { DragIndicator } from "@mui/icons-material"; +import { DragIndicator, ExpandLess, ExpandMore } from "@mui/icons-material"; import { HoveredArea, SortableListContext } from "./sortableList"; import { SortableListDropIndicator } from "./sortableListDropIndicator"; export type SortableListItemProps = { id: string; - title: string; + title: React.ReactNode; + initiallyExpanded?: boolean; startAdornment?: React.ReactNode; endAdornment?: React.ReactNode; children: React.ReactNode; @@ -20,7 +21,7 @@ export type SortableListItemProps = { * * @param {SortableListItemProps} props Object of properties for the SortableListItem component (see below for details). * @param {string} props.id ID that is unique among all components inside the sortable list. - * @param {string} props.title Title of the list item. + * @param {React.ReactNode} props.title Title component of the list item. * @param {React.ReactNode} props.startAdornment Start adornment to display to the left of the title. * @param {React.ReactNode} props.endAdornment End adornment to display to the right of the title. * @param {React.ReactNode} props.children Child components to display as the content of the list item. @@ -28,6 +29,8 @@ export type SortableListItemProps = { * @returns {React.ReactNode} A sortable list item component. */ export function SortableListItem(props: SortableListItemProps): React.ReactNode { + const [isExpanded, setIsExpanded] = React.useState(props.initiallyExpanded ?? true); + const divRef = React.useRef(null); const boundingClientRect = useElementBoundingRect(divRef); @@ -37,6 +40,10 @@ export function SortableListItem(props: SortableListItemProps): React.ReactNode const isDragging = sortableListContext.draggedElementId === props.id; const dragPosition = sortableListContext.dragPosition; + function handleToggleExpanded() { + setIsExpanded(!isExpanded); + } + return ( <> {isHovered && sortableListContext.hoveredArea === HoveredArea.TOP && } @@ -50,7 +57,7 @@ export function SortableListItem(props: SortableListItemProps): React.ReactNode hidden: !isDragging, })} >
-
+
{isDragging && dragPosition && createPortal( @@ -64,10 +71,12 @@ export function SortableListItem(props: SortableListItemProps): React.ReactNode width: isDragging ? boundingClientRect.width : undefined, }} > -
+
)} -
{props.children}
+
+ {props.children} +
{isHovered && sortableListContext.hoveredArea === HoveredArea.BOTTOM && } @@ -75,23 +84,31 @@ export function SortableListItem(props: SortableListItemProps): React.ReactNode } type HeaderProps = { - icon?: React.ReactNode; - title: string; + title: React.ReactNode; + expanded: boolean; + onToggleExpanded?: () => void; startAdornment?: React.ReactNode; endAdornment?: React.ReactNode; }; function Header(props: HeaderProps): React.ReactNode { return ( -
+
-
+
{props.startAdornment}
{props.title}
{props.endAdornment}
+
+ {props.expanded ? : } +
); } diff --git a/frontend/src/modules/2DViewer/view/view.tsx b/frontend/src/modules/2DViewer/view/view.tsx index 446c5d5f4..21f100e4e 100644 --- a/frontend/src/modules/2DViewer/view/view.tsx +++ b/frontend/src/modules/2DViewer/view/view.tsx @@ -7,9 +7,7 @@ import { BaseLayer, LayerStatus, useLayersStatuses } from "@modules/_shared/laye import { LayerManagerTopic, useLayerManagerTopicValue } from "@modules/_shared/layers/LayerManager"; import { ViewportType } from "@webviz/subsurface-viewer"; import SubsurfaceViewer, { ViewsType } from "@webviz/subsurface-viewer/dist/SubsurfaceViewer"; -import { MapLayer } from "@webviz/subsurface-viewer/dist/layers"; -import { SurfaceLayer } from "../layers/SurfaceLayer"; import { SettingsToViewInterface } from "../settingsToViewInterface"; import { State } from "../state"; @@ -24,6 +22,7 @@ export function View(props: ModuleViewProps): Re const groupLayersMap: Map = new Map(); + /* for (const layer of layers) { const groupId = layerManager.getGroupOfLayer(layer.getId())?.getId() ?? "main"; let layerArr = groupLayersMap.get(groupId); @@ -57,6 +56,7 @@ export function View(props: ModuleViewProps): Re } } } + */ const numCols = Math.ceil(Math.sqrt(groupLayersMap.size)); const numRows = Math.ceil(groupLayersMap.size / numCols); diff --git a/frontend/src/modules/_shared/components/Layers/layerComponent.tsx b/frontend/src/modules/_shared/components/Layers/layerComponent.tsx deleted file mode 100644 index f16c684e0..000000000 --- a/frontend/src/modules/_shared/components/Layers/layerComponent.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import React from "react"; - -import { EnsembleSet } from "@framework/EnsembleSet"; -import { StatusMessage } from "@framework/ModuleInstanceStatusController"; -import { WorkbenchSession } from "@framework/WorkbenchSession"; -import { WorkbenchSettings } from "@framework/WorkbenchSettings"; -import { CircularProgress } from "@lib/components/CircularProgress"; -import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect"; -import { createPortal } from "@lib/utils/createPortal"; -import { resolveClassNames } from "@lib/utils/resolveClassNames"; -import { Vec2 } from "@lib/utils/vec2"; -import { - BaseLayer, - LayerStatus, - useIsLayerVisible, - useLayerName, - useLayerStatus, -} from "@modules/_shared/layers/BaseLayer"; -import { - Check, - Delete, - DragIndicator, - Error, - ExpandLess, - ExpandMore, - Settings, - Visibility, - VisibilityOff, -} from "@mui/icons-material"; - -import { MakeSettingsContainerFunc } from "./layersPanel"; - -type LayerItemProps = { - layer: BaseLayer; - inGroup: boolean; - ensembleSet: EnsembleSet; - workbenchSession: WorkbenchSession; - workbenchSettings: WorkbenchSettings; - isDragging: boolean; - dragPosition: Vec2; - onRemoveLayer: (id: string) => void; - makeSettingsContainerFunc: MakeSettingsContainerFunc; -}; - -export function LayerComponent(props: LayerItemProps): React.ReactNode { - const [showSettings, setShowSettings] = React.useState(true); - - const dragIndicatorRef = React.useRef(null); - const divRef = React.useRef(null); - - const boundingClientRect = useElementBoundingRect(divRef); - - const isVisible = useIsLayerVisible(props.layer); - const status = useLayerStatus(props.layer); - - function handleRemoveLayer() { - props.onRemoveLayer(props.layer.getId()); - } - - function handleToggleLayerVisibility() { - props.layer.setIsVisible(!isVisible); - } - - function handleToggleSettingsVisibility() { - setShowSettings(!showSettings); - } - - function makeStatus(): React.ReactNode { - if (status === LayerStatus.LOADING) { - return ( -
- -
- ); - } - if (status === LayerStatus.ERROR) { - const error = props.layer.getError(); - if (typeof error === "string") { - return ( -
- -
- ); - } else { - const statusMessage = error as StatusMessage; - return ( -
- -
- ); - } - } - if (status === LayerStatus.SUCCESS) { - return ( -
- -
- ); - } - return null; - } - - function makeLayerElement(indicatorRef?: React.LegacyRef): React.ReactNode { - return ( - <> -
- -
-
- {isVisible ? : } -
- - {makeStatus()} -
- - {showSettings ? : } -
-
- -
- - ); - } - - return ( -
-
-
- {makeLayerElement(dragIndicatorRef)} -
- {props.isDragging && - createPortal( -
- {makeLayerElement()} -
- )} -
- {props.makeSettingsContainerFunc( - props.layer, - props.ensembleSet, - props.workbenchSession, - props.workbenchSettings - )} -
-
- ); -} - -type LayerNameProps = { - layer: BaseLayer; -}; - -function LayerName(props: LayerNameProps): React.ReactNode { - const layerName = useLayerName(props.layer); - const [editingName, setEditingName] = React.useState(false); - - function handleNameDoubleClick() { - setEditingName(true); - } - - function handleNameChange(e: React.ChangeEvent) { - props.layer.setName(e.target.value); - } - - function handleBlur() { - setEditingName(false); - } - - function handleKeyDown(e: React.KeyboardEvent) { - if (e.key === "Enter") { - setEditingName(false); - } - } - - return ( -
- {editingName ? ( - - ) : ( - layerName - )} -
- ); -} diff --git a/frontend/src/modules/_shared/components/Layers/layerComponents.tsx b/frontend/src/modules/_shared/components/Layers/layerComponents.tsx new file mode 100644 index 000000000..60491caef --- /dev/null +++ b/frontend/src/modules/_shared/components/Layers/layerComponents.tsx @@ -0,0 +1,75 @@ +import React from "react"; + +import { BaseLayer, useIsLayerVisible, useLayerName } from "@modules/_shared/layers/BaseLayer"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; + +type LayerNameProps = { + layer: BaseLayer; +}; + +export function LayerName(props: LayerNameProps): React.ReactNode { + const layerName = useLayerName(props.layer); + const [editingName, setEditingName] = React.useState(false); + + function handleNameDoubleClick() { + setEditingName(true); + } + + function handleNameChange(e: React.ChangeEvent) { + props.layer.setName(e.target.value); + } + + function handleBlur() { + setEditingName(false); + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter") { + setEditingName(false); + } + } + + return ( +
+ {editingName ? ( + + ) : ( + layerName + )} +
+ ); +} + +export type LayerStartAdornmentProps = { + layer: BaseLayer; +}; + +export function LayerStartAdornment(props: LayerStartAdornmentProps): React.ReactNode { + const isVisible = useIsLayerVisible(props.layer); + + function handleToggleLayerVisibility() { + props.layer.setIsVisible(!isVisible); + } + + return ( +
+ {isVisible ? : } +
+ ); +} diff --git a/frontend/src/modules/_shared/components/Layers/layerGroupComponent.tsx b/frontend/src/modules/_shared/components/Layers/layerGroupComponent.tsx deleted file mode 100644 index 3bce46e0a..000000000 --- a/frontend/src/modules/_shared/components/Layers/layerGroupComponent.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import React from "react"; - -import { EnsembleSet } from "@framework/EnsembleSet"; -import { WorkbenchSession } from "@framework/WorkbenchSession"; -import { WorkbenchSettings } from "@framework/WorkbenchSettings"; -import { Menu } from "@lib/components/Menu"; -import { MenuItem } from "@lib/components/MenuItem"; -import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect"; -import { createPortal } from "@lib/utils/createPortal"; -import { resolveClassNames } from "@lib/utils/resolveClassNames"; -import { Vec2 } from "@lib/utils/vec2"; -import { LayerGroup, LayerGroupTopic, useLayerGroupTopicValue } from "@modules/_shared/layers/LayerGroup"; -import { LayerManager } from "@modules/_shared/layers/LayerManager"; -import { Dropdown, MenuButton } from "@mui/base"; -import { - Add, - ArrowDropDown, - Delete, - DragIndicator, - ExpandLess, - ExpandMore, - Folder, - Visibility, - VisibilityOff, -} from "@mui/icons-material"; - -import { MakeSettingsContainerFunc } from "./layersPanel"; - -export type LayerGroupProps = { - group: LayerGroup; - ensembleSet: EnsembleSet; - workbenchSession: WorkbenchSession; - workbenchSettings: WorkbenchSettings; - layerManager: LayerManager; - layerTypeToStringMapping: Record; - isDragging: boolean; - dragPosition: Vec2; - draggingLayerId: string | null; - onRemoveGroup: (id: string) => void; - onAddLayer: (type: TLayerType, groupId: string) => void; - makeSettingsContainerFunc: MakeSettingsContainerFunc; - children: React.ReactNode[]; -}; - -export function LayerGroupComponent(props: LayerGroupProps): React.ReactNode { - const [showChildren, setShowChildren] = React.useState(true); - - const dragIndicatorRef = React.useRef(null); - const divRef = React.useRef(null); - - const boundingClientRect = useElementBoundingRect(divRef); - - const layers = props.layerManager.getLayersInGroup(props.group.getId()); - const isVisible = layers.every((layer) => layer.getIsVisible()); - - function handleRemoveGroup() { - props.onRemoveGroup(props.group.getId()); - } - - function handleAddLayer(type: TLayerType) { - props.onAddLayer(type, props.group.getId()); - } - - function handleToggleLayerVisibility() { - const layers = props.layerManager.getLayersInGroup(props.group.getId()); - const visible = layers.every((layer) => layer.getIsVisible()); - if (visible) { - layers.forEach((layer) => layer.setIsVisible(false)); - return; - } - layers.forEach((layer) => layer.setIsVisible(true)); - } - - function handleToggleChildrenVisibility() { - setShowChildren(!showChildren); - } - - function makeGroupElement(indicatorRef?: React.LegacyRef): React.ReactNode { - return ( - <> -
- -
-
- {showChildren ? : } -
- -
- {isVisible ? : } -
- - - -
- - Add layer - -
-
- - {Object.keys(props.layerTypeToStringMapping).map((layerType, index) => { - return ( - handleAddLayer(layerType as TLayerType)} - > - {props.layerTypeToStringMapping[layerType as TLayerType]} - - ); - })} - -
-
- -
- - ); - } - - return ( -
-
-
- {makeGroupElement(dragIndicatorRef)} -
- {props.isDragging && - createPortal( -
- {makeGroupElement()} -
- )} -
- {props.children} - {props.children.length === 0 && ( -
- No layers -
- )} -
-
- ); -} - -type LayerNameProps = { - group: LayerGroup; -}; - -function GroupName(props: LayerNameProps): React.ReactNode { - const groupName = useLayerGroupTopicValue(props.group, LayerGroupTopic.NAME_CHANGED); - const [editingName, setEditingName] = React.useState(false); - - function handleNameDoubleClick() { - setEditingName(true); - } - - function handleNameChange(e: React.ChangeEvent) { - props.group.setName(e.target.value); - } - - function handleBlur() { - setEditingName(false); - } - - function handleKeyDown(e: React.KeyboardEvent) { - if (e.key === "Enter") { - setEditingName(false); - } - } - - return ( -
- {editingName ? ( - - ) : ( - groupName - )} -
- ); -} diff --git a/frontend/src/modules/_shared/components/Layers/layerGroupComponents.tsx b/frontend/src/modules/_shared/components/Layers/layerGroupComponents.tsx new file mode 100644 index 000000000..2941b5782 --- /dev/null +++ b/frontend/src/modules/_shared/components/Layers/layerGroupComponents.tsx @@ -0,0 +1,78 @@ +import React from "react"; + +import { LayerGroup, LayerGroupTopic, useLayerGroupTopicValue } from "@modules/_shared/layers/LayerGroup"; +import { Folder, Visibility, VisibilityOff } from "@mui/icons-material"; + +type LayerGroupStartAdornmentProps = { + group: LayerGroup; +}; + +export function LayerGroupStartAdornment(props: LayerGroupStartAdornmentProps): React.ReactNode { + const isVisible = useLayerGroupTopicValue(props.group, LayerGroupTopic.VISIBILITY_CHANGED); + + function handleToggleLayerVisibility() { + props.group.setIsVisible(!isVisible); + } + + return ( + <> + +
+ {isVisible ? : } +
+ + ); +} + +type LayerNameProps = { + group: LayerGroup; +}; + +export function LayerGroupName(props: LayerNameProps): React.ReactNode { + const groupName = useLayerGroupTopicValue(props.group, LayerGroupTopic.NAME_CHANGED); + const [editingName, setEditingName] = React.useState(false); + + function handleNameDoubleClick() { + setEditingName(true); + } + + function handleNameChange(e: React.ChangeEvent) { + props.group.setName(e.target.value); + } + + function handleBlur() { + setEditingName(false); + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter") { + setEditingName(false); + } + } + + return ( +
+ {editingName ? ( + + ) : ( + groupName + )} +
+ ); +} diff --git a/frontend/src/modules/_shared/components/Layers/layersPanel.tsx b/frontend/src/modules/_shared/components/Layers/layersPanel.tsx index c088c0af4..942f7ff20 100644 --- a/frontend/src/modules/_shared/components/Layers/layersPanel.tsx +++ b/frontend/src/modules/_shared/components/Layers/layersPanel.tsx @@ -5,9 +5,7 @@ import { WorkbenchSession } from "@framework/WorkbenchSession"; import { WorkbenchSettings } from "@framework/WorkbenchSettings"; import { Menu } from "@lib/components/Menu"; import { MenuItem } from "@lib/components/MenuItem"; -import { createPortal } from "@lib/utils/createPortal"; -import { MANHATTAN_LENGTH, rectContainsPoint } from "@lib/utils/geometry"; -import { Vec2, point2Distance } from "@lib/utils/vec2"; +import { SortableList, SortableListGroup, SortableListItem, SortableListItemProps } from "@lib/components/SortableList"; import { BaseLayer } from "@modules/_shared/layers/BaseLayer"; import { LayerGroup } from "@modules/_shared/layers/LayerGroup"; import { @@ -21,8 +19,8 @@ import { Add, ArrowDropDown, CreateNewFolder } from "@mui/icons-material"; import { isEqual } from "lodash"; -import { LayerComponent } from "./layerComponent"; -import { LayerGroupComponent } from "./layerGroupComponent"; +import { LayerName, LayerStartAdornment } from "./layerComponents"; +import { LayerGroupName, LayerGroupStartAdornment } from "./layerGroupComponents"; export interface LayerFactory { makeLayer(layerType: TLayerType): BaseLayer; @@ -51,29 +49,17 @@ export type LayersPanelProps = { export function LayersPanel(props: LayersPanelProps): React.ReactNode { const items = useLayerManagerTopicValue(props.layerManager, LayerManagerTopic.ITEMS_CHANGED); - const [draggingLayerId, setDraggingLayerId] = React.useState(null); - const [isDragging, setIsDragging] = React.useState(false); - const [dragPosition, setDragPosition] = React.useState({ x: 0, y: 0 }); const [prevItems, setPrevItems] = React.useState(items); - const [currentScrollPosition, setCurrentScrollPosition] = React.useState(0); const [itemsOrder, setItemsOrder] = React.useState(items.map((item) => item.getId())); - const parentDivRef = React.useRef(null); - const scrollDivRef = React.useRef(null); - const upperScrollDivRef = React.useRef(null); - const lowerScrollDivRef = React.useRef(null); - if (!isEqual(prevItems, items)) { setPrevItems(items); setItemsOrder(items.map((layer) => layer.getId())); - if (scrollDivRef.current) { - scrollDivRef.current.scrollTop = currentScrollPosition; - } } - function handleAddLayer(type: TLayerType, groupId?: string) { - if (groupId) { - props.layerManager.addLayerToGroup(props.layerFactory.makeLayer(type), groupId); + function handleAddLayer(type: TLayerType, group?: LayerGroup) { + if (group) { + group.addLayer(props.layerFactory.makeLayer(type)); return; } props.layerManager.addLayer(props.layerFactory.makeLayer(type)); @@ -91,290 +77,78 @@ export function LayersPanel(props: LayersPanelProps layer.getId()); - - let scrollTimeout: ReturnType | null = null; - let doScroll: boolean = false; - let currentScrollTime = 100; - - function findItemElement(element: HTMLElement): [HTMLElement | null, string | null, string | null] { - if (element?.parentElement && element.dataset.itemId && element.dataset.itemType) { - return [element.parentElement, element.dataset.itemId, element.dataset.itemType]; - } - return [null, null, null]; - } - - function handlePointerDown(e: PointerEvent) { - const [element, id, type] = findItemElement(e.target as HTMLElement); - - if (!element || !id || !type) { - return; - } - - draggingActive = false; - setIsDragging(true); - itemId = id; - itemType = type; - pointerDownPosition = { x: e.clientX, y: e.clientY }; - pointerDownPositionRelativeToElement = { - x: e.clientX - element.getBoundingClientRect().left, - y: e.clientY - element.getBoundingClientRect().top, - }; - document.addEventListener("pointermove", handlePointerMove); - document.addEventListener("pointerup", handlePointerUp); - } - - function moveLayerToIndex(id: string, moveToIndex: number, isLayer: boolean, groupId?: string) { - const layer = items.find((layer) => layer.getId() === id); - if (!layer) { - return; - } - - const index = newLayerOrder.indexOf(layer.getId()); - if (index === moveToIndex) { - if (!groupId && isLayer) { - props.layerManager.setLayerGroupId(id, undefined); - } - if (groupId && isLayer) { - props.layerManager.setLayerGroupId(id, groupId); - } - return; - } - - if (moveToIndex <= 0) { - newLayerOrder = [id, ...newLayerOrder.filter((el) => el !== id)]; - } else if (moveToIndex >= items.length - 1) { - newLayerOrder = [...newLayerOrder.filter((el) => el !== id), id]; - } else { - newLayerOrder = [...newLayerOrder]; - newLayerOrder.splice(index, 1); - newLayerOrder.splice(moveToIndex, 0, id); - } - - props.layerManager.setLayerGroupId(id, groupId); - - setItemsOrder(newLayerOrder); - } - - function handleElementDrag(id: string, position: Vec2) { - if (parentDivRef.current === null) { - return; - } - - let index = 0; - for (const child of parentDivRef.current.childNodes) { - if (child instanceof HTMLElement) { - const childBoundingRect = child.getBoundingClientRect(); - - if (child.dataset.itemType === "panel-placeholder") { - if (rectContainsPoint(childBoundingRect, position)) { - moveLayerToIndex(id, props.layerManager.getItems().length - 1, itemType === "layer"); - } - } - - if (!child.dataset.itemId) { - continue; - } - - if (child.dataset.itemId === id) { - continue; - } - - if (!rectContainsPoint(childBoundingRect, position)) { - index++; - continue; - } - - if (itemType === "layer") { - if (child.dataset.itemType === "group") { - const container = child.querySelector(".layer-container"); - if (!container) { - continue; - } - let numChildren = container.childNodes.length; - if ( - numChildren === 1 && - (container.childNodes[0] as HTMLElement)?.dataset.itemType === "group-placeholder" - ) { - numChildren = 0; - } - if (position.y <= childBoundingRect.y + 10) { - moveLayerToIndex(id, index, true); - } else if (position.y >= childBoundingRect.y + childBoundingRect.height - 10) { - moveLayerToIndex(id, index + numChildren + 1, true); - } else { - const groupId = child.dataset.itemId; - if (!groupId) { - continue; - } - let layerIndex = index + 1; - for (const innerChild of container.childNodes) { - if (innerChild instanceof HTMLElement) { - const innerChildBoundingRect = child.getBoundingClientRect(); - - if (innerChild.dataset.itemType === "group-placeholder") { - moveLayerToIndex(id, layerIndex, true, groupId); - } - - if (!innerChild.dataset.itemId) { - continue; - } - - if (innerChild.dataset.itemId === id) { - continue; - } - - if (!rectContainsPoint(innerChildBoundingRect, position)) { - layerIndex++; - continue; - } - - if ( - position.y <= - innerChildBoundingRect.y + innerChildBoundingRect.height / 2 - ) { - moveLayerToIndex(id, layerIndex, true, groupId); - } else { - moveLayerToIndex(id, layerIndex + 1, true, groupId); - } - layerIndex++; - } - } - } - index++; - continue; - } - } - - if (position.y <= childBoundingRect.y + childBoundingRect.height / 2) { - moveLayerToIndex(id, index, itemType === "layer"); - } else { - moveLayerToIndex(id, index + 1, itemType === "layer"); - } - index++; - } - } - } - - function maybeScroll(position: Vec2) { - if ( - upperScrollDivRef.current === null || - lowerScrollDivRef.current === null || - scrollDivRef.current === null - ) { - return; - } - - if (scrollTimeout) { - clearTimeout(scrollTimeout); - currentScrollTime = 100; - } - - if (rectContainsPoint(upperScrollDivRef.current.getBoundingClientRect(), position)) { - doScroll = true; - scrollTimeout = setTimeout(scrollUpRepeatedly, currentScrollTime); - } else if (rectContainsPoint(lowerScrollDivRef.current.getBoundingClientRect(), position)) { - doScroll = true; - scrollTimeout = setTimeout(scrollDownRepeatedly, currentScrollTime); - } else { - doScroll = false; - } - } - - function scrollUpRepeatedly() { - currentScrollTime = Math.max(10, currentScrollTime - 5); - if (scrollDivRef.current) { - scrollDivRef.current.scrollTop = Math.max(0, scrollDivRef.current.scrollTop - 10); - } - if (doScroll) { - scrollTimeout = setTimeout(scrollUpRepeatedly, currentScrollTime); - } - } - - function scrollDownRepeatedly() { - currentScrollTime = Math.max(10, currentScrollTime - 5); - if (scrollDivRef.current) { - scrollDivRef.current.scrollTop = Math.min( - scrollDivRef.current.scrollHeight, - scrollDivRef.current.scrollTop + 10 - ); - } - if (doScroll) { - scrollTimeout = setTimeout(scrollDownRepeatedly, currentScrollTime); - } - } - - function handlePointerMove(e: PointerEvent) { - if (!pointerDownPosition || !itemId || !itemType) { - return; - } - - if ( - !draggingActive && - point2Distance(pointerDownPosition, { x: e.clientX, y: e.clientY }) > MANHATTAN_LENGTH - ) { - draggingActive = true; - setDraggingLayerId(itemId); - } - - if (!draggingActive) { - return; - } + function handleItemMove(itemId: string, originId: string | null, destinationId: string | null, position: number) { + let origin: LayerGroup | LayerManager | null = props.layerManager; + if (originId) { + origin = props.layerManager.getGroup(originId) ?? null; + } - const dx = e.clientX - pointerDownPositionRelativeToElement.x; - const dy = e.clientY - pointerDownPositionRelativeToElement.y; - setDragPosition({ x: dx, y: dy }); + let destination: LayerGroup | LayerManager | null = props.layerManager; + if (destinationId) { + destination = props.layerManager.getGroup(destinationId) ?? null; + } - const point: Vec2 = { x: e.clientX, y: e.clientY }; + if (origin === null || destination === null) { + return; + } - handleElementDrag(itemId, point); + let isLayer: boolean = true; + let item: BaseLayer | LayerGroup | undefined = origin.getLayer(itemId); - maybeScroll(point); - } + if (!item && origin instanceof LayerManager) { + item = origin.getGroup(itemId); + isLayer = false; + } - function handlePointerUp() { - draggingActive = false; - pointerDownPosition = null; - itemId = null; - setIsDragging(false); - setDraggingLayerId(null); - document.removeEventListener("pointermove", handlePointerMove); - document.removeEventListener("pointerup", handlePointerUp); - props.layerManager.changeOrder(newLayerOrder); - } + if (!item) { + return; + } - currentParentDivRef.addEventListener("pointerdown", handlePointerDown); + if (isLayer) { + origin.removeLayer(itemId); + } else if (origin instanceof LayerManager) { + origin.removeGroup(itemId); + } - return function handleUnmount() { - currentParentDivRef.removeEventListener("pointerdown", handlePointerDown); - document.removeEventListener("pointermove", handlePointerMove); - document.removeEventListener("pointerup", handlePointerUp); - setIsDragging(false); - setDraggingLayerId(null); - }; - }, - [items, props.layerManager] - ); + if (isLayer && item instanceof BaseLayer) { + destination.insertLayer(item, position); + } else if (destination instanceof LayerManager && item instanceof LayerGroup) { + destination.insertGroup(item, position); + } + } - function handleScroll(e: React.UIEvent) { - setCurrentScrollPosition(e.currentTarget.scrollTop); + function makeLayerElement(layer: BaseLayer): React.ReactElement { + return ( + } + startAdornment={} + > + {props.makeSettingsContainerFunc( + layer, + props.ensembleSet, + props.workbenchSession, + props.workbenchSettings + )} + + ); } - function makeLayersAndGroupsContent(): React.ReactNode[] { - const nodes: React.ReactNode[] = []; + function makeLayerGroup(group: LayerGroup): React.ReactElement { + return ( + } + startAdornment={} + > + {group.getLayers().map((layer) => makeLayerElement(layer))} + + ); + } + function makeLayersAndGroupsContent(): React.ReactElement[] { + const nodes: React.ReactElement[] = []; const orderedItems = itemsOrder .map((id) => items.find((el) => el.getId() === id)) @@ -384,67 +158,9 @@ export function LayersPanel(props: LayersPanelProps[] = []; - for (let j = i + 1; j < orderedItems.length; j++) { - const nextItem = orderedItems[j]; - if (nextItem instanceof LayerGroup) { - break; - } - const layerGroupId = props.layerManager.getLayerGroupId(nextItem.getId()); - if (layerGroupId !== item.getId()) { - break; - } - layers.push(nextItem as BaseLayer); - i++; - } - - nodes.push( - - {layers.map((layer) => ( - - ))} - - ); + nodes.push(makeLayerGroup(item)); } else { - nodes.push( - - ); + nodes.push(makeLayerElement(item as BaseLayer)); } } @@ -487,34 +203,17 @@ export function LayersPanel(props: LayersPanelProps
- {isDragging && - createPortal( -
- )}
-
-
-
-
- {makeLayersAndGroupsContent()} -
-
- {items.length === 0 && ( + Click on to add a layer.
- )} -
+ } + onItemMove={handleItemMove} + > + {makeLayersAndGroupsContent()} +
); diff --git a/frontend/src/modules/_shared/layers/LayerGroup.ts b/frontend/src/modules/_shared/layers/LayerGroup.ts index 509fd270b..07f0b4aa2 100644 --- a/frontend/src/modules/_shared/layers/LayerGroup.ts +++ b/frontend/src/modules/_shared/layers/LayerGroup.ts @@ -2,20 +2,26 @@ import React from "react"; import { v4 } from "uuid"; +import { BaseLayer } from "./BaseLayer"; + export enum LayerGroupTopic { NAME_CHANGED = "name-changed", - LAYER_IDS_CHANGED = "layer-ids-changed", + LAYERS_CHANGED = "layer-ids-changed", + VISIBILITY_CHANGED = "visibility-changed", } export type LayerGroupTopicValueTypes = { - [LayerGroupTopic.LAYER_IDS_CHANGED]: string[]; + [LayerGroupTopic.LAYERS_CHANGED]: string[]; [LayerGroupTopic.NAME_CHANGED]: string; + [LayerGroupTopic.VISIBILITY_CHANGED]: boolean; }; export class LayerGroup { private _id: string; private _name: string; private _subscribers: Map void>> = new Map(); + private _layers: BaseLayer[] = []; + private _isVisible: boolean = true; constructor(name: string) { this._id = v4(); @@ -26,6 +32,15 @@ export class LayerGroup { return this._id; } + getIsVisible(): boolean { + return this._isVisible; + } + + setIsVisible(isVisible: boolean): void { + this._isVisible = isVisible; + this.notifySubscribers(LayerGroupTopic.VISIBILITY_CHANGED); + } + getName(): string { return this._name; } @@ -35,6 +50,44 @@ export class LayerGroup { this.notifySubscribers(LayerGroupTopic.NAME_CHANGED); } + getLayer(id: string): BaseLayer | undefined { + return this._layers.find((layer) => layer.getId() === id); + } + + getLayers(): BaseLayer[] { + return this._layers; + } + + addLayer(layer: BaseLayer): void { + this._layers = [...this._layers, layer]; + this.notifySubscribers(LayerGroupTopic.LAYERS_CHANGED); + } + + removeLayer(id: string): void { + this._layers = this._layers.filter((layer) => layer.getId() !== id); + this.notifySubscribers(LayerGroupTopic.LAYERS_CHANGED); + } + + moveLayer(id: string, position: number): void { + const layer = this._layers.find((layer) => layer.getId() === id); + if (!layer) { + throw new Error(`Layer with id ${id} not found`); + } + + const layers = this._layers.filter((layer) => layer.getId() !== id); + layers.splice(position, 0, layer); + + this._layers = layers; + this.notifySubscribers(LayerGroupTopic.LAYERS_CHANGED); + } + + insertLayer(layer: BaseLayer, position: number): void { + const layers = [...this._layers]; + layers.splice(position, 0, layer); + this._layers = layers; + this.notifySubscribers(LayerGroupTopic.LAYERS_CHANGED); + } + subscribe(topic: LayerGroupTopic, subscriber: () => void): void { const subscribers = this._subscribers.get(topic) ?? new Set(); subscribers.add(subscriber); @@ -68,6 +121,12 @@ export class LayerGroup { if (topic === LayerGroupTopic.NAME_CHANGED) { return this.getName(); } + if (topic === LayerGroupTopic.LAYERS_CHANGED) { + return this.getLayers().map((layer) => layer.getId()); + } + if (topic === LayerGroupTopic.VISIBILITY_CHANGED) { + return this.getIsVisible(); + } }; return snapshotGetter; diff --git a/frontend/src/modules/_shared/layers/LayerManager.ts b/frontend/src/modules/_shared/layers/LayerManager.ts index c1a1e34da..b3462a6a1 100644 --- a/frontend/src/modules/_shared/layers/LayerManager.ts +++ b/frontend/src/modules/_shared/layers/LayerManager.ts @@ -17,7 +17,6 @@ export type LayerManagerItem = BaseLayer | LayerGroup; export class LayerManager { private _queryClient: QueryClient | null = null; - private _layersGroupMap: Map = new Map(); private _subscribers: Map void>> = new Map(); private _items: LayerManagerItem[] = []; @@ -42,18 +41,13 @@ export class LayerManager { this.notifySubscribers(LayerManagerTopic.ITEMS_CHANGED); } - addLayerToGroup(layer: BaseLayer, groupId: string): void { + insertLayer(layer: BaseLayer, position: number): void { if (!this._queryClient) { throw new Error("Query client not set"); } layer.setName(this.makeUniqueLayerName(layer.getName())); layer.setQueryClient(this._queryClient); - const groupIndex = this._items.findIndex((item) => item.getId() === groupId); - if (groupIndex === -1) { - throw new Error("Group not found"); - } - this._items = [...this._items.slice(0, groupIndex + 1), layer, ...this._items.slice(groupIndex + 1)]; - this._layersGroupMap.set(layer.getId(), groupId); + this._items = [...this._items.slice(0, position), layer, ...this._items.slice(position)]; this.notifySubscribers(LayerManagerTopic.ITEMS_CHANGED); } @@ -64,42 +58,18 @@ export class LayerManager { this.notifySubscribers(LayerManagerTopic.ITEMS_CHANGED); } - removeLayer(id: string): void { - this._items = this._items.filter((item) => item.getId() !== id); - this._layersGroupMap.delete(id); + insertGroup(group: LayerGroup, position: number): void { + this._items = [...this._items.slice(0, position), group, ...this._items.slice(position)]; this.notifySubscribers(LayerManagerTopic.ITEMS_CHANGED); } - removeGroup(id: string): void { - const layerIdsInGroup: string[] = []; - for (const [layerId, groupId] of this._layersGroupMap) { - if (groupId === id) { - this._layersGroupMap.delete(layerId); - layerIdsInGroup.push(layerId); - } - } - this._items = this._items.filter((item) => item.getId() !== id && !layerIdsInGroup.includes(item.getId())); + removeLayer(id: string): void { + this._items = this._items.filter((item) => item.getId() !== id); this.notifySubscribers(LayerManagerTopic.ITEMS_CHANGED); } - getLayerGroupId(layerId: string): string | undefined { - return this._layersGroupMap.get(layerId); - } - - getGroupOfLayer(layerId: string): LayerGroup | undefined { - const groupId = this.getLayerGroupId(layerId); - if (!groupId) { - return undefined; - } - return this.getGroup(groupId); - } - - setLayerGroupId(layerId: string, groupId: string | undefined): void { - if (groupId) { - this._layersGroupMap.set(layerId, groupId); - } else { - this._layersGroupMap.delete(layerId); - } + removeGroup(id: string): void { + this._items = this._items.filter((item) => item.getId() !== id); this.notifySubscribers(LayerManagerTopic.ITEMS_CHANGED); } @@ -115,10 +85,6 @@ export class LayerManager { return undefined; } - getLayersInGroup(groupId: string): BaseLayer[] { - return this._items.filter((item) => this.getLayerGroupId(item.getId()) === groupId) as BaseLayer[]; - } - getGroup(id: string): LayerGroup | undefined { const item = this.getItem(id); if (item instanceof LayerGroup) {