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 protected 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 putValue(SMALL_ICON, ImageProvider.get("dialogs", "add")); 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 putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit")); 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 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 177 } 178 179 @Override 180 public void actionPerformed(ActionEvent e) { 181 int index = userTable.getSelectionModel().getMinSelectionIndex(); 182 if (index < 0) return; 183 filterModel.removeFilter(index); 184 } 185 }); 186 SideButton upButton = new SideButton(new AbstractAction() { 187 { 188 putValue(NAME, tr("Up")); 189 putValue(SHORT_DESCRIPTION, tr("Move filter up.")); 190 putValue(SMALL_ICON, ImageProvider.get("dialogs", "up")); 191 } 192 193 @Override 194 public void actionPerformed(ActionEvent e) { 195 int index = userTable.getSelectionModel().getMinSelectionIndex(); 196 if (index < 0) return; 197 filterModel.moveUpFilter(index); 198 userTable.getSelectionModel().setSelectionInterval(index-1, index-1); 199 } 200 201 }); 202 SideButton downButton = new SideButton(new AbstractAction() { 203 { 204 putValue(NAME, tr("Down")); 205 putValue(SHORT_DESCRIPTION, tr("Move filter down.")); 206 putValue(SMALL_ICON, ImageProvider.get("dialogs", "down")); 207 } 208 209 @Override 210 public void actionPerformed(ActionEvent e) { 211 int index = userTable.getSelectionModel().getMinSelectionIndex(); 212 if (index < 0) return; 213 filterModel.moveDownFilter(index); 214 userTable.getSelectionModel().setSelectionInterval(index+1, index+1); 215 } 216 }); 217 218 // Toggle filter "enabled" on Enter 219 InputMapUtils.addEnterAction(userTable, new AbstractAction() { 220 @Override 221 public void actionPerformed(ActionEvent e) { 222 int index = userTable.getSelectedRow(); 223 if (index < 0) return; 224 Filter filter = filterModel.getFilter(index); 225 filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED); 226 } 227 }); 228 229 // Toggle filter "hiding" on Spacebar 230 InputMapUtils.addSpacebarAction(userTable, new AbstractAction() { 231 @Override 232 public void actionPerformed(ActionEvent e) { 233 int index = userTable.getSelectedRow(); 234 if (index < 0) return; 235 Filter filter = filterModel.getFilter(index); 236 filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING); 237 } 238 }); 239 240 createLayout(userTable, true, Arrays.asList(new SideButton[] { 241 addButton, editButton, deleteButton, upButton, downButton 242 })); 243 } 244 245 @Override 246 public void destroy() { 247 MultikeyActionsHandler.getInstance().removeAction(enableFilterAction); 248 MultikeyActionsHandler.getInstance().removeAction(hidingFilterAction); 249 super.destroy(); 250 } 251 252 static class StringRenderer extends DefaultTableCellRenderer { 253 @Override 254 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 255 FilterTableModel model = (FilterTableModel) table.getModel(); 256 Component cell = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 257 cell.setEnabled(model.isCellEnabled(row, column)); 258 return cell; 259 } 260 } 261 262 static class BooleanRenderer extends JCheckBox implements TableCellRenderer { 263 @Override 264 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 265 FilterTableModel model = (FilterTableModel) table.getModel(); 266 setSelected(value != null && (Boolean) value); 267 setEnabled(model.isCellEnabled(row, column)); 268 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 269 return this; 270 } 271 } 272 273 public void updateDialogHeader() { 274 SwingUtilities.invokeLater(new Runnable() { 275 @Override 276 public void run() { 277 setTitle(tr("Filter Hidden:{0} Disabled:{1}", filterModel.disabledAndHiddenCount, filterModel.disabledCount)); 278 } 279 }); 280 } 281 282 public void drawOSDText(Graphics2D g) { 283 filterModel.drawOSDText(g); 284 } 285 286 /** 287 * Returns the list of primitives whose filtering can be affected by change in primitive 288 * @param primitives list of primitives to check 289 * @return List of primitives whose filtering can be affected by change in source primitives 290 */ 291 private static Collection<OsmPrimitive> getAffectedPrimitives(Collection<? extends OsmPrimitive> primitives) { 292 // Filters can use nested parent/child expression so complete tree is necessary 293 Set<OsmPrimitive> result = new HashSet<>(); 294 Stack<OsmPrimitive> stack = new Stack<>(); 295 stack.addAll(primitives); 296 297 while (!stack.isEmpty()) { 298 OsmPrimitive p = stack.pop(); 299 300 if (result.contains(p)) { 301 continue; 302 } 303 304 result.add(p); 305 306 if (p instanceof Way) { 307 for (OsmPrimitive n: ((Way) p).getNodes()) { 308 stack.push(n); 309 } 310 } else if (p instanceof Relation) { 311 for (RelationMember rm: ((Relation) p).getMembers()) { 312 stack.push(rm.getMember()); 313 } 314 } 315 316 for (OsmPrimitive ref: p.getReferrers()) { 317 stack.push(ref); 318 } 319 } 320 321 return result; 322 } 323 324 @Override 325 public void dataChanged(DataChangedEvent event) { 326 filterModel.executeFilters(); 327 } 328 329 @Override 330 public void nodeMoved(NodeMovedEvent event) { 331 filterModel.executeFilters(); 332 } 333 334 @Override 335 public void otherDatasetChange(AbstractDatasetChangedEvent event) { 336 filterModel.executeFilters(); 337 } 338 339 @Override 340 public void primitivesAdded(PrimitivesAddedEvent event) { 341 filterModel.executeFilters(event.getPrimitives()); 342 } 343 344 @Override 345 public void primitivesRemoved(PrimitivesRemovedEvent event) { 346 filterModel.executeFilters(); 347 } 348 349 @Override 350 public void relationMembersChanged(RelationMembersChangedEvent event) { 351 filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives())); 352 } 353 354 @Override 355 public void tagsChanged(TagsChangedEvent event) { 356 filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives())); 357 } 358 359 @Override 360 public void wayNodesChanged(WayNodesChangedEvent event) { 361 filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives())); 362 } 363 364 /** 365 * This method is intendet for Plugins getting the filtermodel and using .addFilter() to 366 * add a new filter. 367 * @return the filtermodel 368 */ 369 public FilterTableModel getFilterModel() { 370 return filterModel; 371 } 372 373 abstract class AbstractFilterAction extends AbstractAction implements MultikeyShortcutAction { 374 375 protected transient Filter lastFilter; 376 377 @Override 378 public void actionPerformed(ActionEvent e) { 379 throw new UnsupportedOperationException(); 380 } 381 382 @Override 383 public List<MultikeyInfo> getMultikeyCombinations() { 384 List<MultikeyInfo> result = new ArrayList<>(); 385 386 for (int i = 0; i < filterModel.getRowCount(); i++) { 387 Filter filter = filterModel.getFilter(i); 388 MultikeyInfo info = new MultikeyInfo(i, filter.text); 389 result.add(info); 390 } 391 392 return result; 393 } 394 395 protected boolean isLastFilterValid() { 396 return lastFilter != null && filterModel.getFilters().contains(lastFilter); 397 } 398 399 @Override 400 public MultikeyInfo getLastMultikeyAction() { 401 if (isLastFilterValid()) 402 return new MultikeyInfo(-1, lastFilter.text); 403 else 404 return null; 405 } 406 } 407 408 private class EnableFilterAction extends AbstractFilterAction { 409 410 EnableFilterAction() { 411 putValue(SHORT_DESCRIPTION, tr("Enable filter")); 412 ENABLE_FILTER_SHORTCUT.setAccelerator(this); 413 } 414 415 @Override 416 public Shortcut getMultikeyShortcut() { 417 return ENABLE_FILTER_SHORTCUT; 418 } 419 420 @Override 421 public void executeMultikeyAction(int index, boolean repeatLastAction) { 422 if (index >= 0 && index < filterModel.getRowCount()) { 423 Filter filter = filterModel.getFilter(index); 424 filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED); 425 lastFilter = filter; 426 } else if (repeatLastAction && isLastFilterValid()) { 427 filterModel.setValueAt(!lastFilter.enable, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_ENABLED); 428 } 429 } 430 } 431 432 private class HidingFilterAction extends AbstractFilterAction { 433 434 HidingFilterAction() { 435 putValue(SHORT_DESCRIPTION, tr("Hiding filter")); 436 HIDING_FILTER_SHORTCUT.setAccelerator(this); 437 } 438 439 @Override 440 public Shortcut getMultikeyShortcut() { 441 return HIDING_FILTER_SHORTCUT; 442 } 443 444 @Override 445 public void executeMultikeyAction(int index, boolean repeatLastAction) { 446 if (index >= 0 && index < filterModel.getRowCount()) { 447 Filter filter = filterModel.getFilter(index); 448 filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING); 449 lastFilter = filter; 450 } else if (repeatLastAction && isLastFilterValid()) { 451 filterModel.setValueAt(!lastFilter.hiding, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_HIDING); 452 } 453 } 454 } 455}