Skip to content

Commit 766a966

Browse files
optimized scrolling behavior. no scrolling when drop target is in view. automatic scrolling when moving an item near the edge (top or bottom) of the Tree View to allow dragging/dropping beyond the immediately visable tree.
1 parent c5f1a8b commit 766a966

4 files changed

Lines changed: 681 additions & 54 deletions

File tree

src/main/java/net/sourceforge/fddtools/ui/fx/FDDTreeDragAndDropController.java

Lines changed: 177 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package net.sourceforge.fddtools.ui.fx;
22

3+
import javafx.animation.KeyFrame;
34
import javafx.animation.PauseTransition;
5+
import javafx.animation.Timeline;
46
import javafx.application.Platform;
57
import javafx.css.PseudoClass;
8+
import javafx.geometry.Bounds;
69
import javafx.scene.control.TreeCell;
710
import javafx.scene.control.ScrollBar;
811
import javafx.scene.control.Tooltip;
@@ -35,6 +38,13 @@ enum DropType { INTO, BEFORE, AFTER }
3538
private ScrollBar vbar; // vertical scroll bar cache
3639
private boolean dragActive = false;
3740
private Double lockedV = null;
41+
42+
// Edge scrolling configuration
43+
private static final double EDGE_SCROLL_THRESHOLD = 20.0; // pixels from edge to trigger scrolling
44+
private static final double SCROLL_SPEED = 0.02; // scroll increment per timer tick
45+
private Timeline edgeScrollTimer;
46+
private boolean isScrollingUp = false;
47+
private boolean isScrollingDown = false;
3848
FDDTreeDragAndDropController(FDDTreeViewFX tree){ this.tree = tree; }
3949

