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        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    @Override
226    public void destroy() {
227        MainApplication.getLayerManager().removeLayerChangeListener(this);
228        super.destroy();
229    }
230
231    private static class NoteRenderer implements ListCellRenderer<Note> {
232
233        private final DefaultListCellRenderer defaultListCellRenderer = new DefaultListCellRenderer();
234        private final DateFormat dateFormat = DateUtils.getDateTimeFormat(DateFormat.MEDIUM, DateFormat.SHORT);
235
236        @Override
237        public Component getListCellRendererComponent(JList<? extends Note> list, Note note, int index,
238                boolean isSelected, boolean cellHasFocus) {
239            Component comp = defaultListCellRenderer.getListCellRendererComponent(list, note, index, isSelected, cellHasFocus);
240            if (note != null && comp instanceof JLabel) {
241                NoteComment fstComment = note.getFirstComment();
242                JLabel jlabel = (JLabel) comp;
243                if (fstComment != null) {
244                    String text = note.getFirstComment().getText();
245                    String userName = note.getFirstComment().getUser().getName();
246                    if (userName == null || userName.isEmpty()) {
247                        userName = "<Anonymous>";
248                    }
249                    String toolTipText = userName + " @ " + dateFormat.format(note.getCreatedAt());
250                    jlabel.setToolTipText(toolTipText);
251                    jlabel.setText(note.getId() + ": " +text);
252                } else {
253                    jlabel.setToolTipText(null);
254                    jlabel.setText(Long.toString(note.getId()));
255                }
256                ImageIcon icon;
257                if (note.getId() < 0) {
258                    icon = ImageProvider.get("dialogs/notes", "note_new", ImageProvider.ImageSizes.SMALLICON);
259                } else if (note.getState() == State.CLOSED) {
260                    icon = ImageProvider.get("dialogs/notes", "note_closed", ImageProvider.ImageSizes.SMALLICON);
261                } else {
262                    icon = ImageProvider.get("dialogs/notes", "note_open", ImageProvider.ImageSizes.SMALLICON);
263                }
264                jlabel.setIcon(icon);
265            }
266            return comp;
267        }
268    }
269
270    class NoteTableModel extends AbstractListModel<Note> {
271        private final transient List<Note> data;
272
273        /**
274         * Constructs a new {@code NoteTableModel}.
275         */
276        NoteTableModel() {
277            data = new ArrayList<>();
278        }
279
280        @Override
281        public int getSize() {
282            if (data == null) {
283                return 0;
284            }
285            return data.size();
286        }
287
288        @Override
289        public Note getElementAt(int index) {
290            return data.get(index);
291        }
292
293        public void setData(Collection<Note> noteList) {
294            data.clear();
295            data.addAll(noteList);
296            fireContentsChanged(this, 0, noteList.size());
297        }
298
299        public void clearData() {
300            displayList.clearSelection();
301            data.clear();
302            fireIntervalRemoved(this, 0, getSize());
303        }
304    }
305
306    class AddCommentAction extends AbstractAction {
307
308        /**
309         * Constructs a new {@code AddCommentAction}.
310         */
311        AddCommentAction() {
312            putValue(SHORT_DESCRIPTION, tr("Add comment"));
313            putValue(NAME, tr("Comment"));
314            new ImageProvider("dialogs/notes", "note_comment").getResource().attachImageIcon(this, true);
315        }
316
317        @Override
318        public void actionPerformed(ActionEvent e) {
319            Note note = displayList.getSelectedValue();
320            if (note == null) {
321                JOptionPane.showMessageDialog(MainApplication.getMap(),
322                        "You must select a note first",
323                        "No note selected",
324                        JOptionPane.ERROR_MESSAGE);
325                return;
326            }
327            NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Comment on note"), tr("Add comment"));
328            dialog.showNoteDialog(tr("Add comment to note:"), ImageProvider.get("dialogs/notes", "note_comment"));
329            if (dialog.getValue() != 1) {
330                return;
331            }
332            int selectedIndex = displayList.getSelectedIndex();
333            noteData.addCommentToNote(note, dialog.getInputText());
334            noteData.setSelectedNote(model.getElementAt(selectedIndex));
335        }
336    }
337
338    class CloseAction extends AbstractAction {
339
340        /**
341         * Constructs a new {@code CloseAction}.
342         */
343        CloseAction() {
344            putValue(SHORT_DESCRIPTION, tr("Close note"));
345            putValue(NAME, tr("Close"));
346            new ImageProvider("dialogs/notes", "note_closed").getResource().attachImageIcon(this, true);
347        }
348
349        @Override
350        public void actionPerformed(ActionEvent e) {
351            NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Close note"), tr("Close note"));
352            dialog.showNoteDialog(tr("Close note with message:"), ImageProvider.get("dialogs/notes", "note_closed"));
353            if (dialog.getValue() != 1) {
354                return;
355            }
356            Note note = displayList.getSelectedValue();
357            int selectedIndex = displayList.getSelectedIndex();
358            noteData.closeNote(note, dialog.getInputText());
359            noteData.setSelectedNote(model.getElementAt(selectedIndex));
360        }
361    }
362
363    class NewAction extends AbstractAction {
364
365        /**
366         * Constructs a new {@code NewAction}.
367         */
368        NewAction() {
369            putValue(SHORT_DESCRIPTION, tr("Create a new note"));
370            putValue(NAME, tr("Create"));
371            new ImageProvider("dialogs/notes", "note_new").getResource().attachImageIcon(this, true);
372        }
373
374        @Override
375        public void actionPerformed(ActionEvent e) {
376            if (noteData == null) { //there is no notes layer. Create one first
377                MainApplication.getLayerManager().addLayer(new NoteLayer());
378            }
379            MainApplication.getMap().selectMapMode(new AddNoteAction(noteData));
380        }
381    }
382
383    class ReopenAction extends AbstractAction {
384
385        /**
386         * Constructs a new {@code ReopenAction}.
387         */
388        ReopenAction() {
389            putValue(SHORT_DESCRIPTION, tr("Reopen note"));
390            putValue(NAME, tr("Reopen"));
391            new ImageProvider("dialogs/notes", "note_open").getResource().attachImageIcon(this, true);
392        }
393
394        @Override
395        public void actionPerformed(ActionEvent e) {
396            NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Reopen note"), tr("Reopen note"));
397            dialog.showNoteDialog(tr("Reopen note with message:"), ImageProvider.get("dialogs/notes", "note_open"));
398            if (dialog.getValue() != 1) {
399                return;
400            }
401
402            Note note = displayList.getSelectedValue();
403            int selectedIndex = displayList.getSelectedIndex();
404            noteData.reOpenNote(note, dialog.getInputText());
405            noteData.setSelectedNote(model.getElementAt(selectedIndex));
406        }
407    }
408
409    class SortAction extends AbstractAction {
410
411        /**
412         * Constructs a new {@code SortAction}.
413         */
414        SortAction() {
415            putValue(SHORT_DESCRIPTION, tr("Sort notes"));
416            putValue(NAME, tr("Sort"));
417            new ImageProvider("dialogs", "sort").getResource().attachImageIcon(this, true);
418        }
419
420        @Override
421        public void actionPerformed(ActionEvent e) {
422            NoteSortDialog sortDialog = new NoteSortDialog(MainApplication.getMainFrame(), tr("Sort notes"), tr("Apply"));
423            sortDialog.showSortDialog(noteData.getCurrentSortMethod());
424            if (sortDialog.getValue() == 1) {
425                noteData.setSortMethod(sortDialog.getSelectedComparator());
426            }
427        }
428    }
429
430    class OpenInBrowserAction extends AbstractAction {
431        OpenInBrowserAction() {
432            putValue(SHORT_DESCRIPTION, tr("Open the note in an external browser"));
433            new ImageProvider("help", "internet").getResource().attachImageIcon(this, true);
434        }
435
436        @Override
437        public void actionPerformed(ActionEvent e) {
438            final Note note = displayList.getSelectedValue();
439            if (note.getId() > 0) {
440                final String url = Config.getUrls().getBaseBrowseUrl() + "/note/" + note.getId();
441                OpenBrowser.displayUrl(url);
442            }
443        }
444    }
445
446}