001// License: GPL. See LICENSE file for details. 002package org.openstreetmap.josm.gui; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.marktr; 006import static org.openstreetmap.josm.tools.I18n.tr; 007 008import java.awt.Component; 009import java.awt.DefaultFocusTraversalPolicy; 010import java.awt.Dimension; 011import java.awt.GraphicsEnvironment; 012import java.awt.event.ActionEvent; 013import java.awt.event.KeyEvent; 014import java.awt.event.KeyListener; 015import java.util.ArrayList; 016import java.util.HashMap; 017import java.util.List; 018import java.util.Map; 019 020import javax.swing.Action; 021import javax.swing.Box; 022import javax.swing.JCheckBoxMenuItem; 023import javax.swing.JMenu; 024import javax.swing.JMenuBar; 025import javax.swing.JMenuItem; 026import javax.swing.JPopupMenu; 027import javax.swing.JSeparator; 028import javax.swing.JTextField; 029import javax.swing.KeyStroke; 030import javax.swing.MenuElement; 031import javax.swing.MenuSelectionManager; 032import javax.swing.event.DocumentEvent; 033import javax.swing.event.DocumentListener; 034import javax.swing.event.MenuEvent; 035import javax.swing.event.MenuListener; 036 037import org.openstreetmap.josm.Main; 038import org.openstreetmap.josm.actions.AboutAction; 039import org.openstreetmap.josm.actions.AddNodeAction; 040import org.openstreetmap.josm.actions.AlignInCircleAction; 041import org.openstreetmap.josm.actions.AlignInLineAction; 042import org.openstreetmap.josm.actions.AutoScaleAction; 043import org.openstreetmap.josm.actions.ChangesetManagerToggleAction; 044import org.openstreetmap.josm.actions.CloseChangesetAction; 045import org.openstreetmap.josm.actions.CombineWayAction; 046import org.openstreetmap.josm.actions.CopyAction; 047import org.openstreetmap.josm.actions.CopyCoordinatesAction; 048import org.openstreetmap.josm.actions.CreateCircleAction; 049import org.openstreetmap.josm.actions.CreateMultipolygonAction; 050import org.openstreetmap.josm.actions.DeleteAction; 051import org.openstreetmap.josm.actions.DialogsToggleAction; 052import org.openstreetmap.josm.actions.DistributeAction; 053import org.openstreetmap.josm.actions.DownloadAction; 054import org.openstreetmap.josm.actions.DownloadPrimitiveAction; 055import org.openstreetmap.josm.actions.DownloadReferrersAction; 056import org.openstreetmap.josm.actions.DuplicateAction; 057import org.openstreetmap.josm.actions.ExitAction; 058import org.openstreetmap.josm.actions.ExpertToggleAction; 059import org.openstreetmap.josm.actions.FollowLineAction; 060import org.openstreetmap.josm.actions.FullscreenToggleAction; 061import org.openstreetmap.josm.actions.GpxExportAction; 062import org.openstreetmap.josm.actions.HelpAction; 063import org.openstreetmap.josm.actions.HistoryInfoAction; 064import org.openstreetmap.josm.actions.HistoryInfoWebAction; 065import org.openstreetmap.josm.actions.InfoAction; 066import org.openstreetmap.josm.actions.InfoWebAction; 067import org.openstreetmap.josm.actions.JoinAreasAction; 068import org.openstreetmap.josm.actions.JoinNodeWayAction; 069import org.openstreetmap.josm.actions.JosmAction; 070import org.openstreetmap.josm.actions.JumpToAction; 071import org.openstreetmap.josm.actions.MergeLayerAction; 072import org.openstreetmap.josm.actions.MergeNodesAction; 073import org.openstreetmap.josm.actions.MergeSelectionAction; 074import org.openstreetmap.josm.actions.MirrorAction; 075import org.openstreetmap.josm.actions.MoveAction; 076import org.openstreetmap.josm.actions.MoveNodeAction; 077import org.openstreetmap.josm.actions.NewAction; 078import org.openstreetmap.josm.actions.OpenFileAction; 079import org.openstreetmap.josm.actions.OpenLocationAction; 080import org.openstreetmap.josm.actions.OrthogonalizeAction; 081import org.openstreetmap.josm.actions.OrthogonalizeAction.Undo; 082import org.openstreetmap.josm.actions.PasteAction; 083import org.openstreetmap.josm.actions.PasteTagsAction; 084import org.openstreetmap.josm.actions.PreferenceToggleAction; 085import org.openstreetmap.josm.actions.PreferencesAction; 086import org.openstreetmap.josm.actions.PurgeAction; 087import org.openstreetmap.josm.actions.RedoAction; 088import org.openstreetmap.josm.actions.ReportBugAction; 089import org.openstreetmap.josm.actions.RestartAction; 090import org.openstreetmap.josm.actions.ReverseWayAction; 091import org.openstreetmap.josm.actions.SaveAction; 092import org.openstreetmap.josm.actions.SaveAsAction; 093import org.openstreetmap.josm.actions.SearchNotesDownloadAction; 094import org.openstreetmap.josm.actions.SelectAllAction; 095import org.openstreetmap.josm.actions.SelectNonBranchingWaySequencesAction; 096import org.openstreetmap.josm.actions.SessionLoadAction; 097import org.openstreetmap.josm.actions.SessionSaveAsAction; 098import org.openstreetmap.josm.actions.ShowStatusReportAction; 099import org.openstreetmap.josm.actions.SimplifyWayAction; 100import org.openstreetmap.josm.actions.SplitWayAction; 101import org.openstreetmap.josm.actions.ToggleGPXLinesAction; 102import org.openstreetmap.josm.actions.UnGlueAction; 103import org.openstreetmap.josm.actions.UnJoinNodeWayAction; 104import org.openstreetmap.josm.actions.UndoAction; 105import org.openstreetmap.josm.actions.UnselectAllAction; 106import org.openstreetmap.josm.actions.UpdateDataAction; 107import org.openstreetmap.josm.actions.UpdateModifiedAction; 108import org.openstreetmap.josm.actions.UpdateSelectionAction; 109import org.openstreetmap.josm.actions.UploadAction; 110import org.openstreetmap.josm.actions.UploadSelectionAction; 111import org.openstreetmap.josm.actions.ViewportFollowToggleAction; 112import org.openstreetmap.josm.actions.WireframeToggleAction; 113import org.openstreetmap.josm.actions.ZoomInAction; 114import org.openstreetmap.josm.actions.ZoomOutAction; 115import org.openstreetmap.josm.actions.audio.AudioBackAction; 116import org.openstreetmap.josm.actions.audio.AudioFasterAction; 117import org.openstreetmap.josm.actions.audio.AudioFwdAction; 118import org.openstreetmap.josm.actions.audio.AudioNextAction; 119import org.openstreetmap.josm.actions.audio.AudioPlayPauseAction; 120import org.openstreetmap.josm.actions.audio.AudioPrevAction; 121import org.openstreetmap.josm.actions.audio.AudioSlowerAction; 122import org.openstreetmap.josm.actions.search.SearchAction; 123import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 124import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 125import org.openstreetmap.josm.gui.io.RecentlyOpenedFilesMenu; 126import org.openstreetmap.josm.gui.layer.Layer; 127import org.openstreetmap.josm.gui.mappaint.MapPaintMenu; 128import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference; 129import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference; 130import org.openstreetmap.josm.gui.tagging.TaggingPresetSearchAction; 131import org.openstreetmap.josm.gui.tagging.TaggingPresetSearchPrimitiveDialog; 132import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField; 133import org.openstreetmap.josm.tools.ImageProvider; 134import org.openstreetmap.josm.tools.Shortcut; 135 136/** 137 * This is the JOSM main menu bar. It is overwritten to initialize itself and provide all menu 138 * entries as member variables (sort of collect them). 139 * 140 * It also provides possibilities to attach new menu entries (used by plugins). 141 * 142 * @author Immanuel.Scholz 143 */ 144public class MainMenu extends JMenuBar { 145 146 /* File menu */ 147 /** File / New Layer **/ 148 public final NewAction newAction = new NewAction(); 149 /** File / Open... **/ 150 public final OpenFileAction openFile = new OpenFileAction(); 151 /** File / Open Recent > **/ 152 public final RecentlyOpenedFilesMenu recentlyOpened = new RecentlyOpenedFilesMenu(); 153 /** File / Open Location... **/ 154 public final OpenLocationAction openLocation = new OpenLocationAction(); 155 /** File / Save **/ 156 public final SaveAction save = SaveAction.getInstance(); 157 /** File / Save As... **/ 158 public final SaveAsAction saveAs = SaveAsAction.getInstance(); 159 /** File / Session > Load Session **/ 160 public SessionLoadAction sessionLoad; 161 /** File / Session > Save Session As... **/ 162 public SessionSaveAsAction sessionSaveAs; 163 /** File / Export to GPX... **/ 164 public final GpxExportAction gpxExport = new GpxExportAction(); 165 /** File / Download from OSM... **/ 166 public final DownloadAction download = new DownloadAction(); 167 /** File / Download object... **/ 168 public final DownloadPrimitiveAction downloadPrimitive = new DownloadPrimitiveAction(); 169 /** File / Search Notes... **/ 170 public final SearchNotesDownloadAction searchNotes = new SearchNotesDownloadAction(); 171 /** File / Download parent ways/relations... **/ 172 public final DownloadReferrersAction downloadReferrers = new DownloadReferrersAction(); 173 /** File / Close open changesets... **/ 174 public final CloseChangesetAction closeChangesetAction = new CloseChangesetAction(); 175 /** File / Update data **/ 176 public final JosmAction update = new UpdateDataAction(); 177 /** File / Update selection **/ 178 public final JosmAction updateSelection = new UpdateSelectionAction(); 179 /** File / Update modified **/ 180 public final JosmAction updateModified = new UpdateModifiedAction(); 181 /** File / Upload data **/ 182 public final JosmAction upload = new UploadAction(); 183 /** File / Upload selection **/ 184 public final JosmAction uploadSelection = new UploadSelectionAction(); 185 /** File / Restart **/ 186 public final RestartAction restart = new RestartAction(); 187 /** File / Exit **/ 188 public final ExitAction exit = new ExitAction(); 189 190 /* Edit menu */ 191 /** Edit / Undo... */ 192 public final UndoAction undo = new UndoAction(); 193 /** Edit / Redo */ 194 public final RedoAction redo = new RedoAction(); 195 /** Edit / Copy */ 196 public final CopyAction copy = new CopyAction(); 197 /** Edit / Copy Coordinates */ 198 public final JosmAction copyCoordinates = new CopyCoordinatesAction(); 199 /** Edit / Paste */ 200 public final PasteAction paste = new PasteAction(); 201 /** Edit / Paste Tags */ 202 public final PasteTagsAction pasteTags = new PasteTagsAction(); 203 /** Edit / Duplicate */ 204 public final DuplicateAction duplicate = new DuplicateAction(); 205 /** Edit / Delete */ 206 public final DeleteAction delete = new DeleteAction(); 207 /** Edit / Purge... */ 208 public final JosmAction purge = new PurgeAction(); 209 /** Edit / Merge layer */ 210 public final MergeLayerAction merge = new MergeLayerAction(); 211 /** Edit / Merge selection */ 212 public final MergeSelectionAction mergeSelected = new MergeSelectionAction(); 213 /** Edit / Search... */ 214 public final SearchAction search = new SearchAction(); 215 /** Edit / Preferences */ 216 public final PreferencesAction preferences = new PreferencesAction(); 217 218 /* View menu */ 219 /** View / Wireframe View */ 220 public final WireframeToggleAction wireFrameToggleAction = new WireframeToggleAction(); 221 public final JosmAction toggleGPXLines = new ToggleGPXLinesAction(); 222 /** View / Advanced info */ 223 public final InfoAction info = new InfoAction(); 224 /** View / Advanced info (web) */ 225 public final InfoWebAction infoweb = new InfoWebAction(); 226 /** View / History */ 227 public final HistoryInfoAction historyinfo = new HistoryInfoAction(); 228 /** View / History (web) */ 229 public final HistoryInfoWebAction historyinfoweb = new HistoryInfoWebAction(); 230 /** View / "Zoom to"... actions */ 231 public final Map<String, AutoScaleAction> autoScaleActions = new HashMap<>(); 232 /** View / Jump to position */ 233 public final JumpToAction jumpToAct = new JumpToAction(); 234 235 /* Tools menu */ 236 /** Tools / Split Way */ 237 public final SplitWayAction splitWay = new SplitWayAction(); 238 /** Tools / Combine Way */ 239 public final CombineWayAction combineWay = new CombineWayAction(); 240 /** Tools / Reverse Ways */ 241 public final ReverseWayAction reverseWay = new ReverseWayAction(); 242 /** Tools / Simplify Way */ 243 public final SimplifyWayAction simplifyWay = new SimplifyWayAction(); 244 /** Tools / Align Nodes in Circle */ 245 public final AlignInCircleAction alignInCircle = new AlignInCircleAction(); 246 /** Tools / Align Nodes in Line */ 247 public final AlignInLineAction alignInLine = new AlignInLineAction(); 248 /** Tools / Distribute Nodes */ 249 public final DistributeAction distribute = new DistributeAction(); 250 /** Tools / Orthogonalize Shape */ 251 public final OrthogonalizeAction ortho = new OrthogonalizeAction(); 252 /** Orthogonalize undo. Action is not shown in the menu. Only triggered by shortcut */ 253 public final Undo orthoUndo = new Undo(); 254 /** Tools / Mirror */ 255 public final MirrorAction mirror = new MirrorAction(); 256 /** Tools / Follow line */ 257 public final FollowLineAction followLine = new FollowLineAction(); 258 /** Tools / Add Node... */ 259 public final AddNodeAction addNode = new AddNodeAction(); 260 /** Tools / Move Node... */ 261 public final MoveNodeAction moveNode = new MoveNodeAction(); 262 /** Tools / Create Circle */ 263 public final CreateCircleAction createCircle = new CreateCircleAction(); 264 /** Tools / Merge Nodes */ 265 public final MergeNodesAction mergeNodes = new MergeNodesAction(); 266 /** Tools / Join Node to Way */ 267 public final JoinNodeWayAction joinNodeWay = JoinNodeWayAction.createJoinNodeToWayAction(); 268 /** Tools / Join Way to Node */ 269 public final JoinNodeWayAction moveNodeOntoWay = JoinNodeWayAction.createMoveNodeOntoWayAction(); 270 /** Tools / Disconnect Node from Way */ 271 public final UnJoinNodeWayAction unJoinNodeWay = new UnJoinNodeWayAction(); 272 /** Tools / Unglue Ways */ 273 public final UnGlueAction unglueNodes = new UnGlueAction(); 274 /** Tools / Join overlapping Areas */ 275 public final JoinAreasAction joinAreas = new JoinAreasAction(); 276 /** Tools / Create multipolygon */ 277 public final CreateMultipolygonAction createMultipolygon = new CreateMultipolygonAction(false); 278 /** Tools / Update multipolygon */ 279 public final CreateMultipolygonAction updateMultipolygon = new CreateMultipolygonAction(true); 280 281 /* Selection menu */ 282 /** Selection / Select All */ 283 public final SelectAllAction selectAll = new SelectAllAction(); 284 /** Selection / Unselect All */ 285 public final UnselectAllAction unselectAll = new UnselectAllAction(); 286 /** Selection / Non-branching way sequences */ 287 public final SelectNonBranchingWaySequencesAction nonBranchingWaySequences = new SelectNonBranchingWaySequencesAction(); 288 289 /* Audio menu */ 290 /** Audio / Play/Pause */ 291 public final JosmAction audioPlayPause = new AudioPlayPauseAction(); 292 /** Audio / Next marker */ 293 public final JosmAction audioNext = new AudioNextAction(); 294 /** Audio / Previous Marker */ 295 public final JosmAction audioPrev = new AudioPrevAction(); 296 /** Audio / Forward */ 297 public final JosmAction audioFwd = new AudioFwdAction(); 298 /** Audio / Back */ 299 public final JosmAction audioBack = new AudioBackAction(); 300 /** Audio / Faster */ 301 public final JosmAction audioFaster = new AudioFasterAction(); 302 /** Audio / Slower */ 303 public final JosmAction audioSlower = new AudioSlowerAction(); 304 305 /* Windows Menu */ 306 /** Windows / Changeset Manager */ 307 public final ChangesetManagerToggleAction changesetManager = new ChangesetManagerToggleAction(); 308 309 /* Help menu */ 310 /** Help / Help */ 311 public final HelpAction help = new HelpAction(); 312 /** Help / About */ 313 public final AboutAction about = new AboutAction(); 314 /** Help / Show Status Report */ 315 public final ShowStatusReportAction statusreport = new ShowStatusReportAction(); 316 /** Help / Report bug */ 317 public final ReportBugAction reportbug = new ReportBugAction(); 318 319 /** 320 * fileMenu contains I/O actions 321 */ 322 public final JMenu fileMenu = addMenu(marktr("File"), KeyEvent.VK_F, 0, ht("/Menu/File")); 323 /** 324 * sessionMenu is a submenu of File menu containing all session actions 325 */ 326 public final JMenu sessionMenu = new JMenu(tr("Session")); 327 /** 328 * editMenu contains editing actions 329 */ 330 public final JMenu editMenu = addMenu(marktr("Edit"), KeyEvent.VK_E, 1, ht("/Menu/Edit")); 331 /** 332 * viewMenu contains display actions (zoom, map styles, etc.) 333 */ 334 public final JMenu viewMenu = addMenu(marktr("View"), KeyEvent.VK_V, 2, ht("/Menu/View")); 335 /** 336 * toolsMenu contains different geometry manipulation actions from JOSM core (most used) 337 * The plugins should use other menus 338 */ 339 public final JMenu toolsMenu = addMenu(marktr("Tools"), KeyEvent.VK_T, 3, ht("/Menu/Tools")); 340 /** 341 * moreToolsMenu contains geometry-related actions from all the plugins 342 * @since 6082 (moved from Utilsplugin2) 343 */ 344 public final JMenu moreToolsMenu = addMenu(marktr("More tools"), KeyEvent.VK_M, 4, ht("/Menu/MoreTools")); 345 /** 346 * dataMenu contains plugin actions that are related to certain tagging schemes (addressing opening hours), 347 * importing external data and using external web APIs 348 * @since 6082 349 */ 350 public final JMenu dataMenu = addMenu(marktr("Data"), KeyEvent.VK_D, 5, ht("/Menu/Data")); 351 /** 352 * selectionMenu contains all actions related to selecting different objects 353 * @since 6082 (moved from Utilsplugin2) 354 */ 355 public final JMenu selectionMenu = addMenu(marktr("Selection"), KeyEvent.VK_N, 6, ht("/Menu/Selection")); 356 /** 357 * presetsMenu contains presets actions (search, presets tree) 358 */ 359 public final JMenu presetsMenu = addMenu(marktr("Presets"), KeyEvent.VK_P, 7, ht("/Menu/Presets")); 360 /** 361 * submenu in Imagery menu that contains plugin-managed additional imagery layers 362 * @since 6097 363 */ 364 public final JMenu imagerySubMenu = new JMenu(tr("More...")); 365 /** 366 * imageryMenu contains all imagery-related actions 367 */ 368 public final ImageryMenu imageryMenu = addMenu(new ImageryMenu(imagerySubMenu), marktr("Imagery"), KeyEvent.VK_I, 8, ht("/Menu/Imagery")); 369 /** 370 * gpsMenu contains all plugin actions that are related 371 * to using GPS data, including opening, uploading and real-time tracking 372 * @since 6082 373 */ 374 public final JMenu gpsMenu = addMenu(marktr("GPS"), KeyEvent.VK_G, 9, ht("/Menu/GPS")); 375 /** the window menu is split into several groups. The first is for windows that can be opened from 376 * this menu any time, e.g. the changeset editor. The second group is for toggle dialogs and the third 377 * group is for currently open windows that cannot be toggled, e.g. relation editors. It's recommended 378 * to use WINDOW_MENU_GROUP to determine the group integer. 379 */ 380 public final JMenu windowMenu = addMenu(marktr("Windows"), KeyEvent.VK_W, 10, ht("/Menu/Windows")); 381 public static enum WINDOW_MENU_GROUP { ALWAYS, TOGGLE_DIALOG, VOLATILE } 382 383 /** 384 * audioMenu contains all audio-related actions. Be careful, this menu is not guaranteed to be displayed at all 385 */ 386 public JMenu audioMenu = null; 387 /** 388 * helpMenu contains JOSM general actions (Help, About, etc.) 389 */ 390 public final JMenu helpMenu = addMenu(marktr("Help"), KeyEvent.VK_H, 11, ht("/Menu/Help")); 391 392 private static final int defaultMenuPos = 11; 393 394 public final JosmAction moveUpAction = new MoveAction(MoveAction.Direction.UP); 395 public final JosmAction moveDownAction = new MoveAction(MoveAction.Direction.DOWN); 396 public final JosmAction moveLeftAction = new MoveAction(MoveAction.Direction.LEFT); 397 public final JosmAction moveRightAction = new MoveAction(MoveAction.Direction.RIGHT); 398 399 public final TaggingPresetSearchAction presetSearchAction = new TaggingPresetSearchAction(); 400 public final TaggingPresetSearchPrimitiveDialog.Action presetSearchPrimitiveAction = new TaggingPresetSearchPrimitiveDialog.Action(); 401 public final DialogsToggleAction dialogsToggleAction = new DialogsToggleAction(); 402 public FullscreenToggleAction fullscreenToggleAction = null; 403 404 /** 405 * Popup menu to display menu items search result. 406 */ 407 private JPopupMenu searchResultsMenu = new JPopupMenu(); 408 409 /** this menu listener hides unnecessary JSeparators in a menu list but does not remove them. 410 * If at a later time the separators are required, they will be made visible again. Intended 411 * usage is make menus not look broken if separators are used to group the menu and some of 412 * these groups are empty. 413 */ 414 public static final MenuListener menuSeparatorHandler = new MenuListener() { 415 @Override 416 public void menuCanceled(MenuEvent arg0) {} 417 @Override 418 public void menuDeselected(MenuEvent arg0) {} 419 @Override 420 public void menuSelected(MenuEvent a) { 421 if(!(a.getSource() instanceof JMenu)) 422 return; 423 final JPopupMenu m = ((JMenu) a.getSource()).getPopupMenu(); 424 for(int i=0; i < m.getComponentCount()-1; i++) { 425 if(!(m.getComponent(i) instanceof JSeparator)) { 426 continue; 427 } 428 // hide separator if the next menu item is one as well 429 ((JSeparator) m.getComponent(i)).setVisible(!(m.getComponent(i+1) instanceof JSeparator)); 430 } 431 // hide separator at the end of the menu 432 if(m.getComponent(m.getComponentCount()-1) instanceof JSeparator) { 433 ((JSeparator) m.getComponent(m.getComponentCount()-1)).setVisible(false); 434 } 435 } 436 }; 437 438 /** 439 * @since 6088 440 * @return the default position of tnew top-level menus 441 */ 442 public int getDefaultMenuPos() { 443 return defaultMenuPos; 444 } 445 446 /** 447 * Add a JosmAction at the end of a menu. 448 * 449 * This method handles all the shortcut handling. It also makes sure that actions that are 450 * handled by the OS are not duplicated on the menu. 451 * @param menu the menu to add the action to 452 * @param action the action that should get a menu item 453 * @return the created menu item 454 */ 455 public static JMenuItem add(JMenu menu, JosmAction action) { 456 return add(menu, action, false); 457 } 458 459 /** 460 * Add a JosmAction at the end of a menu. 461 * 462 * This method handles all the shortcut handling. It also makes sure that actions that are 463 * handled by the OS are not duplicated on the menu. 464 * @param menu the menu to add the action to 465 * @param action the action that should get a menu item 466 * @param isExpert whether the entry should only be visible if the expert mode is activated 467 * @return the created menu item 468 */ 469 public static JMenuItem add(JMenu menu, JosmAction action, boolean isExpert) { 470 return add(menu, action, isExpert, null); 471 } 472 473 /** 474 * Add a JosmAction at the end of a menu. 475 * 476 * This method handles all the shortcut handling. It also makes sure that actions that are 477 * handled by the OS are not duplicated on the menu. 478 * @param menu the menu to add the action to 479 * @param action the action that should get a menu item 480 * @param isExpert whether the entry should only be visible if the expert mode is activated 481 * @param index an integer specifying the position at which to add the action 482 * @return the created menu item 483 */ 484 public static JMenuItem add(JMenu menu, JosmAction action, boolean isExpert, Integer index) { 485 if (action.getShortcut().getAutomatic()) 486 return null; 487 final JMenuItem menuitem; 488 if (index == null) { 489 menuitem = menu.add(action); 490 } else { 491 menuitem = menu.insert(action, index); 492 } 493 if (isExpert) { 494 ExpertToggleAction.addVisibilitySwitcher(menuitem); 495 } 496 KeyStroke ks = action.getShortcut().getKeyStroke(); 497 if (ks != null) { 498 menuitem.setAccelerator(ks); 499 } 500 // some menus are hidden before they are populated with some items by plugins 501 if (!menu.isVisible()) menu.setVisible(true); 502 return menuitem; 503 } 504 505 /** 506 * Add the JosmAction {@code actionToBeInserted} directly below {@code existingMenuEntryAction}. 507 * 508 * This method handles all the shortcut handling. It also makes sure that actions that are 509 * handled by the OS are not duplicated on the menu. 510 * @param menu the menu to add the action to 511 * @param actionToBeInserted the action that should get a menu item directly below {@code existingMenuEntryAction} 512 * @param isExpert whether the entry should only be visible if the expert mode is activated 513 * @param existingMenuEntryAction an action already added to the menu {@code menu}, the action {@code actionToBeInserted} is added directly below 514 * @return the created menu item 515 */ 516 public static JMenuItem addAfter(JMenu menu, JosmAction actionToBeInserted, boolean isExpert, JosmAction existingMenuEntryAction) { 517 int i = 0; 518 for (Component c : menu.getMenuComponents()) { 519 if (c instanceof JMenuItem && ((JMenuItem) c).getAction() == existingMenuEntryAction) { 520 break; 521 } 522 i++; 523 } 524 return add(menu, actionToBeInserted, isExpert, i + 1); 525 } 526 527 /** 528 * Add a JosmAction to a menu. 529 * 530 * This method handles all the shortcut handling. It also makes sure that actions that are 531 * handled by the OS are not duplicated on the menu. 532 * @param menu to add the action to 533 * @param action the action that should get a menu item 534 * @param group the item should be added to. Groups are split by a separator. 535 * 0 is the first group, -1 will add the item to the end. 536 * @return The created menu item 537 */ 538 public static <E extends Enum<E>> JMenuItem add(JMenu menu, JosmAction action, Enum<E> group) { 539 if (action.getShortcut().getAutomatic()) 540 return null; 541 int i = getInsertionIndexForGroup(menu, group.ordinal()); 542 JMenuItem menuitem = (JMenuItem) menu.add(new JMenuItem(action), i); 543 KeyStroke ks = action.getShortcut().getKeyStroke(); 544 if (ks != null) { 545 menuitem.setAccelerator(ks); 546 } 547 return menuitem; 548 } 549 550 /** 551 * Add a JosmAction to a menu and automatically prints accelerator if available. 552 * Also adds a checkbox that may be toggled. 553 * @param menu to add the action to 554 * @param action the action that should get a menu item 555 * @param group the item should be added to. Groups are split by a separator. Use 556 * one of the enums that are defined for some of the menus to tell in which 557 * group the item should go. 558 * @return The created menu item 559 */ 560 public static <E extends Enum<E>> JCheckBoxMenuItem addWithCheckbox(JMenu menu, JosmAction action, Enum<E> group) { 561 int i = getInsertionIndexForGroup(menu, group.ordinal()); 562 final JCheckBoxMenuItem mi = (JCheckBoxMenuItem) menu.add(new JCheckBoxMenuItem(action), i); 563 final KeyStroke ks = action.getShortcut().getKeyStroke(); 564 if (ks != null) { 565 mi.setAccelerator(ks); 566 } 567 return mi; 568 } 569 570 /** finds the correct insertion index for a given group and adds separators if necessary */ 571 private static int getInsertionIndexForGroup(JMenu menu, int group) { 572 if(group < 0) 573 return -1; 574 // look for separator that *ends* the group (or stop at end of menu) 575 int i; 576 for(i=0; i < menu.getItemCount() && group >= 0; i++) { 577 if(menu.getItem(i) == null) { 578 group--; 579 } 580 } 581 // insert before separator that ends the group 582 if(group < 0) { 583 i--; 584 } 585 // not enough separators have been found, add them 586 while(group > 0) { 587 menu.addSeparator(); 588 group--; 589 i++; 590 } 591 return i; 592 } 593 594 public JMenu addMenu(String name, int mnemonicKey, int position, String relativeHelpTopic) { 595 final JMenu menu = new JMenu(tr(name)); 596 if (!GraphicsEnvironment.isHeadless()) { 597 MenuScroller.setScrollerFor(menu); 598 } 599 return addMenu(menu, name, mnemonicKey, position, relativeHelpTopic); 600 } 601 602 public <T extends JMenu> T addMenu(T menu, String name, int mnemonicKey, int position, String relativeHelpTopic) { 603 Shortcut.registerShortcut("menu:" + name, tr("Menu: {0}", tr(name)), mnemonicKey, 604 Shortcut.MNEMONIC).setMnemonic(menu); 605 add(menu, position); 606 menu.putClientProperty("help", relativeHelpTopic); 607 return menu; 608 } 609 610 /** 611 * Constructs a new {@code MainMenu}. 612 */ 613 public MainMenu() { 614 JMenuItem current; 615 616 moreToolsMenu.setVisible(false); 617 dataMenu.setVisible(false); 618 gpsMenu.setVisible(false); 619 620 add(fileMenu, newAction); 621 add(fileMenu, openFile); 622 fileMenu.add(recentlyOpened); 623 add(fileMenu, openLocation); 624 fileMenu.addSeparator(); 625 add(fileMenu, save); 626 add(fileMenu, saveAs); 627 sessionMenu.setToolTipText(tr("Save and load the current session (list of layers, etc.)")); 628 sessionMenu.setIcon(new ImageProvider("session").setSize(ImageProvider.ImageSizes.MENU).get()); 629 sessionSaveAs = new SessionSaveAsAction(); 630 sessionLoad = new SessionLoadAction(); 631 add(sessionMenu, sessionSaveAs); 632 add(sessionMenu, sessionLoad); 633 fileMenu.add(sessionMenu); 634 ExpertToggleAction.addVisibilitySwitcher(sessionMenu); 635 add(fileMenu, gpxExport, true); 636 fileMenu.addSeparator(); 637 add(fileMenu, download); 638 add(fileMenu, downloadPrimitive); 639 add(fileMenu, searchNotes); 640 add(fileMenu, downloadReferrers); 641 add(fileMenu, update); 642 add(fileMenu, updateSelection); 643 add(fileMenu, updateModified); 644 fileMenu.addSeparator(); 645 add(fileMenu, upload); 646 add(fileMenu, uploadSelection); 647 Component sep = new JPopupMenu.Separator(); 648 fileMenu.add(sep); 649 ExpertToggleAction.addVisibilitySwitcher(sep); 650 add(fileMenu, closeChangesetAction, true); 651 fileMenu.addSeparator(); 652 add(fileMenu, restart); 653 add(fileMenu, exit); 654 655 add(editMenu, undo); 656 Main.main.undoRedo.addCommandQueueListener(undo); 657 add(editMenu, redo); 658 Main.main.undoRedo.addCommandQueueListener(redo); 659 editMenu.addSeparator(); 660 add(editMenu, copy); 661 add(editMenu, copyCoordinates, true); 662 add(editMenu, paste); 663 add(editMenu, pasteTags); 664 add(editMenu, duplicate); 665 add(editMenu, delete); 666 add(editMenu, purge, true); 667 editMenu.addSeparator(); 668 add(editMenu,merge); 669 add(editMenu,mergeSelected); 670 editMenu.addSeparator(); 671 add(editMenu, search); 672 add(editMenu, presetSearchPrimitiveAction); 673 editMenu.addSeparator(); 674 add(editMenu, preferences); 675 676 // -- wireframe toggle action 677 final JCheckBoxMenuItem wireframe = new JCheckBoxMenuItem(wireFrameToggleAction); 678 viewMenu.add(wireframe); 679 wireframe.setAccelerator(wireFrameToggleAction.getShortcut().getKeyStroke()); 680 wireFrameToggleAction.addButtonModel(wireframe.getModel()); 681 682 viewMenu.add(new MapPaintMenu()); 683 viewMenu.addSeparator(); 684 add(viewMenu, new ZoomInAction()); 685 add(viewMenu, new ZoomOutAction()); 686 viewMenu.addSeparator(); 687 for (String mode : AutoScaleAction.MODES) { 688 AutoScaleAction autoScaleAction = new AutoScaleAction(mode); 689 autoScaleActions.put(mode, autoScaleAction); 690 add(viewMenu, autoScaleAction); 691 } 692 693 // -- viewport follow toggle action 694 ViewportFollowToggleAction viewportFollowToggleAction = new ViewportFollowToggleAction(); 695 final JCheckBoxMenuItem vft = new JCheckBoxMenuItem(viewportFollowToggleAction); 696 ExpertToggleAction.addVisibilitySwitcher(vft); 697 viewMenu.add(vft); 698 vft.setAccelerator(viewportFollowToggleAction.getShortcut().getKeyStroke()); 699 viewportFollowToggleAction.addButtonModel(vft.getModel()); 700 701 if(Main.platform.canFullscreen()) { 702 // -- fullscreen toggle action 703 fullscreenToggleAction = new FullscreenToggleAction(); 704 final JCheckBoxMenuItem fullscreen = new JCheckBoxMenuItem(fullscreenToggleAction); 705 viewMenu.addSeparator(); 706 viewMenu.add(fullscreen); 707 fullscreen.setAccelerator(fullscreenToggleAction.getShortcut().getKeyStroke()); 708 fullscreenToggleAction.addButtonModel(fullscreen.getModel()); 709 } 710 711 // -- dialogs panel toggle action 712 final JCheckBoxMenuItem dialogsToggle = new JCheckBoxMenuItem(dialogsToggleAction); 713 dialogsToggle.setAccelerator(dialogsToggleAction.getShortcut().getKeyStroke()); 714 dialogsToggleAction.addButtonModel(dialogsToggle.getModel()); 715 viewMenu.add(dialogsToggle); 716 717 add(viewMenu, jumpToAct, true); 718 viewMenu.addSeparator(); 719 add(viewMenu, info); 720 add(viewMenu, infoweb); 721 add(viewMenu, historyinfo); 722 add(viewMenu, historyinfoweb); 723 viewMenu.addSeparator(); 724 viewMenu.add(new PreferenceToggleAction(tr("Edit toolbar"), 725 tr("Toggles the visibility of the edit toolbar (i.e., the vertical tool)"), 726 "sidetoolbar.visible", true).getCheckbox()); 727 // -- expert mode toggle action 728 final JCheckBoxMenuItem expertItem = new JCheckBoxMenuItem(ExpertToggleAction.getInstance()); 729 viewMenu.add(expertItem); 730 ExpertToggleAction.getInstance().addButtonModel(expertItem.getModel()); 731 732 add(presetsMenu, presetSearchAction); 733 add(presetsMenu, presetSearchPrimitiveAction); 734 add(presetsMenu, PreferencesAction.forPreferenceSubTab(tr("Preset preferences"), 735 tr("Click to open the tagging presets tab in the preferences"), TaggingPresetPreference.class)); 736 presetsMenu.addSeparator(); 737 738 add(imageryMenu, PreferencesAction.forPreferenceTab(tr("Imagery preferences"), 739 tr("Click to open the imagery tab in the preferences"), ImageryPreference.class)); 740 741 add(selectionMenu, selectAll); 742 add(selectionMenu, unselectAll); 743 add(selectionMenu, nonBranchingWaySequences); 744 745 add(toolsMenu, splitWay); 746 add(toolsMenu, combineWay); 747 toolsMenu.addSeparator(); 748 add(toolsMenu, reverseWay); 749 add(toolsMenu, simplifyWay); 750 toolsMenu.addSeparator(); 751 add(toolsMenu, alignInCircle); 752 add(toolsMenu, alignInLine); 753 add(toolsMenu, distribute); 754 add(toolsMenu, ortho); 755 add(toolsMenu, mirror, true); 756 toolsMenu.addSeparator(); 757 add(toolsMenu, followLine, true); 758 add(toolsMenu, addNode, true); 759 add(toolsMenu, moveNode, true); 760 add(toolsMenu, createCircle); 761 toolsMenu.addSeparator(); 762 add(toolsMenu, mergeNodes); 763 add(toolsMenu, joinNodeWay); 764 add(toolsMenu, moveNodeOntoWay); 765 add(toolsMenu, unJoinNodeWay); 766 add(toolsMenu, unglueNodes); 767 toolsMenu.addSeparator(); 768 add(toolsMenu, joinAreas); 769 add(toolsMenu, createMultipolygon); 770 add(toolsMenu, updateMultipolygon); 771 772 // -- changeset manager toggle action 773 final JCheckBoxMenuItem mi = MainMenu.addWithCheckbox(windowMenu, changesetManager, 774 MainMenu.WINDOW_MENU_GROUP.ALWAYS); 775 changesetManager.addButtonModel(mi.getModel()); 776 777 if (!Main.pref.getBoolean("audio.menuinvisible", false)) { 778 showAudioMenu(true); 779 } 780 781 Main.pref.addPreferenceChangeListener(new PreferenceChangedListener() { 782 @Override 783 public void preferenceChanged(PreferenceChangeEvent e) { 784 if ("audio.menuinvisible".equals(e.getKey())) { 785 showAudioMenu(!Boolean.parseBoolean(e.getNewValue().toString())); 786 } 787 } 788 }); 789 790 helpMenu.add(statusreport); 791 helpMenu.add(reportbug); 792 helpMenu.addSeparator(); 793 794 current = helpMenu.add(help); // FIXME why is help not a JosmAction? 795 current.setAccelerator(Shortcut.registerShortcut("system:help", tr("Help"), KeyEvent.VK_F1, 796 Shortcut.DIRECT).getKeyStroke()); 797 add(helpMenu, about); 798 add(Box.createHorizontalGlue()); 799 final DisableShortcutsOnFocusGainedTextField searchField = createSearchField(); 800 add(searchField); 801 802 // Do not let search field take the focus automatically 803 setFocusTraversalPolicyProvider(true); 804 setFocusTraversalPolicy(new DefaultFocusTraversalPolicy() { 805 @Override 806 protected boolean accept(Component aComponent) { 807 return super.accept(aComponent) && !searchField.equals(aComponent); 808 } 809 }); 810 811 windowMenu.addMenuListener(menuSeparatorHandler); 812 813 new PresetsMenuEnabler(presetsMenu).refreshEnabled(); 814 } 815 816 private int getMaximumAvailableWidth() { 817 int maxWidth = getSize().width; 818 for (int i = 0; i < getMenuCount(); i++) { 819 JMenu menu = getMenu(i); 820 if (menu != null) { 821 maxWidth -= menu.getPreferredSize().width; 822 } 823 } 824 return maxWidth; 825 } 826 827 /** 828 * Create search field. 829 */ 830 private DisableShortcutsOnFocusGainedTextField createSearchField() { 831 DisableShortcutsOnFocusGainedTextField searchField = new DisableShortcutsOnFocusGainedTextField() { 832 @Override 833 public Dimension getPreferredSize() { 834 // JMenuBar uses a BoxLayout and it doesn't seem possible to specify a size factor, 835 // so compute the preferred size dynamically 836 return new Dimension(Math.min(200, Math.max(25, getMaximumAvailableWidth())), 837 helpMenu.getPreferredSize().height); 838 } 839 }; 840 searchField.setEditable(true); 841 searchField.setMaximumSize(new Dimension(200, helpMenu.getPreferredSize().height)); 842 searchField.setHint(tr("Search menu items")); 843 searchField.setToolTipText(tr("Search menu items")); 844 searchField.addKeyListener(new SearchFieldKeyListener()); 845 searchField.getDocument().addDocumentListener(new SearchFieldTextListener(this, searchField)); 846 return searchField; 847 } 848 849 /** 850 * Search main menu for items with {@code textToFind} in title. 851 * @param textToFind The text to find 852 * @return not null list of found menu items. 853 */ 854 private List<JMenuItem> findMenuItems(String textToFind) { 855 textToFind = textToFind.toLowerCase(); 856 List<JMenuItem> result = new ArrayList<>(); 857 858 // Iterate over main menus 859 for (MenuElement menuElement : getSubElements()) { 860 if( !(menuElement instanceof JMenu)) continue; 861 862 JMenu mainMenuItem = (JMenu) menuElement; 863 if(mainMenuItem.getAction()!=null && mainMenuItem.getText().toLowerCase().contains(textToFind)) { 864 result.add(mainMenuItem); 865 } 866 867 //Search recursively 868 findMenuItems(mainMenuItem, textToFind, result); 869 } 870 return result; 871 } 872 873 /** 874 * Recursive walker for menu items. Only menu items with action are selected. If menu item 875 * contains {@code textToFind} it's appended to result. 876 * @param menu menu in which search will be performed 877 * @param textToFind The text to find 878 * @param result resulting list ofmenu items 879 */ 880 private void findMenuItems(final JMenu menu, final String textToFind, final List<JMenuItem> result) { 881 for (int i=0; i<menu.getItemCount(); i++) { 882 JMenuItem menuItem = menu.getItem(i); 883 if (menuItem == null) continue; 884 885 if (menuItem.getAction()!=null && menuItem.getText().toLowerCase().contains(textToFind)) { 886 result.add(menuItem); 887 } 888 889 // Go recursive if needed 890 if (menuItem instanceof JMenu) { 891 findMenuItems((JMenu) menuItem, textToFind, result); 892 } 893 } 894 } 895 896 protected void showAudioMenu(boolean showMenu) { 897 if (showMenu && audioMenu == null) { 898 audioMenu = addMenu(marktr("Audio"), KeyEvent.VK_U, defaultMenuPos, ht("/Menu/Audio")); 899 add(audioMenu, audioPlayPause); 900 add(audioMenu, audioNext); 901 add(audioMenu, audioPrev); 902 add(audioMenu, audioFwd); 903 add(audioMenu, audioBack); 904 add(audioMenu, audioSlower); 905 add(audioMenu, audioFaster); 906 validate(); 907 } else if (!showMenu && audioMenu != null) { 908 remove(audioMenu); 909 audioMenu.removeAll(); 910 audioMenu = null; 911 validate(); 912 } 913 } 914 915 static class PresetsMenuEnabler implements MapView.LayerChangeListener { 916 private JMenu presetsMenu; 917 public PresetsMenuEnabler(JMenu presetsMenu) { 918 MapView.addLayerChangeListener(this); 919 this.presetsMenu = presetsMenu; 920 } 921 /** 922 * Refreshes the enabled state 923 */ 924 protected void refreshEnabled() { 925 presetsMenu.setEnabled(Main.main.hasEditLayer()); 926 } 927 928 @Override 929 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 930 refreshEnabled(); 931 } 932 933 @Override 934 public void layerAdded(Layer newLayer) { 935 refreshEnabled(); 936 } 937 938 @Override 939 public void layerRemoved(Layer oldLayer) { 940 refreshEnabled(); 941 } 942 } 943 944 /** 945 * This listener is designed to handle ENTER key pressed in menu search field. 946 * When user presses Enter key then selected item of "searchResultsMenu" is triggered. 947 */ 948 private class SearchFieldKeyListener implements KeyListener { 949 950 @Override 951 public void keyPressed(KeyEvent e) { 952 if (e.getKeyCode() == KeyEvent.VK_ENTER) { 953 // On ENTER selected menu item must be triggered 954 MenuElement[] selection = MenuSelectionManager.defaultManager().getSelectedPath(); 955 if(selection.length > 1) { 956 MenuElement selectedElement = selection[selection.length-1]; 957 if(selectedElement instanceof JMenuItem) { 958 JMenuItem selectedItem = (JMenuItem) selectedElement; 959 Action menuAction = selectedItem.getAction(); 960 menuAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, null)); 961 e.consume(); 962 } 963 } 964 } 965 } 966 967 @Override 968 public void keyTyped(KeyEvent e) { } 969 970 @Override 971 public void keyReleased(KeyEvent e) { } 972 } 973 974 private class SearchFieldTextListener implements DocumentListener { 975 private final JTextField searchField; 976 private final MainMenu mainMenu; 977 private String currentSearchText = null; 978 979 public SearchFieldTextListener(MainMenu mainMenu, JTextField searchField) { 980 this.mainMenu = mainMenu; 981 this.searchField = searchField; 982 } 983 984 @Override 985 public void insertUpdate(DocumentEvent e) { 986 doSearch(searchField.getText()); 987 } 988 989 @Override 990 public void removeUpdate(DocumentEvent e) { 991 doSearch(searchField.getText()); 992 } 993 994 @Override 995 public void changedUpdate(DocumentEvent e) { 996 doSearch(searchField.getText()); 997 } 998 999 //TODO: perform some delay (maybe 200 ms) before actual searching. 1000 void doSearch(String searchTerm) { 1001 searchTerm = searchTerm.trim().toLowerCase(); 1002 1003 if (searchTerm.equals(currentSearchText)) { 1004 return; 1005 } 1006 currentSearchText = searchTerm; 1007 if (searchTerm.length() == 0) { 1008 // No text to search 1009 hideMenu(); 1010 return; 1011 } 1012 1013 List<JMenuItem> searchResult = mainMenu.findMenuItems(currentSearchText); 1014 if(searchResult.size() == 0) { 1015 // Nothing found 1016 hideMenu(); 1017 return; 1018 } 1019 1020 if(searchResult.size() > 20) { 1021 // Too many items found... 1022 searchResult = searchResult.subList(0, 20); 1023 } 1024 1025 // Update Popup menu 1026 searchResultsMenu.removeAll(); 1027 for (JMenuItem foundItem : searchResult) { 1028 searchResultsMenu.add(foundItem.getText()).setAction(foundItem.getAction()); 1029 } 1030 // Put menu right under search field 1031 searchResultsMenu.pack(); 1032 searchResultsMenu.show(mainMenu, searchField.getX(), searchField.getY() + searchField.getHeight()); 1033 1034 // This is tricky. User still is able to edit search text. While Up and Down keys are handled by Popup Menu. 1035 searchField.requestFocusInWindow(); 1036 } 1037 1038 private void hideMenu() { 1039 searchResultsMenu.setVisible(false); 1040 } 1041 } 1042}