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.text.SimpleDateFormat;
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.List;
014
015import javax.swing.AbstractAction;
016import javax.swing.AbstractListModel;
017import javax.swing.DefaultListCellRenderer;
018import javax.swing.ImageIcon;
019import javax.swing.JLabel;
020import javax.swing.JList;
021import javax.swing.JOptionPane;
022import javax.swing.JPanel;
023import javax.swing.JScrollPane;
024import javax.swing.ListCellRenderer;
025import javax.swing.ListSelectionModel;
026import javax.swing.event.ListSelectionEvent;
027import javax.swing.event.ListSelectionListener;
028
029import org.openstreetmap.josm.Main;
030import org.openstreetmap.josm.actions.mapmode.AddNoteAction;
031import org.openstreetmap.josm.data.notes.Note;
032import org.openstreetmap.josm.data.notes.Note.State;
033import org.openstreetmap.josm.data.osm.NoteData;
034import org.openstreetmap.josm.gui.MapView;
035import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
036import org.openstreetmap.josm.gui.SideButton;
037import org.openstreetmap.josm.gui.layer.Layer;
038import org.openstreetmap.josm.gui.layer.NoteLayer;
039import org.openstreetmap.josm.tools.ImageProvider;
040
041/**
042 * Dialog to display and manipulate notes
043 */
044public class NoteDialog extends ToggleDialog implements LayerChangeListener {
045
046
047    /** Small icon size for use in graphics calculations */
048    public static final int ICON_SMALL_SIZE = 16;
049    /** Large icon size for use in graphics calculations */
050    public static final int ICON_LARGE_SIZE = 24;
051    /** 24x24 icon for unresolved notes */
052    public static final ImageIcon ICON_OPEN = ImageProvider.get("dialogs/notes", "note_open.png");
053    /** 16x16 icon for unresolved notes */
054    public static final ImageIcon ICON_OPEN_SMALL =
055            new ImageIcon(ICON_OPEN.getImage().getScaledInstance(ICON_SMALL_SIZE, ICON_SMALL_SIZE, Image.SCALE_SMOOTH));
056    /** 24x24 icon for resolved notes */
057    public static final ImageIcon ICON_CLOSED = ImageProvider.get("dialogs/notes", "note_closed.png");
058    /** 16x16 icon for resolved notes */
059    public static final ImageIcon ICON_CLOSED_SMALL =
060            new ImageIcon(ICON_CLOSED.getImage().getScaledInstance(ICON_SMALL_SIZE, ICON_SMALL_SIZE, Image.SCALE_SMOOTH));
061    /** 24x24 icon for new notes */
062    public static final ImageIcon ICON_NEW = ImageProvider.get("dialogs/notes", "note_new.png");
063    /** 16x16 icon for new notes */
064    public static final ImageIcon ICON_NEW_SMALL =
065            new ImageIcon(ICON_NEW.getImage().getScaledInstance(ICON_SMALL_SIZE, ICON_SMALL_SIZE, Image.SCALE_SMOOTH));
066    /** Icon for note comments */
067    public static final ImageIcon ICON_COMMENT = ImageProvider.get("dialogs/notes", "note_comment.png");
068
069    private NoteTableModel model;
070    private JList<Note> displayList;
071    private final AddCommentAction addCommentAction;
072    private final CloseAction closeAction;
073    private final NewAction newAction;
074    private final ReopenAction reopenAction;
075
076    private NoteData noteData;
077
078    /** Creates a new toggle dialog for notes */
079    public NoteDialog() {
080        super("Notes", "notes/note_open.png", "List of notes", null, 150);
081        Main.debug("constructed note dialog");
082
083        addCommentAction = new AddCommentAction();
084        closeAction = new CloseAction();
085        newAction = new NewAction();
086        reopenAction = new ReopenAction();
087        buildDialog();
088    }
089
090    @Override
091    public void showDialog() {
092        super.showDialog();
093    }
094
095    private void buildDialog() {
096        model = new NoteTableModel();
097        displayList = new JList<Note>(model);
098        displayList.setCellRenderer(new NoteRenderer());
099        displayList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
100        displayList.addListSelectionListener(new ListSelectionListener() {
101            @Override
102            public void valueChanged(ListSelectionEvent e) {
103                if (noteData != null) { //happens when layer is deleted while note selected
104                    noteData.setSelectedNote(displayList.getSelectedValue());
105                }
106                updateButtonStates();
107            }});
108
109        JPanel pane = new JPanel(new BorderLayout());
110        pane.add(new JScrollPane(displayList), BorderLayout.CENTER);
111
112        createLayout(pane, false, Arrays.asList(new SideButton[]{
113                new SideButton(newAction, false),
114                new SideButton(addCommentAction, false),
115                new SideButton(closeAction, false),
116                new SideButton(reopenAction, false)}));
117        updateButtonStates();
118    }
119
120    private void updateButtonStates() {
121        if (noteData == null || noteData.getSelectedNote() == null) {
122            closeAction.setEnabled(false);
123            addCommentAction.setEnabled(false);
124            reopenAction.setEnabled(false);
125        } else if (noteData.getSelectedNote().getState() == State.open){
126            closeAction.setEnabled(true);
127            addCommentAction.setEnabled(true);
128            reopenAction.setEnabled(false);
129        } else { //note is closed
130            closeAction.setEnabled(false);
131            addCommentAction.setEnabled(false);
132            reopenAction.setEnabled(true);
133        }
134    }
135
136    @Override
137    public void showNotify() {
138        MapView.addLayerChangeListener(this);
139    }
140
141    @Override
142    public void hideNotify() {
143        MapView.removeLayerChangeListener(this);
144    }
145
146    @Override
147    public void activeLayerChange(Layer oldLayer, Layer newLayer) { }
148
149    @Override
150    public void layerAdded(Layer newLayer) {
151        Main.debug("layer added: " + newLayer);
152        if (newLayer instanceof NoteLayer) {
153            Main.debug("note layer added");
154            noteData = ((NoteLayer)newLayer).getNoteData();
155            model.setData(noteData.getNotes());
156        }
157    }
158
159    @Override
160    public void layerRemoved(Layer oldLayer) {
161        if (oldLayer instanceof NoteLayer) {
162            Main.debug("note layer removed. Clearing everything");
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    /**
172     * Sets the list of notes to be displayed in the dialog.
173     * The dialog should match the notes displayed in the note layer.
174     * @param noteList List of notes to display
175     */
176    public void setNoteList(List<Note> noteList) {
177        model.setData(noteList);
178        updateButtonStates();
179        this.repaint();
180    }
181
182    /**
183     * Notify the dialog that the note selection has changed.
184     * Causes it to update or clear its selection in the UI.
185     */
186    public void selectionChanged() {
187        if (noteData == null || noteData.getSelectedNote() == null) {
188            displayList.clearSelection();
189        } else {
190            displayList.setSelectedValue(noteData.getSelectedNote(), true);
191        }
192        updateButtonStates();
193    }
194
195    private class NoteRenderer implements ListCellRenderer<Note> {
196
197        private DefaultListCellRenderer defaultListCellRenderer = new DefaultListCellRenderer();
198        private final SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy kk:mm");
199
200        @Override
201        public Component getListCellRendererComponent(JList<? extends Note> list, Note note, int index,
202                boolean isSelected, boolean cellHasFocus) {
203            Component comp = defaultListCellRenderer.getListCellRendererComponent(list, note, index, isSelected, cellHasFocus);
204            if (note != null && comp instanceof JLabel) {
205                String text = note.getFirstComment().getText();
206                String userName = note.getFirstComment().getUser().getName();
207                if (userName == null || userName.isEmpty()) {
208                    userName = "<Anonymous>";
209                }
210                String toolTipText = userName + " @ " + sdf.format(note.getCreatedAt());
211                JLabel jlabel = (JLabel)comp;
212                jlabel.setText(text);
213                ImageIcon icon;
214                if (note.getId() < 0) {
215                    icon = ICON_NEW_SMALL;
216                } else if (note.getState() == State.closed) {
217                    icon = ICON_CLOSED_SMALL;
218                } else {
219                    icon = ICON_OPEN_SMALL;
220                }
221                jlabel.setIcon(icon);
222                jlabel.setToolTipText(toolTipText);
223            }
224            return comp;
225        }
226    }
227
228    class NoteTableModel extends AbstractListModel<Note> {
229        private List<Note> data;
230
231        public NoteTableModel() {
232            data = new ArrayList<Note>();
233        }
234
235        @Override
236        public int getSize() {
237            if (data == null) {
238                return 0;
239            }
240            return data.size();
241        }
242
243        @Override
244        public Note getElementAt(int index) {
245            return data.get(index);
246        }
247
248        public void setData(List<Note> noteList) {
249            data.clear();
250            data.addAll(noteList);
251            fireContentsChanged(this, 0, noteList.size());
252        }
253
254        public void clearData() {
255            displayList.clearSelection();
256            data.clear();
257            fireIntervalRemoved(this, 0, getSize());
258        }
259    }
260
261    class AddCommentAction extends AbstractAction {
262
263        public AddCommentAction() {
264            putValue(SHORT_DESCRIPTION,tr("Add comment"));
265            putValue(NAME, tr("Comment"));
266            putValue(SMALL_ICON, ICON_COMMENT);
267        }
268
269        @Override
270        public void actionPerformed(ActionEvent e) {
271            Note note = displayList.getSelectedValue();
272            if (note == null) {
273                JOptionPane.showMessageDialog(Main.map,
274                        "You must select a note first",
275                        "No note selected",
276                        JOptionPane.ERROR_MESSAGE);
277                return;
278            }
279            Object userInput = JOptionPane.showInputDialog(Main.map,
280                    tr("Add comment to note:"),
281                    tr("Add comment"),
282                    JOptionPane.QUESTION_MESSAGE,
283                    ICON_COMMENT,
284                    null,null);
285            if (userInput == null) { //user pressed cancel
286                return;
287            }
288            noteData.addCommentToNote(note, userInput.toString());
289        }
290    }
291
292    class CloseAction extends AbstractAction {
293
294        public CloseAction() {
295            putValue(SHORT_DESCRIPTION,tr("Close note"));
296            putValue(NAME, tr("Close"));
297            putValue(SMALL_ICON, ICON_CLOSED);
298        }
299
300        @Override
301        public void actionPerformed(ActionEvent e) {
302            Object userInput = JOptionPane.showInputDialog(Main.map,
303                    tr("Close note with message:"),
304                    tr("Close Note"),
305                    JOptionPane.QUESTION_MESSAGE,
306                    ICON_CLOSED,
307                    null,null);
308            if (userInput == null) { //user pressed cancel
309                return;
310            }
311            Note note = displayList.getSelectedValue();
312            noteData.closeNote(note, userInput.toString());
313        }
314    }
315
316    class NewAction extends AbstractAction {
317
318        public NewAction() {
319            putValue(SHORT_DESCRIPTION,tr("Create a new note"));
320            putValue(NAME, tr("Create"));
321            putValue(SMALL_ICON, ICON_NEW);
322        }
323
324        @Override
325        public void actionPerformed(ActionEvent e) {
326            if (noteData == null) { //there is no notes layer. Create one first
327                Main.map.mapView.addLayer(new NoteLayer());
328            }
329            Main.map.selectMapMode(new AddNoteAction(Main.map, noteData));
330        }
331    }
332
333    class ReopenAction extends AbstractAction {
334
335        public ReopenAction() {
336            putValue(SHORT_DESCRIPTION,tr("Reopen note"));
337            putValue(NAME, tr("Reopen"));
338            putValue(SMALL_ICON, ICON_OPEN);
339        }
340
341        @Override
342        public void actionPerformed(ActionEvent e) {
343            Object userInput = JOptionPane.showInputDialog(Main.map,
344                    tr("Reopen note with message:"),
345                    tr("Reopen note"),
346                    JOptionPane.QUESTION_MESSAGE,
347                    ICON_OPEN,
348                    null,null);
349            if (userInput == null) { //user pressed cancel
350                return;
351            }
352            Note note = displayList.getSelectedValue();
353            noteData.reOpenNote(note, userInput.toString());
354        }
355    }
356}