Ticket #15593: todo_plugin_multiselect_improvements_v1.patch

File todo_plugin_multiselect_improvements_v1.patch, 15.3 KB (added by Woazboat, 4 years ago)

Multiple item selection, select + zoom on enter key pressed, highlighting of primitives on click, automatic update of list display on data updates

  • plugins/todo/src/org/openstreetmap/josm/plugins/todo/TodoDialog.java

     
    2020import javax.swing.JList;
    2121import javax.swing.ListSelectionModel;
    2222import javax.swing.SwingUtilities;
     23import javax.swing.event.ListDataEvent;
     24import javax.swing.event.ListDataListener;
    2325import javax.swing.event.ListSelectionEvent;
    2426import javax.swing.event.ListSelectionListener;
    2527
     
    2729import org.openstreetmap.josm.actions.AutoScaleAction.AutoScaleMode;
    2830import org.openstreetmap.josm.data.osm.DataSelectionListener;
    2931import org.openstreetmap.josm.data.osm.OsmPrimitive;
     32import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
    3033import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
     34import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
    3135import org.openstreetmap.josm.gui.MainApplication;
    3236import org.openstreetmap.josm.gui.SideButton;
    3337import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
     
    3539import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
    3640import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
    3741import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
     42import org.openstreetmap.josm.gui.util.HighlightHelper;
    3843import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    3944import org.openstreetmap.josm.gui.widgets.ListPopupMenu;
    4045import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
     46import org.openstreetmap.josm.spi.preferences.Config;
    4147import org.openstreetmap.josm.tools.ImageProvider;
     48import org.openstreetmap.josm.tools.InputMapUtils;
    4249import org.openstreetmap.josm.tools.Shortcut;
    4350
    4451/**
     
    5259    private final TodoListModel model = new TodoListModel(selectionModel);
    5360    private final JList<TodoListItem> lstPrimitives = new JList<>(model);
    5461    private final AddAction actAdd = new AddAction(model);
     62    private final SelectAction actSelect = new SelectAction(model);
    5563    private final PassAction actPass = new PassAction(model);
    5664    private final MarkAction actMark = new MarkAction(model);
    5765    private final MarkSelectedAction actMarkSelected = new MarkSelectedAction(model);
     
    7280                        KeyEvent.VK_T, Shortcut.CTRL_SHIFT), 150);
    7381        buildContentPanel();
    7482
     83        model.addListDataListener(new TitleUpdater());
     84
    7585        MainApplication.getLayerManager().addLayerChangeListener(this);
     86        DatasetEventManager.getInstance().addDatasetListener(model, FireMode.IN_EDT);
    7687        lstPrimitives.addMouseListener(new DblClickHandler());
    7788        lstPrimitives.addMouseListener(new TodoPopupLauncher());
    7889        toggleAction.addPropertyChangeListener(this);
     90
     91        InputMapUtils.addEnterAction(lstPrimitives, actSelect);
    7992    }
    8093
    8194    /**
     
    8396     */
    8497    protected void buildContentPanel() {
    8598        lstPrimitives.setSelectionModel(selectionModel);
    86         lstPrimitives.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
     99        lstPrimitives.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
    87100        lstPrimitives.setCellRenderer(new TodoListItemRenderer());
    88101        lstPrimitives.setTransferHandler(null);
    89102
    90103        // the select action
    91         SelectAction actSelect = new SelectAction(model);
    92104        SideButton selectButton = new SideButton(actSelect);
    93105        lstPrimitives.getSelectionModel().addListSelectionListener(actSelect);
     106        actSelect.updateEnabledState();
    94107
    95108        // the add button
    96109        SideButton addButton = new SideButton(actAdd);
     
    147160            }
    148161            layer.data.setSelected(items);
    149162        }
    150         zoom(layer);
     163        if (!layer.data.selectionEmpty()) {
     164            zoom(layer);
     165        }
    151166    }
    152167
    153168    protected void updateTitle() {
     
    161176        actAdd.updateEnabledState();
    162177    }
    163178
     179    @Override
     180    public void hideNotify() {
     181        SelectionEventManager.getInstance().removeSelectionListener(actAdd);
     182        SelectionEventManager.getInstance().removeSelectionListener(actMarkSelected);
     183    }
     184
    164185    protected Collection<TodoListItem> getItems() {
    165186        OsmDataLayer layer = MainApplication.getLayerManager().getEditLayer();
    166187        if (layer == null)
     
    186207        }
    187208
    188209        public void updateEnabledState() {
    189             setEnabled(model.getSelected() != null);
     210            setEnabled(!model.isSelectionEmpty());
    190211        }
    191212
    192213        @Override
     
    262283        @Override
    263284        public void actionPerformed(ActionEvent e) {
    264285            model.addItems(getItems());
    265             updateTitle();
    266286        }
    267287
    268288        public void updateEnabledState() {
     
    293313        @Override
    294314        public void actionPerformed(ActionEvent e) {
    295315            model.markItems(getItems());
    296             updateTitle();
    297316        }
    298317
    299318        public void updateEnabledState() {
     
    310329        }
    311330    }
    312331
    313     private class MarkAction extends AbstractAction implements ListSelectionListener {
     332    private static class MarkAction extends AbstractAction implements ListSelectionListener {
    314333
    315334        TodoListModel model;
    316335
     
    324343
    325344        @Override
    326345        public void actionPerformed(ActionEvent arg0) {
    327             model.markSelected();
     346            model.markItems(model.getSelected());
    328347            selectAndZoom(model.getSelected());
    329             updateTitle();
    330348        }
    331349
    332350        public void updateEnabledState() {
    333             setEnabled(model.getSelected() != null);
     351            setEnabled(!model.isSelectionEmpty());
    334352        }
    335353
    336354        @Override
     
    339357        }
    340358    }
    341359
    342     private class MarkAllAction extends AbstractAction {
     360    private static class MarkAllAction extends AbstractAction {
    343361
    344362        TodoListModel model;
    345363
     
    354372        public void actionPerformed(ActionEvent arg0) {
    355373            model.markAll();
    356374            selectAndZoom(model.getSelected());
    357             updateTitle();
    358375        }
    359 
    360376    }
    361377
    362     private class UnmarkAllAction extends AbstractAction {
     378    private static class UnmarkAllAction extends AbstractAction {
    363379
    364380        TodoListModel model;
    365381
     
    373389        @Override
    374390        public void actionPerformed(ActionEvent arg0) {
    375391            model.unmarkAll();
    376             updateTitle();
    377392        }
    378393    }
    379394
    380     private class ClearAction extends AbstractAction {
     395    private static class ClearAction extends AbstractAction {
    381396
    382397        TodoListModel model;
    383398
     
    391406        @Override
    392407        public void actionPerformed(ActionEvent arg0) {
    393408            model.clear();
    394             updateTitle();
    395409        }
    396410    }
    397411
     
    411425     * The popup menu launcher.
    412426     */
    413427    class TodoPopupLauncher extends PopupMenuLauncher {
     428        private final HighlightHelper helper = new HighlightHelper();
     429        private final boolean highlightEnabled = Config.getPref().getBoolean("draw.target-highlight", true);
     430       
     431        TodoPopupLauncher() {
     432            super(popupMenu);
     433        }
     434
    414435        @Override
    415         public void launch(MouseEvent evt) {
    416             int idx = lstPrimitives.locationToIndex(evt.getPoint());
    417             if (idx >= 0)
    418                 model.setSelected(model.getElementAt(idx));
     436        public void mouseClicked(MouseEvent e) {
     437            int idx = lstPrimitives.locationToIndex(e.getPoint());
     438            if (idx < 0) return;
     439            if (highlightEnabled && MainApplication.isDisplayingMapView() &&
     440                       helper.highlightOnly(model.getElementAt(idx).primitive)) {
     441                MainApplication.getMap().mapView.repaint();
     442            }
     443        }
    419444
    420             popupMenu.show(lstPrimitives, evt.getX(), evt.getY());
     445        @Override
     446        public void mouseExited(MouseEvent me) {
     447            if (highlightEnabled) helper.clear();
     448            super.mouseExited(me);
    421449        }
    422450    }
    423451
     
    439467        }
    440468    }
    441469
     470    /**
     471     * Updates the dialog title with a summary of the current todo list status
     472     */
     473    class TitleUpdater implements ListDataListener {
     474        @Override
     475        public void contentsChanged(ListDataEvent e) {
     476            updateTitle();
     477        }
     478
     479        @Override
     480        public void intervalAdded(ListDataEvent e) {
     481            updateTitle();
     482        }
     483
     484        @Override
     485        public void intervalRemoved(ListDataEvent e) {
     486            updateTitle();
     487        }
     488    }
     489
    442490    @Override
    443491    public void propertyChange(PropertyChangeEvent arg0) {
    444492        actAdd.updateEnabledState();
     
    448496    public void destroy() {
    449497        super.destroy();
    450498        MainApplication.getLayerManager().removeLayerChangeListener(this);
     499        DatasetEventManager.getInstance().removeDatasetListener(model);
    451500        MainApplication.unregisterActionShortcut(actPass, sctPass);
    452501        MainApplication.unregisterActionShortcut(actMark, sctMark);
    453502    }
     
    460509    @Override
    461510    public void layerRemoving(LayerRemoveEvent e) {
    462511        if (e.getRemovedLayer() instanceof OsmDataLayer) {
    463             if (model.purgeLayerItems((OsmDataLayer) e.getRemovedLayer())) {
    464                 updateTitle();
    465             }
     512            model.purgeLayerItems((OsmDataLayer) e.getRemovedLayer());
    466513        }
    467514    }
    468515
  • plugins/todo/src/org/openstreetmap/josm/plugins/todo/TodoListModel.java

     
    66import java.util.ArrayList;
    77import java.util.Collection;
    88import java.util.List;
     9import java.util.stream.Collectors;
     10import java.util.stream.IntStream;
    911
    1012import javax.swing.AbstractListModel;
    1113import javax.swing.DefaultListSelectionModel;
    1214
     15import org.openstreetmap.josm.data.osm.OsmPrimitive;
     16import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
     17import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
     18import org.openstreetmap.josm.data.osm.event.DataSetListener;
     19import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
     20import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
     21import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
     22import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
     23import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
     24import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
    1325import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     26import org.openstreetmap.josm.gui.util.TableHelper;
    1427
    15 public class TodoListModel extends AbstractListModel<TodoListItem> {
     28/**
     29 * The list model for the todo list items.
     30 *
     31 * The model also maintains a list of already completed items
     32 *
     33 */
     34public class TodoListModel extends AbstractListModel<TodoListItem> implements DataSetListener {
    1635
    1736    private final ArrayList<TodoListItem> todoList = new ArrayList<>();
    1837    private final ArrayList<TodoListItem> doneList = new ArrayList<>();
     
    3251        return todoList.size();
    3352    }
    3453
     54    public boolean isSelectionEmpty() {
     55        return selectionModel.isSelectionEmpty();
     56    }
     57
    3558    public int getDoneSize() {
    3659        return doneList.size();
    3760    }
    3861
    39     public TodoListItem getSelected() {
    40         if (getSize() == 0 || selectionModel.isSelectionEmpty() || selectionModel.getMinSelectionIndex() >= getSize())
    41             return null;
    42         return todoList.get(selectionModel.getMinSelectionIndex());
     62    public synchronized Collection<TodoListItem> getSelected() {
     63        return IntStream.range(0, getSize())
     64                .filter(selectionModel::isSelectedIndex)
     65                .mapToObj(todoList::get)
     66                .collect(Collectors.toSet());
    4367    }
    4468
     69    public Collection<TodoListItem> getItemsForPrimitives(Collection<? extends OsmPrimitive> primitives) {
     70        return todoList.stream().filter(i -> primitives.contains(i.primitive))
     71            .collect(Collectors.toList());
     72    }
     73
    4574    public List<TodoListItem> getTodoList() {
    4675        return todoList;
    4776    }
     
    104133        selectionModel.setSelectionInterval(sel, sel);
    105134    }
    106135
    107     public void setSelected(TodoListItem element) {
    108         int sel = todoList.indexOf(element);
    109         if (sel == -1)
    110             return;
    111         selectionModel.setSelectionInterval(sel, sel);
     136    public synchronized void setSelected(Collection<TodoListItem> sel) {
     137        TableHelper.setSelectedIndices(selectionModel,
     138                sel != null ? sel.stream().mapToInt(todoList::indexOf) : IntStream.empty());
    112139    }
    113140
    114141    public void markAll() {
     
    120147        super.fireIntervalRemoved(this, 0, size-1);
    121148    }
    122149
     150    public void removeItems(Collection<TodoListItem> items) {
     151        if (items == null || items.isEmpty())
     152            return;
     153
     154        int size = getSize();
     155
     156        todoList.removeAll(items);
     157        doneList.removeAll(items);
     158
     159        super.fireIntervalRemoved(this, 0, size-1);
     160    }
     161
    123162    public void markItems(Collection<TodoListItem> items) {
    124163        if (items == null || items.isEmpty())
    125164            return;
     
    175214        else return tr("Todo list {0}/{1} ({2}%)", getDoneSize(), totalSize, 100.0*getDoneSize()/totalSize);
    176215    }
    177216
     217    /**
     218     * Triggers a refresh of the view for all items in {@code toUpdate}
     219     * which are currently displayed in the view
     220     *
     221     * @param toUpdate the collection of items to update
     222     */
     223    public synchronized void update(Collection<? extends TodoListItem> toUpdate) {
     224        if (toUpdate == null) return;
     225        if (toUpdate.isEmpty()) return;
     226        Collection<TodoListItem> sel = getSelected();
     227        for (TodoListItem p: toUpdate) {
     228            int i = todoList.indexOf(p);
     229            if (i >= 0) {
     230                super.fireContentsChanged(this, i, i);
     231            }
     232        }
     233        setSelected(sel);
     234    }
     235
     236    @Override
     237    public void primitivesAdded(PrimitivesAddedEvent event) {
     238        // ignored
     239    }
     240
     241    @Override
     242    public void primitivesRemoved(PrimitivesRemovedEvent event) {
     243        removeItems(getItemsForPrimitives(event.getPrimitives()));
     244    }
     245
     246    @Override
     247    public void tagsChanged(TagsChangedEvent event) {
     248        update(getItemsForPrimitives(event.getPrimitives()));
     249    }
     250
     251    @Override
     252    public void nodeMoved(NodeMovedEvent event) {
     253        update(getItemsForPrimitives(event.getPrimitives()));
     254    }
     255
     256    @Override
     257    public void wayNodesChanged(WayNodesChangedEvent event) {
     258        update(getItemsForPrimitives(event.getPrimitives()));
     259    }
     260
     261    @Override
     262    public void relationMembersChanged(RelationMembersChangedEvent event) {
     263        update(getItemsForPrimitives(event.getPrimitives()));
     264    }
     265
     266    @Override
     267    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
     268        update(getItemsForPrimitives(event.getPrimitives()));
     269    }
     270
     271    @Override
     272    public void dataChanged(DataChangedEvent event) {
     273        update(getItemsForPrimitives(event.getPrimitives()));
     274        for (AbstractDatasetChangedEvent e : event.getEvents()) {
     275            if (e instanceof PrimitivesRemovedEvent) {
     276                primitivesRemoved((PrimitivesRemovedEvent) e);
     277            }
     278        }
     279    }
    178280}