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