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.SimpleDateFormat;
013import java.util.ArrayList;
014import java.util.Arrays;
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.UploadNotesAction;
034import org.openstreetmap.josm.actions.mapmode.AddNoteAction;
035import org.openstreetmap.josm.data.notes.Note;
036import org.openstreetmap.josm.data.notes.Note.State;
037import org.openstreetmap.josm.data.osm.NoteData;
038import org.openstreetmap.josm.gui.MapView;
039import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
040import org.openstreetmap.josm.gui.NoteInputDialog;
041import org.openstreetmap.josm.gui.NoteSortDialog;
042import org.openstreetmap.josm.gui.SideButton;
043import org.openstreetmap.josm.gui.layer.Layer;
044import org.openstreetmap.josm.gui.layer.NoteLayer;
045import org.openstreetmap.josm.tools.ImageProvider;
046
047/**
048 * Dialog to display and manipulate notes.
049 * @since 7852 (renaming)
050 * @since 7608 (creation)
051 */
052public class NotesDialog extends ToggleDialog implements LayerChangeListener {
053
054    /** Small icon size for use in graphics calculations */
055    public static final int ICON_SMALL_SIZE = 16;
056    /** Large icon size for use in graphics calculations */
057    public static final int ICON_LARGE_SIZE = 24;
058    /** 24x24 icon for unresolved notes */
059    public static final ImageIcon ICON_OPEN = ImageProvider.get("dialogs/notes", "note_open");
060    /** 16x16 icon for unresolved notes */
061    public static final ImageIcon ICON_OPEN_SMALL =
062            new ImageIcon(ICON_OPEN.getImage().getScaledInstance(ICON_SMALL_SIZE, ICON_SMALL_SIZE, Image.SCALE_SMOOTH));
063    /** 24x24 icon for resolved notes */
064    public static final ImageIcon ICON_CLOSED = ImageProvider.get("dialogs/notes", "note_closed");
065    /** 16x16 icon for resolved notes */
066    public static final ImageIcon ICON_CLOSED_SMALL =
067            new ImageIcon(ICON_CLOSED.getImage().getScaledInstance(ICON_SMALL_SIZE, ICON_SMALL_SIZE, Image.SCALE_SMOOTH));
068    /** 24x24 icon for new notes */
069    public static final ImageIcon ICON_NEW = ImageProvider.get("dialogs/notes", "note_new");
070    /** 16x16 icon for new notes */
071    public static final ImageIcon ICON_NEW_SMALL =
072            new ImageIcon(ICON_NEW.getImage().getScaledInstance(ICON_SMALL_SIZE, ICON_SMALL_SIZE, Image.SCALE_SMOOTH));
073    /** Icon for note comments */
074    public static final ImageIcon ICON_COMMENT = ImageProvider.get("dialogs/notes", "note_comment");
075
076    private NoteTableModel model;
077    private JList<Note> displayList;
078    private final AddCommentAction addCommentAction;
079    private final CloseAction closeAction;
080    private final NewAction newAction;
081    private final ReopenAction reopenAction;
082    private final SortAction sortAction;
083    private final UploadNotesAction uploadAction;
084
085    private NoteData noteData;
086
087    /** Creates a new toggle dialog for notes */
088    public NotesDialog() {
089        super("Notes", "notes/note_open", "List of notes", null, 150);
090        addCommentAction = new AddCommentAction();
091        closeAction = new CloseAction();
092        newAction = new NewAction();
093        reopenAction = new ReopenAction();
094        sortAction = new SortAction();
095        uploadAction = new UploadNotesAction();
096        buildDialog();
097        MapView.addLayerChangeListener(this);
098    }
099
100    @Override
101    public void showDialog() {
102        super.showDialog();
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        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(newAction, false),
135                new SideButton(addCommentAction, false),
136                new SideButton(closeAction, false),
137                new SideButton(reopenAction, false),
138                new SideButton(sortAction, false),
139                new SideButton(uploadAction, false)}));
140        updateButtonStates();
141    }
142
143    private void updateButtonStates() {
144        if (noteData == null || noteData.getSelectedNote() == null) {
145            closeAction.setEnabled(false);
146            addCommentAction.setEnabled(false);
147            reopenAction.setEnabled(false);
148        } else if (noteData.getSelectedNote().getState() == State.open){
149            closeAction.setEnabled(true);
150            addCommentAction.setEnabled(true);
151            reopenAction.setEnabled(false);
152        } else { //note is closed
153            closeAction.setEnabled(false);
154            addCommentAction.setEnabled(false);
155            reopenAction.setEnabled(true);
156        }
157        if(noteData == null || !noteData.isModified()) {
158            uploadAction.setEnabled(false);
159        } else {
160            uploadAction.setEnabled(true);
161        }
162        //enable sort button if any notes are loaded
163        if (noteData == null || noteData.getNotes().isEmpty()) {
164            sortAction.setEnabled(false);
165        } else {
166            sortAction.setEnabled(true);
167        }
168    }
169
170    @Override
171    public void showNotify() { }
172
173    @Override
174    public void hideNotify() { }
175
176    @Override
177    public void activeLayerChange(Layer oldLayer, Layer newLayer) { }
178
179    @Override
180    public void layerAdded(Layer newLayer) {
181        if (newLayer instanceof NoteLayer) {
182            noteData = ((NoteLayer)newLayer).getNoteData();
183            model.setData(noteData.getNotes());
184            setNoteList(noteData.getNotes());
185        }
186    }
187
188    @Override
189    public void layerRemoved(Layer oldLayer) {
190        if (oldLayer instanceof NoteLayer) {
191            if (Main.isDebugEnabled()) {
192                Main.debug("note layer removed. Clearing everything");
193            }
194            noteData = null;
195            model.clearData();
196            if (Main.map.mapMode instanceof AddNoteAction) {
197                Main.map.selectMapMode(Main.map.mapModeSelect);
198            }
199        }
200    }
201
202    /**
203     * Sets the list of notes to be displayed in the dialog.
204     * The dialog should match the notes displayed in the note layer.
205     * @param noteList List of notes to display
206     */
207    public void setNoteList(List<Note> noteList) {
208        model.setData(noteList);
209        updateButtonStates();
210        this.repaint();
211    }
212
213    /**
214     * Notify the dialog that the note selection has changed.
215     * Causes it to update or clear its selection in the UI.
216     */
217    public void selectionChanged() {
218        if (noteData == null || noteData.getSelectedNote() == null) {
219            displayList.clearSelection();
220        } else {
221            displayList.setSelectedValue(noteData.getSelectedNote(), true);
222        }
223        updateButtonStates();
224    }
225
226    private class NoteRenderer implements ListCellRenderer<Note> {
227
228        private DefaultListCellRenderer defaultListCellRenderer = new DefaultListCellRenderer();
229        private final SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy kk:mm");
230
231        @Override
232        public Component getListCellRendererComponent(JList<? extends Note> list, Note note, int index,
233                boolean isSelected, boolean cellHasFocus) {
234            Component comp = defaultListCellRenderer.getListCellRendererComponent(list, note, index, isSelected, cellHasFocus);
235            if (note != null && comp instanceof JLabel) {
236                String text = note.getFirstComment().getText();
237                String userName = note.getFirstComment().getUser().getName();
238                if (userName == null || userName.isEmpty()) {
239                    userName = "<Anonymous>";
240                }
241                String toolTipText = userName + " @ " + sdf.format(note.getCreatedAt());
242                JLabel jlabel = (JLabel)comp;
243                jlabel.setText(note.getId() + ": " +text);
244                ImageIcon icon;
245                if (note.getId() < 0) {
246                    icon = ICON_NEW_SMALL;
247                } else if (note.getState() == State.closed) {
248                    icon = ICON_CLOSED_SMALL;
249                } else {
250                    icon = ICON_OPEN_SMALL;
251                }
252                jlabel.setIcon(icon);
253                jlabel.setToolTipText(toolTipText);
254            }
255            return comp;
256        }
257    }
258
259    class NoteTableModel extends AbstractListModel<Note> {
260        private List<Note> data;
261
262        public NoteTableModel() {
263            data = new ArrayList<Note>();
264        }
265
266        @Override
267        public int getSize() {
268            if (data == null) {
269                return 0;
270            }
271            return data.size();
272        }
273
274        @Override
275        public Note getElementAt(int index) {
276            return data.get(index);
277        }
278
279        public void setData(List<Note> noteList) {
280            data.clear();
281            data.addAll(noteList);
282            fireContentsChanged(this, 0, noteList.size());
283        }
284
285        public void clearData() {
286            displayList.clearSelection();
287            data.clear();
288            fireIntervalRemoved(this, 0, getSize());
289        }
290    }
291
292    class AddCommentAction extends AbstractAction {
293
294        public AddCommentAction() {
295            putValue(SHORT_DESCRIPTION,tr("Add comment"));
296            putValue(NAME, tr("Comment"));
297            putValue(SMALL_ICON, ICON_COMMENT);
298        }
299
300        @Override
301        public void actionPerformed(ActionEvent e) {
302            Note note = displayList.getSelectedValue();
303            if (note == null) {
304                JOptionPane.showMessageDialog(Main.map,
305                        "You must select a note first",
306                        "No note selected",
307                        JOptionPane.ERROR_MESSAGE);
308                return;
309            }
310            NoteInputDialog dialog = new NoteInputDialog(Main.parent, tr("Comment on note"), tr("Add comment"));
311            dialog.showNoteDialog(tr("Add comment to note:"), NotesDialog.ICON_COMMENT);
312            if (dialog.getValue() != 1) {
313                Main.debug("User aborted note reopening");
314                return;
315            }
316            int selectedIndex = displayList.getSelectedIndex();
317            noteData.addCommentToNote(note, dialog.getInputText());
318            noteData.setSelectedNote(model.getElementAt(selectedIndex));
319        }
320    }
321
322    class CloseAction extends AbstractAction {
323
324        public CloseAction() {
325            putValue(SHORT_DESCRIPTION,tr("Close note"));
326            putValue(NAME, tr("Close"));
327            putValue(SMALL_ICON, ICON_CLOSED);
328        }
329
330        @Override
331        public void actionPerformed(ActionEvent e) {
332            NoteInputDialog dialog = new NoteInputDialog(Main.parent, tr("Close note"), tr("Close note"));
333            dialog.showNoteDialog(tr("Close note with message:"), NotesDialog.ICON_CLOSED);
334            if (dialog.getValue() != 1) {
335                Main.debug("User aborted note closing");
336                return;
337            }
338            Note note = displayList.getSelectedValue();
339            int selectedIndex = displayList.getSelectedIndex();
340            noteData.closeNote(note, dialog.getInputText());
341            noteData.setSelectedNote(model.getElementAt(selectedIndex));
342        }
343    }
344
345    class NewAction extends AbstractAction {
346
347        public NewAction() {
348            putValue(SHORT_DESCRIPTION,tr("Create a new note"));
349            putValue(NAME, tr("Create"));
350            putValue(SMALL_ICON, ICON_NEW);
351        }
352
353        @Override
354        public void actionPerformed(ActionEvent e) {
355            if (noteData == null) { //there is no notes layer. Create one first
356                Main.map.mapView.addLayer(new NoteLayer());
357            }
358            Main.map.selectMapMode(new AddNoteAction(Main.map, noteData));
359        }
360    }
361
362    class ReopenAction extends AbstractAction {
363
364        public ReopenAction() {
365            putValue(SHORT_DESCRIPTION,tr("Reopen note"));
366            putValue(NAME, tr("Reopen"));
367            putValue(SMALL_ICON, ICON_OPEN);
368        }
369
370        @Override
371        public void actionPerformed(ActionEvent e) {
372            NoteInputDialog dialog = new NoteInputDialog(Main.parent, tr("Reopen note"), tr("Reopen note"));
373            dialog.showNoteDialog(tr("Reopen note with message:"), NotesDialog.ICON_OPEN);
374            if (dialog.getValue() != 1) {
375                Main.debug("User aborted note reopening");
376                return;
377            }
378
379            Note note = displayList.getSelectedValue();
380            int selectedIndex = displayList.getSelectedIndex();
381            noteData.reOpenNote(note, dialog.getInputText());
382            noteData.setSelectedNote(model.getElementAt(selectedIndex));
383        }
384    }
385
386    class SortAction extends AbstractAction {
387
388        public SortAction() {
389            putValue(SHORT_DESCRIPTION, tr("Sort notes"));
390            putValue(NAME, tr("Sort"));
391            putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort"));
392        }
393
394        @Override
395        public void actionPerformed(ActionEvent e) {
396            NoteSortDialog sortDialog = new NoteSortDialog(Main.parent, tr("Sort notes"), tr("Apply"));
397            sortDialog.showSortDialog(noteData.getCurrentSortMethod());
398            if (sortDialog.getValue() == 1) {
399                noteData.setSortMethod(sortDialog.getSelectedComparator());
400            }
401        }
402    }
403}