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; 029import javax.swing.event.ListSelectionEvent; 030import javax.swing.event.ListSelectionListener; 031 032import org.openstreetmap.josm.Main; 033import org.openstreetmap.josm.actions.DownloadNotesInViewAction; 034import org.openstreetmap.josm.actions.UploadNotesAction; 035import org.openstreetmap.josm.actions.mapmode.AddNoteAction; 036import org.openstreetmap.josm.data.notes.Note; 037import org.openstreetmap.josm.data.notes.Note.State; 038import org.openstreetmap.josm.data.notes.NoteComment; 039import org.openstreetmap.josm.data.osm.NoteData; 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.tools.ImageProvider; 049import org.openstreetmap.josm.tools.date.DateUtils; 050 051/** 052 * Dialog to display and manipulate notes. 053 * @since 7852 (renaming) 054 * @since 7608 (creation) 055 */ 056public class NotesDialog extends ToggleDialog implements LayerChangeListener { 057 058 private NoteTableModel model; 059 private JList<Note> displayList; 060 private final AddCommentAction addCommentAction; 061 private final CloseAction closeAction; 062 private final DownloadNotesInViewAction downloadNotesInViewAction; 063 private final NewAction newAction; 064 private final ReopenAction reopenAction; 065 private final SortAction sortAction; 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 uploadAction = new UploadNotesAction(); 080 buildDialog(); 081 Main.getLayerManager().addLayerChangeListener(this); 082 } 083 084 private void buildDialog() { 085 model = new NoteTableModel(); 086 displayList = new JList<>(model); 087 displayList.setCellRenderer(new NoteRenderer()); 088 displayList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 089 displayList.addListSelectionListener(new ListSelectionListener() { 090 @Override 091 public void valueChanged(ListSelectionEvent e) { 092 if (noteData != null) { //happens when layer is deleted while note selected 093 noteData.setSelectedNote(displayList.getSelectedValue()); 094 } 095 updateButtonStates(); 096 } 097 }); 098 displayList.addMouseListener(new MouseAdapter() { 099 //center view on selected note on double click 100 @Override 101 public void mouseClicked(MouseEvent e) { 102 if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { 103 if (noteData != null && noteData.getSelectedNote() != null) { 104 Main.map.mapView.zoomTo(noteData.getSelectedNote().getLatLon()); 105 } 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(new SideButton[]{ 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(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 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}