001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BorderLayout;
007import java.awt.Component;
008import java.awt.event.ActionEvent;
009import java.awt.event.MouseAdapter;
010import java.awt.event.MouseEvent;
011import java.text.DateFormat;
012import java.util.ArrayList;
013import java.util.Arrays;
014import java.util.Collection;
015import java.util.List;
016
017import javax.swing.AbstractAction;
018import javax.swing.AbstractListModel;
019import javax.swing.DefaultListCellRenderer;
020import javax.swing.ImageIcon;
021import javax.swing.JLabel;
022import javax.swing.JList;
023import javax.swing.JOptionPane;
024import javax.swing.JPanel;
025import javax.swing.JScrollPane;
026import javax.swing.ListCellRenderer;
027import javax.swing.ListSelectionModel;
028import javax.swing.SwingUtilities;
029
030import org.openstreetmap.josm.actions.DownloadNotesInViewAction;
031import org.openstreetmap.josm.actions.UploadNotesAction;
032import org.openstreetmap.josm.actions.mapmode.AddNoteAction;
033import org.openstreetmap.josm.data.notes.Note;
034import org.openstreetmap.josm.data.notes.Note.State;
035import org.openstreetmap.josm.data.notes.NoteComment;
036import org.openstreetmap.josm.data.osm.NoteData;
037import org.openstreetmap.josm.data.osm.NoteData.NoteDataUpdateListener;
038import org.openstreetmap.josm.gui.MainApplication;
039import org.openstreetmap.josm.gui.MapFrame;
040import org.openstreetmap.josm.gui.NoteInputDialog;
041import org.openstreetmap.josm.gui.NoteSortDialog;
042import org.openstreetmap.josm.gui.SideButton;
043import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
044import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
045import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
046import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
047import org.openstreetmap.josm.gui.layer.NoteLayer;
048import org.openstreetmap.josm.spi.preferences.Config;
049import org.openstreetmap.josm.tools.ImageProvider;
050import org.openstreetmap.josm.tools.OpenBrowser;
051import org.openstreetmap.josm.tools.date.DateUtils;
052
053/**
054 * Dialog to display and manipulate notes.
055 * @since 7852 (renaming)
056 * @since 7608 (creation)
057 */
058public class NotesDialog extends ToggleDialog implements LayerChangeListener, NoteDataUpdateListener {
059
060    private NoteTableModel model;
061    private JList<Note> displayList;
062    private final AddCommentAction addCommentAction;
063    private final CloseAction closeAction;
064    private final DownloadNotesInViewAction downloadNotesInViewAction;
065    private final NewAction newAction;
066    private final ReopenAction reopenAction;
067    private final SortAction sortAction;
068    private final OpenInBrowserAction openInBrowserAction;
069    private final UploadNotesAction uploadAction;
070
071    private transient NoteData noteData;
072
073    /** Creates a new toggle dialog for notes */
074    public NotesDialog() {
075        super(tr("Notes"), "notes/note_open", tr("List of notes"), null, 150);
076        addCommentAction = new AddCommentAction();
077        closeAction = new CloseAction();
078        downloadNotesInViewAction = DownloadNotesInViewAction.newActionWithDownloadIcon();
079        newAction = new NewAction();
080        reopenAction = new ReopenAction();
081        sortAction = new SortAction();
082        openInBrowserAction = new OpenInBrowserAction();
083        uploadAction = new UploadNotesAction();
084        buildDialog();
085        MainApplication.getLayerManager().addLayerChangeListener(this);
086    }
087
088    private void buildDialog() {
089        model = new NoteTableModel();
090        displayList = new JList<>(model);
091        displayList.setCellRenderer(new NoteRenderer());
092        displayList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
093        displayList.addListSelectionListener(e -> {
094            if (noteData != null) { //happens when layer is deleted while note selected
095                noteData.setSelectedNote(displayList.getSelectedValue());
096            }
097            updateButtonStates();
098        });
099        displayList.addMouseListener(new MouseAdapter() {
100            //center view on selected note on double click
101            @Override
102            public void mouseClicked(MouseEvent e) {
103                if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2 && noteData != null && noteData.getSelectedNote() != null) {
104                    MainApplication.getMap().mapView.zoomTo(noteData.getSelectedNote().getLatLon());
105                }
106            }
107        });
108
109        JPanel pane = new JPanel(new BorderLayout());
110        pane.add(new JScrollPane(displayList), BorderLayout.CENTER);
111
112        createLayout(pane, false, Arrays.asList(
113                new SideButton(downloadNotesInViewAction, false),
114                new SideButton(newAction, false),
115                new SideButton(addCommentAction, false),
116                new SideButton(closeAction, false),
117                new SideButton(reopenAction, false),
118                new SideButton(sortAction, false),
119                new SideButton(openInBrowserAction, false),
120                new SideButton(uploadAction, false)));
121        updateButtonStates();
122    }
123
124    private void updateButtonStates() {
125        if (noteData == null || noteData.getSelectedNote() == null) {
126            closeAction.setEnabled(false);
127            addCommentAction.setEnabled(false);
128            reopenAction.setEnabled(false);
129        } else if (noteData.getSelectedNote().getState() == State.OPEN) {
130            closeAction.setEnabled(true);
131            addCommentAction.setEnabled(true);
132            reopenAction.setEnabled(false);
133        } else { //note is closed
134            closeAction.setEnabled(false);
135            addCommentAction.setEnabled(false);
136            reopenAction.setEnabled(true);
137        }
138        openInBrowserAction.setEnabled(noteData != null && noteData.getSelectedNote() != null && noteData.getSelectedNote().getId() > 0);
139        uploadAction.setEnabled(noteData != null && noteData.isModified());
140        //enable sort button if any notes are loaded
141        sortAction.setEnabled(noteData != null && !noteData.getNotes().isEmpty());
142    }
143
144    @Override
145    public void layerAdded(LayerAddEvent e) {
146        if (e.getAddedLayer() instanceof NoteLayer) {
147            noteData = ((NoteLayer) e.getAddedLayer()).getNoteData();
148            model.setData(noteData.getNotes());
149            setNotes(noteData.getSortedNotes());
150            noteData.addNoteDataUpdateListener(this);
151        }
152    }
153
154    @Override
155    public void layerRemoving(LayerRemoveEvent e) {
156        if (e.getRemovedLayer() instanceof NoteLayer) {
157            noteData.removeNoteDataUpdateListener(this);
158            noteData = null;
159            model.clearData();
160            MapFrame map = MainApplication.getMap();
161            if (map.mapMode instanceof AddNoteAction) {
162                map.selectMapMode(map.mapModeSelect);
163            }
164        }
165    }
166
167    @Override
168    public void layerOrderChanged(LayerOrderChangeEvent e) {
169        // ignored
170    }
171
172    @Override
173    public void noteDataUpdated(NoteData data) {
174        setNotes(data.getSortedNotes());
175    }
176
177    @Override
178    public void selectedNoteChanged(NoteData noteData) {
179        selectionChanged();
180    }
181
182    /**
183     * Sets the list of notes to be displayed in the dialog.
184     * The dialog should match the notes displayed in the note layer.
185     * @param noteList List of notes to display
186     */
187    public void setNotes(Collection<Note> noteList) {
188        model.setData(noteList);
189        updateButtonStates();
190        this.repaint();
191    }
192
193    /**
194     * Notify the dialog that the note selection has changed.
195     * Causes it to update or clear its selection in the UI.
196     */
197    public void selectionChanged() {
198        if (noteData == null || noteData.getSelectedNote() == null) {
199            displayList.clearSelection();
200        } else {
201            displayList.setSelectedValue(noteData.getSelectedNote(), true);
202        }
203        updateButtonStates();
204        // TODO make a proper listener mechanism to handle change of note selection
205        MainApplication.getMenu().infoweb.noteSelectionChanged();
206    }
207
208    /**
209     * Returns the currently selected note, if any.
210     * @return currently selected note, or null
211     * @since 8475
212     */
213    public Note getSelectedNote() {
214        return noteData != null ? noteData.getSelectedNote() : null;
215    }
216
217    @Override
218    public void destroy() {
219        MainApplication.getLayerManager().removeLayerChangeListener(this);
220        super.destroy();
221    }
222
223    private static class NoteRenderer implements ListCellRenderer<Note> {
224
225        private final DefaultListCellRenderer defaultListCellRenderer = new DefaultListCellRenderer();
226        private final DateFormat dateFormat = DateUtils.getDateTimeFormat(DateFormat.MEDIUM, DateFormat.SHORT);
227
228        @Override
229        public Component getListCellRendererComponent(JList<? extends Note> list, Note note, int index,
230                boolean isSelected, boolean cellHasFocus) {
231            Component comp = defaultListCellRenderer.getListCellRendererComponent(list, note, index, isSelected, cellHasFocus);
232            if (note != null && comp instanceof JLabel) {
233                NoteComment fstComment = note.getFirstComment();
234                JLabel jlabel = (JLabel) comp;
235                if (fstComment != null) {
236                    String text = note.getFirstComment().getText();
237                    String userName = note.getFirstComment().getUser().getName();
238                    if (userName == null || userName.isEmpty()) {
239                        userName = "<Anonymous>";
240                    }
241                    String toolTipText = userName + " @ " + dateFormat.format(note.getCreatedAt());
242                    jlabel.setToolTipText(toolTipText);
243                    jlabel.setText(note.getId() + ": " +text);
244                } else {
245                    jlabel.setToolTipText(null);
246                    jlabel.setText(Long.toString(note.getId()));
247                }
248                ImageIcon icon;
249                if (note.getId() < 0) {
250                    icon = ImageProvider.get("dialogs/notes", "note_new", ImageProvider.ImageSizes.SMALLICON);
251                } else if (note.getState() == State.CLOSED) {
252                    icon = ImageProvider.get("dialogs/notes", "note_closed", ImageProvider.ImageSizes.SMALLICON);
253                } else {
254                    icon = ImageProvider.get("dialogs/notes", "note_open", ImageProvider.ImageSizes.SMALLICON);
255                }
256                jlabel.setIcon(icon);
257            }
258            return comp;
259        }
260    }
261
262    class NoteTableModel extends AbstractListModel<Note> {
263        private final transient List<Note> data;
264
265        /**
266         * Constructs a new {@code NoteTableModel}.
267         */
268        NoteTableModel() {
269            data = new ArrayList<>();
270        }
271
272        @Override
273        public int getSize() {
274            if (data == null) {
275                return 0;
276            }
277            return data.size();
278        }
279
280        @Override
281        public Note getElementAt(int index) {
282            return data.get(index);
283        }
284
285        public void setData(Collection<Note> noteList) {
286            data.clear();
287            data.addAll(noteList);
288            fireContentsChanged(this, 0, noteList.size());
289        }
290
291        public void clearData() {
292            displayList.clearSelection();
293            data.clear();
294            fireIntervalRemoved(this, 0, getSize());
295        }
296    }
297
298    class AddCommentAction extends AbstractAction {
299
300        /**
301         * Constructs a new {@code AddCommentAction}.
302         */
303        AddCommentAction() {
304            putValue(SHORT_DESCRIPTION, tr("Add comment"));
305            putValue(NAME, tr("Comment"));
306            new ImageProvider("dialogs/notes", "note_comment").getResource().attachImageIcon(this, true);
307        }
308
309        @Override
310        public void actionPerformed(ActionEvent e) {
311            Note note = displayList.getSelectedValue();
312            if (note == null) {
313                JOptionPane.showMessageDialog(MainApplication.getMap(),
314                        "You must select a note first",
315                        "No note selected",
316                        JOptionPane.ERROR_MESSAGE);
317                return;
318            }
319            NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Comment on note"), tr("Add comment"));
320            dialog.showNoteDialog(tr("Add comment to note:"), ImageProvider.get("dialogs/notes", "note_comment"));
321            if (dialog.getValue() != 1) {
322                return;
323            }
324            int selectedIndex = displayList.getSelectedIndex();
325            noteData.addCommentToNote(note, dialog.getInputText());
326            noteData.setSelectedNote(model.getElementAt(selectedIndex));
327        }
328    }
329
330    class CloseAction extends AbstractAction {
331
332        /**
333         * Constructs a new {@code CloseAction}.
334         */
335        CloseAction() {
336            putValue(SHORT_DESCRIPTION, tr("Close note"));
337            putValue(NAME, tr("Close"));
338            new ImageProvider("dialogs/notes", "note_closed").getResource().attachImageIcon(this, true);
339        }
340
341        @Override
342        public void actionPerformed(ActionEvent e) {
343            NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Close note"), tr("Close note"));
344            dialog.showNoteDialog(tr("Close note with message:"), ImageProvider.get("dialogs/notes", "note_closed"));
345            if (dialog.getValue() != 1) {
346                return;
347            }
348            Note note = displayList.getSelectedValue();
349            int selectedIndex = displayList.getSelectedIndex();
350            noteData.closeNote(note, dialog.getInputText());
351            noteData.setSelectedNote(model.getElementAt(selectedIndex));
352        }
353    }
354
355    class NewAction extends AbstractAction {
356
357        /**
358         * Constructs a new {@code NewAction}.
359         */
360        NewAction() {
361            putValue(SHORT_DESCRIPTION, tr("Create a new note"));
362            putValue(NAME, tr("Create"));
363            new ImageProvider("dialogs/notes", "note_new").getResource().attachImageIcon(this, true);
364        }
365
366        @Override
367        public void actionPerformed(ActionEvent e) {
368            if (noteData == null) { //there is no notes layer. Create one first
369                MainApplication.getLayerManager().addLayer(new NoteLayer());
370            }
371            MainApplication.getMap().selectMapMode(new AddNoteAction(noteData));
372        }
373    }
374
375    class ReopenAction extends AbstractAction {
376
377        /**
378         * Constructs a new {@code ReopenAction}.
379         */
380        ReopenAction() {
381            putValue(SHORT_DESCRIPTION, tr("Reopen note"));
382            putValue(NAME, tr("Reopen"));
383            new ImageProvider("dialogs/notes", "note_open").getResource().attachImageIcon(this, true);
384        }
385
386        @Override
387        public void actionPerformed(ActionEvent e) {
388            NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Reopen note"), tr("Reopen note"));
389            dialog.showNoteDialog(tr("Reopen note with message:"), ImageProvider.get("dialogs/notes", "note_open"));
390            if (dialog.getValue() != 1) {
391                return;
392            }
393
394            Note note = displayList.getSelectedValue();
395            int selectedIndex = displayList.getSelectedIndex();
396            noteData.reOpenNote(note, dialog.getInputText());
397            noteData.setSelectedNote(model.getElementAt(selectedIndex));
398        }
399    }
400
401    class SortAction extends AbstractAction {
402
403        /**
404         * Constructs a new {@code SortAction}.
405         */
406        SortAction() {
407            putValue(SHORT_DESCRIPTION, tr("Sort notes"));
408            putValue(NAME, tr("Sort"));
409            new ImageProvider("dialogs", "sort").getResource().attachImageIcon(this, true);
410        }
411
412        @Override
413        public void actionPerformed(ActionEvent e) {
414            NoteSortDialog sortDialog = new NoteSortDialog(MainApplication.getMainFrame(), tr("Sort notes"), tr("Apply"));
415            sortDialog.showSortDialog(noteData.getCurrentSortMethod());
416            if (sortDialog.getValue() == 1) {
417                noteData.setSortMethod(sortDialog.getSelectedComparator());
418            }
419        }
420    }
421
422    class OpenInBrowserAction extends AbstractAction {
423        OpenInBrowserAction() {
424            putValue(SHORT_DESCRIPTION, tr("Open the note in an external browser"));
425            new ImageProvider("help", "internet").getResource().attachImageIcon(this, true);
426        }
427
428        @Override
429        public void actionPerformed(ActionEvent e) {
430            final Note note = displayList.getSelectedValue();
431            if (note.getId() > 0) {
432                final String url = Config.getUrls().getBaseBrowseUrl() + "/note/" + note.getId();
433                OpenBrowser.displayUrl(url);
434            }
435        }
436    }
437
438}