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