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

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

fix #5603, see #5256, see #15240 - add first/last buttons for geoimage dialog. Fixed shortcuts. Switch to SVG icons taken from https://commons.wikimedia.org/wiki/GNOME_Desktop_icons#Navigation

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