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