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