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