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