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}