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