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