source: josm/trunk/src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java@ 8912

Last change on this file since 8912 was 8912, checked in by simon04, 9 years ago

fix #11151 - Not all buttons are shown in exit confirmation dialog

Use GridBagLayout to avoid disappearance of buttons.

  • Property svn:eol-style set to native
File size: 24.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.BorderLayout;
8import java.awt.Component;
9import java.awt.Dimension;
10import java.awt.Graphics2D;
11import java.awt.GridBagConstraints;
12import java.awt.GridBagLayout;
13import java.awt.Image;
14import java.awt.event.ActionEvent;
15import java.awt.event.WindowAdapter;
16import java.awt.event.WindowEvent;
17import java.awt.image.BufferedImage;
18import java.beans.PropertyChangeEvent;
19import java.beans.PropertyChangeListener;
20import java.util.List;
21import java.util.concurrent.CancellationException;
22import java.util.concurrent.ExecutorService;
23import java.util.concurrent.Executors;
24import java.util.concurrent.Future;
25
26import javax.swing.AbstractAction;
27import javax.swing.DefaultListCellRenderer;
28import javax.swing.ImageIcon;
29import javax.swing.JButton;
30import javax.swing.JComponent;
31import javax.swing.JDialog;
32import javax.swing.JLabel;
33import javax.swing.JList;
34import javax.swing.JOptionPane;
35import javax.swing.JPanel;
36import javax.swing.JScrollPane;
37import javax.swing.KeyStroke;
38import javax.swing.ListCellRenderer;
39import javax.swing.WindowConstants;
40import javax.swing.event.TableModelEvent;
41import javax.swing.event.TableModelListener;
42
43import org.openstreetmap.josm.Main;
44import org.openstreetmap.josm.actions.UploadAction;
45import org.openstreetmap.josm.gui.ExceptionDialogUtil;
46import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
47import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
48import org.openstreetmap.josm.gui.progress.ProgressMonitor;
49import org.openstreetmap.josm.gui.progress.SwingRenderingProgressMonitor;
50import org.openstreetmap.josm.gui.util.GuiHelper;
51import org.openstreetmap.josm.tools.GBC;
52import org.openstreetmap.josm.tools.ImageProvider;
53import org.openstreetmap.josm.tools.Utils;
54import org.openstreetmap.josm.tools.WindowGeometry;
55
56public class SaveLayersDialog extends JDialog implements TableModelListener {
57 public enum UserAction {
58 /** save/upload layers was successful, proceed with operation */
59 PROCEED,
60 /** save/upload of layers was not successful or user canceled operation */
61 CANCEL
62 }
63
64 private SaveLayersModel model;
65 private UserAction action = UserAction.CANCEL;
66 private UploadAndSaveProgressRenderer pnlUploadLayers;
67
68 private SaveAndProceedAction saveAndProceedAction;
69 private DiscardAndProceedAction discardAndProceedAction;
70 private CancelAction cancelAction;
71 private transient SaveAndUploadTask saveAndUploadTask;
72
73 /**
74 * builds the GUI
75 */
76 protected void build() {
77 WindowGeometry geometry = WindowGeometry.centerOnScreen(new Dimension(650, 300));
78 geometry.applySafe(this);
79 getContentPane().setLayout(new BorderLayout());
80
81 model = new SaveLayersModel();
82 SaveLayersTable table = new SaveLayersTable(model);
83 JScrollPane pane = new JScrollPane(table);
84 model.addPropertyChangeListener(table);
85 table.getModel().addTableModelListener(this);
86
87 getContentPane().add(pane, BorderLayout.CENTER);
88 getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);
89
90 addWindowListener(new WindowClosingAdapter());
91 setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
92 }
93
94 private JButton saveAndProceedActionButton;
95
96 /**
97 * builds the button row
98 *
99 * @return the panel with the button row
100 */
101 protected JPanel buildButtonRow() {
102 JPanel pnl = new JPanel();
103 pnl.setLayout(new GridBagLayout());
104
105 saveAndProceedAction = new SaveAndProceedAction();
106 model.addPropertyChangeListener(saveAndProceedAction);
107 pnl.add(saveAndProceedActionButton = new JButton(saveAndProceedAction), GBC.std().insets(5, 5, 5, 5).fill(GBC.HORIZONTAL));
108
109 discardAndProceedAction = new DiscardAndProceedAction();
110 model.addPropertyChangeListener(discardAndProceedAction);
111 pnl.add(new JButton(discardAndProceedAction), GBC.std().insets(0, 0, 5, 0).fill(GBC.HORIZONTAL));
112
113 cancelAction = new CancelAction();
114 pnl.add(new JButton(cancelAction), GBC.std().insets(0, 0, 5, 0).fill(GBC.HORIZONTAL));
115
116 JPanel pnl2 = new JPanel();
117 pnl2.setLayout(new BorderLayout());
118 pnl2.add(pnlUploadLayers = new UploadAndSaveProgressRenderer(), BorderLayout.CENTER);
119 model.addPropertyChangeListener(pnlUploadLayers);
120 pnl2.add(pnl, BorderLayout.SOUTH);
121 return pnl2;
122 }
123
124 public void prepareForSavingAndUpdatingLayersBeforeExit() {
125 setTitle(tr("Unsaved changes - Save/Upload before exiting?"));
126 this.saveAndProceedAction.initForSaveAndExit();
127 this.discardAndProceedAction.initForDiscardAndExit();
128 }
129
130 public void prepareForSavingAndUpdatingLayersBeforeDelete() {
131 setTitle(tr("Unsaved changes - Save/Upload before deleting?"));
132 this.saveAndProceedAction.initForSaveAndDelete();
133 this.discardAndProceedAction.initForDiscardAndDelete();
134 }
135
136 public SaveLayersDialog(Component parent) {
137 super(JOptionPane.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL);
138 build();
139 }
140
141 public UserAction getUserAction() {
142 return this.action;
143 }
144
145 public SaveLayersModel getModel() {
146 return model;
147 }
148
149 protected void launchSafeAndUploadTask() {
150 ProgressMonitor monitor = new SwingRenderingProgressMonitor(pnlUploadLayers);
151 monitor.beginTask(tr("Uploading and saving modified layers ..."));
152 this.saveAndUploadTask = new SaveAndUploadTask(model, monitor);
153 new Thread(saveAndUploadTask, saveAndUploadTask.getClass().getName()).start();
154 }
155
156 protected void cancelSafeAndUploadTask() {
157 if (this.saveAndUploadTask != null) {
158 this.saveAndUploadTask.cancel();
159 }
160 model.setMode(Mode.EDITING_DATA);
161 }
162
163 private static class LayerListWarningMessagePanel extends JPanel {
164 private JLabel lblMessage;
165 private JList<SaveLayerInfo> lstLayers;
166
167 protected void build() {
168 setLayout(new GridBagLayout());
169 GridBagConstraints gc = new GridBagConstraints();
170 gc.gridx = 0;
171 gc.gridy = 0;
172 gc.fill = GridBagConstraints.HORIZONTAL;
173 gc.weightx = 1.0;
174 gc.weighty = 0.0;
175 add(lblMessage = new JLabel(), gc);
176 lblMessage.setHorizontalAlignment(JLabel.LEFT);
177 lstLayers = new JList<>();
178 lstLayers.setCellRenderer(
179 new ListCellRenderer<SaveLayerInfo>() {
180 private final DefaultListCellRenderer def = new DefaultListCellRenderer();
181 @Override
182 public Component getListCellRendererComponent(JList<? extends SaveLayerInfo> list, SaveLayerInfo info, int index,
183 boolean isSelected, boolean cellHasFocus) {
184 def.setIcon(info.getLayer().getIcon());
185 def.setText(info.getName());
186 return def;
187 }
188 }
189 );
190 gc.gridx = 0;
191 gc.gridy = 1;
192 gc.fill = GridBagConstraints.HORIZONTAL;
193 gc.weightx = 1.0;
194 gc.weighty = 1.0;
195 add(lstLayers, gc);
196 }
197
198 LayerListWarningMessagePanel(String msg, List<SaveLayerInfo> infos) {
199 build();
200 lblMessage.setText(msg);
201 lstLayers.setListData(infos.toArray(new SaveLayerInfo[0]));
202 }
203 }
204
205 protected void warnLayersWithConflictsAndUploadRequest(List<SaveLayerInfo> infos) {
206 String msg = trn("<html>{0} layer has unresolved conflicts.<br>"
207 + "Either resolve them first or discard the modifications.<br>"
208 + "Layer with conflicts:</html>",
209 "<html>{0} layers have unresolved conflicts.<br>"
210 + "Either resolve them first or discard the modifications.<br>"
211 + "Layers with conflicts:</html>",
212 infos.size(),
213 infos.size());
214 JOptionPane.showConfirmDialog(
215 Main.parent,
216 new LayerListWarningMessagePanel(msg, infos),
217 tr("Unsaved data and conflicts"),
218 JOptionPane.DEFAULT_OPTION,
219 JOptionPane.WARNING_MESSAGE
220 );
221 }
222
223 protected void warnLayersWithoutFilesAndSaveRequest(List<SaveLayerInfo> infos) {
224 String msg = trn("<html>{0} layer needs saving but has no associated file.<br>"
225 + "Either select a file for this layer or discard the changes.<br>"
226 + "Layer without a file:</html>",
227 "<html>{0} layers need saving but have no associated file.<br>"
228 + "Either select a file for each of them or discard the changes.<br>"
229 + "Layers without a file:</html>",
230 infos.size(),
231 infos.size());
232 JOptionPane.showConfirmDialog(
233 Main.parent,
234 new LayerListWarningMessagePanel(msg, infos),
235 tr("Unsaved data and missing associated file"),
236 JOptionPane.DEFAULT_OPTION,
237 JOptionPane.WARNING_MESSAGE
238 );
239 }
240
241 protected void warnLayersWithIllegalFilesAndSaveRequest(List<SaveLayerInfo> infos) {
242 String msg = trn("<html>{0} layer needs saving but has an associated file<br>"
243 + "which cannot be written.<br>"
244 + "Either select another file for this layer or discard the changes.<br>"
245 + "Layer with a non-writable file:</html>",
246 "<html>{0} layers need saving but have associated files<br>"
247 + "which cannot be written.<br>"
248 + "Either select another file for each of them or discard the changes.<br>"
249 + "Layers with non-writable files:</html>",
250 infos.size(),
251 infos.size());
252 JOptionPane.showConfirmDialog(
253 Main.parent,
254 new LayerListWarningMessagePanel(msg, infos),
255 tr("Unsaved data non-writable files"),
256 JOptionPane.DEFAULT_OPTION,
257 JOptionPane.WARNING_MESSAGE
258 );
259 }
260
261 protected boolean confirmSaveLayerInfosOK() {
262 List<SaveLayerInfo> layerInfos = model.getLayersWithConflictsAndUploadRequest();
263 if (!layerInfos.isEmpty()) {
264 warnLayersWithConflictsAndUploadRequest(layerInfos);
265 return false;
266 }
267
268 layerInfos = model.getLayersWithoutFilesAndSaveRequest();
269 if (!layerInfos.isEmpty()) {
270 warnLayersWithoutFilesAndSaveRequest(layerInfos);
271 return false;
272 }
273
274 layerInfos = model.getLayersWithIllegalFilesAndSaveRequest();
275 if (!layerInfos.isEmpty()) {
276 warnLayersWithIllegalFilesAndSaveRequest(layerInfos);
277 return false;
278 }
279
280 return true;
281 }
282
283 protected void setUserAction(UserAction action) {
284 this.action = action;
285 }
286
287 /**
288 * Closes this dialog and frees all native screen resources.
289 */
290 public void closeDialog() {
291 setVisible(false);
292 dispose();
293 }
294
295 class WindowClosingAdapter extends WindowAdapter {
296 @Override
297 public void windowClosing(WindowEvent e) {
298 cancelAction.cancel();
299 }
300 }
301
302 class CancelAction extends AbstractAction {
303 CancelAction() {
304 putValue(NAME, tr("Cancel"));
305 putValue(SHORT_DESCRIPTION, tr("Close this dialog and resume editing in JOSM"));
306 putValue(SMALL_ICON, ImageProvider.get("cancel"));
307 getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
308 .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE");
309 getRootPane().getActionMap().put("ESCAPE", this);
310 }
311
312 protected void cancelWhenInEditingModel() {
313 setUserAction(UserAction.CANCEL);
314 closeDialog();
315 }
316
317 public void cancel() {
318 switch(model.getMode()) {
319 case EDITING_DATA: cancelWhenInEditingModel(); break;
320 case UPLOADING_AND_SAVING: cancelSafeAndUploadTask(); break;
321 }
322 }
323
324 @Override
325 public void actionPerformed(ActionEvent e) {
326 cancel();
327 }
328 }
329
330 class DiscardAndProceedAction extends AbstractAction implements PropertyChangeListener {
331 DiscardAndProceedAction() {
332 initForDiscardAndExit();
333 }
334
335 public void initForDiscardAndExit() {
336 putValue(NAME, tr("Exit now!"));
337 putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost."));
338 putValue(SMALL_ICON, ImageProvider.get("exit"));
339 }
340
341 public void initForDiscardAndDelete() {
342 putValue(NAME, tr("Delete now!"));
343 putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost."));
344 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
345 }
346
347 @Override
348 public void actionPerformed(ActionEvent e) {
349 setUserAction(UserAction.PROCEED);
350 closeDialog();
351 }
352
353 @Override
354 public void propertyChange(PropertyChangeEvent evt) {
355 if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
356 Mode mode = (Mode) evt.getNewValue();
357 switch(mode) {
358 case EDITING_DATA: setEnabled(true); break;
359 case UPLOADING_AND_SAVING: setEnabled(false); break;
360 }
361 }
362 }
363 }
364
365 final class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener {
366 private static final int is = 24; // icon size
367 private static final String BASE_ICON = "BASE_ICON";
368 private final transient Image save = ImageProvider.get("save").getImage();
369 private final transient Image upld = ImageProvider.get("upload").getImage();
370 private final transient Image saveDis = new BufferedImage(is, is, BufferedImage.TYPE_4BYTE_ABGR);
371 private final transient Image upldDis = new BufferedImage(is, is, BufferedImage.TYPE_4BYTE_ABGR);
372
373 SaveAndProceedAction() {
374 // get disabled versions of icons
375 new JLabel(ImageProvider.get("save")).getDisabledIcon().paintIcon(new JPanel(), saveDis.getGraphics(), 0, 0);
376 new JLabel(ImageProvider.get("upload")).getDisabledIcon().paintIcon(new JPanel(), upldDis.getGraphics(), 0, 0);
377 initForSaveAndExit();
378 }
379
380 public void initForSaveAndExit() {
381 putValue(NAME, tr("Perform actions before exiting"));
382 putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved."));
383 putValue(BASE_ICON, ImageProvider.get("exit"));
384 redrawIcon();
385 }
386
387 public void initForSaveAndDelete() {
388 putValue(NAME, tr("Perform actions before deleting"));
389 putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost."));
390 putValue(BASE_ICON, ImageProvider.get("dialogs", "delete"));
391 redrawIcon();
392 }
393
394 public void redrawIcon() {
395 try { // Can fail if model is not yet setup properly
396 Image base = ((ImageIcon) getValue(BASE_ICON)).getImage();
397 BufferedImage newIco = new BufferedImage(is*3, is, BufferedImage.TYPE_4BYTE_ABGR);
398 Graphics2D g = newIco.createGraphics();
399 g.drawImage(model.getLayersToUpload().isEmpty() ? upldDis : upld, is*0, 0, is, is, null);
400 g.drawImage(model.getLayersToSave().isEmpty() ? saveDis : save, is*1, 0, is, is, null);
401 g.drawImage(base, is*2, 0, is, is, null);
402 putValue(SMALL_ICON, new ImageIcon(newIco));
403 } catch (Exception e) {
404 putValue(SMALL_ICON, getValue(BASE_ICON));
405 }
406 }
407
408 @Override
409 public void actionPerformed(ActionEvent e) {
410 if (!confirmSaveLayerInfosOK())
411 return;
412 launchSafeAndUploadTask();
413 }
414
415 @Override
416 public void propertyChange(PropertyChangeEvent evt) {
417 if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
418 SaveLayersModel.Mode mode = (SaveLayersModel.Mode) evt.getNewValue();
419 switch(mode) {
420 case EDITING_DATA: setEnabled(true); break;
421 case UPLOADING_AND_SAVING: setEnabled(false); break;
422 }
423 }
424 }
425 }
426
427 /**
428 * This is the asynchronous task which uploads modified layers to the server and
429 * saves them to files, if requested by the user.
430 *
431 */
432 protected class SaveAndUploadTask implements Runnable {
433
434 private final SaveLayersModel model;
435 private final ProgressMonitor monitor;
436 private final ExecutorService worker;
437 private boolean canceled;
438 private Future<?> currentFuture;
439 private AbstractIOTask currentTask;
440
441 public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) {
442 this.model = model;
443 this.monitor = monitor;
444 this.worker = Executors.newSingleThreadExecutor(Utils.newThreadFactory(getClass() + "-%d", Thread.NORM_PRIORITY));
445 }
446
447 protected void uploadLayers(List<SaveLayerInfo> toUpload) {
448 for (final SaveLayerInfo layerInfo: toUpload) {
449 AbstractModifiableLayer layer = layerInfo.getLayer();
450 if (canceled) {
451 model.setUploadState(layer, UploadOrSaveState.CANCELED);
452 continue;
453 }
454 monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
455
456 if (!UploadAction.checkPreUploadConditions(layer)) {
457 model.setUploadState(layer, UploadOrSaveState.FAILED);
458 continue;
459 }
460
461 AbstractUploadDialog dialog = layer.getUploadDialog();
462 if (dialog != null) {
463 dialog.setVisible(true);
464 if (dialog.isCanceled()) {
465 model.setUploadState(layer, UploadOrSaveState.CANCELED);
466 continue;
467 }
468 dialog.rememberUserInput();
469 }
470
471 currentTask = layer.createUploadTask(monitor);
472 if (currentTask == null) {
473 model.setUploadState(layer, UploadOrSaveState.FAILED);
474 continue;
475 }
476 currentFuture = worker.submit(currentTask);
477 try {
478 // wait for the asynchronous task to complete
479 //
480 currentFuture.get();
481 } catch (CancellationException e) {
482 model.setUploadState(layer, UploadOrSaveState.CANCELED);
483 } catch (Exception e) {
484 Main.error(e);
485 model.setUploadState(layer, UploadOrSaveState.FAILED);
486 ExceptionDialogUtil.explainException(e);
487 }
488 if (currentTask.isCanceled()) {
489 model.setUploadState(layer, UploadOrSaveState.CANCELED);
490 } else if (currentTask.isFailed()) {
491 Main.error(currentTask.getLastException());
492 ExceptionDialogUtil.explainException(currentTask.getLastException());
493 model.setUploadState(layer, UploadOrSaveState.FAILED);
494 } else {
495 model.setUploadState(layer, UploadOrSaveState.OK);
496 }
497 currentTask = null;
498 currentFuture = null;
499 }
500 }
501
502 protected void saveLayers(List<SaveLayerInfo> toSave) {
503 for (final SaveLayerInfo layerInfo: toSave) {
504 if (canceled) {
505 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
506 continue;
507 }
508 // Check save preconditions earlier to avoid a blocking reentring call to EDT (see #10086)
509 if (layerInfo.isDoCheckSaveConditions()) {
510 if (!layerInfo.getLayer().checkSaveConditions()) {
511 continue;
512 }
513 layerInfo.setDoCheckSaveConditions(false);
514 }
515 currentTask = new SaveLayerTask(layerInfo, monitor);
516 currentFuture = worker.submit(currentTask);
517
518 try {
519 // wait for the asynchronous task to complete
520 //
521 currentFuture.get();
522 } catch (CancellationException e) {
523 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
524 } catch (Exception e) {
525 Main.error(e);
526 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
527 ExceptionDialogUtil.explainException(e);
528 }
529 if (currentTask.isCanceled()) {
530 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
531 } else if (currentTask.isFailed()) {
532 if (currentTask.getLastException() != null) {
533 Main.error(currentTask.getLastException());
534 ExceptionDialogUtil.explainException(currentTask.getLastException());
535 }
536 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
537 } else {
538 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK);
539 }
540 this.currentTask = null;
541 this.currentFuture = null;
542 }
543 }
544
545 protected void warnBecauseOfUnsavedData() {
546 int numProblems = model.getNumCancel() + model.getNumFailed();
547 if (numProblems == 0) return;
548 Main.warn(numProblems + " problems occured during upload/save");
549 String msg = trn(
550 "<html>An upload and/or save operation of one layer with modifications<br>"
551 + "was canceled or has failed.</html>",
552 "<html>Upload and/or save operations of {0} layers with modifications<br>"
553 + "were canceled or have failed.</html>",
554 numProblems,
555 numProblems
556 );
557 JOptionPane.showMessageDialog(
558 Main.parent,
559 msg,
560 tr("Incomplete upload and/or save"),
561 JOptionPane.WARNING_MESSAGE
562 );
563 }
564
565 @Override
566 public void run() {
567 GuiHelper.runInEDTAndWait(new Runnable() {
568 @Override
569 public void run() {
570 model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
571 List<SaveLayerInfo> toUpload = model.getLayersToUpload();
572 if (!toUpload.isEmpty()) {
573 uploadLayers(toUpload);
574 }
575 List<SaveLayerInfo> toSave = model.getLayersToSave();
576 if (!toSave.isEmpty()) {
577 saveLayers(toSave);
578 }
579 model.setMode(SaveLayersModel.Mode.EDITING_DATA);
580 if (model.hasUnsavedData()) {
581 warnBecauseOfUnsavedData();
582 model.setMode(Mode.EDITING_DATA);
583 if (canceled) {
584 setUserAction(UserAction.CANCEL);
585 closeDialog();
586 }
587 } else {
588 setUserAction(UserAction.PROCEED);
589 closeDialog();
590 }
591 }
592 });
593 worker.shutdownNow();
594 }
595
596 public void cancel() {
597 if (currentTask != null) {
598 currentTask.cancel();
599 }
600 worker.shutdown();
601 canceled = true;
602 }
603 }
604
605 @Override
606 public void tableChanged(TableModelEvent arg0) {
607 boolean dis = model.getLayersToSave().isEmpty() && model.getLayersToUpload().isEmpty();
608 if (saveAndProceedActionButton != null) {
609 saveAndProceedActionButton.setEnabled(!dis);
610 }
611 saveAndProceedAction.redrawIcon();
612 }
613}
Note: See TracBrowser for help on using the repository browser.