|
| 1 | +/* |
| 2 | + * Copyright (C) 2020-2021 DiffPlug, LLC - All Rights Reserved |
| 3 | + * Unauthorized copying of this file via any medium is strictly prohibited. |
| 4 | + * Proprietary and confidential. |
| 5 | + * Please send any inquiries to Ned Twigg <[email protected]> |
| 6 | + */ |
| 7 | +package com.diffplug.common.swt; |
| 8 | + |
| 9 | +import java.util.Optional; |
| 10 | + |
| 11 | +import org.eclipse.swt.SWT; |
| 12 | +import org.eclipse.swt.custom.ScrolledComposite; |
| 13 | +import org.eclipse.swt.graphics.Point; |
| 14 | +import org.eclipse.swt.graphics.Rectangle; |
| 15 | +import org.eclipse.swt.widgets.Composite; |
| 16 | +import org.eclipse.swt.widgets.Display; |
| 17 | +import org.eclipse.swt.widgets.Event; |
| 18 | +import org.eclipse.swt.widgets.ScrollBar; |
| 19 | +import org.eclipse.swt.widgets.Scrollable; |
| 20 | +import org.eclipse.swt.widgets.Tree; |
| 21 | +import org.eclipse.swt.widgets.TreeItem; |
| 22 | + |
| 23 | +import com.diffplug.common.swt.SwtMisc; |
| 24 | +import com.diffplug.common.tree.TreeStream; |
| 25 | + |
| 26 | +/** |
| 27 | + * Bubbles vertical scroll events up to a parent container, so that you don't get stuck. |
| 28 | + * Doesn't work great on mac due to rubber-banding. |
| 29 | + */ |
| 30 | +public class VScrollBubble { |
| 31 | + private static Display appliedTo; |
| 32 | + |
| 33 | + /** Returns true iff the given scrollable is maxed out for scrolling in the direction of the given event. */ |
| 34 | + private static boolean isMaxed(Event e, Scrollable scrollable) { |
| 35 | + ScrollBar scrollBar = scrollable.getVerticalBar(); |
| 36 | + if (scrollBar == null) { |
| 37 | + return true; |
| 38 | + } else { |
| 39 | + boolean isMaxed; |
| 40 | + // System.out.println("[" + scrollBar.getMinimum() + " " + scrollBar.getMaximum() + "] " + scrollBar.getSelection() + " thumb=" + scrollBar.getThumb()); |
| 41 | + if (e.count > 0) { |
| 42 | + // scrolling up |
| 43 | + isMaxed = scrollBar.getSelection() <= 0; |
| 44 | + } else { |
| 45 | + // scrolling down |
| 46 | + isMaxed = scrollBar.getSelection() >= scrollBar.getMaximum() - scrollBar.getThumb(); |
| 47 | + if (!isMaxed) { |
| 48 | + // special case for trees & tables which have all their items visible, where scrolling down gives |
| 49 | + // scrollBar.getMinimum()/getMaximum=[0 100] scrollBar.getSelection()=0 scrollBar.getThumb()=10 |
| 50 | + if (scrollable instanceof Tree) { |
| 51 | + Tree tree = (Tree) scrollable; |
| 52 | + int itemCount = tree.getItemCount(); |
| 53 | + if (itemCount == 0) { |
| 54 | + return true; |
| 55 | + } |
| 56 | + TreeItem lastItem = tree.getItem(itemCount - 1); |
| 57 | + while (lastItem.getItemCount() > 0) { |
| 58 | + lastItem = lastItem.getItem(lastItem.getItemCount() - 1); |
| 59 | + } |
| 60 | + Rectangle itemBounds = lastItem.getBounds(); |
| 61 | + Rectangle clientArea = tree.getClientArea(); |
| 62 | + return itemBounds.y + itemBounds.height < clientArea.height; |
| 63 | + } |
| 64 | + } |
| 65 | + } |
| 66 | + return isMaxed; |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + /** Applies the necessary display filter, don't worry about calling it more than once. */ |
| 71 | + public static void applyTo(Display display) { |
| 72 | + // prevent double-apply |
| 73 | + if (display == appliedTo) { |
| 74 | + return; |
| 75 | + } |
| 76 | + appliedTo = display; |
| 77 | + display.addFilter(SWT.MouseVerticalWheel, e -> { |
| 78 | + if (!(e.widget instanceof Scrollable)) { |
| 79 | + return; |
| 80 | + } |
| 81 | + // find the parent two levels up |
| 82 | + Composite parentOfScrollable = ((Scrollable) e.widget).getParent(); |
| 83 | + if (parentOfScrollable == null) { |
| 84 | + return; |
| 85 | + } |
| 86 | + parentOfScrollable = parentOfScrollable.getParent(); |
| 87 | + if (parentOfScrollable == null) { |
| 88 | + return; |
| 89 | + } |
| 90 | + |
| 91 | + boolean isMaxed = isMaxed(e, (Scrollable) e.widget); |
| 92 | + if (!isMaxed) { |
| 93 | + return; |
| 94 | + } |
| 95 | + |
| 96 | + Optional<ScrolledComposite> firstNotMaxed = TreeStream.toParent(SwtMisc.treeDefComposite(), parentOfScrollable) |
| 97 | + .filter(c -> c instanceof ScrolledComposite && SwtMisc.flagIsSet(SWT.V_SCROLL, c)) |
| 98 | + .map(ScrolledComposite.class::cast) |
| 99 | + .filter(sc -> !isMaxed(e, sc)) |
| 100 | + .findFirst(); |
| 101 | + if (!firstNotMaxed.isPresent()) { |
| 102 | + // there isn't a parent which we can bubble the scroll to |
| 103 | + return; |
| 104 | + } |
| 105 | + ScrolledComposite toScroll = firstNotMaxed.get(); |
| 106 | + Point origin = toScroll.getOrigin(); |
| 107 | + origin.y -= e.count * toScroll.getVerticalBar().getIncrement(); |
| 108 | + toScroll.setOrigin(origin); |
| 109 | + |
| 110 | + // copy the event |
| 111 | + Event copy = SwtMisc.copyEvent(e); |
| 112 | + // cancel the old one |
| 113 | + e.doit = false; |
| 114 | + // and send the copy to the parent |
| 115 | + copy.widget = toScroll; |
| 116 | + Point d = ((Scrollable) e.widget).toDisplay(e.x, e.y); |
| 117 | + Point mapped = toScroll.toControl(d); |
| 118 | + copy.x = mapped.x; |
| 119 | + copy.y = mapped.y; |
| 120 | + copy.widget.notifyListeners(SWT.MouseVerticalWheel, copy); |
| 121 | + }); |
| 122 | + } |
| 123 | +} |
0 commit comments