Как я могу реализовать обходной путь для ошибки автопрокрутки во время перетаскивания в Linux?

У меня есть список со многими элементами внутри панели прокрутки, и я реализовал перетаскивание в списке. Когда я выбираю элемент из списка и перетаскиваю его в конец списка, список должен автоматически прокручиваться вниз, пока я держу мышь близко к краю. Это нормально работает в Windows, но в Linux список прокручивается на один элемент, а затем останавливается.

Вот короткая программа, которая выявляет эту ошибку:

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);
            }

        });
    }

}

Я реализовал простой TransferHandler, который ничего не делает при перетаскивании, но его достаточно, чтобы показать проблему при перетаскивании к краю списка.

Кажется, это известная ошибка в JDK, которая лучше всего описана в этом отчете. Я видел некоторые предлагаемые обходные пути, такие как этот или этот, но мне непонятно, как я могу их реализовать. Мне кажется, что мне нужно создать подкласс DropTarget, и компонент, который я использую с ним, должен реализовывать интерфейс Autoscroll. Но JList этого не реализует! Кроме того, если я установлю DropTarget в списке вместо TransferHandler, не потеряю ли я все поведение перетаскивания по умолчанию, реализованное TransferHandler?

Итак, как я могу изменить свою программу, чтобы обойти эту ошибку?


person Andrei Vajna II    schedule 18.03.2015    source источник


Ответы (1)


Как упоминалось в описании ошибки, существует два класса, которые обрабатывают перетаскивание:

  • DropTargetAutoScroller, класс-член класса java.awt.dnd.DropTarget, отвечающий за поддержку компонентов, реализующих интерфейс Autoscroll;
  • DropHandler, член класса javax.swing.TransferHandler, который автоматизирует автоматическую прокрутку d&d для компонентов, реализующих интерфейс Scrollable.

Так что, действительно, обходной путь не подходит для JList, реализующего Scrollable, а не Autoscroll. Но если вы посмотрите в исходный код для DropTarget и TransferHandler, вы заметите, что код автопрокрутки в основном одинаков и в обоих случаях неверен. Обходной путь также очень похож на код DropTarget, только с добавлением нескольких строк. По сути, решение состоит в том, чтобы преобразовать положение курсора мыши из системы координат компонента в систему координат экрана. Таким образом, при проверке того, двигалась ли мышь, используются абсолютные координаты. Поэтому мы можем скопировать код из TransferHandler и добавить эти несколько строк.

Это здорово... но куда мы поместим этот код и как его вызвать?

Если мы посмотрим на setTransferHandler(), то увидим, что он на самом деле устанавливает DropTarget, который является приватным статическим классом с именем SwingDropTarget из класса TransferHandler. Он делегирует события перетаскивания частному статическому элементу DropTargetListener с именем DropHandler. Этот класс делает всю магию, которая происходит во время перетаскивания, и, конечно же, использует другие закрытые методы из TransferHandler. Это означает, что мы не можем просто установить собственный DropTarget, не потеряв при этом все, что уже реализовано в TransferHandler. Мы могли бы переписать TransferHandler (около 1800 строк), добавив наши несколько строк, чтобы исправить ошибку, но это не очень реалистично.

Более простое решение — написать DropTargetListener, в котором мы просто скопируем код, связанный с автопрокруткой, из DropHandler (который также реализует этот интерфейс) с добавлением наших строк. Это класс:

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();
    }
}

(Вы заметите, что в основном только вызовы SwingUtilities.convertXYZ() являются дополнительными из кода TransferHandler)

Затем мы можем добавить этот слушатель к DropTarget, установленному при настройке файла TransferHandler. (Обратите внимание, что обычный DropTarget принимает только одного слушателя и выдает исключение, если добавляется другой. SwingDropTarget использует DropHandler, но, к счастью, он также добавляет поддержку других слушателей)

Итак, давайте просто добавим этот статический фабричный метод в класс 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);
        }
    }

Это обеспечивает простой и очень удобный способ применить обходной путь к любому компоненту, который страдает от этой ошибки, путем вызова только одного этого метода. Просто не забудьте вызвать его после того, как у компонента будет setTransferHandler(). Итак, нам нужно добавить только одну строку в исходную программу:

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
}

Автопрокрутка теперь работает нормально как в Windows, так и в Linux. (Хотя в Linux строка места перетаскивания не перерисовывается, пока не сработает автопрокрутка, ну да ладно.)

Этот обходной путь должен работать также для JTable (я тестировал), JTree и, возможно, любых компонентов, реализующих Scrollable.

person Andrei Vajna II    schedule 18.03.2015