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