4050
void attachTo(TreeCell<FDDINode> cell){
@@ -54,14 +64,18 @@ void attachTo(TreeCell<FDDINode> cell){
5464
String id = item.getId() != null ? item.getId() : item.getName();
5565
content.put(FDD_NODE_FORMAT, id); content.putString(id);
5666
db.setContent(content);
57-
try { var img = cell.snapshot(null, null); if (img!=null) db.setDragView(img, img.getWidth()/4, img.getHeight()/2); } catch (Exception ex) { LOGGER.debug("Snapshot for drag view failed: {}", ex.getMessage()); }
67+
try { var img = cell.snapshot(null, null); if (img!=null) db.setDragView(img, img.getWidth()/4, img.getHeight()/2); } catch (Exception ignored) { }
5868
e.consume();
5969
});
6070
cell.setOnDragOver(e -> {
6171
FDDINode dragSource = tree.dragSourceNode;
6272
FDDINode target = cell.getItem();
6373
if (dragSource == null || target == null) { e.consume(); return; }
6474
if (dragSource == target) { clear(cell, DROP_TARGET, INSERT_BEFORE, INSERT_AFTER); e.consume(); return; }
75+
76+
// Check for edge scrolling first
77+
handleEdgeScrolling(e.getSceneY());
78+
6579
boolean okInto = tree.isValidReparent(dragSource, target);
6680
DropType dropType = deriveDropType(e.getY(), cell.getHeight());
6781
boolean ok = switch (dropType){
@@ -74,8 +88,8 @@ void attachTo(TreeCell<FDDINode> cell){
7488
// Only auto-expand when hovering the center (INTO) and near edges to avoid mid-view scroll
7589
boolean nearEdge = shouldAutoExpand(cell, /*edgeOnlyDefault*/ true);
7690
if (dropType == DropType.INTO && nearEdge) scheduleAutoExpand(cell, state); else cancelAutoExpand(state);
77-
// Freeze scroll unless near edges
78-
if (dragActive) maintainScrollLock(nearEdge);
91+
// During edge scrolling, don't maintain scroll lock - allow controlled scrolling
92+
if (dragActive && !isScrollingUp && !isScrollingDown) maintainScrollLockAlways();
7993
} else {
8094
clear(cell, DROP_TARGET, INSERT_BEFORE, INSERT_AFTER);
8195
cancelAutoExpand(state);
@@ -89,34 +103,58 @@ void attachTo(TreeCell<FDDINode> cell){
89103
FDDINode target = cell.getItem();
90104
if (dragSource != null && target != null && dragSource != target){
91105
DropType dt = state.currentDropType == null ? DropType.INTO : state.currentDropType;
92-
double beforeV = captureV();
93-
switch (dt){
94-
case INTO -> {
95-
if (tree.isValidReparent(dragSource, target)) {
96-
CommandExecutionService.getInstance().execute(new MoveNodeCommand(dragSource, target));
97-
// Incremental UI update avoids full refresh (prevents scroll jump)
98-
tree.updateAfterMove(dragSource, target, -1);
99-
success = true;
106+
107+
// Capture the current scroll position BEFORE any changes
108+
double preservedScrollPosition = captureV();
109+
110+
// Enable scroll suppression to prevent automatic scrolling during move
111+
tree.setSuppressAutoScroll(true);
112+
113+
try {
114+
switch (dt){
115+
case INTO -> {
116+
if (tree.isValidReparent(dragSource, target)) {
117+
CommandExecutionService.getInstance().execute(new MoveNodeCommand(dragSource, target));
118+
tree.updateAfterMove(dragSource, target, -1);
119+
success = true;
120+
}
100121
}
101-
}
102-
case BEFORE, AFTER -> {
103-
FDDINode parent = (FDDINode) target.getParentNode();
104-
if (parent != null && canInsertSibling(dragSource, target)) {
105-
int idx = parent.getChildren().indexOf(target); if (dt == DropType.AFTER) idx += 1;
106-
CommandExecutionService.getInstance().execute(new MoveNodeCommand(dragSource, parent, idx));
107-
tree.updateAfterMove(dragSource, parent, idx);
108-
success = true;
122+
case BEFORE, AFTER -> {
123+
FDDINode parent = (FDDINode) target.getParentNode();
124+
if (parent != null && canInsertSibling(dragSource, target)) {
125+
int idx = parent.getChildren().indexOf(target); if (dt == DropType.AFTER) idx += 1;
126+
CommandExecutionService.getInstance().execute(new MoveNodeCommand(dragSource, parent, idx));
127+
tree.updateAfterMove(dragSource, parent, idx);
128+
success = true;
129+
}
109130
}
110131
}
132+
133+
// After all operations are complete, restore the original scroll position
134+
if (success && preservedScrollPosition >= 0) {
135+
Platform.runLater(() -> {
136+
ensureVBar();
137+
if (vbar != null) {
138+
vbar.setValue(preservedScrollPosition);
139+
}
140+
// Delay disabling suppression to allow updateAfterMove's deferred selection to complete
141+
Platform.runLater(() -> {
142+
tree.setSuppressAutoScroll(false);
143+
});
144+
});
145+
} else {
146+
// If not successful, just disable suppression
147+
tree.setSuppressAutoScroll(false);
148+
}
149+
} catch (Exception ex) {
150+
LOGGER.error("Error during drag and drop operation", ex);
151+
tree.setSuppressAutoScroll(false);
111152
}
112-
double afterV = captureV();
113-
if (LOGGER.isDebugEnabled()) LOGGER.debug("DnD drop type={} source={} target={} success={} scrollV:{}->{}", dt, safeName(dragSource), safeName(target), success, beforeV, afterV);
114-
if (!success){ showTransientTooltip(cell, invalidReason(dragSource, target, dt)); LOGGER.debug("Invalid drop {} from {} to {}", dt, dragSource.getName(), target.getName()); }
153+
154+
if (!success){ showTransientTooltip(cell, invalidReason(dragSource, target, dt)); }
115155
}
116156
clear(cell, DROP_TARGET, INSERT_BEFORE, INSERT_AFTER);
117157
e.setDropCompleted(success);
118-
if (success) restoreScrollPositionAsync();
119-
// avoid tree.refresh() to keep viewport stable
120158
e.consume();
121159
});
122160
cell.setOnDragEntered(e -> {
@@ -127,7 +165,15 @@ void attachTo(TreeCell<FDDINode> cell){
127165
e.consume();
128166
});
129167
cell.setOnDragExited(e -> { clear(cell, DROP_TARGET, INSERT_BEFORE, INSERT_AFTER); cancelAutoExpand(state); e.consume(); });
130-
cell.setOnDragDone(e -> { tree.dragSourceNode = null; clear(cell, DROP_TARGET, INSERT_BEFORE, INSERT_AFTER); cancelAutoExpand(state); restoreScrollPositionAsync(); dragActive=false; lockedV=null; });
168+
cell.setOnDragDone(e -> {
169+
tree.dragSourceNode = null;
170+
clear(cell, DROP_TARGET, INSERT_BEFORE, INSERT_AFTER);
171+
cancelAutoExpand(state);
172+
stopEdgeScrolling(); // Stop any active edge scrolling
173+
restoreScrollPositionAsync();
174+
dragActive=false;
175+
lockedV=null;
176+
});
131177
}
132178

133179
/**
@@ -173,7 +219,7 @@ private void scheduleAutoExpand(TreeCell<FDDINode> cell, CellState state){
173219
state.expandDelay = new PauseTransition(Duration.millis(700)); // slightly longer to reduce churn
174220
state.expandDelay.setOnFinished(ev -> {
175221
try { if (cell.getTreeItem()!=null && !cell.getTreeItem().isExpanded()) cell.getTreeItem().setExpanded(true); }
176-
catch (Exception ex){ LOGGER.debug("Auto-expand failed: {}", ex.getMessage()); }
222+
catch (Exception ignored) {}
177223
finally { state.expandDelay = null; }
178224
});
179225
state.expandDelay.play();
@@ -206,22 +252,120 @@ private void ensureVBar(){
206252
}
207253
} catch (Exception ignored) {}
208254
}
209-
private void maintainScrollLock(boolean nearEdge){
210-
ensureVBar(); if (vbar == null) return;
211-
if (!nearEdge) {
212-
if (lockedV == null) lockedV = vbar.getValue();
213-
vbar.setValue(lockedV);
214-
} else {
255+
256+
/**
257+
* Always maintain scroll lock during drag operations.
258+
* This prevents JavaFX's automatic scrolling behavior that occurs when dragging near edges.
259+
* The scroll position will only change if explicitly controlled by our logic.
260+
*/
261+
private void maintainScrollLockAlways(){
262+
ensureVBar();
263+
if (vbar == null) return;
264+
265+
// Capture the initial locked position if not already set
266+
if (lockedV == null) {
215267
lockedV = vbar.getValue();
216268
}
269+
270+
// Always restore the locked position to prevent any automatic scrolling
271+
if (Math.abs(vbar.getValue() - lockedV) > 0.001) {
272+
vbar.setValue(lockedV);
273+
}
217274
}
218275
private double captureV(){ try { ensureVBar(); return vbar==null? -1 : vbar.getValue(); } catch (Exception ex){ return -1; } }
219-
private String safeName(FDDINode n){ return n==null?"null": (n.getName()!=null?n.getName():"#"+n.hashCode()); }
220276

221277
private void restoreScrollPositionAsync(){
222278
if (lockedV == null) return; ensureVBar(); if (vbar == null) return;
223279
Platform.runLater(() -> {
224280
try { vbar.setValue(lockedV); } catch (Exception ignored) {}
225281
});
226282
}
283+
284+
/**
285+
* Handles edge scrolling during drag operations.
286+
* When the user drags near the top or bottom edge of the TreeView,
287+
* automatically scroll to reveal more drop targets.
288+
*/
289+
private void handleEdgeScrolling(double sceneY) {
290+
if (!dragActive) return;
291+
292+
ensureVBar();
293+
if (vbar == null) return;
294+
295+
// Get the TreeView bounds in scene coordinates
296+
Bounds treeBounds = tree.localToScene(tree.getBoundsInLocal());
297+
double treeTop = treeBounds.getMinY();
298+
double treeBottom = treeBounds.getMaxY();
299+
300+
// Check if we're near the top or bottom edge
301+
boolean nearTop = sceneY < (treeTop + EDGE_SCROLL_THRESHOLD);
302+
boolean nearBottom = sceneY > (treeBottom - EDGE_SCROLL_THRESHOLD);
303+
304+
if (nearTop && !isScrollingUp) {
305+
startEdgeScrolling(true); // scroll up
306+
} else if (nearBottom && !isScrollingDown) {
307+
startEdgeScrolling(false); // scroll down
308+
} else if (!nearTop && !nearBottom) {
309+
stopEdgeScrolling();
310+
}
311+
}
312+
313+
/**
314+
* Handles TreeView-level drag over events for edge scrolling detection.
315+
* This catches cases where the user drags outside visible cells.
316+
*/
317+
void handleTreeViewDragOver(javafx.scene.input.DragEvent e) {
318+
if (dragActive && tree.dragSourceNode != null) {
319+
handleEdgeScrolling(e.getSceneY());
320+
}
321+
}
322+
323+
/**
324+
* Starts continuous scrolling in the specified direction.
325+
*/
326+
private void startEdgeScrolling(boolean scrollUp) {
327+
stopEdgeScrolling(); // Stop any existing scrolling
328+
329+
isScrollingUp = scrollUp;
330+
isScrollingDown = !scrollUp;
331+
332+
// Create a timer that continuously scrolls
333+
edgeScrollTimer = new Timeline(new KeyFrame(Duration.millis(50), e -> {
334+
if (vbar != null && dragActive && (isScrollingUp || isScrollingDown)) {
335+
double currentValue = vbar.getValue();
336+
double newValue;
337+
338+
if (scrollUp) {
339+
newValue = Math.max(0, currentValue - SCROLL_SPEED);
340+
} else {
341+
newValue = Math.min(1, currentValue + SCROLL_SPEED);
342+
}
343+
344+
if (Math.abs(newValue - currentValue) > 0.001) {
345+
vbar.setValue(newValue);
346+
// Update locked position since this is user-initiated scrolling
347+
lockedV = newValue;
348+
} else {
349+
// No scroll applied: difference too small
350+
}
351+
} else {
352+
// Edge scroll timer skipped: conditions not met
353+
}
354+
}));
355+
edgeScrollTimer.setCycleCount(Timeline.INDEFINITE);
356+
edgeScrollTimer.play();
357+
}
358+
359+
/**
360+
* Stops edge scrolling.
361+
*/
362+
private void stopEdgeScrolling() {
363+
if (edgeScrollTimer != null) {
364+
edgeScrollTimer.stop();
365+
edgeScrollTimer = null;
366+
}
367+
368+
isScrollingUp = false;
369+
isScrollingDown = false;
370+
}
227371
}

0 commit comments

Comments
 (0)