简体   繁体   English

如何在Linux上拖放错误期间实现autoscroll的变通方法?

[英]How can I implement a workaround for the autoscroll during drag and drop bug on Linux?

I have a list with many elements inside a scroll pane and I've implemented drag and drop on the list. 我在滚动窗格中有一个包含许多元素的列表,我在列表中实现了拖放操作。 When I select an item from the list and drag it to the bottom of the list, the list should automatically scroll down as long as I keep the mouse close to the edge. 当我从列表中选择一个项目并将其拖动到列表的底部时,只要我将鼠标靠近边缘,列表就会自动向下滚动。 This works ok on Windows, but on Linux the list scrolls one element and then stops. 这在Windows上运行正常,但在Linux上列表会滚动一个元素然后停止。

Here is a short program which reveals this bug: 这是一个简短的程序,揭示了这个bug:

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;

import javax.swing.DropMode;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.WindowConstants;


public class JListAutoscroll {

    protected static Container createUI() {
        JList<String> jlist = new JList<>(generateData(100));
        setDragAndDrop(jlist);
        JScrollPane scrollPane = new JScrollPane(jlist);
        JPanel panel = new JPanel(new BorderLayout());
        panel.add(scrollPane, BorderLayout.CENTER);
        return panel;
    }

    private static void setDragAndDrop(JList<String> jlist) {
        jlist.setDragEnabled(true);
        jlist.setDropMode(DropMode.INSERT);
        jlist.setTransferHandler(new ListTransferHandler());
    }

    private static String[] generateData(int nRows) {
        String rows[] = new String[nRows];
        for (int i = 0; i < rows.length; i++) {
            rows[i] = "element " + i;
        }
        return rows;
    }

    private static class ListTransferHandler extends TransferHandler {

        @Override
        public int getSourceActions(JComponent component) {
            return COPY_OR_MOVE;
        }

        @Override
        protected Transferable createTransferable(JComponent component) {
            return new ListItemTransferable((JList)component);
        }

        @Override
        public boolean canImport(TransferHandler.TransferSupport support) {
            return true;
        }

        @Override
        public boolean importData(TransferHandler.TransferSupport support) {
            return true;
        }
    }

    private static class ListItemTransferable implements Transferable {

        private String item;

        public ListItemTransferable(JList<String> jlist) {
            item = jlist.getSelectedValue();
        }

        @Override
        public DataFlavor[] getTransferDataFlavors() {
            return new DataFlavor[] { DataFlavor.stringFlavor };
        }

        @Override
        public boolean isDataFlavorSupported(DataFlavor flavor) {
            return flavor.equals(DataFlavor.stringFlavor);
        }

        @Override
        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
            if(!isDataFlavorSupported(flavor)) {
                throw new UnsupportedFlavorException(flavor);
            }
            return item;
        }

    }

    public static void main(String args[]) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame frame = new JFrame("JList Autoscroll");
                frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
                frame.setContentPane(createUI());
                frame.setPreferredSize(new Dimension(400, 600));
                frame.pack();
                frame.setVisible(true);
            }

        });
    }

}

I've implemented a simple TransferHandler, which does nothing on drop, but is enough to show the problem while dragging to the edge of the list. 我已经实现了一个简单的TransferHandler,它在拖放时什么都不做,但足以在拖动到列表边缘时显示问题。

It seems this is a known bug in the JDK, which is best described in this report . 这似乎是JDK中的已知错误, 本报告对此进行了最佳描述。 I've seen some suggested workarounds, like this one or this one , but it's not clear to me how I can implement them. 我已经看到了一些建议的解决方法,比如这个或者这个 ,但是我不清楚如何实现它们。 It looks to me like I have to create a DropTarget subclass and the component that I use with it should implement the Autoscroll interface. 在我看来,我必须创建一个DropTarget子类,我使用它的组件应该实现Autoscroll接口。 But JList does not implement it! 但是JList没有实现它! Also, if I set a DropTarget on the list, instead of the TransferHandler, won't I lose all the default drag and drop behaviour implemented by the TransferHandler? 另外,如果我在列表上设置DropTarget而不是TransferHandler,我是否会失去TransferHandler实现的所有默认拖放行为?

So how can I modify my program to workaround this bug? 那么如何修改我的程序来解决这个bug呢?

As mentioned in the bug description , there are two classes that handle drag and drop: 错误描述中所述,有两个类可以处理拖放:

  • DropTargetAutoScroller , a member class of java.awt.dnd.DropTarget , responsible of supporting components implementing the Autoscroll interface; DropTargetAutoScrollerjava.awt.dnd.DropTarget的成员类,负责支持实现Autoscroll接口的组件;
  • DropHandler , a member class of javax.swing.TransferHandler , that automates d&d autoscrolling on components implementing the Scrollable interface. DropHandlerjavax.swing.TransferHandler的成员类,可自动对实现Scrollable接口的组件进行d&d自动滚动。

