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.Collection; 014import java.util.HashSet; 015import java.util.List; 016import java.util.Set; 017import java.util.Stack; 018 019import javax.swing.AbstractAction; 020import javax.swing.JCheckBox; 021import javax.swing.JTable; 022import javax.swing.ListSelectionModel; 023import javax.swing.SwingUtilities; 024import javax.swing.table.DefaultTableCellRenderer; 025import javax.swing.table.JTableHeader; 026import javax.swing.table.TableCellRenderer; 027import javax.swing.table.TableColumnModel; 028import javax.swing.table.TableModel; 029 030import org.openstreetmap.josm.Main; 031import org.openstreetmap.josm.actions.search.SearchAction; 032import org.openstreetmap.josm.data.osm.Filter; 033import org.openstreetmap.josm.data.osm.OsmPrimitive; 034import org.openstreetmap.josm.data.osm.Relation; 035import org.openstreetmap.josm.data.osm.RelationMember; 036import org.openstreetmap.josm.data.osm.Way; 037import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 038import org.openstreetmap.josm.data.osm.event.DataChangedEvent; 039import org.openstreetmap.josm.data.osm.event.DataSetListener; 040import org.openstreetmap.josm.data.osm.event.DatasetEventManager; 041import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode; 042import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 043import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; 044import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; 045import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; 046import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; 047import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 048import org.openstreetmap.josm.gui.SideButton; 049import org.openstreetmap.josm.tools.ImageProvider; 050import org.openstreetmap.josm.tools.InputMapUtils; 051import org.openstreetmap.josm.tools.MultikeyActionsHandler; 052import org.openstreetmap.josm.tools.MultikeyShortcutAction; 053import org.openstreetmap.josm.tools.Shortcut; 054 055/** 056 * 057 * @author Petr_DlouhĂ˝ 058 */ 059public class FilterDialog extends ToggleDialog implements DataSetListener { 060 061 private JTable userTable; 062 private final FilterTableModel filterModel = new FilterTableModel(); 063 064 private final EnableFilterAction enableFilterAction; 065 private final HidingFilterAction hidingFilterAction; 066 067 /** 068 * Constructs a new {@code FilterDialog} 069 */ 070 public FilterDialog() { 071 super(tr("Filter"), "filter", tr("Filter objects and hide/disable them."), 072 Shortcut.registerShortcut("subwindow:filter", tr("Toggle: {0}", tr("Filter")), 073 KeyEvent.VK_F, Shortcut.ALT_SHIFT), 162); 074 build(); 075 enableFilterAction = new EnableFilterAction(); 076 hidingFilterAction = new HidingFilterAction(); 077 MultikeyActionsHandler.getInstance().addAction(enableFilterAction); 078 MultikeyActionsHandler.getInstance().addAction(hidingFilterAction); 079 } 080 081 @Override 082 public void showNotify() { 083 DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT_CONSOLIDATED); 084 filterModel.executeFilters(); 085 } 086 087 @Override 088 public void hideNotify() { 089 DatasetEventManager.getInstance().removeDatasetListener(this); 090 filterModel.clearFilterFlags(); 091 Main.map.mapView.repaint(); 092 } 093 094 private static final Shortcut ENABLE_FILTER_SHORTCUT 095 = Shortcut.registerShortcut("core_multikey:enableFilter", tr("Multikey: {0}", tr("Enable filter")), 096 KeyEvent.VK_E, Shortcut.ALT_CTRL); 097 098 private static final Shortcut HIDING_FILTER_SHORTCUT 099 = Shortcut.registerShortcut("core_multikey:hidingFilter", tr("Multikey: {0}", tr("Hide filter")), 100 KeyEvent.VK_H, Shortcut.ALT_CTRL); 101 102 private static final String[] COLUMN_TOOLTIPS = { 103 Main.platform.makeTooltip(tr("Enable filter"), ENABLE_FILTER_SHORTCUT), 104 Main.platform.makeTooltip(tr("Hiding filter"), HIDING_FILTER_SHORTCUT), 105 null, 106 tr("Inverse filter"), 107 tr("Filter mode") 108 }; 109 110 protected void build() { 111 userTable = new UserTable(filterModel); 112 113 userTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 114 115 userTable.getColumnModel().getColumn(0).setMaxWidth(1); 116 userTable.getColumnModel().getColumn(1).setMaxWidth(1); 117 userTable.getColumnModel().getColumn(3).setMaxWidth(1); 118 userTable.getColumnModel().getColumn(4).setMaxWidth(1); 119 120 userTable.getColumnModel().getColumn(0).setResizable(false); 121 userTable.getColumnModel().getColumn(1).setResizable(false); 122 userTable.getColumnModel().getColumn(3).setResizable(false); 123 userTable.getColumnModel().getColumn(4).setResizable(false); 124 125 userTable.setDefaultRenderer(Boolean.class, new BooleanRenderer()); 126 userTable.setDefaultRenderer(String.class, new StringRenderer()); 127 128 SideButton addButton = new SideButton(new AbstractAction() { 129 { 130 putValue(NAME, tr("Add")); 131 putValue(SHORT_DESCRIPTION, tr("Add filter.")); 132 new ImageProvider("dialogs", "add").getResource().attachImageIcon(this, true); 133 } 134 135 @Override 136 public void actionPerformed(ActionEvent e) { 137 Filter filter = (Filter) SearchAction.showSearchDialog(new Filter()); 138 if (filter != null) { 139 filterModel.addFilter(filter); 140 } 141 } 142 }); 143 SideButton editButton = new SideButton(new AbstractAction() { 144 { 145 putValue(NAME, tr("Edit")); 146 putValue(SHORT_DESCRIPTION, tr("Edit filter.")); 147 new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this, true); 148 } 149 150 @Override 151 public void actionPerformed(ActionEvent e) { 152 int index = userTable.getSelectionModel().getMinSelectionIndex(); 153 if (index < 0) return; 154 Filter f = filterModel.getFilter(index); 155 Filter filter = (Filter) SearchAction.showSearchDialog(f); 156 if (filter != null) { 157 filterModel.setFilter(index, filter); 158 } 159 } 160 }); 161 SideButton deleteButton = new SideButton(new AbstractAction() { 162 { 163 putValue(NAME, tr("Delete")); 164 putValue(SHORT_DESCRIPTION, tr("Delete filter.")); 165 new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this, true); 166 } 167 168 @Override 169 public void actionPerformed(ActionEvent e) { 170 int index = userTable.getSelectionModel().getMinSelectionIndex(); 171 if (index >= 0) { 172 filterModel.removeFilter(index); 173 } 174 } 175 }); 176 SideButton upButton = new SideButton(new AbstractAction() { 177 { 178 putValue(NAME, tr("Up")); 179 putValue(SHORT_DESCRIPTION, tr("Move filter up.")); 180 new ImageProvider("dialogs", "up").getResource().attachImageIcon(this, true); 181 } 182 183 @Override 184 public void actionPerformed(ActionEvent e) { 185 int index = userTable.getSelectionModel().getMinSelectionIndex(); 186 if (index >= 0) { 187 filterModel.moveUpFilter(index); 188 userTable.getSelectionModel().setSelectionInterval(index-1, index-1); 189 } 190 } 191 }); 192 SideButton downButton = new SideButton(new AbstractAction() { 193 { 194 putValue(NAME, tr("Down")); 195 putValue(SHORT_DESCRIPTION, tr("Move filter down.")); 196 new ImageProvider("dialogs", "down").getResource().attachImageIcon(this, true); 197 } 198 199 @Override 200 public void actionPerformed(ActionEvent e) { 201 int index = userTable.getSelectionModel().getMinSelectionIndex(); 202 if (index >= 0) { 203 filterModel.moveDownFilter(index); 204 userTable.getSelectionModel().setSelectionInterval(index+1, index+1); 205 } 206 } 207 }); 208 209 // Toggle filter "enabled" on Enter 210 InputMapUtils.addEnterAction(userTable, new AbstractAction() { 211 @Override 212 public void actionPerformed(ActionEvent e) { 213 int index = userTable.getSelectedRow(); 214 if (index >= 0) { 215 Filter filter = filterModel.getFilter(index); 216 filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED); 217 } 218 } 219 }); 220 221 // Toggle filter "hiding" on Spacebar 222 InputMapUtils.addSpacebarAction(userTable, new AbstractAction() { 223 @Override 224 public void actionPerformed(ActionEvent e) { 225 int index = userTable.getSelectedRow(); 226 if (index >= 0) { 227 Filter filter = filterModel.getFilter(index); 228 filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING); 229 } 230 } 231 }); 232 233 createLayout(userTable, true, Arrays.asList(new SideButton[] { 234 addButton, editButton, deleteButton, upButton, downButton 235 })); 236 } 237 238 @Override 239 public void destroy() { 240 MultikeyActionsHandler.getInstance().removeAction(enableFilterAction); 241 MultikeyActionsHandler.getInstance().removeAction(hidingFilterAction); 242 super.destroy(); 243 } 244 245 static final class UserTable extends JTable { 246 static final class UserTableHeader extends JTableHeader { 247 UserTableHeader(TableColumnModel cm) { 248 super(cm); 249 } 250 251 @Override 252 public String getToolTipText(MouseEvent e) { 253 int index = columnModel.getColumnIndexAtX(e.getPoint().x); 254 int realIndex = columnModel.getColumn(index).getModelIndex(); 255 return COLUMN_TOOLTIPS[realIndex]; 256 } 257 } 258 259 UserTable(TableModel dm) { 260 super(dm); 261 } 262 263 @Override 264 protected JTableHeader createDefaultTableHeader() { 265 return new UserTableHeader(columnModel); 266 } 267 } 268 269 static class StringRenderer extends DefaultTableCellRenderer { 270 @Override 271 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 272 FilterTableModel model = (FilterTableModel) table.getModel(); 273 Component cell = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 274 cell.setEnabled(model.isCellEnabled(row, column)); 275 return cell; 276 } 277 } 278 279 static class BooleanRenderer extends JCheckBox implements TableCellRenderer { 280 @Override 281 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 282 FilterTableModel model = (FilterTableModel) table.getModel(); 283 setSelected(value != null && (Boolean) value); 284 setEnabled(model.isCellEnabled(row, column)); 285 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 286 return this; 287 } 288 } 289 290 public void updateDialogHeader() { 291 SwingUtilities.invokeLater(() -> setTitle( 292 tr("Filter Hidden:{0} Disabled:{1}", filterModel.disabledAndHiddenCount, filterModel.disabledCount))); 293 } 294 295 public void drawOSDText(Graphics2D g) { 296 filterModel.drawOSDText(g); 297 } 298 299 /** 300 * Returns the list of primitives whose filtering can be affected by change in primitive 301 * @param primitives list of primitives to check 302 * @return List of primitives whose filtering can be affected by change in source primitives 303 */ 304 private static Collection<OsmPrimitive> getAffectedPrimitives(Collection<? extends OsmPrimitive> primitives) { 305 // Filters can use nested parent/child expression so complete tree is necessary 306 Set<OsmPrimitive> result = new HashSet<>(); 307 Stack<OsmPrimitive> stack = new Stack<>(); 308 stack.addAll(primitives); 309 310 while (!stack.isEmpty()) { 311 OsmPrimitive p = stack.pop(); 312 313 if (result.contains(p)) { 314 continue; 315 } 316 317 result.add(p); 318 319 if (p instanceof Way) { 320 for (OsmPrimitive n: ((Way) p).getNodes()) { 321 stack.push(n); 322 } 323 } else if (p instanceof Relation) { 324 for (RelationMember rm: ((Relation) p).getMembers()) { 325 stack.push(rm.getMember()); 326 } 327 } 328 329 for (OsmPrimitive ref: p.getReferrers()) { 330 stack.push(ref); 331 } 332 } 333 334 return result; 335 } 336 337 @Override 338 public void dataChanged(DataChangedEvent event) { 339 filterModel.executeFilters(); 340 } 341 342 @Override 343 public void nodeMoved(NodeMovedEvent event) { 344 filterModel.executeFilters(); 345 } 346 347 @Override 348 public void otherDatasetChange(AbstractDatasetChangedEvent event) { 349 filterModel.executeFilters(); 350 } 351 352 @Override 353 public void primitivesAdded(PrimitivesAddedEvent event) { 354 filterModel.executeFilters(event.getPrimitives()); 355 } 356 357 @Override 358 public void primitivesRemoved(PrimitivesRemovedEvent event) { 359 filterModel.executeFilters(); 360 } 361 362 @Override 363 public void relationMembersChanged(RelationMembersChangedEvent event) { 364 filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives())); 365 } 366 367 @Override 368 public void tagsChanged(TagsChangedEvent event) { 369 filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives())); 370 } 371 372 @Override 373 public void wayNodesChanged(WayNodesChangedEvent event) { 374 filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives())); 375 } 376 377 /** 378 * This method is intendet for Plugins getting the filtermodel and using .addFilter() to 379 * add a new filter. 380 * @return the filtermodel 381 */ 382 public FilterTableModel getFilterModel() { 383 return filterModel; 384 } 385 386 abstract class AbstractFilterAction extends AbstractAction implements MultikeyShortcutAction { 387 388 protected transient Filter lastFilter; 389 390 @Override 391 public void actionPerformed(ActionEvent e) { 392 throw new UnsupportedOperationException(); 393 } 394 395 @Override 396 public List<MultikeyInfo> getMultikeyCombinations() { 397 List<MultikeyInfo> result = new ArrayList<>(); 398 399 for (int i = 0; i < filterModel.getRowCount(); i++) { 400 Filter filter = filterModel.getFilter(i); 401 MultikeyInfo info = new MultikeyInfo(i, filter.text); 402 result.add(info); 403 } 404 405 return result; 406 } 407 408 protected final boolean isLastFilterValid() { 409 return lastFilter != null && filterModel.getFilters().contains(lastFilter); 410 } 411 412 @Override 413 public MultikeyInfo getLastMultikeyAction() { 414 if (isLastFilterValid()) 415 return new MultikeyInfo(-1, lastFilter.text); 416 else 417 return null; 418 } 419 } 420 421 private class EnableFilterAction extends AbstractFilterAction { 422 423 EnableFilterAction() { 424 putValue(SHORT_DESCRIPTION, tr("Enable filter")); 425 ENABLE_FILTER_SHORTCUT.setAccelerator(this); 426 } 427 428 @Override 429 public Shortcut getMultikeyShortcut() { 430 return ENABLE_FILTER_SHORTCUT; 431 } 432 433 @Override 434 public void executeMultikeyAction(int index, boolean repeatLastAction) { 435 if (index >= 0 && index < filterModel.getRowCount()) { 436 Filter filter = filterModel.getFilter(index); 437 filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED); 438 lastFilter = filter; 439 } else if (repeatLastAction && isLastFilterValid()) { 440 filterModel.setValueAt(!lastFilter.enable, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_ENABLED); 441 } 442 } 443 } 444 445 private class HidingFilterAction extends AbstractFilterAction { 446 447 HidingFilterAction() { 448 putValue(SHORT_DESCRIPTION, tr("Hiding filter")); 449 HIDING_FILTER_SHORTCUT.setAccelerator(this); 450 } 451 452 @Override 453 public Shortcut getMultikeyShortcut() { 454 return HIDING_FILTER_SHORTCUT; 455 } 456 457 @Override 458 public void executeMultikeyAction(int index, boolean repeatLastAction) { 459 if (index >= 0 && index < filterModel.getRowCount()) { 460 Filter filter = filterModel.getFilter(index); 461 filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING); 462 lastFilter = filter; 463 } else if (repeatLastAction && isLastFilterValid()) { 464 filterModel.setValueAt(!lastFilter.hiding, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_HIDING); 465 } 466 } 467 } 468}