diff --git a/todo.txt b/todo.txt index 42e7e07..b3e48db 100644 --- a/todo.txt +++ b/todo.txt @@ -1,7 +1,9 @@ +window.addEventListener .... done wrong everywhere! + +touch scroll in file propertty dialog. channel bindings help dialog. ) => void; - onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent) => boolean; + onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent | React.MouseEvent) => boolean; onLongPressMove?: (e: React.PointerEvent) => void; - onLongPressEnd?: (e: React.PointerEvent) => void; - longPressDelay? : number; + onLongPressEnd?: (e: React.PointerEvent| React.MouseEvent) => void; + instantMouseLongPress?: boolean; + longPressDelay?: number; } interface Point { @@ -58,37 +59,43 @@ function screenToClient(e: React.PointerEvent): Point { export default function DraggableButtonBase(props: DraggableButtonBaseProps) { // Ensure that the props are spread correctly - const { onClick, onLongPressStart, onLongPressEnd,longPressDelay, ...rest } = props; - + const { onClick, onLongPressStart, onLongPressMove: + doLongPressMove,onLongPressEnd, longPressDelay,instantMouseLongPress, ...rest } = props; + let [hTimeout, setHTimeout] = React.useState(null); let [pointerId, setPointerId] = React.useState(null); let [longPressed, setLongPressed] = React.useState(false); let [clickSuppressed, setClickSuppressed] = React.useState(false); - let [pointerDownPoint, setPointerDownPoint] = React.useState({x: 0, y: 0}); + let [pointerDownPoint, setPointerDownPoint] = React.useState({ x: 0, y: 0 }); + let [ longPressedElement, setLongPressedElement ] = React.useState(null); function handleSuppressClick(e: MouseEvent) { - e.stopPropagation(); - e.preventDefault(); - setSuppressClick(false); + e.stopPropagation(); + e.preventDefault(); + setSuppressClick(false); } function setSuppressClick(value: boolean) { - - if (value !== clickSuppressed) { - setClickSuppressed(value); - if (value) { - window.addEventListener( - "click", - handleSuppressClick, - { - once: true, - capture: true - }); - } else { - window.removeEventListener("click", handleSuppressClick); + setClickSuppressed(value); + } + function startLongPress(currentTarget: HTMLButtonElement, e: React.PointerEvent | React.MouseEvent) + { + if (hTimeout !== null) { + window.clearTimeout(hTimeout); + setHTimeout(null); + } + if (props.onLongPressStart) { + if (!props.onLongPressStart(currentTarget, e)) { + return; } } + setLongPressedElement(currentTarget); + setLongPressed(true); + setSuppressClick(true); + currentTarget.style.touchAction = "none"; // prevent scrolling. + // console.log("DraggableButtonBase: long press started"); + } function cancelLongPress() { @@ -99,23 +106,106 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) { setPointerId(null); setLongPressed(false); setSuppressClick(false); + if (longPressedElement) { + longPressedElement.style.touchAction = ""; + setLongPressedElement(null); + } + if (longPressed) { + // console.log("DraggableButtonBase: long press canceled"); + } } + function stopLongPress(e: React.PointerEvent | React.MouseEvent) { + if (longPressed) { + if (props.onLongPressEnd) { + props.onLongPressEnd(e); + } + } + cancelLongPress(); + } + + const handleTouchMove = (e: TouchEvent) => { + if (longPressed) { + e.preventDefault(); // Prevent scrolling during long press + e.stopPropagation(); + } + } + const handleTouchstart = (e: TouchEvent) => { + if (longPressed) { + e.preventDefault(); // Prevent default touch behavior during long press + e.stopPropagation(); + } + } useEffect(() => { + + let hTouchMove = (e: TouchEvent) => { + handleTouchMove(e); + } + addEventListener("touchmove", hTouchMove, { passive: false, capture: true }); + + let hTouchStart = (e: TouchEvent) => { + handleTouchstart(e); + } + addEventListener("touchstart", hTouchStart, { passive: false, capture: true }); + return () => { + removeEventListener("touchmove", hTouchMove, { capture: true }); + removeEventListener("touchstart", hTouchStart, { capture: true }); + setSuppressClick(false); cancelLongPress(); }; }, []); + useEffect(() => { + if (clickSuppressed) { + let hclick = (e: MouseEvent) => { + handleSuppressClick(e); + }; + window.addEventListener("click",hclick, { capture: true }); + let hTimeout: number | null = null; + hTimeout = window.setTimeout(() => { + setSuppressClick(false); + hTimeout = null; + }, 100); + return () => { + if (hTimeout !== null) { + window.clearTimeout(hTimeout); + } + window.removeEventListener("click", hclick, { capture: true }); + } + } else { + return ()=>{}; + } + }, [clickSuppressed]); + useEffect(() => { + + if (longPressed) { + const suppressTouchMove = (e: TouchEvent) => { + e.preventDefault(); + } + window.addEventListener("touchmove", suppressTouchMove, { capture: true, passive: false } ); + return () => { + window.removeEventListener("touchmove", suppressTouchMove, {capture: true}) + } + } + else { + return () => { + }; + } + }, [longPressed]); + return ( { e.preventDefault(); e.stopPropagation(); + if (e.button === -1) { // touch long press + startLongPress(e.currentTarget as HTMLButtonElement,e); + } return false; }} onPointerDown={(e) => { @@ -130,19 +220,14 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) { setPointerDownPoint(screenToClient(e)); let currentTarget = e.currentTarget as HTMLButtonElement; - if (longPressDelay === undefined || longPressDelay >= 0) - { + if (longPressDelay === undefined || longPressDelay >= 0) { + let delay = longPressDelay ?? 1250; + if (instantMouseLongPress === true && e.pointerType === "mouse") { + delay = 0; + } setHTimeout(window.setTimeout(() => { - setHTimeout(null); - if (props.onLongPressStart) { - if (!props.onLongPressStart(currentTarget,e)) - { - return; - } - } - setLongPressed(true); - setSuppressClick(true); - }, longPressDelay??1250)); + startLongPress(currentTarget, e); + }, delay)); } e.preventDefault(); e.stopPropagation(); @@ -150,17 +235,17 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) { }} onPointerMove={(e) => { if (e.pointerId === pointerId) { - if(longPressed) { - if (props.onLongPressMove) { - props.onLongPressMove(e); + if (longPressed) { + if (doLongPressMove) { + doLongPressMove(e); } } else { let clientPoint = screenToClient(e); - let dx = pointerDownPoint.x- clientPoint.x; + let dx = pointerDownPoint.x - clientPoint.x; let dy = pointerDownPoint.y - clientPoint.y; if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { - cancelLongPress(); + stopLongPress(e); } } } @@ -171,14 +256,8 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) { onPointerUp={(e) => { if (e.pointerId === pointerId) { e.currentTarget.releasePointerCapture(e.pointerId); - setPointerId(null); - if (longPressed) { - if (props.onLongPressEnd) { - props.onLongPressEnd(e); - } - } - cancelLongPress(); - if (e.isPropagationStopped()) { + stopLongPress(e); + if (e.isPropagationStopped() || e.isDefaultPrevented()) { setSuppressClick(true); // only way to cancel the click return; } @@ -187,30 +266,24 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) { } }} - // onTouchStart={(e)=> { - // //e.preventDefault(); - // }} - // onTouchMove={(e)=> { - // //e.preventDefault(); - // }} + onTouchStart={(e)=> { + //e.preventDefault(); + }} onClick={(e) => { if (clickSuppressed) { e.stopPropagation(); e.preventDefault(); setSuppressClick(false); } else { - if (props.onClick) { - props.onClick(e); + if (onClick) { + onClick(e); } } }} onPointerCancelCapture={(e) => { if (pointerId !== null) { e.currentTarget.releasePointerCapture(e.pointerId); - if (longPressed && props.onLongPressEnd) { - props.onLongPressEnd(e) - } - cancelLongPress(); + stopLongPress(e); } }}> {props.children} diff --git a/vite/src/pipedal/FilePropertyDialog.tsx b/vite/src/pipedal/FilePropertyDialog.tsx index 09a96f5..f6afd42 100644 --- a/vite/src/pipedal/FilePropertyDialog.tsx +++ b/vite/src/pipedal/FilePropertyDialog.tsx @@ -63,6 +63,13 @@ import HomeIcon from '@mui/icons-material/Home'; import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog'; import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata'; + +const AUTOSCROLL_TICK_DELAY = 30; +const AUTOSCROLL_THRESHOLD = 48; +const AUTOSCROLL_SCROLL_PER_SECOND = 500; +const AUTOSCROLL_SCROLL_RATE = AUTOSCROLL_SCROLL_PER_SECOND * AUTOSCROLL_TICK_DELAY / 1000; + + interface Point { x: number; y: number; @@ -93,7 +100,7 @@ const audioFileExtensions: { [name: string]: boolean } = { ".ra": true }; -function screenToClient(currentTarget: HTMLElement, e: React.PointerEvent): Point { +function screenToClient(currentTarget: HTMLElement, e: React.PointerEvent | React.MouseEvent): Point { let rect = currentTarget.getBoundingClientRect(); const x = e.clientX + rect.left; const y = e.clientY + rect.right; @@ -157,11 +164,14 @@ export interface FilePropertyDialogState { }; -class DragState { - activeItem: string = ""; - height: number = 0; - from: number = 0; - to: number = 0; +interface DragState { + activeItem: string; + height: number; + dragElement: HTMLElement; + dragElementDy: number; + from: number; + to: number; + lastMousePoint: Point; }; export default withStyles( @@ -323,8 +333,6 @@ export default withStyles( longPressStartPoint: Point | null = null; - dragFromPosition: number = -1; - dragToPosition: number = -1; maxPosition: number = 0; handleMultisSelectClose() { @@ -336,24 +344,29 @@ export default withStyles( handleLongPressStart( currentTarget: HTMLButtonElement, - e: React.PointerEvent, + e: React.PointerEvent | React.MouseEvent, fileEntry: FileEntry): boolean { if (this.state.reordering) { if (!this.isTracksDirectory()) { return false; } - this.dragFromPosition = parseInt(currentTarget.getAttribute("data-position") as string, 10); - this.dragToPosition = this.dragToPosition; let element = currentTarget; - element.style.position = "relative"; - element.style.zIndex = "1000"; - element.style.top = "5px"; - element.style.left = "5px"; - element.style.background = isDarkMode() ? "#555" : "#EEF" // xxx: dark mode. + let index = parseInt(element.getAttribute("data-position") as string, 10); if (this.listContainerElementRef) { - this.longPressStartPoint = screenToClient(currentTarget,e); + this.longPressStartPoint = screenToClient(currentTarget, e); + let dragState: DragState = { + activeItem: fileEntry.pathname, + dragElement: element, + height: element.clientHeight, + dragElementDy: 0, + from: index, + to: index, + lastMousePoint: {...this.longPressStartPoint} + }; + this.setState({ dragState: dragState }); } + return true; } else if (!this.state.multiSelect) { if (fileEntry.isProtected) { @@ -370,6 +383,48 @@ export default withStyles( return false; } + dragTarget: HTMLButtonElement | null = null; + + + updateReorderPosition(element: HTMLButtonElement, point: Point) { + if (this.longPressStartPoint) { + let dragDy = point.y - this.longPressStartPoint.y; + let height = element.clientHeight; + + let dragFromPosition = parseInt(element.getAttribute("data-position") as string, 10); + + let elementOffset = dragFromPosition * height; + let newOffset = elementOffset + dragDy; + if (newOffset < 0) { + dragDy = -elementOffset; + newOffset = 0; + } + let maxOffset = (this.maxPosition - 1) * height; + if (newOffset > maxOffset) { + dragDy = maxOffset - elementOffset; + newOffset = maxOffset; + } + let toPosition = Math.floor((newOffset + height / 2) / height); + if (toPosition < 0) { + toPosition = 0; + } + if (toPosition > this.maxPosition) { + toPosition = this.maxPosition; + } + // console.log("drag from " + dragFromPosition + " to " + toPosition + " dy: " + dragDy); + this.setState({ + dragState: { + activeItem: element.getAttribute("data-pathname") || "", + dragElement: element, + height: height, + dragElementDy: dragDy, + from: dragFromPosition, + to: toPosition, + lastMousePoint: point + } + }); + } + } handleLongPressMove(e: React.PointerEvent) { if (this.state.reordering) { if (!this.isTracksDirectory()) { @@ -377,37 +432,10 @@ export default withStyles( } if (this.listContainerElementRef) { let element = e.target as HTMLButtonElement; - let point = screenToClient(e.currentTarget,e); - if (this.longPressStartPoint) { - let dy = point.y - this.longPressStartPoint.y; - element.style.left = (5) + "px"; - element.style.top = (5 + dy) + "px"; - let height = element.clientHeight; - let positionChange_: number = 0; - if (dy > 0) { - positionChange_ = Math.floor((dy + height / 2) / height); - } else { - positionChange_ = Math.ceil((dy - height / 2) / height) - } - let toPosition = this.dragFromPosition + positionChange_; - if (toPosition < 0) { - toPosition = 0; - } - if (toPosition > this.maxPosition) { - toPosition = this.maxPosition; - } - if (toPosition !== this.dragToPosition) { - this.dragToPosition = toPosition; - this.setState({ - dragState: { - activeItem: element.getAttribute("data-pathname") || "", - height: height, - from: this.dragFromPosition, - to: toPosition - } - }); - } - } + let point = screenToClient(e.currentTarget, e); + this.updateReorderPosition(element, point); + this.startAutoScroll() + } } } @@ -457,7 +485,7 @@ export default withStyles( this.model.moveAudioFile( this.state.currentDirectory, from, to); } - handleLongPressEnd(e: React.PointerEvent, fileEntry: FileEntry) { + handleLongPressEnd(e: React.PointerEvent | React.MouseEvent, fileEntry: FileEntry) { if (this.state.reordering) { if (!this.isTracksDirectory()) { return; @@ -471,14 +499,8 @@ export default withStyles( e.preventDefault(); e.stopPropagation(); - let element = e.currentTarget as HTMLButtonElement; - - element.style.position = ""; - element.style.zIndex = ""; - element.style.top = ""; - element.style.left = ""; - element.style.background = ""; // xxx: dark mode. this.setState({ dragState: null }); + this.stopAutoScroll(); } else if (!this.state.multiSelect) { let selectedFile = fileEntry.pathname; if (selectedFile === "") { @@ -526,6 +548,7 @@ export default withStyles( this.requestScroll = true; } componentWillUnmount() { + this.stopAutoScroll(); this.cancelProgressTimeout(); super.componentWillUnmount(); @@ -925,7 +948,98 @@ export default withStyles( return (); } - listContainerElementRef: HTMLDivElement | null = null; + private listContainerElementRef: HTMLDivElement | null = null; + private scrollContainerElementRef: HTMLDivElement | null = null; + private autoScrollTimer: number | null = null; + + handleAutoScrollTick() { + let dragState = this.state.dragState; + if (!dragState) return; + + let scrollContainer = this.scrollContainerElementRef; + if (!scrollContainer) { + return; + } + let scrollBounds_ = scrollContainer.getBoundingClientRect(); + let scrollClientTop = scrollBounds_.top; + let scrollClientBottom = scrollClientTop + scrollContainer.clientHeight; + let dragBounds = dragState.dragElement?.getBoundingClientRect(); + + let dy = 0; + let y = scrollContainer.scrollTop; + if (dragBounds.top < scrollClientTop + AUTOSCROLL_THRESHOLD) { + dy = -AUTOSCROLL_SCROLL_RATE; + + y = scrollContainer.scrollTop + dy; + if (y < 0) y = 0; + dy = y - scrollContainer.scrollTop; + } else if (dragBounds.bottom > scrollClientBottom - AUTOSCROLL_THRESHOLD) { + dy = AUTOSCROLL_SCROLL_RATE; + + y = scrollContainer.scrollTop + dy; + let maxScrollTop = scrollContainer.scrollHeight - scrollContainer.clientHeight; + if (y > maxScrollTop) { + y = maxScrollTop; + } + dy = y - scrollContainer.scrollTop; + } else { + return; + } + if (this.longPressStartPoint === null) { + return; + } + if (dy === 0) { + return; + } + this.longPressStartPoint = { + x: this.longPressStartPoint.x, + y: this.longPressStartPoint.y - dy + }; + let dragElementDy = dragState.lastMousePoint.y - this.longPressStartPoint.y; + let originalY = dragState.from * dragState.height; + let newY = originalY + dragElementDy; + if (newY < 0) + { + newY = 0; + dragElementDy = -originalY; + } + let maxDy = (this.maxPosition - 1) * dragState.height; + if (newY > maxDy) { + newY = maxDy; + dragElementDy = maxDy - originalY; + } + let newTo = Math.floor((newY + dragState.height / 2) / dragState.height); + + + let newDragState = { ...dragState }; + + newDragState.dragElementDy = dragElementDy; + newDragState.to = newTo; + + // console.log("autoscroll from " + dragState.from + " to " + newTo + " dy: " + dragElementDy); + // avoid jitter. + newDragState.dragElement.style.top = (newDragState.dragElementDy + 5) + "px"; + + this.setState({ dragState: newDragState }); + scrollContainer.scrollTo({ left: 0, top: y, behavior: "instant" }); + this.startAutoScroll(); + + } + + + stopAutoScroll() { + if (this.autoScrollTimer !== null) { + clearTimeout(this.autoScrollTimer); + this.autoScrollTimer = null; + } + } + startAutoScroll() { + this.stopAutoScroll(); + + this.autoScrollTimer = setTimeout( + () => { this.handleAutoScrollTick() }, AUTOSCROLL_TICK_DELAY); + } + render() { const isTracksDirectory = this.isTracksDirectory(); @@ -1146,7 +1260,9 @@ export default withStyles( + }} + ref={(element: HTMLDivElement | null) => { this.scrollContainerElementRef = element; }} + >
{ this.onScrollRef(element) }; } let dragOffset = 0; + let dragLeft = 0; let dragState = this.state.dragState; + let zIndex: number | undefined = undefined; + let background: string | undefined = undefined; if (dragState && value.metadata) { if (myTrackPosition !== dragState.from) { if (myTrackPosition >= dragState.to && myTrackPosition < dragState.from) { @@ -1218,21 +1337,31 @@ export default withStyles( } else if (myTrackPosition > dragState.from && myTrackPosition <= dragState.to) { dragOffset = -dragState.height; } + } else { + dragOffset = dragState.dragElementDy + 5; + dragLeft = 5; + zIndex = 1000; + background = isDarkMode() ? "#555" : "#EEF"; } } + let dragButtonStyle: React.CSSProperties = { + width: columnWidth, flex: "0 0 auto", height: (value.metadata && !compactVertical) ? 64 : 48, + position: "relative", + top: dragOffset, + left: dragLeft, + zIndex: zIndex, + background: background, + }; return ( this.handleFileClick(e, value)} onDoubleClick={() => { this.onDoubleClickValue(value.pathname); }} @@ -1559,7 +1688,7 @@ export default withStyles( await this.model.renameFilePropertyFile(oldFilePath, newFilePath, this.props.fileProperty); let index = newFileList.indexOf(file); if (index !== -1) { - newFileList.splice(index,1); + newFileList.splice(index, 1); } } @@ -1570,8 +1699,7 @@ export default withStyles( if (!this.mounted) { return; } - if (newFileList.length === 0 ) - { + if (newFileList.length === 0) { this.setState({ multiSelect: false, selectedFiles: [] diff --git a/vite/src/pipedal/ToolTipEx.tsx b/vite/src/pipedal/ToolTipEx.tsx index 9c8326b..b2e0bfa 100644 --- a/vite/src/pipedal/ToolTipEx.tsx +++ b/vite/src/pipedal/ToolTipEx.tsx @@ -81,7 +81,7 @@ function ToolTipEx(props: ToolTipExProps) { if (valueTooltip === undefined) // no timeout if there's a value tooltip { if (t > 0) { - console.log("ToolTipEx: starting timeout for ", t); + // console.log("ToolTipEx: starting timeout for ", t); handle = window.setTimeout(() => { setOpen(true); },t); @@ -89,7 +89,7 @@ function ToolTipEx(props: ToolTipExProps) { } return () => { if (handle !== null) { - console.log("ToolTipEx: clearing timeout for ", t); + // console.log("ToolTipEx: clearing timeout for ", t); window.clearTimeout(handle); } }; @@ -181,39 +181,39 @@ function ToolTipEx(props: ToolTipExProps) { return (
{ - console.log("ToolTipEx: onClickCapture"); + // console.log("ToolTipEx: onClickCapture"); handleClickCapture(e); }} onMouseEnter={(e)=> { - console.log("ToolTipEx: onMouseEnter"); + // console.log("ToolTipEx: onMouseEnter"); handleMouseEnter(e); }} onMouseLeave={(e)=> { - console.log("ToolTipEx: onMouseLeave"); + // console.log("ToolTipEx: onMouseLeave"); handleMouseLeave(e); }} onPointerCancelCapture={(e) => { - console.log("ToolTipEx: onPointerCancelCapture"); + // console.log("ToolTipEx: onPointerCancelCapture"); handlePointerCancel(e); }} onContextMenuCapture={(e) => { - console.log("ToolTipEx: onContextMenuCapture"); + // console.log("ToolTipEx: onContextMenuCapture"); handleConextMenu(e); return false; }} onPointerDownCapture={(e) => { - console.log("ToolTipEx: onPointerDownCapture"); + // console.log("ToolTipEx: onPointerDownCapture"); handlePointerDownCapture(e); }} onPointerMoveCapture={(e) => { - console.log("ToolTipEx: onPointerMoveCapture"); + // console.log("ToolTipEx: onPointerMoveCapture"); handlePointerMoveCapture(e); }} onPointerUpCapture={(e) => { - console.log("ToolTipEx: onPointerUpCapture"); + // console.log("ToolTipEx: onPointerUpCapture"); handlePointerUpCapture(e); }} >