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