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 uploadAction.setEnabled(noteData != null && noteData.isModified()); 140 //enable sort button if any notes are loaded 141 sortAction.setEnabled(noteData != null && !noteData.getNotes().isEmpty()); 142 } 143 144 @Override 145 public void layerAdded(LayerAddEvent e) { 146 if (e.getAddedLayer() instanceof NoteLayer) { 147 noteData = ((NoteLayer) e.getAddedLayer()).getNoteData(); 148 model.setData(noteData.getNotes()); 149 setNotes(noteData.getSortedNotes()); 150 noteData.addNoteDataUpdateListener(this); 151 } 152 } 153 154 @Override 155 public void layerRemoving(LayerRemoveEvent e) { 156 if (e.getRemovedLayer() instanceof NoteLayer) { 157 noteData.removeNoteDataUpdateListener(this); 158 noteData = null; 159 model.clearData(); 160 MapFrame map = MainApplication.getMap(); 161 if (map.mapMode instanceof AddNoteAction) { 162 map.selectMapMode(map.mapModeSelect); 163 } 164 } 165 } 166 167 @Override 168 public void layerOrderChanged(LayerOrderChangeEvent e) { 169 // ignored 170 } 171 172 @Override 173 public void noteDataUpdated(NoteData data) { 174 setNotes(data.getSortedNotes()); 175 } 176 177 @Override 178 public void selectedNoteChanged(NoteData noteData) { 179 selectionChanged(); 180 } 181 182 /** 183 * Sets the list of notes to be displayed in the dialog. 184 * The dialog should match the notes displayed in the note layer. 185 * @param noteList List of notes to display 186 */ 187 public void setNotes(Collection<Note> noteList) { 188 model.setData(noteList); 189 updateButtonStates(); 190 this.repaint(); 191 } 192 193 /** 194 * Notify the dialog that the note selection has changed. 195 * Causes it to update or clear its selection in the UI. 196 */ 197 public void selectionChanged() { 198 if (noteData == null || noteData.getSelectedNote() == null) { 199 displayList.clearSelection(); 200 } else { 201 displayList.setSelectedValue(noteData.getSelectedNote(), true); 202 } 203 updateButtonStates(); 204 // TODO make a proper listener mechanism to handle change of note selection 205 MainApplication.getMenu().infoweb.noteSelectionChanged(); 206 } 207 208 /** 209 * Returns the currently selected note, if any. 210 * @return currently selected note, or null 211 * @since 8475 212 */ 213 public Note getSelectedNote() { 214 return noteData != null ? noteData.getSelectedNote() : null; 215 } 216 217 @Override 218 public void destroy() { 219 MainApplication.getLayerManager().removeLayerChangeListener(this); 220 super.destroy(); 221 } 222 223 private static class NoteRenderer implements ListCellRenderer<Note> { 224 225 private final DefaultListCellRenderer defaultListCellRenderer = new DefaultListCellRenderer(); 226 private final DateFormat dateFormat = DateUtils.getDateTimeFormat(DateFormat.MEDIUM, DateFormat.SHORT); 227 228 @Override 229 public Component getListCellRendererComponent(JList<? extends Note> list, Note note, int index, 230 boolean isSelected, boolean cellHasFocus) { 231 Component comp = defaultListCellRenderer.getListCellRendererComponent(list, note, index, isSelected, cellHasFocus); 232 if (note != null && comp instanceof JLabel) { 233 NoteComment fstComment = note.getFirstComment(); 234 JLabel jlabel = (JLabel) comp; 235 if (fstComment != null) { 236 String text = note.getFirstComment().getText(); 237 String userName = note.getFirstComment().getUser().getName(); 238 if (userName == null || userName.isEmpty()) { 239 userName = "<Anonymous>"; 240 } 241 String toolTipText = userName + " @ " + dateFormat.format(note.getCreatedAt()); 242 jlabel.setToolTipText(toolTipText); 243 jlabel.setText(note.getId() + ": " +text); 244 } else { 245 jlabel.setToolTipText(null); 246 jlabel.setText(Long.toString(note.getId())); 247 } 248 ImageIcon icon; 249 if (note.getId() < 0) { 250 icon = ImageProvider.get("dialogs/notes", "note_new", ImageProvider.ImageSizes.SMALLICON); 251 } else if (note.getState() == State.CLOSED) { 252 icon = ImageProvider.get("dialogs/notes", "note_closed", ImageProvider.ImageSizes.SMALLICON); 253 } else { 254 icon = ImageProvider.get("dialogs/notes", "note_open", ImageProvider.ImageSizes.SMALLICON); 255 } 256 jlabel.setIcon(icon); 257 } 258 return comp; 259 } 260 } 261 262 class NoteTableModel extends AbstractListModel<Note> { 263 private final transient List<Note> data; 264 265 /** 266 * Constructs a new {@code NoteTableModel}. 267 */ 268 NoteTableModel() { 269 data = new ArrayList<>(); 270 } 271 272 @Override 273 public int getSize() { 274 if (data == null) { 275 return 0; 276 } 277 return data.size(); 278 } 279 280 @Override 281 public Note getElementAt(int index) { 282 return data.get(index); 283 } 284 285 public void setData(Collection<Note> noteList) { 286 data.clear(); 287 data.addAll(noteList); 288 fireContentsChanged(this, 0, noteList.size()); 289 } 290 291 public void clearData() { 292 displayList.clearSelection(); 293 data.clear(); 294 fireIntervalRemoved(this, 0, getSize()); 295 } 296 } 297 298 class AddCommentAction extends AbstractAction { 299 300 /** 301 * Constructs a new {@code AddCommentAction}. 302 */ 303 AddCommentAction() { 304 putValue(SHORT_DESCRIPTION, tr("Add comment")); 305 putValue(NAME, tr("Comment")); 306 new ImageProvider("dialogs/notes", "note_comment").getResource().attachImageIcon(this, true); 307 } 308 309 @Override 310 public void actionPerformed(ActionEvent e) { 311 Note note = displayList.getSelectedValue(); 312 if (note == null) { 313 JOptionPane.showMessageDialog(MainApplication.getMap(), 314 "You must select a note first", 315 "No note selected", 316 JOptionPane.ERROR_MESSAGE); 317 return; 318 } 319 NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Comment on note"), tr("Add comment")); 320 dialog.showNoteDialog(tr("Add comment to note:"), ImageProvider.get("dialogs/notes", "note_comment")); 321 if (dialog.getValue() != 1) { 322 return; 323 } 324 int selectedIndex = displayList.getSelectedIndex(); 325 noteData.addCommentToNote(note, dialog.getInputText()); 326 noteData.setSelectedNote(model.getElementAt(selectedIndex)); 327 } 328 } 329 330 class CloseAction extends AbstractAction { 331 332 /** 333 * Constructs a new {@code CloseAction}. 334 */ 335 CloseAction() { 336 putValue(SHORT_DESCRIPTION, tr("Close note")); 337 putValue(NAME, tr("Close")); 338 new ImageProvider("dialogs/notes", "note_closed").getResource().attachImageIcon(this, true); 339 } 340 341 @Override 342 public void actionPerformed(ActionEvent e) { 343 NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Close note"), tr("Close note")); 344 dialog.showNoteDialog(tr("Close note with message:"), ImageProvider.get("dialogs/notes", "note_closed")); 345 if (dialog.getValue() != 1) { 346 return; 347 } 348 Note note = displayList.getSelectedValue(); 349 int selectedIndex = displayList.getSelectedIndex(); 350 noteData.closeNote(note, dialog.getInputText()); 351 noteData.setSelectedNote(model.getElementAt(selectedIndex)); 352 } 353 } 354 355 class NewAction extends AbstractAction { 356 357 /** 358 * Constructs a new {@code NewAction}. 359 */ 360 NewAction() { 361 putValue(SHORT_DESCRIPTION, tr("Create a new note")); 362 putValue(NAME, tr("Create")); 363 new ImageProvider("dialogs/notes", "note_new").getResource().attachImageIcon(this, true); 364 } 365 366 @Override 367 public void actionPerformed(ActionEvent e) { 368 if (noteData == null) { //there is no notes layer. Create one first 369 MainApplication.getLayerManager().addLayer(new NoteLayer()); 370 } 371 MainApplication.getMap().selectMapMode(new AddNoteAction(noteData)); 372 } 373 } 374 375 class ReopenAction extends AbstractAction { 376 377 /** 378 * Constructs a new {@code ReopenAction}. 379 */ 380 ReopenAction() { 381 putValue(SHORT_DESCRIPTION, tr("Reopen note")); 382 putValue(NAME, tr("Reopen")); 383 new ImageProvider("dialogs/notes", "note_open").getResource().attachImageIcon(this, true); 384 } 385 386 @Override 387 public void actionPerformed(ActionEvent e) { 388 NoteInputDialog dialog = new NoteInputDialog(MainApplication.getMainFrame(), tr("Reopen note"), tr("Reopen note")); 389 dialog.showNoteDialog(tr("Reopen note with message:"), ImageProvider.get("dialogs/notes", "note_open")); 390 if (dialog.getValue() != 1) { 391 return; 392 } 393 394 Note note = displayList.getSelectedValue(); 395 int selectedIndex = displayList.getSelectedIndex(); 396 noteData.reOpenNote(note, dialog.getInputText()); 397 noteData.setSelectedNote(model.getElementAt(selectedIndex)); 398 } 399 } 400 401 class SortAction extends AbstractAction { 402 403 /** 404 * Constructs a new {@code SortAction}. 405 */ 406 SortAction() { 407 putValue(SHORT_DESCRIPTION, tr("Sort notes")); 408 putValue(NAME, tr("Sort")); 409 new ImageProvider("dialogs", "sort").getResource().attachImageIcon(this, true); 410 } 411 412 @Override 413 public void actionPerformed(ActionEvent e) { 414 NoteSortDialog sortDialog = new NoteSortDialog(MainApplication.getMainFrame(), tr("Sort notes"), tr("Apply")); 415 sortDialog.showSortDialog(noteData.getCurrentSortMethod()); 416 if (sortDialog.getValue() == 1) { 417 noteData.setSortMethod(sortDialog.getSelectedComparator()); 418 } 419 } 420 } 421 422 class OpenInBrowserAction extends AbstractAction { 423 OpenInBrowserAction() { 424 putValue(SHORT_DESCRIPTION, tr("Open the note in an external browser")); 425 new ImageProvider("help", "internet").getResource().attachImageIcon(this, true); 426 } 427 428 @Override 429 public void actionPerformed(ActionEvent e) { 430 final Note note = displayList.getSelectedValue(); 431 if (note.getId() > 0) { 432 final String url = Config.getUrls().getBaseBrowseUrl() + "/note/" + note.getId(); 433 OpenBrowser.displayUrl(url); 434 } 435 } 436 } 437 438}