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.Image;
009import java.awt.event.ActionEvent;
010import java.awt.event.MouseAdapter;
011import java.awt.event.MouseEvent;
012import java.text.DateFormat;
013import java.util.ArrayList;
014import java.util.Arrays;
015import java.util.Collection;
016import java.util.List;
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;
030import javax.swing.event.ListSelectionEvent;
031import javax.swing.event.ListSelectionListener;
032
033import org.openstreetmap.josm.Main;
034import org.openstreetmap.josm.actions.DownloadNotesInViewAction;
035import org.openstreetmap.josm.actions.UploadNotesAction;
036import org.openstreetmap.josm.actions.mapmode.AddNoteAction;
037import org.openstreetmap.josm.data.notes.Note;
038import org.openstreetmap.josm.data.notes.Note.State;
039import org.openstreetmap.josm.data.notes.NoteComment;
040import org.openstreetmap.josm.data.osm.NoteData;
041import org.openstreetmap.josm.gui.MapView;
042import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
043import org.openstreetmap.josm.gui.NoteInputDialog;
044import org.openstreetmap.josm.gui.NoteSortDialog;
045import org.openstreetmap.josm.gui.SideButton;
046import org.openstreetmap.josm.gui.layer.Layer;
047import org.openstreetmap.josm.gui.layer.NoteLayer;
048import org.openstreetmap.josm.tools.ImageProvider;
049import org.openstreetmap.josm.tools.date.DateUtils;
050
051/**
052 * Dialog to display and manipulate notes.
053 * @since 7852 (renaming)
054 * @since 7608 (creation)
055 */
056public class NotesDialog extends ToggleDialog implements LayerChangeListener {
057
058    /** Small icon size for use in graphics calculations */
059    public static final int ICON_SMALL_SIZE = 16;
060    /** 24x24 icon for unresolved notes */
061    public static final ImageIcon ICON_OPEN = ImageProvider.get("dialogs/notes", "note_open");
062    /** 16x16 icon for unresolved notes */
063    public static final ImageIcon ICON_OPEN_SMALL =
064            new ImageIcon(ICON_OPEN.getImage().getScaledInstance(ICON_SMALL_SIZE, ICON_SMALL_SIZE, Image.SCALE_SMOOTH));
065    /** 24x24 icon for resolved notes */
066    public static final ImageIcon ICON_CLOSED = ImageProvider.get("dialogs/notes", "note_closed");
067    /** 16x16 icon for resolved notes */
068    public static final ImageIcon ICON_CLOSED_SMALL =
069            new ImageIcon(ICON_CLOSED.getImage().getScaledInstance(ICON_SMALL_SIZE, ICON_SMALL_SIZE, Image.SCALE_SMOOTH));
070    /** 24x24 icon for new notes */
071    public static final ImageIcon ICON_NEW = ImageProvider.get("dialogs/notes", "note_new");
072    /** 16x16 icon for new notes */
073    public static final ImageIcon ICON_NEW_SMALL =
074            new ImageIcon(ICON_NEW.getImage().getScaledInstance(ICON_SMALL_SIZE, ICON_SMALL_SIZE, Image.SCALE_SMOOTH));
075    /** Icon for note comments */
076    public static final ImageIcon ICON_COMMENT = ImageProvider.get("dialogs/notes", "note_comment");
077
078    private NoteTableModel model;
079    private JList<Note> displayList;
080    private final AddCommentAction addCommentAction;
081    private final CloseAction closeAction;
082    private final DownloadNotesInViewAction downloadNotesInViewAction;
083    private final NewAction newAction;
084    private final ReopenAction reopenAction;
085    private final SortAction sortAction;
086    private final UploadNotesAction uploadAction;
087
088    private transient NoteData noteData;
089
090    /** Creates a new toggle dialog for notes */
091    public NotesDialog() {
092        super(tr("Notes"), "notes/note_open", tr("List of notes"), null, 150);
093        addCommentAction = new AddCommentAction();
094        closeAction = new CloseAction();
095        downloadNotesInViewAction = DownloadNotesInViewAction.newActionWithDownloadIcon();
096        newAction = new NewAction();
097        reopenAction = new ReopenAction();
098        sortAction = new SortAction();
099        uploadAction = new UploadNotesAction();
100        buildDialog();
101        MapView.addLayerChangeListener(this);
102    }
103
104    private void buildDialog() {
105        model = new NoteTableModel();
106        displayList = new JList<>(model);
107        displayList.setCellRenderer(new NoteRenderer());
108        displayList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
109        displayList.addListSelectionListener(new ListSelectionListener() {
110            @Override
111            public void valueChanged(ListSelectionEvent e) {
112                if (noteData != null) { //happens when layer is deleted while note selected
113                    noteData.setSelectedNote(displayList.getSelectedValue());
114                }
115                updateButtonStates();
116            }
117        });
118        displayList.addMouseListener(new MouseAdapter() {
119            //center view on selected note on double click
120            @Override
121            public void mouseClicked(MouseEvent e) {
122                if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) {
123                    if (noteData != null && noteData.getSelectedNote() != null) {
124                        Main.map.mapView.zoomTo(noteData.getSelectedNote().getLatLon());
125                    }
126                }
127            }
128        });
129
130        JPanel pane = new JPanel(new BorderLayout());
131        pane.add(new JScrollPane(displayList), BorderLayout.CENTER);
132
133        createLayout(pane, false, Arrays.asList(new SideButton[]{
134                new SideButton(downloadNotesInViewAction, false),
135                new SideButton(newAction, false),
136                new SideButton(addCommentAction, false),
137                new SideButton(closeAction, false),
138                new SideButton(reopenAction, false),
139                new SideButton(sortAction, false),
140                new SideButton(uploadAction, false)}));
141        updateButtonStates();
142    }
143
144    private void updateButtonStates() {
145        if (noteData == null || noteData.getSelectedNote() == null) {
146            closeAction.setEnabled(false);
147            addCommentAction.setEnabled(false);
148            reopenAction.setEnabled(false);
149        } else if (noteData.getSelectedNote().getState() == State.OPEN) {
150            closeAction.setEnabled(true);
151            addCommentAction.setEnabled(true);
152            reopenAction.setEnabled(false);
153        } else { //note is closed
154            closeAction.setEnabled(false);
155            addCommentAction.setEnabled(false);
156            reopenAction.setEnabled(true);
157        }
158        if (noteData == null || !noteData.isModified()) {
159            uploadAction.setEnabled(false);
160        } else {
161            uploadAction.setEnabled(true);
162        }
163        //enable sort button if any notes are loaded
164        if (noteData == null || noteData.getNotes().isEmpty()) {
165            sortAction.setEnabled(false);
166        } else {
167            sortAction.setEnabled(true);
168        }
169    }
170
171    @Override
172    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
173        // Do nothing
174    }
175
176    @Override
177    public void layerAdded(Layer newLayer) {
178        if (newLayer instanceof NoteLayer) {
179            noteData = ((NoteLayer) newLayer).getNoteData();
180            model.setData(noteData.getNotes());
181            setNotes(noteData.getSortedNotes());
182        }
183    }
184
185    @Override
186    public void layerRemoved(Layer oldLayer) {
187        if (oldLayer instanceof NoteLayer) {
188            noteData = null;
189            model.clearData();
190            if (Main.map.mapMode instanceof AddNoteAction) {
191                Main.map.selectMapMode(Main.map.mapModeSelect);
192            }
193        }
194    }
195
196    /**
197     * Sets the list of notes to be displayed in the dialog.
198     * The dialog should match the notes displayed in the note layer.
199     * @param noteList List of notes to display
200     */
201    public void setNotes(Collection<Note> noteList) {
202        model.setData(noteList);
203        updateButtonStates();
204        this.repaint();
205    }
206
207    /**
208     * Notify the dialog that the note selection has changed.
209     * Causes it to update or clear its selection in the UI.
210     */
211    public void selectionChanged() {
212        if (noteData == null || noteData.getSelectedNote() == null) {
213            displayList.clearSelection();
214        } else {
215            displayList.setSelectedValue(noteData.getSelectedNote(), true);
216        }
217        updateButtonStates();
218        // TODO make a proper listener mechanism to handle change of note selection
219        Main.main.menu.infoweb.noteSelectionChanged();
220    }
221
222    /**
223     * Returns the currently selected note, if any.
224     * @return currently selected note, or null
225     * @since 8475
226     */
227    public Note getSelectedNote() {
228        return noteData != null ? noteData.getSelectedNote() : null;
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 = ICON_NEW_SMALL;
259                } else if (note.getState() == State.CLOSED) {
260                    icon = ICON_CLOSED_SMALL;
261                } else {
262                    icon = ICON_OPEN_SMALL;
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            putValue(SMALL_ICON, ICON_COMMENT);
315        }
316
317        @Override
318        public void actionPerformed(ActionEvent e) {
319            Note note = displayList.getSelectedValue();
320            if (note == null) {
321                JOptionPane.showMessageDialog(Main.map,
322                        "You must select a note first",
323                        "No note selected",
324                        JOptionPane.ERROR_MESSAGE);
325                return;
326            }
327            NoteInputDialog dialog = new NoteInputDialog(Main.parent, tr("Comment on note"), tr("Add comment"));
328            dialog.showNoteDialog(tr("Add comment to note:"), NotesDialog.ICON_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            putValue(SMALL_ICON, ICON_CLOSED);
347        }
348
349        @Override
350        public void actionPerformed(ActionEvent e) {
351            NoteInputDialog dialog = new NoteInputDialog(Main.parent, tr("Close note"), tr("Close note"));
352            dialog.showNoteDialog(tr("Close note with message:"), NotesDialog.ICON_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            putValue(SMALL_ICON, ICON_NEW);
372        }
373
374        @Override
375        public void actionPerformed(ActionEvent e) {
376            if (noteData == null) { //there is no notes layer. Create one first
377                Main.map.mapView.addLayer(new NoteLayer());
378            }
379            Main.map.selectMapMode(new AddNoteAction(Main.map, 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            putValue(SMALL_ICON, ICON_OPEN);
392        }
393
394        @Override
395        public void actionPerformed(ActionEvent e) {
396            NoteInputDialog dialog = new NoteInputDialog(Main.parent, tr("Reopen note"), tr("Reopen note"));
397            dialog.showNoteDialog(tr("Reopen note with message:"), NotesDialog.ICON_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            putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort"));
418        }
419
420        @Override
421        public void actionPerformed(ActionEvent e) {
422            NoteSortDialog sortDialog = new NoteSortDialog(Main.parent, tr("Sort notes"), tr("Apply"));
423            sortDialog.showSortDialog(noteData.getCurrentSortMethod());
424            if (sortDialog.getValue() == 1) {
425                noteData.setSortMethod(sortDialog.getSelectedComparator());
426            }
427        }
428    }
429}