001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.BorderLayout; 007import java.awt.Component; 008import java.awt.Container; 009import java.awt.Dimension; 010import java.awt.Font; 011import java.awt.GridBagLayout; 012import java.awt.Rectangle; 013import java.awt.event.ActionEvent; 014import java.awt.event.KeyEvent; 015import java.util.ArrayList; 016import java.util.Collection; 017import java.util.HashMap; 018import java.util.List; 019import java.util.Map; 020import java.util.concurrent.CopyOnWriteArrayList; 021 022import javax.swing.AbstractAction; 023import javax.swing.AbstractButton; 024import javax.swing.Action; 025import javax.swing.BorderFactory; 026import javax.swing.BoxLayout; 027import javax.swing.ButtonGroup; 028import javax.swing.ImageIcon; 029import javax.swing.InputMap; 030import javax.swing.JButton; 031import javax.swing.JCheckBoxMenuItem; 032import javax.swing.JComponent; 033import javax.swing.JPanel; 034import javax.swing.JPopupMenu; 035import javax.swing.JSplitPane; 036import javax.swing.JToggleButton; 037import javax.swing.JToolBar; 038import javax.swing.KeyStroke; 039import javax.swing.border.Border; 040import javax.swing.event.PopupMenuEvent; 041import javax.swing.event.PopupMenuListener; 042import javax.swing.plaf.basic.BasicSplitPaneDivider; 043import javax.swing.plaf.basic.BasicSplitPaneUI; 044 045import org.openstreetmap.josm.actions.LassoModeAction; 046import org.openstreetmap.josm.actions.mapmode.DeleteAction; 047import org.openstreetmap.josm.actions.mapmode.DrawAction; 048import org.openstreetmap.josm.actions.mapmode.ExtrudeAction; 049import org.openstreetmap.josm.actions.mapmode.ImproveWayAccuracyAction; 050import org.openstreetmap.josm.actions.mapmode.MapMode; 051import org.openstreetmap.josm.actions.mapmode.ParallelWayAction; 052import org.openstreetmap.josm.actions.mapmode.SelectAction; 053import org.openstreetmap.josm.actions.mapmode.ZoomAction; 054import org.openstreetmap.josm.data.ViewportData; 055import org.openstreetmap.josm.data.preferences.BooleanProperty; 056import org.openstreetmap.josm.data.preferences.IntegerProperty; 057import org.openstreetmap.josm.gui.dialogs.ChangesetDialog; 058import org.openstreetmap.josm.gui.dialogs.CommandStackDialog; 059import org.openstreetmap.josm.gui.dialogs.ConflictDialog; 060import org.openstreetmap.josm.gui.dialogs.DialogsPanel; 061import org.openstreetmap.josm.gui.dialogs.FilterDialog; 062import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 063import org.openstreetmap.josm.gui.dialogs.MapPaintDialog; 064import org.openstreetmap.josm.gui.dialogs.MinimapDialog; 065import org.openstreetmap.josm.gui.dialogs.NotesDialog; 066import org.openstreetmap.josm.gui.dialogs.RelationListDialog; 067import org.openstreetmap.josm.gui.dialogs.SelectionListDialog; 068import org.openstreetmap.josm.gui.dialogs.ToggleDialog; 069import org.openstreetmap.josm.gui.dialogs.UserListDialog; 070import org.openstreetmap.josm.gui.dialogs.ValidatorDialog; 071import org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog; 072import org.openstreetmap.josm.gui.layer.Layer; 073import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 074import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 075import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 076import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 077import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 078import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 079import org.openstreetmap.josm.gui.util.AdvancedKeyPressDetector; 080import org.openstreetmap.josm.spi.preferences.Config; 081import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 082import org.openstreetmap.josm.tools.Destroyable; 083import org.openstreetmap.josm.tools.GBC; 084import org.openstreetmap.josm.tools.ImageProvider; 085import org.openstreetmap.josm.tools.Shortcut; 086 087/** 088 * One Map frame with one dataset behind. This is the container gui class whose 089 * display can be set to the different views. 090 * 091 * @author imi 092 */ 093public class MapFrame extends JPanel implements Destroyable, ActiveLayerChangeListener, LayerChangeListener { 094 /** 095 * Default width of the toggle dialog area. 096 */ 097 public static final int DEF_TOGGLE_DLG_WIDTH = 330; 098 099 private static final IntegerProperty TOGGLE_DIALOGS_WIDTH = new IntegerProperty("toggleDialogs.width", DEF_TOGGLE_DLG_WIDTH); 100 /** 101 * Do not require to switch modes (potlatch style workflow) for drawing/selecting map modes. 102 * @since 12347 103 */ 104 public static final BooleanProperty MODELESS = new BooleanProperty("modeless", false); 105 /** 106 * The current mode, this frame operates. 107 */ 108 public MapMode mapMode; 109 110 /** 111 * The view control displayed. 112 */ 113 public final MapView mapView; 114 115 /** 116 * This object allows to detect key press and release events 117 */ 118 public final transient AdvancedKeyPressDetector keyDetector = new AdvancedKeyPressDetector(); 119 120 /** 121 * The toolbar with the action icons. To add new toggle dialog buttons, 122 * use addToggleDialog, to add a new map mode button use addMapMode. 123 */ 124 private JComponent sideToolBar = new JToolBar(JToolBar.VERTICAL); 125 private final ButtonGroup toolBarActionsGroup = new ButtonGroup(); 126 private final JToolBar toolBarActions = new JToolBar(JToolBar.VERTICAL); 127 private final JToolBar toolBarToggle = new JToolBar(JToolBar.VERTICAL); 128 129 private final List<ToggleDialog> allDialogs = new ArrayList<>(); 130 private final List<IconToggleButton> allDialogButtons = new ArrayList<>(); 131 /** 132 * All map mode buttons. Should only be read form the outside 133 */ 134 public final List<IconToggleButton> allMapModeButtons = new ArrayList<>(); 135 136 private final ListAllButtonsAction listAllDialogsAction = new ListAllButtonsAction(allDialogButtons); 137 private final ListAllButtonsAction listAllMapModesAction = new ListAllButtonsAction(allMapModeButtons); 138 private final JButton listAllToggleDialogsButton = new JButton(listAllDialogsAction); 139 private final JButton listAllMapModesButton = new JButton(listAllMapModesAction); 140 141 { 142 listAllDialogsAction.setButton(listAllToggleDialogsButton); 143 listAllMapModesAction.setButton(listAllMapModesButton); 144 } 145 146 // Toggle dialogs 147 148 /** Conflict dialog */ 149 public final ConflictDialog conflictDialog; 150 /** Filter dialog */ 151 public final FilterDialog filterDialog; 152 /** Relation list dialog */ 153 public final RelationListDialog relationListDialog; 154 /** Validator dialog */ 155 public final ValidatorDialog validatorDialog; 156 /** Selection list dialog */ 157 public final SelectionListDialog selectionListDialog; 158 /** Properties dialog */ 159 public final PropertiesDialog propertiesDialog; 160 /** Map paint dialog */ 161 public final MapPaintDialog mapPaintDialog; 162 /** Notes dialog */ 163 public final NotesDialog noteDialog; 164 165 // Map modes 166 167 /** Select mode */ 168 public final SelectAction mapModeSelect; 169 /** Draw mode */ 170 public final DrawAction mapModeDraw; 171 /** Zoom mode */ 172 public final ZoomAction mapModeZoom; 173 /** Delete mode */ 174 public final DeleteAction mapModeDelete; 175 /** Select Lasso mode */ 176 public LassoModeAction mapModeSelectLasso; 177 178 private final transient Map<Layer, MapMode> lastMapMode = new HashMap<>(); 179 180 /** 181 * The status line below the map 182 */ 183 public MapStatus statusLine; 184 185 /** 186 * The split pane with the mapview (leftPanel) and toggle dialogs (dialogsPanel). 187 */ 188 private final JSplitPane splitPane; 189 private final JPanel leftPanel; 190 private final DialogsPanel dialogsPanel; 191 192 /** 193 * Constructs a new {@code MapFrame}. 194 * @param viewportData the initial viewport of the map. Can be null, then 195 * the viewport is derived from the layer data. 196 * @since 11713 197 */ 198 public MapFrame(ViewportData viewportData) { 199 setSize(400, 400); 200 setLayout(new BorderLayout()); 201 202 mapView = new MapView(MainApplication.getLayerManager(), viewportData); 203 204 splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true); 205 206 leftPanel = new JPanel(new GridBagLayout()); 207 leftPanel.add(mapView, GBC.std().fill()); 208 splitPane.setLeftComponent(leftPanel); 209 210 dialogsPanel = new DialogsPanel(splitPane); 211 splitPane.setRightComponent(dialogsPanel); 212 213 /** 214 * All additional space goes to the mapView 215 */ 216 splitPane.setResizeWeight(1.0); 217 218 /** 219 * Some beautifications. 220 */ 221 splitPane.setDividerSize(5); 222 splitPane.setBorder(null); 223 splitPane.setUI(new NoBorderSplitPaneUI()); 224 225 // JSplitPane supports F6, F8, Home and End shortcuts by default, but we need them for Audio and Image Mapping actions 226 InputMap splitInputMap = splitPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 227 splitInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F6, 0), new Object()); 228 splitInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0), new Object()); 229 splitInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), new Object()); 230 splitInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), new Object()); 231 232 add(splitPane, BorderLayout.CENTER); 233 234 dialogsPanel.setLayout(new BoxLayout(dialogsPanel, BoxLayout.Y_AXIS)); 235 dialogsPanel.setPreferredSize(new Dimension(TOGGLE_DIALOGS_WIDTH.get(), 0)); 236 dialogsPanel.setMinimumSize(new Dimension(24, 0)); 237 mapView.setMinimumSize(new Dimension(10, 0)); 238 239 // toolBarActions, map mode buttons 240 mapModeSelect = new SelectAction(this); 241 mapModeSelectLasso = new LassoModeAction(); 242 mapModeDraw = new DrawAction(); 243 mapModeZoom = new ZoomAction(this); 244 mapModeDelete = new DeleteAction(); 245 246 addMapMode(new IconToggleButton(mapModeSelect)); 247 addMapMode(new IconToggleButton(mapModeSelectLasso, true)); 248 addMapMode(new IconToggleButton(mapModeDraw)); 249 addMapMode(new IconToggleButton(mapModeZoom, true)); 250 addMapMode(new IconToggleButton(mapModeDelete, true)); 251 addMapMode(new IconToggleButton(new ParallelWayAction(this), true)); 252 addMapMode(new IconToggleButton(new ExtrudeAction(), true)); 253 addMapMode(new IconToggleButton(new ImproveWayAccuracyAction(), false)); 254 toolBarActionsGroup.setSelected(allMapModeButtons.get(0).getModel(), true); 255 toolBarActions.setFloatable(false); 256 257 // toolBarToggles, toggle dialog buttons 258 LayerListDialog.createInstance(mapView.getLayerManager()); 259 propertiesDialog = new PropertiesDialog(); 260 selectionListDialog = new SelectionListDialog(); 261 relationListDialog = new RelationListDialog(); 262 conflictDialog = new ConflictDialog(); 263 validatorDialog = new ValidatorDialog(); 264 filterDialog = new FilterDialog(); 265 mapPaintDialog = new MapPaintDialog(); 266 noteDialog = new NotesDialog(); 267 268 addToggleDialog(LayerListDialog.getInstance()); 269 addToggleDialog(propertiesDialog); 270 addToggleDialog(selectionListDialog); 271 addToggleDialog(relationListDialog); 272 addToggleDialog(new MinimapDialog()); 273 addToggleDialog(new CommandStackDialog()); 274 addToggleDialog(new UserListDialog()); 275 addToggleDialog(conflictDialog); 276 addToggleDialog(validatorDialog); 277 addToggleDialog(filterDialog); 278 addToggleDialog(new ChangesetDialog(), true); 279 addToggleDialog(mapPaintDialog); 280 addToggleDialog(noteDialog); 281 toolBarToggle.setFloatable(false); 282 283 // status line below the map 284 statusLine = new MapStatus(this); 285 MainApplication.getLayerManager().addLayerChangeListener(this); 286 MainApplication.getLayerManager().addActiveLayerChangeListener(this); 287 288 boolean unregisterTab = Shortcut.findShortcut(KeyEvent.VK_TAB, 0).isPresent(); 289 if (unregisterTab) { 290 for (JComponent c: allDialogButtons) { 291 c.setFocusTraversalKeysEnabled(false); 292 } 293 for (JComponent c: allMapModeButtons) { 294 c.setFocusTraversalKeysEnabled(false); 295 } 296 } 297 298 if (Config.getPref().getBoolean("debug.advanced-keypress-detector.enable", true)) { 299 keyDetector.register(); 300 } 301 } 302 303 /** 304 * Enables the select tool 305 * @param onlyIfModeless Only enable if modeless mode is active 306 * @return <code>true</code> if it is selected 307 */ 308 public boolean selectSelectTool(boolean onlyIfModeless) { 309 if (onlyIfModeless && !MODELESS.get()) 310 return false; 311 312 return selectMapMode(mapModeSelect); 313 } 314 315 /** 316 * Enables the draw tool 317 * @param onlyIfModeless Only enable if modeless mode is active 318 * @return <code>true</code> if it is selected 319 */ 320 public boolean selectDrawTool(boolean onlyIfModeless) { 321 if (onlyIfModeless && !MODELESS.get()) 322 return false; 323 324 return selectMapMode(mapModeDraw); 325 } 326 327 /** 328 * Enables the zoom tool 329 * @param onlyIfModeless Only enable if modeless mode is active 330 * @return <code>true</code> if it is selected 331 */ 332 public boolean selectZoomTool(boolean onlyIfModeless) { 333 if (onlyIfModeless && !MODELESS.get()) 334 return false; 335 336 return selectMapMode(mapModeZoom); 337 } 338 339 /** 340 * Called as some kind of destructor when the last layer has been removed. 341 * Delegates the call to all Destroyables within this component (e.g. MapModes) 342 */ 343 @Override 344 public void destroy() { 345 MainApplication.getLayerManager().removeLayerChangeListener(this); 346 MainApplication.getLayerManager().removeActiveLayerChangeListener(this); 347 dialogsPanel.destroy(); 348 Config.getPref().removePreferenceChangeListener(sidetoolbarPreferencesChangedListener); 349 for (int i = 0; i < toolBarActions.getComponentCount(); ++i) { 350 if (toolBarActions.getComponent(i) instanceof Destroyable) { 351 ((Destroyable) toolBarActions.getComponent(i)).destroy(); 352 } 353 } 354 toolBarActions.removeAll(); 355 for (int i = 0; i < toolBarToggle.getComponentCount(); ++i) { 356 if (toolBarToggle.getComponent(i) instanceof Destroyable) { 357 ((Destroyable) toolBarToggle.getComponent(i)).destroy(); 358 } 359 } 360 toolBarToggle.removeAll(); 361 362 statusLine.destroy(); 363 mapView.destroy(); 364 keyDetector.unregister(); 365 366 allDialogs.clear(); 367 allDialogButtons.clear(); 368 allMapModeButtons.clear(); 369 } 370 371 /** 372 * Gets the action of the default (first) map mode 373 * @return That action 374 */ 375 public Action getDefaultButtonAction() { 376 return ((AbstractButton) toolBarActions.getComponent(0)).getAction(); 377 } 378 379 /** 380 * Open all ToggleDialogs that have their preferences property set. Close all others. 381 */ 382 public void initializeDialogsPane() { 383 dialogsPanel.initialize(allDialogs); 384 } 385 386 /** 387 * Adds a new toggle dialog to the left button list. It is displayed in expert and normal mode 388 * @param dlg The dialog 389 * @return The button 390 */ 391 public IconToggleButton addToggleDialog(final ToggleDialog dlg) { 392 return addToggleDialog(dlg, false); 393 } 394 395 /** 396 * Call this to add new toggle dialogs to the left button-list 397 * @param dlg The toggle dialog. It must not be in the list already. 398 * @param isExpert {@code true} if it's reserved to expert mode 399 * @return button allowing to toggle the dialog 400 */ 401 public IconToggleButton addToggleDialog(final ToggleDialog dlg, boolean isExpert) { 402 final IconToggleButton button = new IconToggleButton(dlg.getToggleAction(), isExpert); 403 button.setShowHideButtonListener(dlg); 404 button.setInheritsPopupMenu(true); 405 dlg.setButton(button); 406 toolBarToggle.add(button); 407 allDialogs.add(dlg); 408 allDialogButtons.add(button); 409 button.applyButtonHiddenPreferences(); 410 if (dialogsPanel.initialized) { 411 dialogsPanel.add(dlg); 412 } 413 return button; 414 } 415 416 /** 417 * Call this to remove existing toggle dialog from the left button-list 418 * @param dlg The toggle dialog. It must be already in the list. 419 * @since 10851 420 */ 421 public void removeToggleDialog(final ToggleDialog dlg) { 422 final JToggleButton button = dlg.getButton(); 423 if (button != null) { 424 allDialogButtons.remove(button); 425 toolBarToggle.remove(button); 426 } 427 dialogsPanel.remove(dlg); 428 allDialogs.remove(dlg); 429 } 430 431 /** 432 * Adds a new map mode button 433 * @param b The map mode button with a {@link MapMode} action. 434 */ 435 public void addMapMode(IconToggleButton b) { 436 if (!(b.getAction() instanceof MapMode)) 437 throw new IllegalArgumentException("MapMode action must be subclass of MapMode"); 438 allMapModeButtons.add(b); 439 toolBarActionsGroup.add(b); 440 toolBarActions.add(b); 441 b.applyButtonHiddenPreferences(); 442 b.setInheritsPopupMenu(true); 443 } 444 445 /** 446 * Fires an property changed event "visible". 447 * @param aFlag {@code true} if display should be visible 448 */ 449 @Override public void setVisible(boolean aFlag) { 450 boolean old = isVisible(); 451 super.setVisible(aFlag); 452 if (old != aFlag) { 453 firePropertyChange("visible", old, aFlag); 454 } 455 } 456 457 /** 458 * Change the operating map mode for the view. Will call unregister on the 459 * old MapMode and register on the new one. Now this function also verifies 460 * if new map mode is correct mode for current layer and does not change mode 461 * in such cases. 462 * @param newMapMode The new mode to set. 463 * @return {@code true} if mode is really selected 464 */ 465 public boolean selectMapMode(MapMode newMapMode) { 466 return selectMapMode(newMapMode, mapView.getLayerManager().getActiveLayer()); 467 } 468 469 /** 470 * Another version of the selectMapMode for changing layer action. 471 * Pass newly selected layer to this method. 472 * @param newMapMode The new mode to set. 473 * @param newLayer newly selected layer 474 * @return {@code true} if mode is really selected 475 */ 476 public boolean selectMapMode(MapMode newMapMode, Layer newLayer) { 477 MapMode oldMapMode = this.mapMode; 478 if (newMapMode == oldMapMode) 479 return true; 480 if (newMapMode == null || !newMapMode.layerIsSupported(newLayer)) { 481 newMapMode = null; 482 } 483 484 if (oldMapMode != null) { 485 oldMapMode.exitMode(); 486 } 487 this.mapMode = newMapMode; 488 if (newMapMode != null) { 489 newMapMode.enterMode(); 490 } 491 lastMapMode.put(newLayer, newMapMode); 492 fireMapModeChanged(oldMapMode, newMapMode); 493 return newMapMode != null; 494 } 495 496 /** 497 * Fill the given panel by adding all necessary components to the different 498 * locations. 499 * 500 * @param panel The container to fill. Must have a BorderLayout. 501 */ 502 public void fillPanel(Container panel) { 503 panel.add(this, BorderLayout.CENTER); 504 505 /** 506 * sideToolBar: add map modes icons 507 */ 508 if (Config.getPref().getBoolean("sidetoolbar.mapmodes.visible", true)) { 509 toolBarActions.setAlignmentX(0.5f); 510 toolBarActions.setBorder(null); 511 toolBarActions.setInheritsPopupMenu(true); 512 sideToolBar.add(toolBarActions); 513 listAllMapModesButton.setAlignmentX(0.5f); 514 listAllMapModesButton.setBorder(null); 515 listAllMapModesButton.setFont(listAllMapModesButton.getFont().deriveFont(Font.PLAIN)); 516 listAllMapModesButton.setInheritsPopupMenu(true); 517 sideToolBar.add(listAllMapModesButton); 518 } 519 520 /** 521 * sideToolBar: add toggle dialogs icons 522 */ 523 if (Config.getPref().getBoolean("sidetoolbar.toggledialogs.visible", true)) { 524 ((JToolBar) sideToolBar).addSeparator(new Dimension(0, 18)); 525 toolBarToggle.setAlignmentX(0.5f); 526 toolBarToggle.setBorder(null); 527 toolBarToggle.setInheritsPopupMenu(true); 528 sideToolBar.add(toolBarToggle); 529 listAllToggleDialogsButton.setAlignmentX(0.5f); 530 listAllToggleDialogsButton.setBorder(null); 531 listAllToggleDialogsButton.setFont(listAllToggleDialogsButton.getFont().deriveFont(Font.PLAIN)); 532 listAllToggleDialogsButton.setInheritsPopupMenu(true); 533 sideToolBar.add(listAllToggleDialogsButton); 534 } 535 536 /** 537 * sideToolBar: add dynamic popup menu 538 */ 539 sideToolBar.setComponentPopupMenu(new SideToolbarPopupMenu()); 540 ((JToolBar) sideToolBar).setFloatable(false); 541 sideToolBar.setBorder(BorderFactory.createEmptyBorder(0, 1, 0, 1)); 542 543 /** 544 * sideToolBar: decide scroll- and visibility 545 */ 546 if (Config.getPref().getBoolean("sidetoolbar.scrollable", true)) { 547 final ScrollViewport svp = new ScrollViewport(sideToolBar, ScrollViewport.VERTICAL_DIRECTION); 548 sideToolBar = svp; 549 } 550 sideToolBar.setVisible(Config.getPref().getBoolean("sidetoolbar.visible", true)); 551 sidetoolbarPreferencesChangedListener = e -> { 552 if ("sidetoolbar.visible".equals(e.getKey())) { 553 sideToolBar.setVisible(Config.getPref().getBoolean("sidetoolbar.visible")); 554 } 555 }; 556 Config.getPref().addPreferenceChangeListener(sidetoolbarPreferencesChangedListener); 557 558 /** 559 * sideToolBar: add it to the panel 560 */ 561 panel.add(sideToolBar, BorderLayout.WEST); 562 563 /** 564 * statusLine: add to panel 565 */ 566 if (statusLine != null && Config.getPref().getBoolean("statusline.visible", true)) { 567 panel.add(statusLine, BorderLayout.SOUTH); 568 } 569 } 570 571 static final class NoBorderSplitPaneUI extends BasicSplitPaneUI { 572 static final class NoBorderBasicSplitPaneDivider extends BasicSplitPaneDivider { 573 NoBorderBasicSplitPaneDivider(BasicSplitPaneUI ui) { 574 super(ui); 575 } 576 577 @Override 578 public void setBorder(Border b) { 579 // Do nothing 580 } 581 } 582 583 @Override 584 public BasicSplitPaneDivider createDefaultDivider() { 585 return new NoBorderBasicSplitPaneDivider(this); 586 } 587 } 588 589 private final class SideToolbarPopupMenu extends JPopupMenu { 590 private static final int staticMenuEntryCount = 2; 591 private final JCheckBoxMenuItem doNotHide = new JCheckBoxMenuItem(new AbstractAction(tr("Do not hide toolbar")) { 592 @Override 593 public void actionPerformed(ActionEvent e) { 594 boolean sel = ((JCheckBoxMenuItem) e.getSource()).getState(); 595 Config.getPref().putBoolean("sidetoolbar.always-visible", sel); 596 } 597 }); 598 { 599 addPopupMenuListener(new PopupMenuListener() { 600 @Override 601 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 602 final Object src = ((JPopupMenu) e.getSource()).getInvoker(); 603 if (src instanceof IconToggleButton) { 604 insert(new Separator(), 0); 605 insert(new AbstractAction() { 606 { 607 putValue(NAME, tr("Hide this button")); 608 putValue(SHORT_DESCRIPTION, tr("Click the arrow at the bottom to show it again.")); 609 } 610 611 @Override 612 public void actionPerformed(ActionEvent e) { 613 ((IconToggleButton) src).setButtonHidden(true); 614 validateToolBarsVisibility(); 615 } 616 }, 0); 617 } 618 doNotHide.setSelected(Config.getPref().getBoolean("sidetoolbar.always-visible", true)); 619 } 620 621 @Override 622 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 623 while (getComponentCount() > staticMenuEntryCount) { 624 remove(0); 625 } 626 } 627 628 @Override 629 public void popupMenuCanceled(PopupMenuEvent e) { 630 // Do nothing 631 } 632 }); 633 634 add(new AbstractAction(tr("Hide edit toolbar")) { 635 @Override 636 public void actionPerformed(ActionEvent e) { 637 Config.getPref().putBoolean("sidetoolbar.visible", false); 638 } 639 }); 640 add(doNotHide); 641 } 642 } 643 644 class ListAllButtonsAction extends AbstractAction { 645 646 private JButton button; 647 private final transient Collection<? extends HideableButton> buttons; 648 649 ListAllButtonsAction(Collection<? extends HideableButton> buttons) { 650 this.buttons = buttons; 651 } 652 653 public void setButton(JButton button) { 654 this.button = button; 655 final ImageIcon icon = ImageProvider.get("audio-fwd"); 656 putValue(SMALL_ICON, icon); 657 button.setPreferredSize(new Dimension(icon.getIconWidth(), icon.getIconHeight() + 64)); 658 } 659 660 @Override 661 public void actionPerformed(ActionEvent e) { 662 JPopupMenu menu = new JPopupMenu(); 663 for (HideableButton b : buttons) { 664 final HideableButton t = b; 665 menu.add(new JCheckBoxMenuItem(new AbstractAction() { 666 { 667 putValue(NAME, t.getActionName()); 668 putValue(SMALL_ICON, t.getIcon()); 669 putValue(SELECTED_KEY, t.isButtonVisible()); 670 putValue(SHORT_DESCRIPTION, tr("Hide or show this toggle button")); 671 } 672 673 @Override 674 public void actionPerformed(ActionEvent e) { 675 if ((Boolean) getValue(SELECTED_KEY)) { 676 t.showButton(); 677 } else { 678 t.hideButton(); 679 } 680 validateToolBarsVisibility(); 681 } 682 })); 683 } 684 if (button != null) { 685 Rectangle bounds = button.getBounds(); 686 menu.show(button, bounds.x + bounds.width, 0); 687 } 688 } 689 } 690 691 /** 692 * Validate the visibility of all tool bars and hide the ones that should be hidden 693 */ 694 public void validateToolBarsVisibility() { 695 for (IconToggleButton b : allDialogButtons) { 696 b.applyButtonHiddenPreferences(); 697 } 698 toolBarToggle.repaint(); 699 for (IconToggleButton b : allMapModeButtons) { 700 b.applyButtonHiddenPreferences(); 701 } 702 toolBarActions.repaint(); 703 } 704 705 /** 706 * Replies the instance of a toggle dialog of type <code>type</code> managed by this map frame 707 * 708 * @param <T> toggle dialog type 709 * @param type the class of the toggle dialog, i.e. UserListDialog.class 710 * @return the instance of a toggle dialog of type <code>type</code> managed by this 711 * map frame; null, if no such dialog exists 712 * 713 */ 714 public <T> T getToggleDialog(Class<T> type) { 715 return dialogsPanel.getToggleDialog(type); 716 } 717 718 /** 719 * Shows or hides the side dialog panel 720 * @param visible The new visibility 721 */ 722 public void setDialogsPanelVisible(boolean visible) { 723 rememberToggleDialogWidth(); 724 dialogsPanel.setVisible(visible); 725 splitPane.setDividerLocation(visible ? splitPane.getWidth() - TOGGLE_DIALOGS_WIDTH.get() : 0); 726 splitPane.setDividerSize(visible ? 5 : 0); 727 } 728 729 /** 730 * Remember the current width of the (possibly resized) toggle dialog area 731 */ 732 public void rememberToggleDialogWidth() { 733 if (dialogsPanel.isVisible()) { 734 TOGGLE_DIALOGS_WIDTH.put(splitPane.getWidth() - splitPane.getDividerLocation()); 735 } 736 } 737 738 /** 739 * Remove panel from top of MapView by class 740 * @param type type of panel 741 */ 742 public void removeTopPanel(Class<?> type) { 743 int n = leftPanel.getComponentCount(); 744 for (int i = 0; i < n; i++) { 745 Component c = leftPanel.getComponent(i); 746 if (type.isInstance(c)) { 747 leftPanel.remove(i); 748 leftPanel.doLayout(); 749 return; 750 } 751 } 752 } 753 754 /** 755 * Find panel on top of MapView by class 756 * @param <T> type 757 * @param type type of panel 758 * @return found panel 759 */ 760 public <T> T getTopPanel(Class<T> type) { 761 int n = leftPanel.getComponentCount(); 762 for (int i = 0; i < n; i++) { 763 Component c = leftPanel.getComponent(i); 764 if (type.isInstance(c)) 765 return type.cast(c); 766 } 767 return null; 768 } 769 770 /** 771 * Add component {@code c} on top of MapView 772 * @param c component 773 */ 774 public void addTopPanel(Component c) { 775 leftPanel.add(c, GBC.eol().fill(GBC.HORIZONTAL), leftPanel.getComponentCount()-1); 776 leftPanel.doLayout(); 777 c.doLayout(); 778 } 779 780 /** 781 * Interface to notify listeners of the change of the mapMode. 782 * @since 10600 (functional interface) 783 */ 784 @FunctionalInterface 785 public interface MapModeChangeListener { 786 /** 787 * Trigerred when map mode changes. 788 * @param oldMapMode old map mode 789 * @param newMapMode new map mode 790 */ 791 void mapModeChange(MapMode oldMapMode, MapMode newMapMode); 792 } 793 794 /** 795 * the mapMode listeners 796 */ 797 private static final CopyOnWriteArrayList<MapModeChangeListener> mapModeChangeListeners = new CopyOnWriteArrayList<>(); 798 799 private transient PreferenceChangedListener sidetoolbarPreferencesChangedListener; 800 /** 801 * Adds a mapMode change listener 802 * 803 * @param listener the listener. Ignored if null or already registered. 804 */ 805 public static void addMapModeChangeListener(MapModeChangeListener listener) { 806 if (listener != null) { 807 mapModeChangeListeners.addIfAbsent(listener); 808 } 809 } 810 811 /** 812 * Removes a mapMode change listener 813 * 814 * @param listener the listener. Ignored if null or already registered. 815 */ 816 public static void removeMapModeChangeListener(MapModeChangeListener listener) { 817 mapModeChangeListeners.remove(listener); 818 } 819 820 protected static void fireMapModeChanged(MapMode oldMapMode, MapMode newMapMode) { 821 for (MapModeChangeListener l : mapModeChangeListeners) { 822 l.mapModeChange(oldMapMode, newMapMode); 823 } 824 } 825 826 @Override 827 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 828 boolean modeChanged = false; 829 Layer newLayer = e.getSource().getActiveLayer(); 830 if (mapMode == null || !mapMode.layerIsSupported(newLayer)) { 831 MapMode newMapMode = getLastMapMode(newLayer); 832 modeChanged = newMapMode != mapMode; 833 if (newMapMode != null) { 834 // it would be nice to select first supported mode when layer is first selected, 835 // but it don't work well with for example editgpx layer 836 selectMapMode(newMapMode, newLayer); 837 } else if (mapMode != null) { 838 mapMode.exitMode(); // if new mode is null - simply exit from previous mode 839 mapMode = null; 840 } 841 } 842 // if this is really a change (and not the first active layer) 843 if (e.getPreviousActiveLayer() != null && !modeChanged && mapMode != null) { 844 // Let mapmodes know about new active layer 845 mapMode.exitMode(); 846 mapMode.enterMode(); 847 } 848 849 // After all listeners notice new layer, some buttons will be disabled/enabled 850 // and possibly need to be hidden/shown. 851 validateToolBarsVisibility(); 852 } 853 854 private MapMode getLastMapMode(Layer newLayer) { 855 MapMode mode = lastMapMode.get(newLayer); 856 if (mode == null) { 857 // if no action is selected - try to select default action 858 Action defaultMode = getDefaultButtonAction(); 859 if (defaultMode instanceof MapMode && ((MapMode) defaultMode).layerIsSupported(newLayer)) { 860 mode = (MapMode) defaultMode; 861 } 862 } 863 return mode; 864 } 865 866 @Override 867 public void layerAdded(LayerAddEvent e) { 868 // ignored 869 } 870 871 @Override 872 public void layerRemoving(LayerRemoveEvent e) { 873 lastMapMode.remove(e.getRemovedLayer()); 874 } 875 876 @Override 877 public void layerOrderChanged(LayerOrderChangeEvent e) { 878 // ignored 879 } 880 881}