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