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 FilterTableModel filterModel = new FilterTableModel(); 061 062 private EnableFilterAction enableFilterAction; 063 private 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 101 protected final String[] columnToolTips = { 102 Main.platform.makeTooltip(tr("Enable filter"), ENABLE_FILTER_SHORTCUT), 103 Main.platform.makeTooltip(tr("Hiding filter"), HIDING_FILTER_SHORTCUT), 104 null, 105 tr("Inverse filter"), 106 tr("Filter mode") 107 }; 108 109 protected void build() { 110 userTable = new JTable(filterModel){ 111 @Override 112 protected JTableHeader createDefaultTableHeader() { 113 return new JTableHeader(columnModel) { 114 @Override 115 public String getToolTipText(MouseEvent e) { 116 java.awt.Point p = e.getPoint(); 117 int index = columnModel.getColumnIndexAtX(p.x); 118 int realIndex = columnModel.getColumn(index).getModelIndex(); 119 return columnToolTips[realIndex]; 120 } 121 }; 122 } 123 }; 124 125 userTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 126 127 userTable.getColumnModel().getColumn(0).setMaxWidth(1); 128 userTable.getColumnModel().getColumn(1).setMaxWidth(1); 129 userTable.getColumnModel().getColumn(3).setMaxWidth(1); 130 userTable.getColumnModel().getColumn(4).setMaxWidth(1); 131 132 userTable.getColumnModel().getColumn(0).setResizable(false); 133 userTable.getColumnModel().getColumn(1).setResizable(false); 134 userTable.getColumnModel().getColumn(3).setResizable(false); 135 userTable.getColumnModel().getColumn(4).setResizable(false); 136 137 userTable.setDefaultRenderer(Boolean.class, new BooleanRenderer()); 138 userTable.setDefaultRenderer(String.class, new StringRenderer()); 139 140 SideButton addButton = new SideButton(new AbstractAction() { 141 { 142 putValue(NAME, tr("Add")); 143 putValue(SHORT_DESCRIPTION, tr("Add filter.")); 144 putValue(SMALL_ICON, ImageProvider.get("dialogs","add")); 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 SideButton editButton = new SideButton(new AbstractAction() { 154 { 155 putValue(NAME, tr("Edit")); 156 putValue(SHORT_DESCRIPTION, tr("Edit filter.")); 157 putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit")); 158 } 159 @Override 160 public void actionPerformed(ActionEvent e) { 161 int index = userTable.getSelectionModel().getMinSelectionIndex(); 162 if(index < 0) return; 163 Filter f = filterModel.getFilter(index); 164 Filter filter = (Filter)SearchAction.showSearchDialog(f); 165 if(filter != null){ 166 filterModel.setFilter(index, filter); 167 } 168 } 169 }); 170 SideButton deleteButton = new SideButton(new AbstractAction() { 171 { 172 putValue(NAME, tr("Delete")); 173 putValue(SHORT_DESCRIPTION, tr("Delete filter.")); 174 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 175 } 176 @Override 177 public void actionPerformed(ActionEvent e) { 178 int index = userTable.getSelectionModel().getMinSelectionIndex(); 179 if(index < 0) return; 180 filterModel.removeFilter(index); 181 } 182 }); 183 SideButton upButton = new SideButton(new AbstractAction() { 184 { 185 putValue(NAME, tr("Up")); 186 putValue(SHORT_DESCRIPTION, tr("Move filter up.")); 187 putValue(SMALL_ICON, ImageProvider.get("dialogs", "up")); 188 } 189 @Override 190 public void actionPerformed(ActionEvent e) { 191 int index = userTable.getSelectionModel().getMinSelectionIndex(); 192 if(index < 0) return; 193 filterModel.moveUpFilter(index); 194 userTable.getSelectionModel().setSelectionInterval(index-1, index-1); 195 } 196 197 }); 198 SideButton downButton = new SideButton(new AbstractAction() { 199 { 200 putValue(NAME, tr("Down")); 201 putValue(SHORT_DESCRIPTION, tr("Move filter down.")); 202 putValue(SMALL_ICON, ImageProvider.get("dialogs", "down")); 203 } 204 @Override 205 public void actionPerformed(ActionEvent e) { 206 int index = userTable.getSelectionModel().getMinSelectionIndex(); 207 if(index < 0) return; 208 filterModel.moveDownFilter(index); 209 userTable.getSelectionModel().setSelectionInterval(index+1, index+1); 210 } 211 }); 212 213 // Toggle filter "enabled" on Enter 214 InputMapUtils.addEnterAction(userTable, new AbstractAction() { 215 @Override 216 public void actionPerformed(ActionEvent e) { 217 int index = userTable.getSelectedRow(); 218 if (index<0) return; 219 Filter filter = filterModel.getFilter(index); 220 filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED); 221 } 222 }); 223 224 // Toggle filter "hiding" on Spacebar 225 InputMapUtils.addSpacebarAction(userTable, new AbstractAction() { 226 @Override 227 public void actionPerformed(ActionEvent e) { 228 int index = userTable.getSelectedRow(); 229 if (index<0) return; 230 Filter filter = filterModel.getFilter(index); 231 filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING); 232 } 233 }); 234 235 createLayout(userTable, true, Arrays.asList(new SideButton[] { 236 addButton, editButton, deleteButton, upButton, downButton 237 })); 238 } 239 240 @Override 241 public void destroy() { 242 MultikeyActionsHandler.getInstance().removeAction(enableFilterAction); 243 MultikeyActionsHandler.getInstance().removeAction(hidingFilterAction); 244 super.destroy(); 245 } 246 247 static class StringRenderer extends DefaultTableCellRenderer { 248 @Override 249 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,int row,int column) { 250 FilterTableModel model = (FilterTableModel)table.getModel(); 251 Component cell = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 252 cell.setEnabled(model.isCellEnabled(row, column)); 253 return cell; 254 } 255 } 256 257 static class BooleanRenderer extends JCheckBox implements TableCellRenderer { 258 @Override 259 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,int row,int column) { 260 FilterTableModel model = (FilterTableModel)table.getModel(); 261 setSelected(value != null && (Boolean)value); 262 setEnabled(model.isCellEnabled(row, column)); 263 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 264 return this; 265 } 266 } 267 268 public void updateDialogHeader() { 269 SwingUtilities.invokeLater(new Runnable() { 270 @Override 271 public void run() { 272 setTitle(tr("Filter Hidden:{0} Disabled:{1}", filterModel.disabledAndHiddenCount, filterModel.disabledCount)); 273 } 274 }); 275 } 276 277 public void drawOSDText(Graphics2D g) { 278 filterModel.drawOSDText(g); 279 } 280 281 /** 282 * Returns the list of primitives whose filtering can be affected by change in primitive 283 * @param primitives list of primitives to check 284 * @return List of primitives whose filtering can be affected by change in source primitives 285 */ 286 private Collection<OsmPrimitive> getAffectedPrimitives(Collection<? extends OsmPrimitive> primitives) { 287 // Filters can use nested parent/child expression so complete tree is necessary 288 Set<OsmPrimitive> result = new HashSet<OsmPrimitive>(); 289 Stack<OsmPrimitive> stack = new Stack<OsmPrimitive>(); 290 stack.addAll(primitives); 291 292 while (!stack.isEmpty()) { 293 OsmPrimitive p = stack.pop(); 294 295 if (result.contains(p)) { 296 continue; 297 } 298 299 result.add(p); 300 301 if (p instanceof Way) { 302 for (OsmPrimitive n: ((Way)p).getNodes()) { 303 stack.push(n); 304 } 305 } else if (p instanceof Relation) { 306 for (RelationMember rm: ((Relation)p).getMembers()) { 307 stack.push(rm.getMember()); 308 } 309 } 310 311 for (OsmPrimitive ref: p.getReferrers()) { 312 stack.push(ref); 313 } 314 } 315 316 return result; 317 } 318 319 @Override 320 public void dataChanged(DataChangedEvent event) { 321 filterModel.executeFilters(); 322 } 323 324 @Override 325 public void nodeMoved(NodeMovedEvent event) { 326 // Do nothing 327 } 328 329 @Override 330 public void otherDatasetChange(AbstractDatasetChangedEvent event) { 331 filterModel.executeFilters(); 332 } 333 334 @Override 335 public void primitivesAdded(PrimitivesAddedEvent event) { 336 filterModel.executeFilters(event.getPrimitives()); 337 } 338 339 @Override 340 public void primitivesRemoved(PrimitivesRemovedEvent event) { 341 filterModel.executeFilters(); 342 } 343 344 @Override 345 public void relationMembersChanged(RelationMembersChangedEvent event) { 346 filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives())); 347 } 348 349 @Override 350 public void tagsChanged(TagsChangedEvent event) { 351 filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives())); 352 } 353 354 @Override 355 public void wayNodesChanged(WayNodesChangedEvent event) { 356 filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives())); 357 } 358 359 abstract class AbstractFilterAction extends AbstractAction implements MultikeyShortcutAction { 360 361 protected Filter lastFilter; 362 363 @Override 364 public void actionPerformed(ActionEvent e) { 365 throw new UnsupportedOperationException(); 366 } 367 368 @Override 369 public List<MultikeyInfo> getMultikeyCombinations() { 370 List<MultikeyInfo> result = new ArrayList<MultikeyShortcutAction.MultikeyInfo>(); 371 372 for (int i=0; i<filterModel.getRowCount(); i++) { 373 Filter filter = filterModel.getFilter(i); 374 MultikeyInfo info = new MultikeyInfo(i, filter.text); 375 result.add(info); 376 } 377 378 return result; 379 } 380 381 protected boolean isLastFilterValid() { 382 return lastFilter != null && filterModel.getFilters().contains(lastFilter); 383 } 384 385 @Override 386 public MultikeyInfo getLastMultikeyAction() { 387 if (isLastFilterValid()) 388 return new MultikeyInfo(-1, lastFilter.text); 389 else 390 return null; 391 } 392 393 } 394 395 private class EnableFilterAction extends AbstractFilterAction { 396 397 EnableFilterAction() { 398 putValue(SHORT_DESCRIPTION, tr("Enable filter")); 399 ENABLE_FILTER_SHORTCUT.setAccelerator(this); 400 } 401 402 @Override 403 public Shortcut getMultikeyShortcut() { 404 return ENABLE_FILTER_SHORTCUT; 405 } 406 407 @Override 408 public void executeMultikeyAction(int index, boolean repeatLastAction) { 409 if (index >= 0 && index < filterModel.getRowCount()) { 410 Filter filter = filterModel.getFilter(index); 411 filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED); 412 lastFilter = filter; 413 } else if (repeatLastAction && isLastFilterValid()) { 414 filterModel.setValueAt(!lastFilter.enable, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_ENABLED); 415 } 416 } 417 } 418 419 private class HidingFilterAction extends AbstractFilterAction { 420 421 public HidingFilterAction() { 422 putValue(SHORT_DESCRIPTION, tr("Hiding filter")); 423 HIDING_FILTER_SHORTCUT.setAccelerator(this); 424 } 425 426 @Override 427 public Shortcut getMultikeyShortcut() { 428 return HIDING_FILTER_SHORTCUT; 429 } 430 431 @Override 432 public void executeMultikeyAction(int index, boolean repeatLastAction) { 433 if (index >= 0 && index < filterModel.getRowCount()) { 434 Filter filter = filterModel.getFilter(index); 435 filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING); 436 lastFilter = filter; 437 } else if (repeatLastAction && isLastFilterValid()) { 438 filterModel.setValueAt(!lastFilter.hiding, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_HIDING); 439 } 440 } 441 442 } 443}