Ticket #21605: 21605.3.patch
| File 21605.3.patch, 24.0 KB (added by , 3 years ago) |
|---|
-
src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java b/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java
a b 62 62 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker; 63 63 import org.openstreetmap.josm.gui.layer.Layer; 64 64 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 65 import org.openstreetmap.josm.gui.util.GuiHelper; 65 66 import org.openstreetmap.josm.gui.util.imagery.Vector3D; 66 67 import org.openstreetmap.josm.tools.ImageProvider; 67 68 import org.openstreetmap.josm.tools.Utils; … … 71 72 * @since 99 72 73 */ 73 74 public class GeoImageLayer extends AbstractModifiableLayer implements 74 JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener { 75 JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener, 76 IGeoImageLayer { 75 77 76 78 private static final List<Action> menuAdditions = new LinkedList<>(); 77 79 … … 172 174 this.useThumbs = useThumbs; 173 175 this.data.addImageDataUpdateListener(this); 174 176 this.data.setLayer(this); 177 if (!ImageViewerDialog.hasInstance()) { 178 this.data.setSelectedImage(this.data.getFirstImage()); 179 // This must be called *after* this layer is added to the layer manager. 180 // But it must also be run in the EDT. By adding this to the worker queue 181 // and then running the actual code in the EDT, we ensure that the layer 182 // will be added to the layer manager regardless of whether or not this 183 // was instantiated in the worker thread or the EDT thread. 184 MainApplication.worker.submit(() -> GuiHelper.runInEDT(() -> 185 ImageViewerDialog.getInstance().displayImages(this, Collections.singletonList(this.data.getSelectedImage())))); 186 } 175 187 } 176 188 177 189 private final class ImageMouseListener extends MouseAdapter { … … 247 259 MainApplication.worker.execute(new ImagesLoader(files, gpxLayer)); 248 260 } 249 261 262 @Override 263 public void clearSelection() { 264 this.getImageData().clearSelectedImage(); 265 } 266 250 267 @Override 251 268 public Icon getIcon() { 252 269 return ImageProvider.get("dialogs/geoimage", ImageProvider.ImageSizes.LAYER); 253 270 } 254 271 272 @Override 273 public List<ImageEntry> getSelection() { 274 return this.getImageData().getSelectedImages(); 275 } 276 255 277 /** 256 278 * Register actions on the layer 257 279 * @param addition the action to be added -
new file src/org/openstreetmap/josm/gui/layer/geoimage/IGeoImageLayer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/IGeoImageLayer.java b/src/org/openstreetmap/josm/gui/layer/geoimage/IGeoImageLayer.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.geoimage; 3 4 import java.util.List; 5 6 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 7 8 /** 9 * An interface for layers which want to show images 10 * @since xxx 11 */ 12 public interface IGeoImageLayer { 13 /** 14 * Clear the selection of the layer 15 */ 16 void clearSelection(); 17 18 /** 19 * Get the current selection 20 * @return The currently selected images 21 */ 22 List<? extends IImageEntry<?>> getSelection(); 23 } -
src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java
a b 11 11 import java.awt.GridBagConstraints; 12 12 import java.awt.GridBagLayout; 13 13 import java.awt.event.ActionEvent; 14 import java.awt.event.ActionListener;15 14 import java.awt.event.KeyEvent; 16 15 import java.awt.event.WindowEvent; 17 16 import java.io.IOException; … … 31 30 import java.util.concurrent.Future; 32 31 import java.util.function.UnaryOperator; 33 32 import java.util.stream.Collectors; 33 import java.util.stream.IntStream; 34 import java.util.stream.Stream; 34 35 36 import javax.annotation.Nonnull; 37 import javax.annotation.Nullable; 35 38 import javax.swing.AbstractAction; 39 import javax.swing.AbstractButton; 36 40 import javax.swing.Box; 37 41 import javax.swing.JButton; 38 42 import javax.swing.JLabel; … … 42 46 import javax.swing.SwingConstants; 43 47 import javax.swing.SwingUtilities; 44 48 49 import org.openstreetmap.josm.actions.ExpertToggleAction; 45 50 import org.openstreetmap.josm.actions.JosmAction; 46 51 import org.openstreetmap.josm.data.ImageData; 47 52 import org.openstreetmap.josm.data.ImageData.ImageDataUpdateListener; … … 64 69 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings; 65 70 import org.openstreetmap.josm.gui.util.GuiHelper; 66 71 import org.openstreetmap.josm.gui.util.imagery.Vector3D; 72 import org.openstreetmap.josm.gui.widgets.HideableTabbedPane; 73 import org.openstreetmap.josm.spi.preferences.Config; 67 74 import org.openstreetmap.josm.tools.ImageProvider; 68 75 import org.openstreetmap.josm.tools.Logging; 69 76 import org.openstreetmap.josm.tools.PlatformManager; … … 124 131 return dialog; 125 132 } 126 133 134 /** 135 * Check if there is an instance for the {@link ImageViewerDialog} 136 * @return {@code true} if there is a static singleton instance of {@link ImageViewerDialog} 137 * @since xxx 138 */ 139 public static boolean hasInstance() { 140 return dialog != null; 141 } 142 143 /** 144 * Destroy the current dialog 145 */ 146 private static void destroyInstance() { 147 dialog = null; 148 } 149 127 150 private JButton btnLast; 128 151 private JButton btnNext; 129 152 private JButton btnPrevious; … … 135 158 private JButton btnDeleteFromDisk; 136 159 private JToggleButton tbCentre; 137 160 /** The layer tab (used to select images when multiple layers provide images, makes for easy switching) */ 138 private JPanellayers;161 private HideableTabbedPane layers; 139 162 140 163 private ImageViewerDialog() { 141 164 super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged", … … 168 191 169 192 private void build() { 170 193 JPanel content = new JPanel(new BorderLayout()); 171 this.layers = new JPanel(new GridBagLayout()); 172 content.add(layers, BorderLayout.NORTH); 173 174 content.add(imgDisplay, BorderLayout.CENTER); 194 this.layers = new HideableTabbedPane(); 195 content.add(layers, BorderLayout.CENTER); 175 196 176 197 Dimension buttonDim = new Dimension(26, 26); 177 198 … … 187 208 btnLast = createNavigationButton(imageLastAction, buttonDim); 188 209 189 210 tbCentre = new JToggleButton(imageCenterViewAction); 211 tbCentre.setSelected(Config.getPref().getBoolean("geoimage.viewer.centre.on.image", false)); 190 212 tbCentre.setPreferredSize(buttonDim); 191 213 192 214 JButton btnZoomBestFit = new JButton(imageZoomAction); … … 196 218 btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT); 197 219 198 220 JPanel buttons = new JPanel(); 199 buttons.add(btnFirst); 200 buttons.add(btnPrevious); 201 buttons.add(btnNext); 202 buttons.add(btnLast); 203 buttons.add(Box.createRigidArea(new Dimension(7, 0))); 204 buttons.add(tbCentre); 205 buttons.add(btnZoomBestFit); 206 buttons.add(Box.createRigidArea(new Dimension(7, 0))); 207 buttons.add(btnDelete); 208 buttons.add(btnDeleteFromDisk); 209 buttons.add(Box.createRigidArea(new Dimension(7, 0))); 210 buttons.add(btnCopyPath); 211 buttons.add(btnOpenExternal); 212 buttons.add(Box.createRigidArea(new Dimension(7, 0))); 213 buttons.add(createButton(visibilityAction, buttonDim)); 221 addButtonGroup(buttons, this.btnFirst, this.btnPrevious, this.btnNext, this.btnLast); 222 addButtonGroup(buttons, this.tbCentre, btnZoomBestFit); 223 addButtonGroup(buttons, this.btnDelete, this.btnDeleteFromDisk); 224 addButtonGroup(buttons, this.btnCopyPath, this.btnOpenExternal); 225 addButtonGroup(buttons, createButton(visibilityAction, buttonDim)); 214 226 215 227 JPanel bottomPane = new JPanel(new GridBagLayout()); 216 228 GridBagConstraints gc = new GridBagConstraints(); … … 231 243 createLayout(content, false, null); 232 244 } 233 245 234 private void updateLayers() { 235 if (this.tabbedEntries.size() <= 1) { 246 /** 247 * Add a button group to a panel 248 * @param buttonPanel The panel holding the buttons 249 * @param buttons The button group to add 250 */ 251 private static void addButtonGroup(JPanel buttonPanel, AbstractButton... buttons) { 252 if (buttonPanel.getComponentCount() != 0) { 253 buttonPanel.add(Box.createRigidArea(new Dimension(7, 0))); 254 } 255 256 for (AbstractButton jButton : buttons) { 257 buttonPanel.add(jButton); 258 } 259 } 260 261 /** 262 * Update the tabs for the different image layers 263 * @param changed {@code true} if the tabs changed 264 */ 265 private void updateLayers(boolean changed) { 266 if (this.tabbedEntries.isEmpty()) { 236 267 this.layers.setVisible(false); 237 this.layers.removeAll();238 268 } else { 239 269 this.layers.setVisible(true); 240 270 // Remove all old components 241 this.layers.removeAll();242 271 MainLayerManager layerManager = MainApplication.getLayerManager(); 243 272 List<Layer> invalidLayers = this.tabbedEntries.keySet().stream().filter(layer -> !layerManager.containsLayer(layer)) 244 273 .collect(Collectors.toList()); 245 274 // `null` is for anything using the old methods, without telling us what layer it comes from. 246 invalidLayers.remove(null); 275 if (this.tabbedEntries.containsKey(null) && !this.tabbedEntries.getOrDefault(null, Collections.emptyList()).isEmpty()) { 276 invalidLayers.remove(null); 277 } 247 278 // We need to do multiple calls to avoid ConcurrentModificationExceptions 248 279 invalidLayers.forEach(this.tabbedEntries::remove); 249 addButtonsForImageLayers(); 280 if (changed) { 281 addButtonsForImageLayers(); 282 } 283 MoveImgDisplayPanel selected = (MoveImgDisplayPanel) this.layers.getSelectedComponent(); 284 if ((this.imgDisplay.getParent() == null || this.imgDisplay.getParent().getParent() == null) 285 && selected != null && selected.entries.contains(this.currentEntry)) { 286 selected.setVisible(selected.isVisible()); 287 } else if (selected != null && !selected.entries.contains(this.currentEntry)) { 288 this.getImageTabs().filter(m -> m.entries.contains(this.currentEntry)).mapToInt(this.layers::indexOfComponent).findFirst() 289 .ifPresent(this.layers::setSelectedIndex); 290 } 250 291 this.layers.invalidate(); 251 292 } 293 this.layers.getParent().invalidate(); 252 294 this.revalidate(); 253 295 } 254 296 … … 256 298 * Add the buttons for image layers 257 299 */ 258 300 private void addButtonsForImageLayers() { 259 final IImageEntry<?> current; 260 synchronized (this) { 261 current = this.currentEntry; 262 } 263 List<JButton> layerButtons = new ArrayList<>(this.tabbedEntries.size()); 301 List<MoveImgDisplayPanel> alreadyAdded = this.getImageTabs().collect(Collectors.toList()); 264 302 if (this.tabbedEntries.containsKey(null)) { 265 303 List<IImageEntry<?>> nullEntries = this.tabbedEntries.get(null); 266 JButton layerButton = createImageLayerButton(null, nullEntries); 267 layerButtons.add(layerButton); 268 layerButton.setEnabled(!nullEntries.contains(current)); 304 if (alreadyAdded.stream().noneMatch(m -> Objects.isNull(m.layer) && Objects.equals(nullEntries, m.entries))) { 305 this.layers.addTab(tr("Default"), new MoveImgDisplayPanel(this.imgDisplay, null, nullEntries)); 306 } 307 } else { 308 this.removeImageTab(null); 269 309 } 310 List<Layer> availableLayers = MainApplication.getLayerManager().getLayers(); 270 311 for (Map.Entry<Layer, List<IImageEntry<?>>> entry : 271 312 this.tabbedEntries.entrySet().stream().filter(entry -> entry.getKey() != null) 272 .sorted(Comparator.comparing(entry -> entry.getKey().getName())).collect(Collectors.toList())) { 273 JButton layerButton = createImageLayerButton(entry.getKey(), entry.getValue()); 274 layerButtons.add(layerButton); 275 layerButton.setEnabled(!entry.getValue().contains(current)); 313 .sorted(Comparator.comparingInt(entry -> /*reverse*/-availableLayers.indexOf(entry.getKey()))) 314 .collect(Collectors.toList())) { 315 final Layer layer = entry.getKey(); 316 final int index = availableLayers.size() - availableLayers.indexOf(layer); 317 final String label = (ExpertToggleAction.isExpert() ? "[" + index + "] " : "") + layer.getLabel(); 318 final Optional<MoveImgDisplayPanel> originalPanel = alreadyAdded.stream() 319 .filter(m -> Objects.equals(m.layer, entry.getKey())).findFirst(); 320 if (originalPanel.isPresent()) { 321 int componentIndex = this.layers.indexOfComponent(originalPanel.get()); 322 this.layers.setTitleAt(componentIndex, label); 323 } else { 324 this.layers.addTab(label, new MoveImgDisplayPanel(this.imgDisplay, entry.getKey(), entry.getValue())); 325 } 276 326 } 277 layerButtons.forEach(this.layers::add); 327 this.getImageTabs().map(p -> p.layer).filter(layer -> !this.tabbedEntries.containsKey(layer)) 328 // We have to collect to a list prior to removal -- if we don't, then the stream may get a layer at index 0, remove that layer, 329 // and then get a layer at index 1, which was previously at index 2. 330 .collect(Collectors.toList()).forEach(this::removeImageTab); 278 331 } 332 333 /** 334 * Remove a tab for a layer from the {@link #layers} tab pane 335 * @param layer The layer to remove 336 */ 337 private void removeImageTab(Layer layer) { 338 // This must be reversed to avoid removing the wrong tab 339 for (int i = this.layers.getTabCount() - 1; i >= 0; i--) { 340 Component component = this.layers.getComponentAt(i); 341 if (component instanceof MoveImgDisplayPanel) { 342 MoveImgDisplayPanel moveImgDisplayPanel = (MoveImgDisplayPanel) component; 343 if (Objects.equals(layer, moveImgDisplayPanel.layer)) { 344 this.layers.removeTabAt(i); 345 this.layers.remove(moveImgDisplayPanel); 346 } 347 } 348 } 349 } 279 350 280 351 /** 281 * Create a button for a specific layer and its entries 282 * 283 * @param layer The layer to switch to 284 * @param entries The entries to display 285 * @return The button to use to switch to the specified layer 352 * Get the {@link MoveImgDisplayPanel} objects in {@link #layers}. 353 * @return The individual panels 286 354 */ 287 private static JButton createImageLayerButton(Layer layer, List<IImageEntry<?>> entries) {288 final JButton layerButton = new JButton();289 layerButton.addActionListener(new ImageActionListener(layer, entries));290 layerButton.setText(layer != null ? layer.getLabel() : tr("Default"));291 return layerButton;355 private Stream<MoveImgDisplayPanel> getImageTabs() { 356 return IntStream.range(0, this.layers.getTabCount()) 357 .mapToObj(this.layers::getComponentAt) 358 .filter(MoveImgDisplayPanel.class::isInstance) 359 .map(MoveImgDisplayPanel.class::cast); 292 360 } 293 361 294 362 @Override … … 309 377 imageZoomAction.destroy(); 310 378 cancelLoadingImage(); 311 379 super.destroy(); 312 d ialog = null;380 destroyInstance(); 313 381 } 314 382 315 383 /** … … 433 501 } 434 502 } 435 503 436 /**437 * A listener that is called to change the viewing layer438 */439 private static class ImageActionListener implements ActionListener {440 441 private final Layer layer;442 private final List<IImageEntry<?>> entries;443 444 ImageActionListener(Layer layer, List<IImageEntry<?>> entries) {445 this.layer = layer;446 this.entries = entries;447 }448 449 @Override450 public void actionPerformed(ActionEvent e) {451 ImageViewerDialog.getInstance().displayImages(this.layer, this.entries);452 }453 }454 455 504 private class ImageFirstAction extends ImageRememberAction { 456 505 ImageFirstAction() { 457 506 super(null, new ImageProvider(DIALOG_FOLDER, "first"), tr("First"), Shortcut.registerShortcut( … … 478 527 public void actionPerformed(ActionEvent e) { 479 528 final JToggleButton button = (JToggleButton) e.getSource(); 480 529 centerView = button.isEnabled() && button.isSelected(); 530 Config.getPref().putBoolean("geoimage.viewer.centre.on.image", centerView); 481 531 if (centerView && currentEntry != null && currentEntry.getPos() != null) { 482 532 MainApplication.getMap().mapView.zoomTo(currentEntry.getPos()); 483 533 } … … 618 668 } 619 669 } 620 670 671 /** 672 * A JPanel whose entire purpose is to display an image by (a) moving the imgDisplay arround and (b) setting the imgDisplay as a child 673 * for this panel. 674 */ 675 private static class MoveImgDisplayPanel extends JPanel { 676 private final Layer layer; 677 private final List<IImageEntry<?>> entries; 678 private final ImageDisplay imgDisplay; 679 MoveImgDisplayPanel(ImageDisplay imgDisplay, Layer layer, List<IImageEntry<?>> entries) { 680 super(new BorderLayout()); 681 this.layer = layer; 682 this.entries = entries; 683 this.imgDisplay = imgDisplay; 684 } 685 686 @Override 687 public void setVisible(boolean visible) { 688 super.setVisible(visible); 689 if (visible) { 690 if (!ImageViewerDialog.getInstance().getDisplayedImages(this.layer).isEmpty() 691 && !this.entries.contains(ImageViewerDialog.getCurrentImage())) { 692 ImageViewerDialog.getInstance().displayImages(this.layer, this.entries); 693 } 694 if (this.imgDisplay.getParent() != this) { 695 this.add(this.imgDisplay, BorderLayout.CENTER); 696 this.imgDisplay.invalidate(); 697 this.revalidate(); 698 } 699 } 700 } 701 } 702 703 /** 704 * Get the images displayed for a layer 705 * @param layer The layer to get the displayed images for 706 * @return The images for the layer that are displayed (assuming the tab for the layer is selected) 707 * @since xxx 708 */ 709 @Nonnull 710 public List<IImageEntry<?>> getDisplayedImages(Layer layer) { 711 return this.tabbedEntries.getOrDefault(layer, Collections.emptyList()); 712 } 713 621 714 /** 622 715 * Enables (or disables) the "Previous" button. 623 716 * @param value {@code true} to enable the button, {@code false} otherwise … … 680 773 * @since 18246 681 774 */ 682 775 public void displayImages(List<IImageEntry<?>> entries) { 683 this.displayImages( (Layer)null, entries);776 this.displayImages(null, entries); 684 777 } 685 778 686 779 /** … … 689 782 * @param entries image entries 690 783 * @since 18591 691 784 */ 692 public void displayImages( Layer layer,List<IImageEntry<?>> entries) {785 public void displayImages(@Nullable Layer layer, @Nullable List<IImageEntry<?>> entries) { 693 786 boolean imageChanged; 694 787 IImageEntry<?> entry = entries != null && entries.size() == 1 ? entries.get(0) : null; 695 788 … … 710 803 } 711 804 } 712 805 713 if (entries == null || entries.isEmpty() || entries.stream().allMatch(Objects::isNull)) { 806 807 final boolean updateRequired; 808 if (!Config.getPref().getBoolean("geoimage.viewer.show.tabs", true)) { 809 updateRequired = true; 810 // Clear the selected images in other geoimage layers 811 this.getImageTabs().map(m -> m.layer).filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast) 812 .filter(l -> !Objects.equals(entries, l.getSelection())) 813 .forEach(IGeoImageLayer::clearSelection); 814 this.tabbedEntries.clear(); 815 this.tabbedEntries.put(layer, entries); 816 } else if (entries == null || entries.isEmpty() || entries.stream().allMatch(Objects::isNull)) { 817 updateRequired = this.tabbedEntries.containsKey(layer); 714 818 this.tabbedEntries.remove(layer); 715 819 } else { 820 updateRequired = !this.tabbedEntries.containsKey(layer); 716 821 this.tabbedEntries.put(layer, entries); 717 822 } 718 this.updateLayers( );823 this.updateLayers(updateRequired); 719 824 if (entry != null) { 720 825 this.updateButtonsNonNullEntry(entry, imageChanged); 721 826 } else if (this.tabbedEntries.isEmpty()) { … … 818 923 imgDisplay.setOsdText(osd.toString()); 819 924 } 820 925 821 /**822 * Displays images for the given layer.823 * @param ignoredData the image data (unused, may be {@code null})824 * @param entries image entries825 * @since 18246 (signature)826 * @deprecated Use {@link #displayImages(List)} (The data param is no longer used)827 */828 @Deprecated829 public void displayImages(ImageData ignoredData, List<IImageEntry<?>> entries) {830 this.displayImages(entries);831 }832 833 926 private static boolean isLastImageSelected(List<IImageEntry<?>> data) { 834 927 return data.stream().anyMatch(image -> data.contains(image.getLastImage())); 835 928 } … … 857 950 if (btnCollapse != null) { 858 951 btnCollapse.setVisible(!isDocked); 859 952 } 860 this.updateLayers( );953 this.updateLayers(true); 861 954 } 862 955 863 956 /** … … 912 1005 removedData.removeImageDataUpdateListener(this); 913 1006 } 914 1007 // Unfortunately, there will be no way to remove the default null layer. This will be fixed as plugins update. 915 this. tabbedEntries.remove(e.getRemovedLayer());1008 this.updateLayers(true); 916 1009 } 917 1010 918 1011 @Override 919 1012 public void layerOrderChanged(LayerOrderChangeEvent e) { 920 // ignored1013 this.updateLayers(true); 921 1014 } 922 1015 923 1016 @Override … … 944 1037 if (layer instanceof GeoImageLayer) { 945 1038 ((GeoImageLayer) layer).getImageData().addImageDataUpdateListener(this); 946 1039 } 1040 layer.addPropertyChangeListener(l -> { 1041 if (Layer.NAME_PROP.equals(l.getPropertyName()) && this.tabbedEntries.containsKey(layer)) { 1042 this.updateLayers(true); 1043 if (this.tabbedEntries.get(layer).contains(this.currentEntry)) { 1044 this.setTitle(layer.getLabel()); 1045 } 1046 } else if (Layer.VISIBLE_PROP.equals(l.getPropertyName()) && this.tabbedEntries.containsKey(layer)) { 1047 this.getImageTabs().filter(m -> Objects.equals(m.layer, layer)).mapToInt(this.layers::indexOfComponent) 1048 .filter(i -> i >= 0).forEach(i -> this.layers.setEnabledAt(i, layer.isVisible())); 1049 } 1050 }); 947 1051 } 948 1052 949 1053 private void showLayer(Layer newLayer) {
