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