So, indeed, the workaround is not suitable for JList , which implements Scrollable and not Autoscroll . 实际上,解决方法不适用于JList ,它实现了Scrollable而不是Autoscroll But, if you look in the source code for DropTarget and TransferHandler , you'll notice that the autoscroll code is basically the same, and in both cases wrong. 但是,如果您查看DropTargetTransferHandler的源代码,您会注意到自动滚动代码基本相同,并且在两种情况下都是错误的。 The workaround is also very similar to the DropTarget code, with only a few lines added. 解决方法也与DropTarget代码非常相似,只添加了几行。 Basically, the solution is to convert the location of the mouse cursor from the component coordinate system to the screen coordinate system. 基本上,解决方案是将鼠标光标的位置从组件坐标系转换为屏幕坐标系。 That way, when checking whether the mouse has moved, absolute coordinates are used. 这样,当检查鼠标是否移动时,使用绝对坐标。 So we can copy the code from TransferHandler instead and add these few lines. 因此,我们可以从TransferHandler复制代码并添加这几行。

That's great... but where do we put this code and how do we get it called? 那很好......但是我们把这些代码放在哪里?我们如何调用它?

If we look in setTransferHandler() we see that it actually sets a DropTarget , which is a package-private static class called SwingDropTarget from the TransferHandler class. 如果我们查看setTransferHandler()我们会看到它实际上设置了一个DropTarget ,它是一个来自TransferHandler类的名为SwingDropTarget包私有静态类。 It delegates drag and drop events to a private static DropTargetListener called DropHandler . 它将拖放事件委托给名为DropHandler私有静态 DropTargetListener This class does all of the magic that happens during drag and drop, and of course it uses other private methods from TransferHandler . 这个类完成了拖放过程中发生的所有魔法,当然它还使用了TransferHandler其他私有方法。 This means we can't just set our own DropTarget without losing everything already implemented in TransferHandler . 这意味着我们不能只设置我们自己的DropTarget而不会丢失已在TransferHandler实现的所有内容。 We could rewrite TransferHandler (about 1800 lines) with our few lines added to fix the bug, but that's not very realistic. 我们可以用添加的几行来重写TransferHandler (大约1800行)来修复bug,但这不太现实。

A simpler solution is to write a DropTargetListener , in which we simply copy the autoscroll-related code from DropHandler (which also implements this interface), with our lines added. 一个更简单的解决方案是编写一个DropTargetListener ,我们只需从DropHandler (也实现此接口)复制与DropHandler相关的代码,并添加我们的行。 This is the class: 这是班级:

import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.TooManyListenersException;

import javax.swing.JComponent;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;


public class AutoscrollWorkaround implements DropTargetListener, ActionListener {

    private JComponent component;

    private Point lastPosition;

    private Rectangle outer;
    private Rectangle inner;

    private Timer timer;
    private int hysteresis = 10;

    private static final int AUTOSCROLL_INSET = 10;

    public AutoscrollWorkaround(JComponent component) {
        if (!(component instanceof Scrollable)) {
            throw new IllegalArgumentException("Component must be Scrollable for autoscroll to work!");
        }
        this.component = component;
        outer = new Rectangle();
        inner = new Rectangle();

        Toolkit t = Toolkit.getDefaultToolkit();
        Integer prop;

        prop = (Integer)t.getDesktopProperty("DnD.Autoscroll.interval");
        timer = new Timer(prop == null ? 100 : prop.intValue(), this);

        prop = (Integer)t.getDesktopProperty("DnD.Autoscroll.initialDelay");
        timer.setInitialDelay(prop == null ? 100 : prop.intValue());

        prop = (Integer)t.getDesktopProperty("DnD.Autoscroll.cursorHysteresis");
        if (prop != null) {
            hysteresis = prop.intValue();
        }
    }

    @Override
    public void dragEnter(DropTargetDragEvent e) {
        lastPosition = e.getLocation();
        SwingUtilities.convertPointToScreen(lastPosition, component);
        updateRegion();
    }

    @Override
    public void dragOver(DropTargetDragEvent e) {
        Point p = e.getLocation();
        SwingUtilities.convertPointToScreen(p, component);

        if (Math.abs(p.x - lastPosition.x) > hysteresis
                || Math.abs(p.y - lastPosition.y) > hysteresis) {
            // no autoscroll
            if (timer.isRunning()) timer.stop();
        } else {
            if (!timer.isRunning()) timer.start();
        }

        lastPosition = p;
    }

    @Override
    public void dragExit(DropTargetEvent dte) {
        cleanup();
    }

    @Override
    public void drop(DropTargetDropEvent dtde) {
        cleanup();
    }

    @Override
    public void dropActionChanged(DropTargetDragEvent e) {
    }

