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.actions.DownloadNotesInViewAction; 031import org.openstreetmap.josm.actions.UploadNotesAction; 032import org.openstreetmap.josm.actions.mapmode.AddNoteAction; 033import org.openstreetmap.josm.data.notes.Note; 034import org.openstreetmap.josm.data.notes.Note.State; 035import org.openstreetmap.josm.data.notes.NoteComment; 036import org.openstreetmap.josm.data.osm.NoteData; 037import org.openstreetmap.josm.data.osm.NoteData.NoteDataUpdateListener; 038import org.openstreetmap.josm.gui.MainApplication; 039import org.openstreetmap.josm.gui.MapFrame; 040import org.openstreetmap.josm.gui.NoteInputDialog; 041import org.openstreetmap.josm.gui.NoteSortDialog; 042import org.openstreetmap.josm.gui.SideButton; 043import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 044import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 045import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 046import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 047import org.openstreetmap.josm.gui.layer.NoteLayer; 048import org.openstreetmap.josm.spi.preferences.Config; 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 @Override 226 public void destroy() { 227 MainApplication.getLayerManager().removeLayerChangeListener(this); 228 super.destroy(); 229 } 230 231 private static class NoteRenderer implements ListCellRenderer<Note> { 232 233 private final DefaultListCellRenderer defaultListCellRenderer = new DefaultListCellRenderer(); 234 private final DateFormat dateFormat = DateUtils.getDateTimeFormat(DateFormat.MEDIUM, DateFormat.SHORT); 235 236 @Override 237 public Component getListCellRendererComponent(JList<? extends Note> list, Note note, int index, 238 boolean isSelected, boolean cellHasFocus) { 239 Component comp = defaultListCellRenderer.getListCellRendererComponent(list, note, index, isSelected, cellHasFocus); 240 if (note != null && comp instanceof JLabel) { 241 NoteComment fstComment = note.getFirstComment(); 242 JLabel jlabel = (JLabel) comp; 243 if (fstComment != null) { 244 String text = note.getFirstComment().getText(); 245 String userName = note.getFirstComment().getUser().getName(); 246 if (userName == null || userName.isEmpty()) { 247 userName = "<Anonymous>"; 248 } 249 String toolTipText = userName + " @ " + dateFormat.format(note.getCreatedAt()); 250 jlabel.setToolTipText(toolTipText); 251 jlabel.setText(note.getId() + ": " +text); 252 } else { 253 jlabel.setToolTipText(null); 254 jlabel.setText(Long.toString(note.getId())); 255 } 256 ImageIcon icon; 257 if (note.getId() < 0) { 258 icon = ImageProvider.get("dialogs/notes", "note_new", ImageProvider.ImageSizes.SMALLICON); 259 } else if (note.getState() == State.CLOSED) { 260 icon = ImageProvider.get("dialogs/notes", "note_closed", ImageProvider.ImageSizes.SMALLICON); 261 } else { 262 icon = ImageProvider.get("dialogs/notes", "note_open", ImageProvider.ImageSizes.SMALLICON); 263 } 264 jlabel.setIcon(icon); 265 } 266 return comp; 267 } 268 } 269 270 class NoteTableModel extends AbstractListModel<Note> { 271 private final transient List<Note> data; 272 273 /** 274 * Constructs a new {@code NoteTableModel}. 275 */ 276 NoteTableModel() { 277 data = new ArrayList<>(); 278 } 279 280 @Override 281 public int getSize() { 282 if (data == null) { 283 return 0; 284 } 285 return data.size(); 286 } 287 288 @Override 289 public Note getElementAt(int index) { 290 return data.get(index); 291 } 292 293 public void setData(Collection<Note> noteList) { 294 data.clear(); 295 data.addAll(noteList); 296 fireContentsChanged(this, 0, noteList.size()); 297 } 298 299 public void clearData() { 300 displayList.clearSelection(); 301 data.clear(); 302 fireIntervalRemoved(this, 0, getSize()); 303 } 304 } 305 306 class AddCommentAction extends AbstractAction { 307 308 /** 309 * Constructs a new {@code AddCommentAction}. 310 */ 311 AddCommentAction() { 312 putValue(SHORT_DESCRIPTION, tr("Add comment")); 313 putValue(NAME, tr("Comment")); 314 new ImageProvider("dialogs/notes", "note_comment").getResource().attachImageIcon(this, true); 315 } 316 317 @Override 318 public void actionPerformed(ActionEvent e) { 319 Note note = displayList.getSelectedValue(); 320 if (note == null) { 321 JOptionPane.showMessageDialog(MainApplication.getMap(), 322 "You must select a note first", 323 "No note selected", 324 JOptionPane.ERROR_MESSAGE); 325 return; 326 } 327 NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Comment on note"), tr("Add comment")); 328 dialog.showNoteDialog(tr("Add comment to note:"), ImageProvider.get("dialogs/notes", "note_comment")); 329 if (dialog.getValue() != 1) { 330 return; 331 } 332 int selectedIndex = displayList.getSelectedIndex(); 333 noteData.addCommentToNote(note, dialog.getInputText()); 334 noteData.setSelectedNote(model.getElementAt(selectedIndex)); 335 } 336 } 337 338 class CloseAction extends AbstractAction { 339 340 /** 341 * Constructs a new {@code CloseAction}. 342 */ 343 CloseAction() { 344 putValue(SHORT_DESCRIPTION, tr("Close note")); 345 putValue(NAME, tr("Close")); 346 new ImageProvider("dialogs/notes", "note_closed").getResource().attachImageIcon(this, true); 347 } 348 349 @Override 350 public void actionPerformed(ActionEvent e) { 351 NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Close note"), tr("Close note")); 352 dialog.showNoteDialog(tr("Close note with message:"), ImageProvider.get("dialogs/notes", "note_closed")); 353 if (dialog.getValue() != 1) { 354 return; 355 } 356 Note note = displayList.getSelectedValue(); 357 int selectedIndex = displayList.getSelectedIndex(); 358 noteData.closeNote(note, dialog.getInputText()); 359 noteData.setSelectedNote(model.getElementAt(selectedIndex)); 360 } 361 } 362 363 class NewAction extends AbstractAction { 364 365 /** 366 * Constructs a new {@code NewAction}. 367 */ 368 NewAction() { 369 putValue(SHORT_DESCRIPTION, tr("Create a new note")); 370 putValue(NAME, tr("Create")); 371 new ImageProvider("dialogs/notes", "note_new").getResource().attachImageIcon(this, true); 372 } 373 374 @Override 375 public void actionPerformed(ActionEvent e) { 376 if (noteData == null) { //there is no notes layer. Create one first 377 MainApplication.getLayerManager().addLayer(new NoteLayer()); 378 } 379 MainApplication.getMap().selectMapMode(new AddNoteAction(noteData)); 380 } 381 } 382 383 class ReopenAction extends AbstractAction { 384 385 /** 386 * Constructs a new {@code ReopenAction}. 387 */ 388 ReopenAction() { 389 putValue(SHORT_DESCRIPTION, tr("Reopen note")); 390 putValue(NAME, tr("Reopen")); 391 new ImageProvider("dialogs/notes", "note_open").getResource().attachImageIcon(this, true); 392 } 393 394 @Override 395 public void actionPerformed(ActionEvent e) { 396 NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Reopen note"), tr("Reopen note")); 397 dialog.showNoteDialog(tr("Reopen note with message:"), ImageProvider.get("dialogs/notes", "note_open")); 398 if (dialog.getValue() != 1) { 399 return; 400 } 401 402 Note note = displayList.getSelectedValue(); 403 int selectedIndex = displayList.getSelectedIndex(); 404 noteData.reOpenNote(note, dialog.getInputText()); 405 noteData.setSelectedNote(model.getElementAt(selectedIndex)); 406 } 407 } 408 409 class SortAction extends AbstractAction { 410 411 /** 412 * Constructs a new {@code SortAction}. 413 */ 414 SortAction() { 415 putValue(SHORT_DESCRIPTION, tr("Sort notes")); 416 putValue(NAME, tr("Sort")); 417 new ImageProvider("dialogs", "sort").getResource().attachImageIcon(this, true); 418 } 419 420 @Override 421 public void actionPerformed(ActionEvent e) { 422 NoteSortDialog sortDialog = new NoteSortDialog(MainApplication.getMainFrame(), tr("Sort notes"), tr("Apply")); 423 sortDialog.showSortDialog(noteData.getCurrentSortMethod()); 424 if (sortDialog.getValue() == 1) { 425 noteData.setSortMethod(sortDialog.getSelectedComparator()); 426 } 427 } 428 } 429 430 class OpenInBrowserAction extends AbstractAction { 431 OpenInBrowserAction() { 432 putValue(SHORT_DESCRIPTION, tr("Open the note in an external browser")); 433 new ImageProvider("help", "internet").getResource().attachImageIcon(this, true); 434 } 435 436 @Override 437 public void actionPerformed(ActionEvent e) { 438 final Note note = displayList.getSelectedValue(); 439 if (note.getId() > 0) { 440 final String url = Config.getUrls().getBaseBrowseUrl() + "/note/" + note.getId(); 441 OpenBrowser.displayUrl(url); 442 } 443 } 444 } 445 446}