001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.BorderLayout; 007import java.awt.Component; 008import java.awt.GridBagConstraints; 009import java.awt.GridBagLayout; 010import java.awt.Window; 011import java.awt.event.ComponentEvent; 012import java.awt.event.ComponentListener; 013import java.awt.event.KeyEvent; 014import java.awt.event.WindowAdapter; 015import java.awt.event.WindowEvent; 016import java.io.File; 017import java.lang.ref.WeakReference; 018import java.net.URI; 019import java.net.URISyntaxException; 020import java.net.URL; 021import java.text.MessageFormat; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.HashSet; 028import java.util.Iterator; 029import java.util.List; 030import java.util.Map; 031import java.util.Objects; 032import java.util.Set; 033import java.util.StringTokenizer; 034import java.util.concurrent.Callable; 035import java.util.concurrent.ExecutorService; 036import java.util.concurrent.Executors; 037import java.util.concurrent.Future; 038 039import javax.swing.Action; 040import javax.swing.InputMap; 041import javax.swing.JComponent; 042import javax.swing.JFrame; 043import javax.swing.JOptionPane; 044import javax.swing.JPanel; 045import javax.swing.JTextArea; 046import javax.swing.KeyStroke; 047import javax.swing.LookAndFeel; 048import javax.swing.UIManager; 049import javax.swing.UnsupportedLookAndFeelException; 050 051import org.openstreetmap.gui.jmapviewer.FeatureAdapter; 052import org.openstreetmap.josm.actions.JosmAction; 053import org.openstreetmap.josm.actions.OpenFileAction; 054import org.openstreetmap.josm.actions.downloadtasks.DownloadGpsTask; 055import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask; 056import org.openstreetmap.josm.actions.downloadtasks.DownloadTask; 057import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler; 058import org.openstreetmap.josm.actions.mapmode.DrawAction; 059import org.openstreetmap.josm.actions.mapmode.MapMode; 060import org.openstreetmap.josm.actions.search.SearchAction; 061import org.openstreetmap.josm.data.Bounds; 062import org.openstreetmap.josm.data.Preferences; 063import org.openstreetmap.josm.data.UndoRedoHandler; 064import org.openstreetmap.josm.data.ViewportData; 065import org.openstreetmap.josm.data.coor.CoordinateFormat; 066import org.openstreetmap.josm.data.coor.LatLon; 067import org.openstreetmap.josm.data.osm.DataSet; 068import org.openstreetmap.josm.data.osm.OsmPrimitive; 069import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy; 070import org.openstreetmap.josm.data.projection.Projection; 071import org.openstreetmap.josm.data.projection.ProjectionChangeListener; 072import org.openstreetmap.josm.data.validation.OsmValidator; 073import org.openstreetmap.josm.gui.GettingStarted; 074import org.openstreetmap.josm.gui.MainApplication.Option; 075import org.openstreetmap.josm.gui.MainMenu; 076import org.openstreetmap.josm.gui.MapFrame; 077import org.openstreetmap.josm.gui.MapFrameListener; 078import org.openstreetmap.josm.gui.MapView; 079import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 080import org.openstreetmap.josm.gui.help.HelpUtil; 081import org.openstreetmap.josm.gui.io.SaveLayersDialog; 082import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer; 083import org.openstreetmap.josm.gui.layer.Layer; 084import org.openstreetmap.josm.gui.layer.OsmDataLayer; 085import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener; 086import org.openstreetmap.josm.gui.preferences.ToolbarPreferences; 087import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference; 088import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference; 089import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; 090import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor; 091import org.openstreetmap.josm.gui.progress.ProgressMonitorExecutor; 092import org.openstreetmap.josm.gui.tagging.TaggingPresets; 093import org.openstreetmap.josm.gui.util.RedirectInputMap; 094import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 095import org.openstreetmap.josm.io.FileWatcher; 096import org.openstreetmap.josm.io.OnlineResource; 097import org.openstreetmap.josm.io.OsmApi; 098import org.openstreetmap.josm.plugins.PluginHandler; 099import org.openstreetmap.josm.tools.CheckParameterUtil; 100import org.openstreetmap.josm.tools.I18n; 101import org.openstreetmap.josm.tools.ImageProvider; 102import org.openstreetmap.josm.tools.OpenBrowser; 103import org.openstreetmap.josm.tools.OsmUrlToBounds; 104import org.openstreetmap.josm.tools.PlatformHook; 105import org.openstreetmap.josm.tools.PlatformHookOsx; 106import org.openstreetmap.josm.tools.PlatformHookUnixoid; 107import org.openstreetmap.josm.tools.PlatformHookWindows; 108import org.openstreetmap.josm.tools.Shortcut; 109import org.openstreetmap.josm.tools.Utils; 110import org.openstreetmap.josm.tools.WindowGeometry; 111 112/** 113 * Abstract class holding various static global variables and methods used in large parts of JOSM application. 114 * @since 98 115 */ 116public abstract class Main { 117 118 /** 119 * The JOSM website URL. 120 * @since 6897 (was public from 6143 to 6896) 121 */ 122 private static final String JOSM_WEBSITE = "https://josm.openstreetmap.de"; 123 124 /** 125 * The OSM website URL. 126 * @since 6897 (was public from 6453 to 6896) 127 */ 128 private static final String OSM_WEBSITE = "https://www.openstreetmap.org"; 129 130 /** 131 * Replies true if JOSM currently displays a map view. False, if it doesn't, i.e. if 132 * it only shows the MOTD panel. 133 * 134 * @return <code>true</code> if JOSM currently displays a map view 135 */ 136 public static boolean isDisplayingMapView() { 137 if (map == null) return false; 138 if (map.mapView == null) return false; 139 return true; 140 } 141 142 /** 143 * Global parent component for all dialogs and message boxes 144 */ 145 public static Component parent; 146 147 /** 148 * Global application. 149 */ 150 public static Main main; 151 152 /** 153 * Command-line arguments used to run the application. 154 */ 155 public static String[] commandLineArgs; 156 157 /** 158 * The worker thread slave. This is for executing all long and intensive 159 * calculations. The executed runnables are guaranteed to be executed separately 160 * and sequential. 161 */ 162 public static final ExecutorService worker = new ProgressMonitorExecutor(); 163 164 /** 165 * Global application preferences 166 */ 167 public static Preferences pref; 168 169 /** 170 * The global paste buffer. 171 */ 172 public static final PrimitiveDeepCopy pasteBuffer = new PrimitiveDeepCopy(); 173 174 /** 175 * The layer source from which {@link Main#pasteBuffer} data comes from. 176 */ 177 public static Layer pasteSource; 178 179 /** 180 * The MapFrame. Use {@link Main#setMapFrame} to set or clear it. 181 */ 182 public static MapFrame map; 183 184 /** 185 * The toolbar preference control to register new actions. 186 */ 187 public static ToolbarPreferences toolbar; 188 189 /** 190 * The commands undo/redo handler. 191 */ 192 public final UndoRedoHandler undoRedo = new UndoRedoHandler(); 193 194 /** 195 * The progress monitor being currently displayed. 196 */ 197 public static PleaseWaitProgressMonitor currentProgressMonitor; 198 199 /** 200 * The main menu bar at top of screen. 201 */ 202 public MainMenu menu; 203 204 /** 205 * The data validation handler. 206 */ 207 public OsmValidator validator; 208 209 /** 210 * The file watcher service. 211 */ 212 public static final FileWatcher fileWatcher = new FileWatcher(); 213 214 /** 215 * The MOTD Layer. 216 */ 217 private GettingStarted gettingStarted = new GettingStarted(); 218 219 private static final Collection<MapFrameListener> mapFrameListeners = new ArrayList<>(); 220 221 protected static final Map<String, Throwable> NETWORK_ERRORS = new HashMap<>(); 222 223 // First lines of last 5 error and warning messages, used for bug reports 224 private static final List<String> ERRORS_AND_WARNINGS = Collections.<String>synchronizedList(new ArrayList<String>()); 225 226 private static final Set<OnlineResource> OFFLINE_RESOURCES = new HashSet<>(); 227 228 /** 229 * Logging level (5 = trace, 4 = debug, 3 = info, 2 = warn, 1 = error, 0 = none). 230 * @since 6248 231 */ 232 public static int logLevel = 3; 233 234 private static void rememberWarnErrorMsg(String msg) { 235 // Only remember first line of message 236 int idx = msg.indexOf('\n'); 237 if (idx > 0) { 238 ERRORS_AND_WARNINGS.add(msg.substring(0, idx)); 239 } else { 240 ERRORS_AND_WARNINGS.add(msg); 241 } 242 // Only keep 5 lines to avoid memory leak and incomplete stacktraces in bug reports 243 while (ERRORS_AND_WARNINGS.size() > 5) { 244 ERRORS_AND_WARNINGS.remove(0); 245 } 246 } 247 248 /** 249 * Replies the first lines of last 10 error and warning messages, used for bug reports 250 * @return the first lines of last 10 error and warning messages 251 * @since 7420 252 */ 253 public static final Collection<String> getLastErrorAndWarnings() { 254 return Collections.unmodifiableList(ERRORS_AND_WARNINGS); 255 } 256 257 /** 258 * Prints an error message if logging is on. 259 * @param msg The message to print. 260 * @since 6248 261 */ 262 public static void error(String msg) { 263 if (logLevel < 1) 264 return; 265 if (msg != null && !msg.isEmpty()) { 266 System.err.println(tr("ERROR: {0}", msg)); 267 rememberWarnErrorMsg("E: "+msg); 268 } 269 } 270 271 /** 272 * Prints a warning message if logging is on. 273 * @param msg The message to print. 274 */ 275 public static void warn(String msg) { 276 if (logLevel < 2) 277 return; 278 if (msg != null && !msg.isEmpty()) { 279 System.err.println(tr("WARNING: {0}", msg)); 280 rememberWarnErrorMsg("W: "+msg); 281 } 282 } 283 284 /** 285 * Prints an informational message if logging is on. 286 * @param msg The message to print. 287 */ 288 public static void info(String msg) { 289 if (logLevel < 3) 290 return; 291 if (msg != null && !msg.isEmpty()) { 292 System.out.println(tr("INFO: {0}", msg)); 293 } 294 } 295 296 /** 297 * Prints a debug message if logging is on. 298 * @param msg The message to print. 299 */ 300 public static void debug(String msg) { 301 if (logLevel < 4) 302 return; 303 if (msg != null && !msg.isEmpty()) { 304 System.out.println(tr("DEBUG: {0}", msg)); 305 } 306 } 307 308 /** 309 * Prints a trace message if logging is on. 310 * @param msg The message to print. 311 */ 312 public static void trace(String msg) { 313 if (logLevel < 5) 314 return; 315 if (msg != null && !msg.isEmpty()) { 316 System.out.print("TRACE: "); 317 System.out.println(msg); 318 } 319 } 320 321 /** 322 * Determines if debug log level is enabled. 323 * Useful to avoid costly construction of debug messages when not enabled. 324 * @return {@code true} if log level is at least debug, {@code false} otherwise 325 * @since 6852 326 */ 327 public static boolean isDebugEnabled() { 328 return logLevel >= 4; 329 } 330 331 /** 332 * Determines if trace log level is enabled. 333 * Useful to avoid costly construction of trace messages when not enabled. 334 * @return {@code true} if log level is at least trace, {@code false} otherwise 335 * @since 6852 336 */ 337 public static boolean isTraceEnabled() { 338 return logLevel >= 5; 339 } 340 341 /** 342 * Prints a formatted error message if logging is on. Calls {@link MessageFormat#format} 343 * function to format text. 344 * @param msg The formatted message to print. 345 * @param objects The objects to insert into format string. 346 * @since 6248 347 */ 348 public static void error(String msg, Object... objects) { 349 error(MessageFormat.format(msg, objects)); 350 } 351 352 /** 353 * Prints a formatted warning message if logging is on. Calls {@link MessageFormat#format} 354 * function to format text. 355 * @param msg The formatted message to print. 356 * @param objects The objects to insert into format string. 357 */ 358 public static void warn(String msg, Object... objects) { 359 warn(MessageFormat.format(msg, objects)); 360 } 361 362 /** 363 * Prints a formatted informational message if logging is on. Calls {@link MessageFormat#format} 364 * function to format text. 365 * @param msg The formatted message to print. 366 * @param objects The objects to insert into format string. 367 */ 368 public static void info(String msg, Object... objects) { 369 info(MessageFormat.format(msg, objects)); 370 } 371 372 /** 373 * Prints a formatted debug message if logging is on. Calls {@link MessageFormat#format} 374 * function to format text. 375 * @param msg The formatted message to print. 376 * @param objects The objects to insert into format string. 377 */ 378 public static void debug(String msg, Object... objects) { 379 debug(MessageFormat.format(msg, objects)); 380 } 381 382 /** 383 * Prints an error message for the given Throwable. 384 * @param t The throwable object causing the error 385 * @since 6248 386 */ 387 public static void error(Throwable t) { 388 error(t, true); 389 } 390 391 /** 392 * Prints a warning message for the given Throwable. 393 * @param t The throwable object causing the error 394 * @since 6248 395 */ 396 public static void warn(Throwable t) { 397 warn(t, true); 398 } 399 400 /** 401 * Prints an error message for the given Throwable. 402 * @param t The throwable object causing the error 403 * @param stackTrace {@code true}, if the stacktrace should be displayed 404 * @since 6642 405 */ 406 public static void error(Throwable t, boolean stackTrace) { 407 error(getErrorMessage(t)); 408 if (stackTrace) { 409 t.printStackTrace(); 410 } 411 } 412 413 /** 414 * Prints a warning message for the given Throwable. 415 * @param t The throwable object causing the error 416 * @param stackTrace {@code true}, if the stacktrace should be displayed 417 * @since 6642 418 */ 419 public static void warn(Throwable t, boolean stackTrace) { 420 warn(getErrorMessage(t)); 421 if (stackTrace) { 422 t.printStackTrace(); 423 } 424 } 425 426 /** 427 * Returns a human-readable message of error, also usable for developers. 428 * @param t The error 429 * @return The human-readable error message 430 * @since 6642 431 */ 432 public static String getErrorMessage(Throwable t) { 433 if (t == null) { 434 return null; 435 } 436 StringBuilder sb = new StringBuilder(t.getClass().getName()); 437 String msg = t.getMessage(); 438 if (msg != null) { 439 sb.append(": ").append(msg.trim()); 440 } 441 Throwable cause = t.getCause(); 442 if (cause != null && !cause.equals(t)) { 443 sb.append(". ").append(tr("Cause: ")).append(getErrorMessage(cause)); 444 } 445 return sb.toString(); 446 } 447 448 /** 449 * Platform specific code goes in here. 450 * Plugins may replace it, however, some hooks will be called before any plugins have been loeaded. 451 * So if you need to hook into those early ones, split your class and send the one with the early hooks 452 * to the JOSM team for inclusion. 453 */ 454 public static PlatformHook platform; 455 456 /** 457 * Whether or not the java vm is openjdk 458 * We use this to work around openjdk bugs 459 */ 460 public static boolean isOpenjdk; 461 462 /** 463 * Initializes {@code Main.pref} in normal application context. 464 * @since 6471 465 */ 466 public static void initApplicationPreferences() { 467 Main.pref = new Preferences(); 468 } 469 470 /** 471 * Set or clear (if passed <code>null</code>) the map. 472 * @param map The map to set {@link Main#map} to. Can be null. 473 */ 474 public final void setMapFrame(final MapFrame map) { 475 MapFrame old = Main.map; 476 panel.setVisible(false); 477 panel.removeAll(); 478 if (map != null) { 479 map.fillPanel(panel); 480 } else { 481 old.destroy(); 482 panel.add(gettingStarted, BorderLayout.CENTER); 483 } 484 panel.setVisible(true); 485 redoUndoListener.commandChanged(0,0); 486 487 Main.map = map; 488 489 for (MapFrameListener listener : mapFrameListeners ) { 490 listener.mapFrameInitialized(old, map); 491 } 492 if (map == null && currentProgressMonitor != null) { 493 currentProgressMonitor.showForegroundDialog(); 494 } 495 } 496 497 /** 498 * Remove the specified layer from the map. If it is the last layer, 499 * remove the map as well. 500 * @param layer The layer to remove 501 */ 502 public final synchronized void removeLayer(final Layer layer) { 503 if (map != null) { 504 map.mapView.removeLayer(layer); 505 if (isDisplayingMapView() && map.mapView.getAllLayers().isEmpty()) { 506 setMapFrame(null); 507 } 508 } 509 } 510 511 private static InitStatusListener initListener = null; 512 513 public static interface InitStatusListener { 514 515 void updateStatus(String event); 516 } 517 518 public static void setInitStatusListener(InitStatusListener listener) { 519 initListener = listener; 520 } 521 522 /** 523 * Constructs new {@code Main} object. A lot of global variables are initialized here. 524 */ 525 public Main() { 526 main = this; 527 isOpenjdk = System.getProperty("java.vm.name").toUpperCase().indexOf("OPENJDK") != -1; 528 529 if (initListener != null) { 530 initListener.updateStatus(tr("Executing platform startup hook")); 531 } 532 platform.startupHook(); 533 534 if (initListener != null) { 535 initListener.updateStatus(tr("Building main menu")); 536 } 537 contentPanePrivate.add(panel, BorderLayout.CENTER); 538 panel.add(gettingStarted, BorderLayout.CENTER); 539 menu = new MainMenu(); 540 541 undoRedo.addCommandQueueListener(redoUndoListener); 542 543 // creating toolbar 544 contentPanePrivate.add(toolbar.control, BorderLayout.NORTH); 545 546 registerActionShortcut(menu.help, Shortcut.registerShortcut("system:help", tr("Help"), 547 KeyEvent.VK_F1, Shortcut.DIRECT)); 548 549 // contains several initialization tasks to be executed (in parallel) by a ExecutorService 550 List<Callable<Void>> tasks = new ArrayList<>(); 551 552 tasks.add(new InitializationTask(tr("Initializing OSM API")) { 553 554 @Override 555 public void initialize() throws Exception { 556 // We try to establish an API connection early, so that any API 557 // capabilities are already known to the editor instance. However 558 // if it goes wrong that's not critical at this stage. 559 try { 560 OsmApi.getOsmApi().initialize(null, true); 561 } catch (Exception e) { 562 Main.warn(getErrorMessage(Utils.getRootCause(e))); 563 } 564 } 565 }); 566 567 tasks.add(new InitializationTask(tr("Initializing validator")) { 568 569 @Override 570 public void initialize() throws Exception { 571 validator = new OsmValidator(); 572 MapView.addLayerChangeListener(validator); 573 } 574 }); 575 576 tasks.add(new InitializationTask(tr("Initializing presets")) { 577 578 @Override 579 public void initialize() throws Exception { 580 TaggingPresets.initialize(); 581 } 582 }); 583 584 tasks.add(new InitializationTask(tr("Initializing map styles")) { 585 586 @Override 587 public void initialize() throws Exception { 588 MapPaintPreference.initialize(); 589 } 590 }); 591 592 tasks.add(new InitializationTask(tr("Loading imagery preferences")) { 593 594 @Override 595 public void initialize() throws Exception { 596 ImageryPreference.initialize(); 597 } 598 }); 599 600 try { 601 for (Future<Void> i : Executors.newFixedThreadPool( 602 Runtime.getRuntime().availableProcessors()).invokeAll(tasks)) { 603 i.get(); 604 } 605 } catch (Exception ex) { 606 throw new RuntimeException(ex); 607 } 608 609 // hooks for the jmapviewer component 610 FeatureAdapter.registerBrowserAdapter(new FeatureAdapter.BrowserAdapter() { 611 @Override 612 public void openLink(String url) { 613 OpenBrowser.displayUrl(url); 614 } 615 }); 616 FeatureAdapter.registerTranslationAdapter(I18n.getTranslationAdapter()); 617 618 if (initListener != null) { 619 initListener.updateStatus(tr("Updating user interface")); 620 } 621 622 toolbar.refreshToolbarControl(); 623 624 toolbar.control.updateUI(); 625 contentPanePrivate.updateUI(); 626 } 627 628 private abstract class InitializationTask implements Callable<Void> { 629 630 private final String name; 631 632 protected InitializationTask(String name) { 633 this.name = name; 634 } 635 636 public abstract void initialize() throws Exception; 637 638 @Override 639 public Void call() throws Exception { 640 if (initListener != null) { 641 initListener.updateStatus(name); 642 } 643 final long startTime = System.currentTimeMillis(); 644 initialize(); 645 if (isDebugEnabled()) { 646 final long elapsedTime = System.currentTimeMillis() - startTime; 647 Main.debug(tr("{0} completed in {1}", name, Utils.getDurationString(elapsedTime))); 648 } 649 return null; 650 } 651 } 652 653 /** 654 * Add a new layer to the map. If no map exists, create one. 655 */ 656 public final synchronized void addLayer(final Layer layer) { 657 boolean noMap = map == null; 658 if (noMap) { 659 createMapFrame(layer, null); 660 } 661 layer.hookUpMapView(); 662 map.mapView.addLayer(layer); 663 if (noMap) { 664 Main.map.setVisible(true); 665 } 666 } 667 668 public synchronized void createMapFrame(Layer firstLayer, ViewportData viewportData) { 669 MapFrame mapFrame = new MapFrame(contentPanePrivate, viewportData); 670 setMapFrame(mapFrame); 671 if (firstLayer != null) { 672 mapFrame.selectMapMode((MapMode)mapFrame.getDefaultButtonAction(), firstLayer); 673 } 674 mapFrame.initializeDialogsPane(); 675 // bootstrapping problem: make sure the layer list dialog is going to 676 // listen to change events of the very first layer 677 // 678 if (firstLayer != null) { 679 firstLayer.addPropertyChangeListener(LayerListDialog.getInstance().getModel()); 680 } 681 } 682 683 /** 684 * Replies <code>true</code> if there is an edit layer 685 * 686 * @return <code>true</code> if there is an edit layer 687 */ 688 public boolean hasEditLayer() { 689 if (getEditLayer() == null) return false; 690 return true; 691 } 692 693 /** 694 * Replies the current edit layer 695 * 696 * @return the current edit layer. <code>null</code>, if no current edit layer exists 697 */ 698 public OsmDataLayer getEditLayer() { 699 if (!isDisplayingMapView()) return null; 700 return map.mapView.getEditLayer(); 701 } 702 703 /** 704 * Replies the current data set. 705 * 706 * @return the current data set. <code>null</code>, if no current data set exists 707 */ 708 public DataSet getCurrentDataSet() { 709 if (!hasEditLayer()) return null; 710 return getEditLayer().data; 711 } 712 713 /** 714 * Replies the current selected primitives, from a end-user point of view. 715 * It is not always technically the same collection of primitives than {@link DataSet#getSelected()}. 716 * Indeed, if the user is currently in drawing mode, only the way currently being drawn is returned, 717 * see {@link DrawAction#getInProgressSelection()}. 718 * 719 * @return The current selected primitives, from a end-user point of view. Can be {@code null}. 720 * @since 6546 721 */ 722 public Collection<OsmPrimitive> getInProgressSelection() { 723 if (map != null && map.mapMode instanceof DrawAction) { 724 return ((DrawAction) map.mapMode).getInProgressSelection(); 725 } else { 726 DataSet ds = getCurrentDataSet(); 727 if (ds == null) return null; 728 return ds.getSelected(); 729 } 730 } 731 732 /** 733 * Returns the currently active layer 734 * 735 * @return the currently active layer. <code>null</code>, if currently no active layer exists 736 */ 737 public Layer getActiveLayer() { 738 if (!isDisplayingMapView()) return null; 739 return map.mapView.getActiveLayer(); 740 } 741 742 protected static final JPanel contentPanePrivate = new JPanel(new BorderLayout()); 743 744 public static void redirectToMainContentPane(JComponent source) { 745 RedirectInputMap.redirect(source, contentPanePrivate); 746 } 747 748 public static void registerActionShortcut(JosmAction action) { 749 registerActionShortcut(action, action.getShortcut()); 750 } 751 752 public static void registerActionShortcut(Action action, Shortcut shortcut) { 753 KeyStroke keyStroke = shortcut.getKeyStroke(); 754 if (keyStroke == null) 755 return; 756 757 InputMap inputMap = contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); 758 Object existing = inputMap.get(keyStroke); 759 if (existing != null && !existing.equals(action)) { 760 info(String.format("Keystroke %s is already assigned to %s, will be overridden by %s", keyStroke, existing, action)); 761 } 762 inputMap.put(keyStroke, action); 763 764 contentPanePrivate.getActionMap().put(action, action); 765 } 766 767 public static void unregisterShortcut(Shortcut shortcut) { 768 contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).remove(shortcut.getKeyStroke()); 769 } 770 771 public static void unregisterActionShortcut(JosmAction action) { 772 unregisterActionShortcut(action, action.getShortcut()); 773 } 774 775 public static void unregisterActionShortcut(Action action, Shortcut shortcut) { 776 unregisterShortcut(shortcut); 777 contentPanePrivate.getActionMap().remove(action); 778 } 779 780 /** 781 * Replies the registered action for the given shortcut 782 * @param shortcut The shortcut to look for 783 * @return the registered action for the given shortcut 784 * @since 5696 785 */ 786 public static Action getRegisteredActionShortcut(Shortcut shortcut) { 787 KeyStroke keyStroke = shortcut.getKeyStroke(); 788 if (keyStroke == null) 789 return null; 790 Object action = contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).get(keyStroke); 791 if (action instanceof Action) 792 return (Action) action; 793 return null; 794 } 795 796 /////////////////////////////////////////////////////////////////////////// 797 // Implementation part 798 /////////////////////////////////////////////////////////////////////////// 799 800 /** 801 * Global panel. 802 */ 803 public static final JPanel panel = new JPanel(new BorderLayout()); 804 805 protected static WindowGeometry geometry; 806 protected static int windowState = JFrame.NORMAL; 807 808 private final CommandQueueListener redoUndoListener = new CommandQueueListener(){ 809 @Override 810 public void commandChanged(final int queueSize, final int redoSize) { 811 menu.undo.setEnabled(queueSize > 0); 812 menu.redo.setEnabled(redoSize > 0); 813 } 814 }; 815 816 /** 817 * Should be called before the main constructor to setup some parameter stuff 818 * @param args The parsed argument list. 819 */ 820 public static void preConstructorInit(Map<Option, Collection<String>> args) { 821 ProjectionPreference.setProjection(); 822 823 try { 824 String defaultlaf = platform.getDefaultStyle(); 825 String laf = Main.pref.get("laf", defaultlaf); 826 try { 827 UIManager.setLookAndFeel(laf); 828 } 829 catch (final NoClassDefFoundError | ClassNotFoundException e) { 830 // Try to find look and feel in plugin classloaders 831 Class<?> klass = null; 832 for (ClassLoader cl : PluginHandler.getResourceClassLoaders()) { 833 try { 834 klass = cl.loadClass(laf); 835 break; 836 } catch (ClassNotFoundException ex) { 837 // Do nothing 838 } 839 } 840 if (klass != null && LookAndFeel.class.isAssignableFrom(klass)) { 841 try { 842 UIManager.setLookAndFeel((LookAndFeel) klass.newInstance()); 843 } catch (Exception ex) { 844 warn("Cannot set Look and Feel: " + laf + ": "+ex.getMessage()); 845 } 846 } else { 847 info("Look and Feel not found: " + laf); 848 Main.pref.put("laf", defaultlaf); 849 } 850 } 851 catch (final UnsupportedLookAndFeelException e) { 852 info("Look and Feel not supported: " + laf); 853 Main.pref.put("laf", defaultlaf); 854 } 855 toolbar = new ToolbarPreferences(); 856 contentPanePrivate.updateUI(); 857 panel.updateUI(); 858 } catch (final Exception e) { 859 error(e); 860 } 861 UIManager.put("OptionPane.okIcon", ImageProvider.get("ok")); 862 UIManager.put("OptionPane.yesIcon", UIManager.get("OptionPane.okIcon")); 863 UIManager.put("OptionPane.cancelIcon", ImageProvider.get("cancel")); 864 UIManager.put("OptionPane.noIcon", UIManager.get("OptionPane.cancelIcon")); 865 866 I18n.translateJavaInternalMessages(); 867 868 // init default coordinate format 869 // 870 try { 871 CoordinateFormat.setCoordinateFormat(CoordinateFormat.valueOf(Main.pref.get("coordinates"))); 872 } catch (IllegalArgumentException iae) { 873 CoordinateFormat.setCoordinateFormat(CoordinateFormat.DECIMAL_DEGREES); 874 } 875 876 geometry = WindowGeometry.mainWindow("gui.geometry", 877 (args.containsKey(Option.GEOMETRY) ? args.get(Option.GEOMETRY).iterator().next() : null), 878 !args.containsKey(Option.NO_MAXIMIZE) && Main.pref.getBoolean("gui.maximized", false)); 879 } 880 881 protected static void postConstructorProcessCmdLine(Map<Option, Collection<String>> args) { 882 if (args.containsKey(Option.DOWNLOAD)) { 883 List<File> fileList = new ArrayList<>(); 884 for (String s : args.get(Option.DOWNLOAD)) { 885 File f = null; 886 switch(paramType(s)) { 887 case httpUrl: 888 downloadFromParamHttp(false, s); 889 break; 890 case bounds: 891 downloadFromParamBounds(false, s); 892 break; 893 case fileUrl: 894 try { 895 f = new File(new URI(s)); 896 } catch (URISyntaxException e) { 897 JOptionPane.showMessageDialog( 898 Main.parent, 899 tr("Ignoring malformed file URL: \"{0}\"", s), 900 tr("Warning"), 901 JOptionPane.WARNING_MESSAGE 902 ); 903 } 904 if (f!=null) { 905 fileList.add(f); 906 } 907 break; 908 case fileName: 909 f = new File(s); 910 fileList.add(f); 911 break; 912 } 913 } 914 if(!fileList.isEmpty()) 915 { 916 OpenFileAction.openFiles(fileList, true); 917 } 918 } 919 if (args.containsKey(Option.DOWNLOADGPS)) { 920 for (String s : args.get(Option.DOWNLOADGPS)) { 921 switch(paramType(s)) { 922 case httpUrl: 923 downloadFromParamHttp(true, s); 924 break; 925 case bounds: 926 downloadFromParamBounds(true, s); 927 break; 928 case fileUrl: 929 case fileName: 930 JOptionPane.showMessageDialog( 931 Main.parent, 932 tr("Parameter \"downloadgps\" does not accept file names or file URLs"), 933 tr("Warning"), 934 JOptionPane.WARNING_MESSAGE 935 ); 936 } 937 } 938 } 939 if (args.containsKey(Option.SELECTION)) { 940 for (String s : args.get(Option.SELECTION)) { 941 SearchAction.search(s, SearchAction.SearchMode.add); 942 } 943 } 944 } 945 946 /** 947 * Asks user to perform "save layer" operations (save on disk and/or upload data to server) for all {@link AbstractModifiableLayer} before JOSM exits. 948 * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. {@code false} if the user cancels. 949 * @since 2025 950 */ 951 public static boolean saveUnsavedModifications() { 952 if (!isDisplayingMapView()) return true; 953 return saveUnsavedModifications(map.mapView.getLayersOfType(AbstractModifiableLayer.class), true); 954 } 955 956 /** 957 * Asks user to perform "save layer" operations (save on disk and/or upload data to server) before data layers deletion. 958 * 959 * @param selectedLayers The layers to check. Only instances of {@link AbstractModifiableLayer} are considered. 960 * @param exit {@code true} if JOSM is exiting, {@code false} otherwise. 961 * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. {@code false} if the user cancels. 962 * @since 5519 963 */ 964 public static boolean saveUnsavedModifications(Iterable<? extends Layer> selectedLayers, boolean exit) { 965 SaveLayersDialog dialog = new SaveLayersDialog(parent); 966 List<AbstractModifiableLayer> layersWithUnmodifiedChanges = new ArrayList<>(); 967 for (Layer l: selectedLayers) { 968 if (!(l instanceof AbstractModifiableLayer)) { 969 continue; 970 } 971 AbstractModifiableLayer odl = (AbstractModifiableLayer)l; 972 if ((odl.requiresSaveToFile() || (odl.requiresUploadToServer() && !odl.isUploadDiscouraged())) && odl.isModified()) { 973 layersWithUnmodifiedChanges.add(odl); 974 } 975 } 976 if (exit) { 977 dialog.prepareForSavingAndUpdatingLayersBeforeExit(); 978 } else { 979 dialog.prepareForSavingAndUpdatingLayersBeforeDelete(); 980 } 981 if (!layersWithUnmodifiedChanges.isEmpty()) { 982 dialog.getModel().populate(layersWithUnmodifiedChanges); 983 dialog.setVisible(true); 984 switch(dialog.getUserAction()) { 985 case CANCEL: return false; 986 case PROCEED: return true; 987 default: return false; 988 } 989 } 990 991 return true; 992 } 993 994 /** 995 * Closes JOSM and optionally terminates the Java Virtual Machine (JVM). If there are some unsaved data layers, asks first for user confirmation. 996 * @param exit If {@code true}, the JVM is terminated by running {@link System#exit} with a given return code. 997 * @param exitCode The return code 998 * @return {@code true} if JOSM has been closed, {@code false} if the user has cancelled the operation. 999 * @since 3378 1000 */ 1001 public static boolean exitJosm(boolean exit, int exitCode) { 1002 if (Main.saveUnsavedModifications()) { 1003 geometry.remember("gui.geometry"); 1004 if (map != null) { 1005 map.rememberToggleDialogWidth(); 1006 } 1007 pref.put("gui.maximized", (windowState & JFrame.MAXIMIZED_BOTH) != 0); 1008 // Remove all layers because somebody may rely on layerRemoved events (like AutosaveTask) 1009 if (Main.isDisplayingMapView()) { 1010 Collection<Layer> layers = new ArrayList<>(Main.map.mapView.getAllLayers()); 1011 for (Layer l: layers) { 1012 Main.main.removeLayer(l); 1013 } 1014 } 1015 if (exit) { 1016 System.exit(exitCode); 1017 } 1018 return true; 1019 } 1020 return false; 1021 } 1022 1023 /** 1024 * The type of a command line parameter, to be used in switch statements. 1025 * @see #paramType 1026 */ 1027 private enum DownloadParamType { httpUrl, fileUrl, bounds, fileName } 1028 1029 /** 1030 * Guess the type of a parameter string specified on the command line with --download= or --downloadgps. 1031 * @param s A parameter string 1032 * @return The guessed parameter type 1033 */ 1034 private static DownloadParamType paramType(String s) { 1035 if(s.startsWith("http:") || s.startsWith("https:")) return DownloadParamType.httpUrl; 1036 if(s.startsWith("file:")) return DownloadParamType.fileUrl; 1037 String coorPattern = "\\s*[+-]?[0-9]+(\\.[0-9]+)?\\s*"; 1038 if(s.matches(coorPattern+"(,"+coorPattern+"){3}")) return DownloadParamType.bounds; 1039 // everything else must be a file name 1040 return DownloadParamType.fileName; 1041 } 1042 1043 /** 1044 * Download area specified on the command line as OSM URL. 1045 * @param rawGps Flag to download raw GPS tracks 1046 * @param s The URL parameter 1047 */ 1048 private static void downloadFromParamHttp(final boolean rawGps, String s) { 1049 final Bounds b = OsmUrlToBounds.parse(s); 1050 if (b == null) { 1051 JOptionPane.showMessageDialog( 1052 Main.parent, 1053 tr("Ignoring malformed URL: \"{0}\"", s), 1054 tr("Warning"), 1055 JOptionPane.WARNING_MESSAGE 1056 ); 1057 } else { 1058 downloadFromParamBounds(rawGps, b); 1059 } 1060 } 1061 1062 /** 1063 * Download area specified on the command line as bounds string. 1064 * @param rawGps Flag to download raw GPS tracks 1065 * @param s The bounds parameter 1066 */ 1067 private static void downloadFromParamBounds(final boolean rawGps, String s) { 1068 final StringTokenizer st = new StringTokenizer(s, ","); 1069 if (st.countTokens() == 4) { 1070 Bounds b = new Bounds( 1071 new LatLon(Double.parseDouble(st.nextToken()),Double.parseDouble(st.nextToken())), 1072 new LatLon(Double.parseDouble(st.nextToken()),Double.parseDouble(st.nextToken())) 1073 ); 1074 downloadFromParamBounds(rawGps, b); 1075 } 1076 } 1077 1078 /** 1079 * Download area specified as Bounds value. 1080 * @param rawGps Flag to download raw GPS tracks 1081 * @param b The bounds value 1082 * @see #downloadFromParamBounds(boolean, String) 1083 * @see #downloadFromParamHttp 1084 */ 1085 private static void downloadFromParamBounds(final boolean rawGps, Bounds b) { 1086 DownloadTask task = rawGps ? new DownloadGpsTask() : new DownloadOsmTask(); 1087 // asynchronously launch the download task ... 1088 Future<?> future = task.download(true, b, null); 1089 // ... and the continuation when the download is finished (this will wait for the download to finish) 1090 Main.worker.execute(new PostDownloadHandler(task, future)); 1091 } 1092 1093 /** 1094 * Identifies the current operating system family and initializes the platform hook accordingly. 1095 * @since 1849 1096 */ 1097 public static void determinePlatformHook() { 1098 String os = System.getProperty("os.name"); 1099 if (os == null) { 1100 warn("Your operating system has no name, so I'm guessing its some kind of *nix."); 1101 platform = new PlatformHookUnixoid(); 1102 } else if (os.toLowerCase().startsWith("windows")) { 1103 platform = new PlatformHookWindows(); 1104 } else if ("Linux".equals(os) || "Solaris".equals(os) || 1105 "SunOS".equals(os) || "AIX".equals(os) || 1106 "FreeBSD".equals(os) || "NetBSD".equals(os) || "OpenBSD".equals(os)) { 1107 platform = new PlatformHookUnixoid(); 1108 } else if (os.toLowerCase().startsWith("mac os x")) { 1109 platform = new PlatformHookOsx(); 1110 } else { 1111 warn("I don't know your operating system '"+os+"', so I'm guessing its some kind of *nix."); 1112 platform = new PlatformHookUnixoid(); 1113 } 1114 } 1115 1116 private static class WindowPositionSizeListener extends WindowAdapter implements 1117 ComponentListener { 1118 @Override 1119 public void windowStateChanged(WindowEvent e) { 1120 Main.windowState = e.getNewState(); 1121 } 1122 1123 @Override 1124 public void componentHidden(ComponentEvent e) { 1125 } 1126 1127 @Override 1128 public void componentMoved(ComponentEvent e) { 1129 handleComponentEvent(e); 1130 } 1131 1132 @Override 1133 public void componentResized(ComponentEvent e) { 1134 handleComponentEvent(e); 1135 } 1136 1137 @Override 1138 public void componentShown(ComponentEvent e) { 1139 } 1140 1141 private void handleComponentEvent(ComponentEvent e) { 1142 Component c = e.getComponent(); 1143 if (c instanceof JFrame && c.isVisible()) { 1144 if(Main.windowState == JFrame.NORMAL) { 1145 Main.geometry = new WindowGeometry((JFrame) c); 1146 } else { 1147 Main.geometry.fixScreen((JFrame) c); 1148 } 1149 } 1150 } 1151 } 1152 1153 protected static void addListener() { 1154 parent.addComponentListener(new WindowPositionSizeListener()); 1155 ((JFrame)parent).addWindowStateListener(new WindowPositionSizeListener()); 1156 } 1157 1158 /** 1159 * Checks that JOSM is at least running with Java 7. 1160 * @since 7001 1161 */ 1162 public static void checkJavaVersion() { 1163 String version = System.getProperty("java.version"); 1164 if (version != null) { 1165 if (version.matches("^(1\\.)?[789].*")) 1166 return; 1167 if (version.matches("^(1\\.)?[56].*")) { 1168 JMultilineLabel ho = new JMultilineLabel("<html>"+ 1169 tr("<h2>JOSM requires Java version {0}.</h2>"+ 1170 "Detected Java version: {1}.<br>"+ 1171 "You can <ul><li>update your Java (JRE) or</li>"+ 1172 "<li>use an earlier (Java {2} compatible) version of JOSM.</li></ul>"+ 1173 "More Info:", "7", version, "6")+"</html>"); 1174 JTextArea link = new JTextArea(HelpUtil.getWikiBaseHelpUrl()+"/Help/SystemRequirements"); 1175 link.setEditable(false); 1176 link.setBackground(panel.getBackground()); 1177 JPanel panel = new JPanel(new GridBagLayout()); 1178 GridBagConstraints gbc = new GridBagConstraints(); 1179 gbc.gridwidth = GridBagConstraints.REMAINDER; 1180 gbc.anchor = GridBagConstraints.WEST; 1181 gbc.weightx = 1.0; 1182 panel.add(ho, gbc); 1183 panel.add(link, gbc); 1184 final String EXIT = tr("Exit JOSM"); 1185 final String CONTINUE = tr("Continue, try anyway"); 1186 int ret = JOptionPane.showOptionDialog(null, panel, tr("Error"), JOptionPane.YES_NO_OPTION, 1187 JOptionPane.ERROR_MESSAGE, null, new String[] {EXIT, CONTINUE}, EXIT); 1188 if (ret == 0) { 1189 System.exit(0); 1190 } 1191 return; 1192 } 1193 } 1194 error("Could not recognize Java Version: "+version); 1195 } 1196 1197 /* ----------------------------------------------------------------------------------------- */ 1198 /* projection handling - Main is a registry for a single, global projection instance */ 1199 /* */ 1200 /* TODO: For historical reasons the registry is implemented by Main. An alternative approach */ 1201 /* would be a singleton org.openstreetmap.josm.data.projection.ProjectionRegistry class. */ 1202 /* ----------------------------------------------------------------------------------------- */ 1203 /** 1204 * The projection method used. 1205 * use {@link #getProjection()} and {@link #setProjection(Projection)} for access. 1206 * Use {@link #setProjection(Projection)} in order to trigger a projection change event. 1207 */ 1208 private static Projection proj; 1209 1210 /** 1211 * Replies the current projection. 1212 * 1213 * @return the currently active projection 1214 */ 1215 public static Projection getProjection() { 1216 return proj; 1217 } 1218 1219 /** 1220 * Sets the current projection 1221 * 1222 * @param p the projection 1223 */ 1224 public static void setProjection(Projection p) { 1225 CheckParameterUtil.ensureParameterNotNull(p); 1226 Projection oldValue = proj; 1227 Bounds b = isDisplayingMapView() ? map.mapView.getRealBounds() : null; 1228 proj = p; 1229 fireProjectionChanged(oldValue, proj, b); 1230 } 1231 1232 /* 1233 * Keep WeakReferences to the listeners. This relieves clients from the burden of 1234 * explicitly removing the listeners and allows us to transparently register every 1235 * created dataset as projection change listener. 1236 */ 1237 private static final List<WeakReference<ProjectionChangeListener>> listeners = new ArrayList<>(); 1238 1239 private static void fireProjectionChanged(Projection oldValue, Projection newValue, Bounds oldBounds) { 1240 if (newValue == null ^ oldValue == null 1241 || (newValue != null && oldValue != null && !Objects.equals(newValue.toCode(), oldValue.toCode()))) { 1242 1243 synchronized(Main.class) { 1244 Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator(); 1245 while (it.hasNext()){ 1246 WeakReference<ProjectionChangeListener> wr = it.next(); 1247 ProjectionChangeListener listener = wr.get(); 1248 if (listener == null) { 1249 it.remove(); 1250 continue; 1251 } 1252 listener.projectionChanged(oldValue, newValue); 1253 } 1254 } 1255 if (newValue != null && oldBounds != null) { 1256 Main.map.mapView.zoomTo(oldBounds); 1257 } 1258 /* TODO - remove layers with fixed projection */ 1259 } 1260 } 1261 1262 /** 1263 * Register a projection change listener. 1264 * 1265 * @param listener the listener. Ignored if <code>null</code>. 1266 */ 1267 public static void addProjectionChangeListener(ProjectionChangeListener listener) { 1268 if (listener == null) return; 1269 synchronized (Main.class) { 1270 for (WeakReference<ProjectionChangeListener> wr : listeners) { 1271 // already registered ? => abort 1272 if (wr.get() == listener) return; 1273 } 1274 listeners.add(new WeakReference<>(listener)); 1275 } 1276 } 1277 1278 /** 1279 * Removes a projection change listener. 1280 * 1281 * @param listener the listener. Ignored if <code>null</code>. 1282 */ 1283 public static void removeProjectionChangeListener(ProjectionChangeListener listener) { 1284 if (listener == null) return; 1285 synchronized(Main.class){ 1286 Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator(); 1287 while (it.hasNext()){ 1288 WeakReference<ProjectionChangeListener> wr = it.next(); 1289 // remove the listener - and any other listener which got garbage 1290 // collected in the meantime 1291 if (wr.get() == null || wr.get() == listener) { 1292 it.remove(); 1293 } 1294 } 1295 } 1296 } 1297 1298 /** 1299 * Listener for window switch events. 1300 * 1301 * These are events, when the user activates a window of another application 1302 * or comes back to JOSM. Window switches from one JOSM window to another 1303 * are not reported. 1304 */ 1305 public static interface WindowSwitchListener { 1306 /** 1307 * Called when the user activates a window of another application. 1308 */ 1309 void toOtherApplication(); 1310 /** 1311 * Called when the user comes from a window of another application 1312 * back to JOSM. 1313 */ 1314 void fromOtherApplication(); 1315 } 1316 1317 private static final List<WeakReference<WindowSwitchListener>> windowSwitchListeners = new ArrayList<>(); 1318 1319 /** 1320 * Register a window switch listener. 1321 * 1322 * @param listener the listener. Ignored if <code>null</code>. 1323 */ 1324 public static void addWindowSwitchListener(WindowSwitchListener listener) { 1325 if (listener == null) return; 1326 synchronized (Main.class) { 1327 for (WeakReference<WindowSwitchListener> wr : windowSwitchListeners) { 1328 // already registered ? => abort 1329 if (wr.get() == listener) return; 1330 } 1331 boolean wasEmpty = windowSwitchListeners.isEmpty(); 1332 windowSwitchListeners.add(new WeakReference<>(listener)); 1333 if (wasEmpty) { 1334 // The following call will have no effect, when there is no window 1335 // at the time. Therefore, MasterWindowListener.setup() will also be 1336 // called, as soon as the main window is shown. 1337 MasterWindowListener.setup(); 1338 } 1339 } 1340 } 1341 1342 /** 1343 * Removes a window switch listener. 1344 * 1345 * @param listener the listener. Ignored if <code>null</code>. 1346 */ 1347 public static void removeWindowSwitchListener(WindowSwitchListener listener) { 1348 if (listener == null) return; 1349 synchronized (Main.class){ 1350 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator(); 1351 while (it.hasNext()){ 1352 WeakReference<WindowSwitchListener> wr = it.next(); 1353 // remove the listener - and any other listener which got garbage 1354 // collected in the meantime 1355 if (wr.get() == null || wr.get() == listener) { 1356 it.remove(); 1357 } 1358 } 1359 if (windowSwitchListeners.isEmpty()) { 1360 MasterWindowListener.teardown(); 1361 } 1362 } 1363 } 1364 1365 /** 1366 * WindowListener, that is registered on all Windows of the application. 1367 * 1368 * Its purpose is to notify WindowSwitchListeners, that the user switches to 1369 * another application, e.g. a browser, or back to JOSM. 1370 * 1371 * When changing from JOSM to another application and back (e.g. two times 1372 * alt+tab), the active Window within JOSM may be different. 1373 * Therefore, we need to register listeners to <strong>all</strong> (visible) 1374 * Windows in JOSM, and it does not suffice to monitor the one that was 1375 * deactivated last. 1376 * 1377 * This class is only "active" on demand, i.e. when there is at least one 1378 * WindowSwitchListener registered. 1379 */ 1380 protected static class MasterWindowListener extends WindowAdapter { 1381 1382 private static MasterWindowListener INSTANCE; 1383 1384 public static MasterWindowListener getInstance() { 1385 if (INSTANCE == null) { 1386 INSTANCE = new MasterWindowListener(); 1387 } 1388 return INSTANCE; 1389 } 1390 1391 /** 1392 * Register listeners to all non-hidden windows. 1393 * 1394 * Windows that are created later, will be cared for in {@link #windowDeactivated(WindowEvent)}. 1395 */ 1396 public static void setup() { 1397 if (!windowSwitchListeners.isEmpty()) { 1398 for (Window w : Window.getWindows()) { 1399 if (w.isShowing()) { 1400 if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) { 1401 w.addWindowListener(getInstance()); 1402 } 1403 } 1404 } 1405 } 1406 } 1407 1408 /** 1409 * Unregister all listeners. 1410 */ 1411 public static void teardown() { 1412 for (Window w : Window.getWindows()) { 1413 w.removeWindowListener(getInstance()); 1414 } 1415 } 1416 1417 @Override 1418 public void windowActivated(WindowEvent e) { 1419 if (e.getOppositeWindow() == null) { // we come from a window of a different application 1420 // fire WindowSwitchListeners 1421 synchronized (Main.class) { 1422 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator(); 1423 while (it.hasNext()){ 1424 WeakReference<WindowSwitchListener> wr = it.next(); 1425 WindowSwitchListener listener = wr.get(); 1426 if (listener == null) { 1427 it.remove(); 1428 continue; 1429 } 1430 listener.fromOtherApplication(); 1431 } 1432 } 1433 } 1434 } 1435 1436 @Override 1437 public void windowDeactivated(WindowEvent e) { 1438 // set up windows that have been created in the meantime 1439 for (Window w : Window.getWindows()) { 1440 if (!w.isShowing()) { 1441 w.removeWindowListener(getInstance()); 1442 } else { 1443 if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) { 1444 w.addWindowListener(getInstance()); 1445 } 1446 } 1447 } 1448 if (e.getOppositeWindow() == null) { // we go to a window of a different application 1449 // fire WindowSwitchListeners 1450 synchronized (Main.class) { 1451 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator(); 1452 while (it.hasNext()){ 1453 WeakReference<WindowSwitchListener> wr = it.next(); 1454 WindowSwitchListener listener = wr.get(); 1455 if (listener == null) { 1456 it.remove(); 1457 continue; 1458 } 1459 listener.toOtherApplication(); 1460 } 1461 } 1462 } 1463 } 1464 } 1465 1466 /** 1467 * Registers a new {@code MapFrameListener} that will be notified of MapFrame changes 1468 * @param listener The MapFrameListener 1469 * @return {@code true} if the listeners collection changed as a result of the call 1470 * @since 5957 1471 */ 1472 public static boolean addMapFrameListener(MapFrameListener listener) { 1473 return listener != null ? mapFrameListeners.add(listener) : false; 1474 } 1475 1476 /** 1477 * Unregisters the given {@code MapFrameListener} from MapFrame changes 1478 * @param listener The MapFrameListener 1479 * @return {@code true} if the listeners collection changed as a result of the call 1480 * @since 5957 1481 */ 1482 public static boolean removeMapFrameListener(MapFrameListener listener) { 1483 return listener != null ? mapFrameListeners.remove(listener) : false; 1484 } 1485 1486 /** 1487 * Adds a new network error that occur to give a hint about broken Internet connection. 1488 * Do not use this method for errors known for sure thrown because of a bad proxy configuration. 1489 * 1490 * @param url The accessed URL that caused the error 1491 * @param t The network error 1492 * @return The previous error associated to the given resource, if any. Can be {@code null} 1493 * @since 6642 1494 */ 1495 public static Throwable addNetworkError(URL url, Throwable t) { 1496 if (url != null && t != null) { 1497 Throwable old = addNetworkError(url.toExternalForm(), t); 1498 if (old != null) { 1499 Main.warn("Already here "+old); 1500 } 1501 return old; 1502 } 1503 return null; 1504 } 1505 1506 /** 1507 * Adds a new network error that occur to give a hint about broken Internet connection. 1508 * Do not use this method for errors known for sure thrown because of a bad proxy configuration. 1509 * 1510 * @param url The accessed URL that caused the error 1511 * @param t The network error 1512 * @return The previous error associated to the given resource, if any. Can be {@code null} 1513 * @since 6642 1514 */ 1515 public static Throwable addNetworkError(String url, Throwable t) { 1516 if (url != null && t != null) { 1517 return NETWORK_ERRORS.put(url, t); 1518 } 1519 return null; 1520 } 1521 1522 /** 1523 * Returns the network errors that occured until now. 1524 * @return the network errors that occured until now, indexed by URL 1525 * @since 6639 1526 */ 1527 public static Map<String, Throwable> getNetworkErrors() { 1528 return new HashMap<>(NETWORK_ERRORS); 1529 } 1530 1531 /** 1532 * Returns the JOSM website URL. 1533 * @return the josm website URL 1534 * @since 6897 1535 */ 1536 public static String getJOSMWebsite() { 1537 if (Main.pref != null) 1538 return Main.pref.get("josm.url", JOSM_WEBSITE); 1539 return JOSM_WEBSITE; 1540 } 1541 1542 /** 1543 * Returns the JOSM XML URL. 1544 * @return the josm XML URL 1545 * @since 6897 1546 */ 1547 public static String getXMLBase() { 1548 // Always return HTTP (issues reported with HTTPS) 1549 return "http://josm.openstreetmap.de"; 1550 } 1551 1552 /** 1553 * Returns the OSM website URL. 1554 * @return the OSM website URL 1555 * @since 6897 1556 */ 1557 public static String getOSMWebsite() { 1558 if (Main.pref != null) 1559 return Main.pref.get("osm.url", OSM_WEBSITE); 1560 return OSM_WEBSITE; 1561 } 1562 1563 /** 1564 * Determines if we are currently running on OSX. 1565 * @return {@code true} if we are currently running on OSX 1566 * @since 6957 1567 */ 1568 public static boolean isPlatformOsx() { 1569 return Main.platform instanceof PlatformHookOsx; 1570 } 1571 1572 /** 1573 * Determines if we are currently running on Windows. 1574 * @return {@code true} if we are currently running on Windows 1575 * @since 7335 1576 */ 1577 public static boolean isPlatformWindows() { 1578 return Main.platform instanceof PlatformHookWindows; 1579 } 1580 1581 /** 1582 * Determines if the given online resource is currently offline. 1583 * @param r the online resource 1584 * @return {@code true} if {@code r} is offline and should not be accessed 1585 * @since 7434 1586 */ 1587 public static boolean isOffline(OnlineResource r) { 1588 return OFFLINE_RESOURCES.contains(r) || OFFLINE_RESOURCES.contains(OnlineResource.ALL); 1589 } 1590 1591 /** 1592 * Sets the given online resource to offline state. 1593 * @param r the online resource 1594 * @return {@code true} if {@code r} was not already offline 1595 * @since 7434 1596 */ 1597 public static boolean setOffline(OnlineResource r) { 1598 return OFFLINE_RESOURCES.add(r); 1599 } 1600 1601 /** 1602 * Replies the set of online resources currently offline. 1603 * @return the set of online resources currently offline 1604 * @since 7434 1605 */ 1606 public static Set<OnlineResource> getOfflineResources() { 1607 return new HashSet<>(OFFLINE_RESOURCES); 1608 } 1609}