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