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