001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Component; 007import java.awt.Graphics2D; 008import java.awt.event.ActionEvent; 009import java.awt.event.KeyEvent; 010import java.awt.event.MouseEvent; 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.List; 014import java.util.stream.Collectors; 015 016import javax.swing.AbstractAction; 017import javax.swing.DefaultCellEditor; 018import javax.swing.DefaultListSelectionModel; 019import javax.swing.JCheckBox; 020import javax.swing.JTable; 021import javax.swing.ListSelectionModel; 022import javax.swing.SwingUtilities; 023import javax.swing.table.DefaultTableCellRenderer; 024import javax.swing.table.JTableHeader; 025import javax.swing.table.TableCellRenderer; 026import javax.swing.table.TableColumnModel; 027import javax.swing.table.TableModel; 028 029import org.openstreetmap.josm.actions.mapmode.MapMode; 030import org.openstreetmap.josm.actions.search.SearchAction; 031import org.openstreetmap.josm.data.osm.Filter; 032import org.openstreetmap.josm.data.osm.FilterModel; 033import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 034import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent.DatasetEventType; 035import org.openstreetmap.josm.data.osm.event.DataChangedEvent; 036import org.openstreetmap.josm.data.osm.event.DataSetListener; 037import org.openstreetmap.josm.data.osm.event.DatasetEventManager; 038import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode; 039import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 040import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; 041import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; 042import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; 043import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; 044import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 045import org.openstreetmap.josm.data.osm.search.SearchSetting; 046import org.openstreetmap.josm.gui.MainApplication; 047import org.openstreetmap.josm.gui.MapFrame; 048import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener; 049import org.openstreetmap.josm.gui.SideButton; 050import org.openstreetmap.josm.gui.util.MultikeyActionsHandler; 051import org.openstreetmap.josm.gui.util.MultikeyShortcutAction; 052import org.openstreetmap.josm.gui.util.TableHelper; 053import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField; 054import org.openstreetmap.josm.tools.ImageProvider; 055import org.openstreetmap.josm.tools.InputMapUtils; 056import org.openstreetmap.josm.tools.Shortcut; 057 058/** 059 * The filter dialog displays a list of filters that are active on the current edit layer. 060 * 061 * @author Petr_DlouhĂ˝ 062 */ 063public class FilterDialog extends ToggleDialog implements DataSetListener, MapModeChangeListener { 064 065 private JTable userTable; 066 private final FilterTableModel filterModel = new FilterTableModel(new DefaultListSelectionModel()); 067 068 private final AddAction addAction = new AddAction(); 069 private final EditAction editAction = new EditAction(); 070 private final DeleteAction deleteAction = new DeleteAction(); 071 private final MoveUpAction moveUpAction = new MoveUpAction(); 072 private final MoveDownAction moveDownAction = new MoveDownAction(); 073 private final SortAction sortAction = new SortAction(); 074 private final ReverseAction reverseAction = new ReverseAction(); 075 private final EnableFilterAction enableFilterAction = new EnableFilterAction(); 076 private final HidingFilterAction hidingFilterAction = new HidingFilterAction(); 077 078 /** 079 * Constructs a new {@code FilterDialog} 080 */ 081 public FilterDialog() { 082 super(tr("Filter"), "filter", tr("Filter objects and hide/disable them."), 083 Shortcut.registerShortcut("subwindow:filter", tr("Toggle: {0}", tr("Filter")), 084 KeyEvent.VK_F, Shortcut.ALT_SHIFT), 162); 085 build(); 086 MultikeyActionsHandler.getInstance().addAction(enableFilterAction); 087 MultikeyActionsHandler.getInstance().addAction(hidingFilterAction); 088 } 089 090 @Override 091 public void showNotify() { 092 DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT_CONSOLIDATED); 093 MapFrame.addMapModeChangeListener(this); 094 filterModel.executeFilters(true); 095 } 096 097 @Override 098 public void hideNotify() { 099 DatasetEventManager.getInstance().removeDatasetListener(this); 100 MapFrame.removeMapModeChangeListener(this); 101 filterModel.model.clearFilterFlags(); 102 MainApplication.getLayerManager().invalidateEditLayer(); 103 } 104 105 private static final Shortcut ENABLE_FILTER_SHORTCUT 106 = Shortcut.registerShortcut("core_multikey:enableFilter", tr("Multikey: {0}", tr("Enable filter")), 107 KeyEvent.VK_E, Shortcut.ALT_CTRL); 108 109 private static final Shortcut HIDING_FILTER_SHORTCUT 110 = Shortcut.registerShortcut("core_multikey:hidingFilter", tr("Multikey: {0}", tr("Hide filter")), 111 KeyEvent.VK_H, Shortcut.ALT_CTRL); 112 113 private static final String[] COLUMN_TOOLTIPS = { 114 Shortcut.makeTooltip(tr("Enable filter"), ENABLE_FILTER_SHORTCUT.getKeyStroke()), 115 Shortcut.makeTooltip(tr("Hiding filter"), HIDING_FILTER_SHORTCUT.getKeyStroke()), 116 null, 117 tr("Inverse filter"), 118 tr("Filter mode") 119 }; 120 121 private abstract class FilterAction extends AbstractAction implements IEnabledStateUpdating { 122 123 FilterAction(String name, String description, String icon) { 124 putValue(NAME, name); 125 putValue(SHORT_DESCRIPTION, description); 126 new ImageProvider("dialogs", icon).getResource().attachImageIcon(this, true); 127 } 128 129 @Override 130 public void updateEnabledState() { 131 setEnabled(!filterModel.getSelectionModel().isSelectionEmpty()); 132 } 133 } 134 135 private class AddAction extends FilterAction { 136 AddAction() { 137 super(tr("Add"), tr("Add filter."), /* ICON(dialogs/) */ "add"); 138 } 139 140 @Override 141 public void actionPerformed(ActionEvent e) { 142 SearchSetting searchSetting = SearchAction.showSearchDialog(new Filter()); 143 if (searchSetting != null) { 144 filterModel.addFilter(new Filter(searchSetting)); 145 } 146 } 147 148 @Override 149 public void updateEnabledState() { 150 // Do nothing 151 } 152 } 153 154 private class EditAction extends FilterAction { 155 EditAction() { 156 super(tr("Edit"), tr("Edit filter."), /* ICON(dialogs/) */ "edit"); 157 } 158 159 @Override 160 public void actionPerformed(ActionEvent e) { 161 int index = filterModel.getSelectionModel().getMinSelectionIndex(); 162 if (index < 0) return; 163 Filter f = filterModel.getValue(index); 164 SearchSetting searchSetting = SearchAction.showSearchDialog(f); 165 if (searchSetting != null) { 166 filterModel.setValue(index, new Filter(searchSetting)); 167 } 168 } 169 } 170 171 private class DeleteAction extends FilterAction { 172 DeleteAction() { 173 super(tr("Delete"), tr("Delete filter."), /* ICON(dialogs/) */ "delete"); 174 } 175 176 @Override 177 public void actionPerformed(ActionEvent e) { 178 int index = filterModel.getSelectionModel().getMinSelectionIndex(); 179 if (index >= 0) { 180 filterModel.removeFilter(index); 181 } 182 } 183 } 184 185 private class MoveUpAction extends FilterAction { 186 MoveUpAction() { 187 super(tr("Up"), tr("Move filter up."), /* ICON(dialogs/) */ "up"); 188 } 189 190 @Override 191 public void actionPerformed(ActionEvent e) { 192 int index = userTable.convertRowIndexToModel(userTable.getSelectionModel().getMinSelectionIndex()); 193 if (index >= 0 && filterModel.moveUp(index)) { 194 filterModel.getSelectionModel().setSelectionInterval(index-1, index-1); 195 } 196 } 197 198 @Override 199 public void updateEnabledState() { 200 setEnabled(filterModel.canMoveUp()); 201 } 202 } 203 204 private class MoveDownAction extends FilterAction { 205 MoveDownAction() { 206 super(tr("Down"), tr("Move filter down."), /* ICON(dialogs/) */ "down"); 207 } 208 209 @Override 210 public void actionPerformed(ActionEvent e) { 211 int index = userTable.convertRowIndexToModel(userTable.getSelectionModel().getMinSelectionIndex()); 212 if (index >= 0 && filterModel.moveDown(index)) { 213 filterModel.getSelectionModel().setSelectionInterval(index+1, index+1); 214 } 215 } 216 217 @Override 218 public void updateEnabledState() { 219 setEnabled(filterModel.canMoveDown()); 220 } 221 } 222 223 private class SortAction extends FilterAction { 224 SortAction() { 225 super(tr("Sort"), tr("Sort filters."), /* ICON(dialogs/) */ "sort"); 226 } 227 228 @Override 229 public void actionPerformed(ActionEvent e) { 230 filterModel.sort(); 231 } 232 233 @Override 234 public void updateEnabledState() { 235 setEnabled(filterModel.getRowCount() > 1); 236 } 237 } 238 239 private class ReverseAction extends FilterAction { 240 ReverseAction() { 241 super(tr("Reverse"), tr("Reverse the filters order."), /* ICON(dialogs/) */ "reverse"); 242 } 243 244 @Override 245 public void actionPerformed(ActionEvent e) { 246 filterModel.reverse(); 247 } 248 249 @Override 250 public void updateEnabledState() { 251 setEnabled(filterModel.getRowCount() > 1); 252 } 253 } 254 255 /** 256 * Builds the GUI. 257 */ 258 protected void build() { 259 userTable = new UserTable(filterModel); 260 261 userTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); 262 userTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 263 userTable.setSelectionModel(filterModel.getSelectionModel()); 264 265 TableHelper.adjustColumnWidth(userTable, 0, false); 266 TableHelper.adjustColumnWidth(userTable, 1, false); 267 TableHelper.adjustColumnWidth(userTable, 3, false); 268 TableHelper.adjustColumnWidth(userTable, 4, false); 269 270 userTable.setDefaultRenderer(Boolean.class, new BooleanRenderer()); 271 userTable.setDefaultRenderer(String.class, new StringRenderer()); 272 userTable.setDefaultEditor(String.class, new DefaultCellEditor(new DisableShortcutsOnFocusGainedTextField())); 273 274 // Toggle filter "enabled" on Enter 275 InputMapUtils.addEnterAction(userTable, new AbstractAction() { 276 @Override 277 public void actionPerformed(ActionEvent e) { 278 int index = userTable.getSelectedRow(); 279 if (index >= 0) { 280 Filter filter = filterModel.getValue(index); 281 filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED); 282 } 283 } 284 }); 285 286 // Toggle filter "hiding" on Spacebar 287 InputMapUtils.addSpacebarAction(userTable, new AbstractAction() { 288 @Override 289 public void actionPerformed(ActionEvent e) { 290 int index = userTable.getSelectedRow(); 291 if (index >= 0) { 292 Filter filter = filterModel.getValue(index); 293 filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING); 294 } 295 } 296 }); 297 298 List<FilterAction> actions = Arrays.asList(addAction, editAction, deleteAction, moveUpAction, moveDownAction, sortAction, reverseAction); 299 for (FilterAction action : actions) { 300 TableHelper.adaptTo(action, filterModel); 301 TableHelper.adaptTo(action, filterModel.getSelectionModel()); 302 action.updateEnabledState(); 303 } 304 createLayout(userTable, true, actions.stream().map(a -> new SideButton(a, false)).collect(Collectors.toList())); 305 } 306 307 @Override 308 public void destroy() { 309 MultikeyActionsHandler.getInstance().removeAction(enableFilterAction); 310 MultikeyActionsHandler.getInstance().removeAction(hidingFilterAction); 311 super.destroy(); 312 } 313 314 static final class UserTable extends JTable { 315 static final class UserTableHeader extends JTableHeader { 316 UserTableHeader(TableColumnModel cm) { 317 super(cm); 318 } 319 320 @Override 321 public String getToolTipText(MouseEvent e) { 322 int index = columnModel.getColumnIndexAtX(e.getPoint().x); 323 if (index == -1) 324 return null; 325 int realIndex = columnModel.getColumn(index).getModelIndex(); 326 return COLUMN_TOOLTIPS[realIndex]; 327 } 328 } 329 330 UserTable(TableModel dm) { 331 super(dm); 332 } 333 334 @Override 335 protected JTableHeader createDefaultTableHeader() { 336 return new UserTableHeader(columnModel); 337 } 338 } 339 340 static class StringRenderer extends DefaultTableCellRenderer { 341 @Override 342 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 343 Component cell = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 344 TableModel model = table.getModel(); 345 if (model instanceof FilterTableModel) { 346 cell.setEnabled(((FilterTableModel) model).isCellEnabled(row, column)); 347 } 348 return cell; 349 } 350 } 351 352 static class BooleanRenderer extends JCheckBox implements TableCellRenderer { 353 @Override 354 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 355 FilterTableModel model = (FilterTableModel) table.getModel(); 356 setSelected(value != null && (Boolean) value); 357 setEnabled(model.isCellEnabled(row, column)); 358 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 359 return this; 360 } 361 } 362 363 /** 364 * Updates the headline of this dialog to display the number of active filters. 365 */ 366 public void updateDialogHeader() { 367 SwingUtilities.invokeLater(() -> setTitle( 368 tr("Filter Hidden:{0} Disabled:{1}", 369 filterModel.model.getDisabledAndHiddenCount(), filterModel.model.getDisabledCount()))); 370 } 371 372 /** 373 * Draws a text on the map display that indicates that filters are active. 374 * @param g The graphics to draw that text on. 375 */ 376 public void drawOSDText(Graphics2D g) { 377 filterModel.drawOSDText(g); 378 } 379 380 @Override 381 public void dataChanged(DataChangedEvent event) { 382 filterModel.executeFilters(); 383 } 384 385 @Override 386 public void nodeMoved(NodeMovedEvent event) { 387 filterModel.executeFilters(); 388 } 389 390 @Override 391 public void otherDatasetChange(AbstractDatasetChangedEvent event) { 392 if (DatasetEventType.FILTERS_CHANGED != event.getType()) { 393 filterModel.executeFilters(); 394 } 395 } 396 397 @Override 398 public void primitivesAdded(PrimitivesAddedEvent event) { 399 filterModel.executeFilters(event.getPrimitives()); 400 } 401 402 @Override 403 public void primitivesRemoved(PrimitivesRemovedEvent event) { 404 filterModel.executeFilters(); 405 } 406 407 @Override 408 public void relationMembersChanged(RelationMembersChangedEvent event) { 409 filterModel.executeFilters(FilterModel.getAffectedPrimitives(event.getPrimitives())); 410 } 411 412 @Override 413 public void tagsChanged(TagsChangedEvent event) { 414 filterModel.executeFilters(FilterModel.getAffectedPrimitives(event.getPrimitives())); 415 } 416 417 @Override 418 public void wayNodesChanged(WayNodesChangedEvent event) { 419 filterModel.executeFilters(FilterModel.getAffectedPrimitives(event.getPrimitives())); 420 } 421 422 @Override 423 public void mapModeChange(MapMode oldMapMode, MapMode newMapMode) { 424 filterModel.executeFilters(); 425 } 426 427 /** 428 * This method is intended for Plugins getting the filtermodel and using .addFilter() to 429 * add a new filter. 430 * @return the filtermodel 431 */ 432 public FilterTableModel getFilterModel() { 433 return filterModel; 434 } 435 436 abstract class AbstractFilterAction extends AbstractAction implements MultikeyShortcutAction { 437 438 protected transient Filter lastFilter; 439 440 @Override 441 public void actionPerformed(ActionEvent e) { 442 throw new UnsupportedOperationException(); 443 } 444 445 @Override 446 public List<MultikeyInfo> getMultikeyCombinations() { 447 List<MultikeyInfo> result = new ArrayList<>(); 448 449 for (int i = 0; i < filterModel.getRowCount(); i++) { 450 result.add(new MultikeyInfo(i, filterModel.getValue(i).text)); 451 } 452 453 return result; 454 } 455 456 protected final boolean isLastFilterValid() { 457 return lastFilter != null && filterModel.getFilters().contains(lastFilter); 458 } 459 460 @Override 461 public MultikeyInfo getLastMultikeyAction() { 462 if (isLastFilterValid()) 463 return new MultikeyInfo(-1, lastFilter.text); 464 else 465 return null; 466 } 467 } 468 469 private class EnableFilterAction extends AbstractFilterAction { 470 471 EnableFilterAction() { 472 putValue(SHORT_DESCRIPTION, tr("Enable filter")); 473 ENABLE_FILTER_SHORTCUT.setAccelerator(this); 474 } 475 476 @Override 477 public Shortcut getMultikeyShortcut() { 478 return ENABLE_FILTER_SHORTCUT; 479 } 480 481 @Override 482 public void executeMultikeyAction(int index, boolean repeatLastAction) { 483 if (index >= 0 && index < filterModel.getRowCount()) { 484 Filter filter = filterModel.getValue(index); 485 filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED); 486 lastFilter = filter; 487 } else if (repeatLastAction && isLastFilterValid()) { 488 filterModel.setValueAt(!lastFilter.enable, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_ENABLED); 489 } 490 } 491 } 492 493 private class HidingFilterAction extends AbstractFilterAction { 494 495 HidingFilterAction() { 496 putValue(SHORT_DESCRIPTION, tr("Hiding filter")); 497 HIDING_FILTER_SHORTCUT.setAccelerator(this); 498 } 499 500 @Override 501 public Shortcut getMultikeyShortcut() { 502 return HIDING_FILTER_SHORTCUT; 503 } 504 505 @Override 506 public void executeMultikeyAction(int index, boolean repeatLastAction) { 507 if (index >= 0 && index < filterModel.getRowCount()) { 508 Filter filter = filterModel.getValue(index); 509 filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING); 510 lastFilter = filter; 511 } else if (repeatLastAction && isLastFilterValid()) { 512 filterModel.setValueAt(!lastFilter.hiding, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_HIDING); 513 } 514 } 515 } 516}