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<>(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 final 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 final 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}