source: josm/trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java@ 13130

Last change on this file since 13130 was 13130, checked in by Don-vip, 6 years ago

fix #15572 - use ImageProvider attach API for all JOSM actions to ensure proper icon size everywhere

  • Property svn:eol-style set to native
File size: 18.8 KB
RevLine 
[8378]1// License: GPL. For details, see LICENSE file.
[2566]2package org.openstreetmap.josm.gui.layer.geoimage;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
[2602]7import java.awt.Component;
[2566]8import java.awt.Dimension;
[2602]9import java.awt.GridBagConstraints;
10import java.awt.GridBagLayout;
[2566]11import java.awt.event.ActionEvent;
12import java.awt.event.KeyEvent;
[2602]13import java.awt.event.WindowEvent;
[5432]14import java.text.DateFormat;
[13042]15import java.text.SimpleDateFormat;
[2566]16
[3252]17import javax.swing.AbstractAction;
[2602]18import javax.swing.Box;
[2566]19import javax.swing.JButton;
[2601]20import javax.swing.JComponent;
[2566]21import javax.swing.JPanel;
22import javax.swing.JToggleButton;
23
[12630]24import org.openstreetmap.josm.gui.MainApplication;
[6362]25import org.openstreetmap.josm.gui.dialogs.DialogsPanel.Action;
[2566]26import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
[7444]27import org.openstreetmap.josm.gui.layer.Layer;
[10345]28import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
29import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
30import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
31import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
32import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
33import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
[2566]34import org.openstreetmap.josm.tools.ImageProvider;
35import org.openstreetmap.josm.tools.Shortcut;
[7299]36import org.openstreetmap.josm.tools.date.DateUtils;
[2566]37
[12460]38/**
39 * Dialog to view and manipulate geo-tagged images from a {@link GeoImageLayer}.
40 */
[10345]41public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener {
[2566]42
43 private static final String COMMAND_ZOOM = "zoom";
44 private static final String COMMAND_CENTERVIEW = "centre";
45 private static final String COMMAND_NEXT = "next";
46 private static final String COMMAND_REMOVE = "remove";
[2627]47 private static final String COMMAND_REMOVE_FROM_DISK = "removefromdisk";
[2566]48 private static final String COMMAND_PREVIOUS = "previous";
[2602]49 private static final String COMMAND_COLLAPSE = "collapse";
[6456]50 private static final String COMMAND_FIRST = "first";
51 private static final String COMMAND_LAST = "last";
[7788]52 private static final String COMMAND_COPY_PATH = "copypath";
[2566]53
[9078]54 private final ImageDisplay imgDisplay = new ImageDisplay();
[8840]55 private boolean centerView;
[2566]56
[2969]57 // Only one instance of that class is present at one time
[8126]58 private static volatile ImageViewerDialog dialog;
[2566]59
[8840]60 private boolean collapseButtonClicked;
[2602]61
[2969]62 static void newInstance() {
63 dialog = new ImageViewerDialog();
64 }
65
[7426]66 /**
67 * Replies the unique instance of this dialog
68 * @return the unique instance
69 */
[2566]70 public static ImageViewerDialog getInstance() {
[3252]71 if (dialog == null)
[7299]72 throw new AssertionError("a new instance needs to be created first");
[2969]73 return dialog;
[2566]74 }
[2577]75
[2566]76 private JButton btnNext;
77 private JButton btnPrevious;
[2602]78 private JButton btnCollapse;
[9270]79 private JToggleButton tbCentre;
[2566]80
81 private ImageViewerDialog() {
[4842]82 super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged",
[4982]83 tr("Tool: {0}", tr("Display geotagged images")), KeyEvent.VK_Y, Shortcut.DIRECT), 200);
[8069]84 build();
[12636]85 MainApplication.getLayerManager().addActiveLayerChangeListener(this);
86 MainApplication.getLayerManager().addLayerChangeListener(this);
[8069]87 }
[2566]88
[10763]89 private void build() {
[9543]90 JPanel content = new JPanel(new BorderLayout());
[2566]91
92 content.add(imgDisplay, BorderLayout.CENTER);
93
[8510]94 Dimension buttonDim = new Dimension(26, 26);
[2566]95
[13130]96 ImageAction prevAction = new ImageAction(COMMAND_PREVIOUS, new ImageProvider("dialogs", "previous"), tr("Previous"));
[2601]97 btnPrevious = new JButton(prevAction);
98 btnPrevious.setPreferredSize(buttonDim);
99 Shortcut scPrev = Shortcut.registerShortcut(
[4982]100 "geoimage:previous", tr("Geoimage: {0}", tr("Show previous Image")), KeyEvent.VK_PAGE_UP, Shortcut.DIRECT);
[10001]101 final String previousImage = "Previous Image";
[12639]102 MainApplication.registerActionShortcut(prevAction, scPrev);
[10001]103 btnPrevious.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scPrev.getKeyStroke(), previousImage);
104 btnPrevious.getActionMap().put(previousImage, prevAction);
[7444]105 btnPrevious.setEnabled(false);
[2566]106
[10001]107 final String removePhoto = tr("Remove photo from layer");
[13130]108 ImageAction delAction = new ImageAction(COMMAND_REMOVE, new ImageProvider("dialogs", "delete"), removePhoto);
[2627]109 JButton btnDelete = new JButton(delAction);
[2601]110 btnDelete.setPreferredSize(buttonDim);
[2627]111 Shortcut scDelete = Shortcut.registerShortcut(
[4982]112 "geoimage:deleteimagefromlayer", tr("Geoimage: {0}", tr("Remove photo from layer")), KeyEvent.VK_DELETE, Shortcut.SHIFT);
[12639]113 MainApplication.registerActionShortcut(delAction, scDelete);
[10001]114 btnDelete.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scDelete.getKeyStroke(), removePhoto);
115 btnDelete.getActionMap().put(removePhoto, delAction);
[2602]116
[8509]117 ImageAction delFromDiskAction = new ImageAction(COMMAND_REMOVE_FROM_DISK,
[13130]118 new ImageProvider("dialogs", "geoimage/deletefromdisk"), tr("Delete image file from disk"));
[2627]119 JButton btnDeleteFromDisk = new JButton(delFromDiskAction);
120 btnDeleteFromDisk.setPreferredSize(buttonDim);
121 Shortcut scDeleteFromDisk = Shortcut.registerShortcut(
[4982]122 "geoimage:deletefilefromdisk", tr("Geoimage: {0}", tr("Delete File from disk")), KeyEvent.VK_DELETE, Shortcut.CTRL_SHIFT);
[10001]123 final String deleteImage = "Delete image file from disk";
[12639]124 MainApplication.registerActionShortcut(delFromDiskAction, scDeleteFromDisk);
[10001]125 btnDeleteFromDisk.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scDeleteFromDisk.getKeyStroke(), deleteImage);
126 btnDeleteFromDisk.getActionMap().put(deleteImage, delFromDiskAction);
[2627]127
[13130]128 ImageAction copyPathAction = new ImageAction(COMMAND_COPY_PATH, new ImageProvider("copy"), tr("Copy image path"));
[7788]129 JButton btnCopyPath = new JButton(copyPathAction);
130 btnCopyPath.setPreferredSize(buttonDim);
[7789]131 Shortcut scCopyPath = Shortcut.registerShortcut(
132 "geoimage:copypath", tr("Geoimage: {0}", tr("Copy image path")), KeyEvent.VK_C, Shortcut.ALT_CTRL_SHIFT);
[10001]133 final String copyImage = "Copy image path";
[12639]134 MainApplication.registerActionShortcut(copyPathAction, scCopyPath);
[10001]135 btnCopyPath.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scCopyPath.getKeyStroke(), copyImage);
136 btnCopyPath.getActionMap().put(copyImage, copyPathAction);
[7788]137
[13130]138 ImageAction nextAction = new ImageAction(COMMAND_NEXT, new ImageProvider("dialogs", "next"), tr("Next"));
[2601]139 btnNext = new JButton(nextAction);
140 btnNext.setPreferredSize(buttonDim);
141 Shortcut scNext = Shortcut.registerShortcut(
[4982]142 "geoimage:next", tr("Geoimage: {0}", tr("Show next Image")), KeyEvent.VK_PAGE_DOWN, Shortcut.DIRECT);
[10001]143 final String nextImage = "Next Image";
[12639]144 MainApplication.registerActionShortcut(nextAction, scNext);
[10001]145 btnNext.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scNext.getKeyStroke(), nextImage);
146 btnNext.getActionMap().put(nextImage, nextAction);
[7444]147 btnNext.setEnabled(false);
[2566]148
[12639]149 MainApplication.registerActionShortcut(
[6456]150 new ImageAction(COMMAND_FIRST, null, null),
151 Shortcut.registerShortcut(
152 "geoimage:first", tr("Geoimage: {0}", tr("Show first Image")), KeyEvent.VK_HOME, Shortcut.DIRECT)
153 );
[12639]154 MainApplication.registerActionShortcut(
[6456]155 new ImageAction(COMMAND_LAST, null, null),
156 Shortcut.registerShortcut(
157 "geoimage:last", tr("Geoimage: {0}", tr("Show last Image")), KeyEvent.VK_END, Shortcut.DIRECT)
158 );
159
[9270]160 tbCentre = new JToggleButton(new ImageAction(COMMAND_CENTERVIEW,
[13130]161 new ImageProvider("dialogs", "centreview"), tr("Center view")));
[2601]162 tbCentre.setPreferredSize(buttonDim);
[2602]163
[8509]164 JButton btnZoomBestFit = new JButton(new ImageAction(COMMAND_ZOOM,
[13130]165 new ImageProvider("dialogs", "zoom-best-fit"), tr("Zoom best fit and 1:1")));
[2601]166 btnZoomBestFit.setPreferredSize(buttonDim);
[2602]167
[8509]168 btnCollapse = new JButton(new ImageAction(COMMAND_COLLAPSE,
[13130]169 new ImageProvider("dialogs", "collapse"), tr("Move dialog to the side pane")));
[8510]170 btnCollapse.setPreferredSize(new Dimension(20, 20));
[2602]171 btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT);
172
173 JPanel buttons = new JPanel();
174 buttons.add(btnPrevious);
175 buttons.add(btnNext);
[7788]176 buttons.add(Box.createRigidArea(new Dimension(7, 0)));
[2602]177 buttons.add(tbCentre);
[2601]178 buttons.add(btnZoomBestFit);
[7788]179 buttons.add(Box.createRigidArea(new Dimension(7, 0)));
[2602]180 buttons.add(btnDelete);
[2627]181 buttons.add(btnDeleteFromDisk);
[7788]182 buttons.add(Box.createRigidArea(new Dimension(7, 0)));
183 buttons.add(btnCopyPath);
[2566]184
[9543]185 JPanel bottomPane = new JPanel(new GridBagLayout());
[2602]186 GridBagConstraints gc = new GridBagConstraints();
187 gc.gridx = 0;
188 gc.gridy = 0;
189 gc.anchor = GridBagConstraints.CENTER;
190 gc.weightx = 1;
191 bottomPane.add(buttons, gc);
[2566]192
[2602]193 gc.gridx = 1;
194 gc.gridy = 0;
195 gc.anchor = GridBagConstraints.PAGE_END;
196 gc.weightx = 0;
197 bottomPane.add(btnCollapse, gc);
198
199 content.add(bottomPane, BorderLayout.SOUTH);
200
[8069]201 createLayout(content, false, null);
[2566]202 }
203
[7444]204 @Override
205 public void destroy() {
[12636]206 MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
207 MainApplication.getLayerManager().removeLayerChangeListener(this);
[7444]208 super.destroy();
209 }
210
[2601]211 class ImageAction extends AbstractAction {
212 private final String action;
[8510]213
[13130]214 ImageAction(String action, ImageProvider provider, String toolTipText) {
[2601]215 this.action = action;
216 putValue(SHORT_DESCRIPTION, toolTipText);
[13130]217 if (provider != null) {
218 provider.getResource().attachImageIcon(this, true);
219 }
[2601]220 }
[2566]221
[6084]222 @Override
[2601]223 public void actionPerformed(ActionEvent e) {
224 if (COMMAND_NEXT.equals(action)) {
225 if (currentLayer != null) {
226 currentLayer.showNextPhoto();
227 }
228 } else if (COMMAND_PREVIOUS.equals(action)) {
229 if (currentLayer != null) {
230 currentLayer.showPreviousPhoto();
231 }
[6456]232 } else if (COMMAND_FIRST.equals(action) && currentLayer != null) {
233 currentLayer.showFirstPhoto();
234 } else if (COMMAND_LAST.equals(action) && currentLayer != null) {
235 currentLayer.showLastPhoto();
[2601]236 } else if (COMMAND_CENTERVIEW.equals(action)) {
[9270]237 final JToggleButton button = (JToggleButton) e.getSource();
238 centerView = button.isEnabled() && button.isSelected();
[2662]239 if (centerView && currentEntry != null && currentEntry.getPos() != null) {
[12630]240 MainApplication.getMap().mapView.zoomTo(currentEntry.getPos());
[2601]241 }
242 } else if (COMMAND_ZOOM.equals(action)) {
243 imgDisplay.zoomBestFitOrOne();
244 } else if (COMMAND_REMOVE.equals(action)) {
245 if (currentLayer != null) {
[3252]246 currentLayer.removeCurrentPhoto();
[2601]247 }
[2627]248 } else if (COMMAND_REMOVE_FROM_DISK.equals(action)) {
249 if (currentLayer != null) {
250 currentLayer.removeCurrentPhotoFromDisk();
251 }
[7788]252 } else if (COMMAND_COPY_PATH.equals(action)) {
253 if (currentLayer != null) {
254 currentLayer.copyCurrentPhotoPath();
255 }
[2602]256 } else if (COMMAND_COLLAPSE.equals(action)) {
257 collapseButtonClicked = true;
258 detachedDialog.getToolkit().getSystemEventQueue().postEvent(new WindowEvent(detachedDialog, WindowEvent.WINDOW_CLOSING));
[2566]259 }
260 }
261 }
262
263 public static void showImage(GeoImageLayer layer, ImageEntry entry) {
264 getInstance().displayImage(layer, entry);
[7444]265 if (layer != null) {
266 layer.checkPreviousNextButtons();
267 } else {
268 setPreviousEnabled(false);
269 setNextEnabled(false);
270 }
[2566]271 }
[7444]272
273 /**
274 * Enables (or disables) the "Previous" button.
275 * @param value {@code true} to enable the button, {@code false} otherwise
276 */
277 public static void setPreviousEnabled(boolean value) {
[2566]278 getInstance().btnPrevious.setEnabled(value);
279 }
[7444]280
281 /**
282 * Enables (or disables) the "Next" button.
283 * @param value {@code true} to enable the button, {@code false} otherwise
284 */
285 public static void setNextEnabled(boolean value) {
[2566]286 getInstance().btnNext.setEnabled(value);
287 }
288
[9270]289 /**
290 * Enables (or disables) the "Center view" button.
291 * @param value {@code true} to enable the button, {@code false} otherwise
292 * @return the old enabled value. Can be used to restore the original enable state
293 */
294 public static synchronized boolean setCentreEnabled(boolean value) {
295 final ImageViewerDialog instance = getInstance();
296 final boolean wasEnabled = instance.tbCentre.isEnabled();
297 instance.tbCentre.setEnabled(value);
298 instance.tbCentre.getAction().actionPerformed(new ActionEvent(instance.tbCentre, 0, null));
299 return wasEnabled;
300 }
301
[8840]302 private transient GeoImageLayer currentLayer;
303 private transient ImageEntry currentEntry;
[2566]304
305 public void displayImage(GeoImageLayer layer, ImageEntry entry) {
[6392]306 boolean imageChanged;
307
[8510]308 synchronized (this) {
[6296]309 // TODO: pop up image dialog but don't load image again
[2566]310
[6392]311 imageChanged = currentEntry != entry;
312
[12630]313 if (centerView && entry != null && MainApplication.isDisplayingMapView() && entry.getPos() != null) {
314 MainApplication.getMap().mapView.zoomTo(entry.getPos());
[2566]315 }
316
317 currentLayer = layer;
318 currentEntry = entry;
319 }
320
321 if (entry != null) {
[6392]322 if (imageChanged) {
[7299]323 // Set only if the image is new to preserve zoom and position if the same image is redisplayed
[6392]324 // (e.g. to update the OSD).
325 imgDisplay.setImage(entry.getFile(), entry.getExifOrientation());
326 }
[7660]327 setTitle(tr("Geotagged Images") + (entry.getFile() != null ? " - " + entry.getFile().getName() : ""));
[6822]328 StringBuilder osd = new StringBuilder(entry.getFile() != null ? entry.getFile().getName() : "");
[2662]329 if (entry.getElevation() != null) {
[9270]330 osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation())));
[2566]331 }
[2662]332 if (entry.getSpeed() != null) {
[7912]333 osd.append(tr("\nSpeed: {0} km/h", Math.round(entry.getSpeed())));
[2566]334 }
[3261]335 if (entry.getExifImgDir() != null) {
[3263]336 osd.append(tr("\nDirection {0}\u00b0", Math.round(entry.getExifImgDir())));
[3261]337 }
[7320]338 DateFormat dtf = DateUtils.getDateTimeFormat(DateFormat.SHORT, DateFormat.MEDIUM);
[13042]339 // Make sure date/time format includes milliseconds
340 if (dtf instanceof SimpleDateFormat) {
341 String pattern = ((SimpleDateFormat) dtf).toPattern();
342 if (!pattern.contains(".SSS")) {
343 dtf = new SimpleDateFormat(pattern.replace(":ss", ":ss.SSS"));
344 }
345 }
[6450]346 if (entry.hasExifTime()) {
[5432]347 osd.append(tr("\nEXIF time: {0}", dtf.format(entry.getExifTime())));
348 }
[6450]349 if (entry.hasGpsTime()) {
[5432]350 osd.append(tr("\nGPS time: {0}", dtf.format(entry.getGpsTime())));
351 }
[2969]352
[2566]353 imgDisplay.setOsdText(osd.toString());
354 } else {
[7660]355 // if this method is called to reinitialize dialog content with a blank image,
356 // do not actually show the dialog again with a blank image if currently hidden (fix #10672)
357 setTitle(tr("Geotagged Images"));
[4241]358 imgDisplay.setImage(null, null);
[2566]359 imgDisplay.setOsdText("");
[7660]360 return;
[2566]361 }
[8444]362 if (!isDialogShowing()) {
[2577]363 setIsDocked(false); // always open a detached window when an image is clicked and dialog is closed
364 showDialog();
365 } else {
366 if (isDocked && isCollapsed) {
367 expand();
368 dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, this);
369 }
370 }
[2566]371 }
[2577]372
[2566]373 /**
[2577]374 * When an image is closed, really close it and do not pop
375 * up the side dialog.
376 */
377 @Override
378 protected boolean dockWhenClosingDetachedDlg() {
[2602]379 if (collapseButtonClicked) {
380 collapseButtonClicked = false;
381 return true;
382 }
[2577]383 return false;
384 }
385
[2602]386 @Override
387 protected void stateChanged() {
388 super.stateChanged();
389 if (btnCollapse != null) {
390 btnCollapse.setVisible(!isDocked);
391 }
392 }
393
[2577]394 /**
[2566]395 * Returns whether an image is currently displayed
396 * @return If image is currently displayed
397 */
398 public boolean hasImage() {
399 return currentEntry != null;
400 }
[6392]401
402 /**
403 * Returns the currently displayed image.
404 * @return Currently displayed image or {@code null}
405 * @since 6392
406 */
407 public static ImageEntry getCurrentImage() {
408 return getInstance().currentEntry;
409 }
410
411 /**
412 * Returns the layer associated with the image.
413 * @return Layer associated with the image
414 * @since 6392
415 */
416 public static GeoImageLayer getCurrentLayer() {
417 return getInstance().currentLayer;
418 }
[7444]419
[9416]420 /**
421 * Returns whether the center view is currently active.
422 * @return {@code true} if the center view is active, {@code false} otherwise
423 * @since 9416
424 */
425 public static boolean isCenterView() {
426 return getInstance().centerView;
427 }
428
[7444]429 @Override
[10345]430 public void layerAdded(LayerAddEvent e) {
431 showLayer(e.getAddedLayer());
[7444]432 }
433
434 @Override
[10345]435 public void layerRemoving(LayerRemoveEvent e) {
[7444]436 // Clear current image and layer if current layer is deleted
[10345]437 if (currentLayer != null && currentLayer.equals(e.getRemovedLayer())) {
[7444]438 showImage(null, null);
439 }
[7619]440 // Check buttons state in case of layer merging
[10345]441 if (currentLayer != null && e.getRemovedLayer() instanceof GeoImageLayer) {
[7619]442 currentLayer.checkPreviousNextButtons();
443 }
[7444]444 }
[10345]445
446 @Override
447 public void layerOrderChanged(LayerOrderChangeEvent e) {
448 // ignored
449 }
450
451 @Override
452 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
453 showLayer(e.getSource().getActiveLayer());
454 }
455
456 private void showLayer(Layer newLayer) {
457 if (currentLayer == null && newLayer instanceof GeoImageLayer) {
458 ((GeoImageLayer) newLayer).showFirstPhoto();
459 }
460 }
461
[2566]462}
Note: See TracBrowser for help on using the repository browser.