    private void updateRegion() {
        // compute the outer
        Rectangle visible = component.getVisibleRect();
        outer.setBounds(visible.x, visible.y, visible.width, visible.height);

        // compute the insets
        Insets i = new Insets(0, 0, 0, 0);
        if (component instanceof Scrollable) {
            int minSize = 2 * AUTOSCROLL_INSET;

            if (visible.width >= minSize) {
                i.left = i.right = AUTOSCROLL_INSET;
            }

            if (visible.height >= minSize) {
                i.top = i.bottom = AUTOSCROLL_INSET;
            }
        }

        // set the inner from the insets
        inner.setBounds(visible.x + i.left,
                      visible.y + i.top,
                      visible.width - (i.left + i.right),
                      visible.height - (i.top  + i.bottom));
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        updateRegion();
        Point componentPosition = new Point(lastPosition);
        SwingUtilities.convertPointFromScreen(componentPosition, component);
        if (outer.contains(componentPosition) && !inner.contains(componentPosition)) {
            autoscroll(componentPosition);
        }
    }

    private void autoscroll(Point position) {
        Scrollable s = (Scrollable) component;
        if (position.y < inner.y) {
            // scroll upward
            int dy = s.getScrollableUnitIncrement(outer, SwingConstants.VERTICAL, -1);
            Rectangle r = new Rectangle(inner.x, outer.y - dy, inner.width, dy);
            component.scrollRectToVisible(r);
        } else if (position.y > (inner.y + inner.height)) {
            // scroll downard
            int dy = s.getScrollableUnitIncrement(outer, SwingConstants.VERTICAL, 1);
            Rectangle r = new Rectangle(inner.x, outer.y + outer.height, inner.width, dy);
            component.scrollRectToVisible(r);
        }

        if (position.x < inner.x) {
            // scroll left
            int dx = s.getScrollableUnitIncrement(outer, SwingConstants.HORIZONTAL, -1);
            Rectangle r = new Rectangle(outer.x - dx, inner.y, dx, inner.height);
            component.scrollRectToVisible(r);
        } else if (position.x > (inner.x + inner.width)) {
            // scroll right
            int dx = s.getScrollableUnitIncrement(outer, SwingConstants.HORIZONTAL, 1);
            Rectangle r = new Rectangle(outer.x + outer.width, inner.y, dx, inner.height);
            component.scrollRectToVisible(r);
        }
    }

    private void cleanup() {
        timer.stop();
    }
}

(You'll notice that basically only the SwingUtilities.convertXYZ() calls are extra from the TransferHandler code) (您会注意到,基本上只有来自TransferHandler代码的SwingUtilities.convertXYZ()调用是额外的)

Next, we can add this listener to the DropTarget installed when setting the TransferHandler . 接下来,我们可以在设置TransferHandler时将此侦听DropTarget添加到已安装的DropTarget (Note that a regular DropTarget only accepts one listener and will throw an exception if another one is added. SwingDropTarget uses DropHandler , but fortunately it also adds support for other listeners as well) (请注意,常规DropTarget只接受一个侦听器,并且如果添加另一个侦听器将抛出异常SwingDropTarget使用DropHandler ,但幸运的是它还增加了对其他侦听器的支持)

So let's just add this static factory method to the AutoscrollWorkaround class, which does this for us: 所以,让我们将这个静态工厂方法添加到AutoscrollWorkaround类,它为我们这样做:

    public static void applyTo(JComponent component) {
        if (component.getTransferHandler() == null) {
            throw new IllegalStateException("A TransferHandler must be set before calling this method!");
        }
        try {
            component.getDropTarget().addDropTargetListener(new AutoscrollWorkaround(component));
        } catch (TooManyListenersException e) {
            throw new IllegalStateException("Something went wrong! DropTarget should have been " +
                    "SwingDropTarget which accepts multiple listeners", e);
        }
    }

This provides an easy and very convenient way to apply the workaround to any component that suffers from this bug, by only calling this one method. 通过仅调用这一方法,这提供了一种简单且非常方便的方法,将变通方法应用于任何遭受此错误的组件。 Just make sure to call it after having setTransferHandler() on the component. 只需确保在组件上使用setTransferHandler()之后调用它。 So, we only have to add one line to the original program: 所以,我们只需要在原始程序中添加一行:

private static void setDragAndDrop(JList<String> jlist) {
    jlist.setDragEnabled(true);
    jlist.setDropMode(DropMode.INSERT);
    jlist.setTransferHandler(new ListTransferHandler());
    AutoscrollWorkaround.applyTo(jlist); // <--- just this line added
}

The autoscroll now works OK on both Windows and Linux. 现在,autoscroll在Windows和Linux上运行正常。 (Although on Linux the line for the drop location is not repainted until autoscroll works, but oh well.) (虽然在Linux上,在自动滚动工作之前,不会重新绘制掉落位置的行,但是很好。)

This workaround should work also for JTable (I tested), JTree and probably any components that implement Scrollable . 此解决方法也适用于JTable (我测试过), JTree以及可能实现Scrollable任何组件。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM