001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.io; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.BorderLayout; 007import java.awt.GridBagLayout; 008import java.awt.event.ActionEvent; 009import java.awt.event.ActionListener; 010import java.awt.event.FocusAdapter; 011import java.awt.event.FocusEvent; 012import java.awt.event.ItemEvent; 013import java.awt.event.KeyAdapter; 014import java.awt.event.KeyEvent; 015import java.util.Arrays; 016import java.util.LinkedList; 017import java.util.List; 018import java.util.Objects; 019import java.util.concurrent.TimeUnit; 020 021import javax.swing.BorderFactory; 022import javax.swing.JCheckBox; 023import javax.swing.JEditorPane; 024import javax.swing.JLabel; 025import javax.swing.JPanel; 026import javax.swing.event.AncestorEvent; 027import javax.swing.event.AncestorListener; 028import javax.swing.event.ChangeEvent; 029import javax.swing.event.ChangeListener; 030import javax.swing.event.HyperlinkEvent; 031 032import org.openstreetmap.josm.data.osm.Changeset; 033import org.openstreetmap.josm.gui.MainApplication; 034import org.openstreetmap.josm.gui.widgets.HistoryComboBox; 035import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 036import org.openstreetmap.josm.spi.preferences.Config; 037import org.openstreetmap.josm.tools.GBC; 038import org.openstreetmap.josm.tools.Utils; 039 040/** 041 * BasicUploadSettingsPanel allows to enter the basic parameters required for uploading data. 042 * @since 2599 043 */ 044public class BasicUploadSettingsPanel extends JPanel { 045 /** 046 * Preference name for history collection 047 */ 048 public static final String HISTORY_KEY = "upload.comment.history"; 049 /** 050 * Preference name for last used upload comment 051 */ 052 public static final String HISTORY_LAST_USED_KEY = "upload.comment.last-used"; 053 /** 054 * Preference name for the max age search comments may have 055 */ 056 public static final String HISTORY_MAX_AGE_KEY = "upload.comment.max-age"; 057 /** 058 * Preference name for the history of source values 059 */ 060 public static final String SOURCE_HISTORY_KEY = "upload.source.history"; 061 062 /** the history combo box for the upload comment */ 063 private final HistoryComboBox hcbUploadComment = new HistoryComboBox(); 064 private final HistoryComboBox hcbUploadSource = new HistoryComboBox(); 065 /** the panel with a summary of the upload parameters */ 066 private final UploadParameterSummaryPanel pnlUploadParameterSummary = new UploadParameterSummaryPanel(); 067 /** the checkbox to request feedback from other users */ 068 private final JCheckBox cbRequestReview = new JCheckBox(tr("I would like someone to review my edits.")); 069 /** the changeset comment model */ 070 private final transient ChangesetCommentModel changesetCommentModel; 071 private final transient ChangesetCommentModel changesetSourceModel; 072 private final transient ChangesetReviewModel changesetReviewModel; 073 074 protected JPanel buildUploadCommentPanel() { 075 JPanel pnl = new JPanel(new GridBagLayout()); 076 077 JEditorPane commentLabel = new JMultilineLabel("<html><b>" + tr("Provide a brief comment for the changes you are uploading:")); 078 pnl.add(commentLabel, GBC.eol().insets(0, 5, 10, 3).fill(GBC.HORIZONTAL)); 079 hcbUploadComment.setToolTipText(tr("Enter an upload comment")); 080 hcbUploadComment.setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH); 081 populateHistoryComboBox(hcbUploadComment, HISTORY_KEY, new LinkedList<String>()); 082 CommentModelListener commentModelListener = new CommentModelListener(hcbUploadComment, changesetCommentModel); 083 hcbUploadComment.getEditor().addActionListener(commentModelListener); 084 hcbUploadComment.getEditorComponent().addFocusListener(commentModelListener); 085 pnl.add(hcbUploadComment, GBC.eol().fill(GBC.HORIZONTAL)); 086 087 JEditorPane sourceLabel = new JMultilineLabel("<html><b>" + tr("Specify the data source for the changes") + ":</b>"); 088 pnl.add(sourceLabel, GBC.eol().insets(0, 8, 10, 0).fill(GBC.HORIZONTAL)); 089 JEditorPane obtainSourceOnce = new JMultilineLabel( 090 "<html><a href=\"urn:changeset-source\">" + tr("just once") + "</a></html>"); 091 obtainSourceOnce.addHyperlinkListener(e -> { 092 if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) { 093 automaticallyAddSource(); 094 } 095 }); 096 JCheckBox obtainSourceAutomatically = new JCheckBox(tr("Automatically obtain source from current layers")); 097 obtainSourceAutomatically.setSelected(Config.getPref().getBoolean("upload.source.obtainautomatically", false)); 098 obtainSourceAutomatically.addActionListener(e -> { 099 if (obtainSourceAutomatically.isSelected()) 100 automaticallyAddSource(); 101 102 obtainSourceOnce.setVisible(!obtainSourceAutomatically.isSelected()); 103 }); 104 JPanel obtainSource = new JPanel(new GridBagLayout()); 105 obtainSource.add(obtainSourceAutomatically, GBC.std().anchor(GBC.WEST)); 106 obtainSource.add(obtainSourceOnce, GBC.std().anchor(GBC.WEST)); 107 obtainSource.add(new JLabel(), GBC.eol().fill(GBC.HORIZONTAL)); 108 pnl.add(obtainSource, GBC.eol().insets(0, 0, 10, 3).fill(GBC.HORIZONTAL)); 109 110 hcbUploadSource.setToolTipText(tr("Enter a source")); 111 hcbUploadSource.setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH); 112 populateHistoryComboBox(hcbUploadSource, SOURCE_HISTORY_KEY, getDefaultSources()); 113 CommentModelListener sourceModelListener = new CommentModelListener(hcbUploadSource, changesetSourceModel); 114 hcbUploadSource.getEditor().addActionListener(sourceModelListener); 115 hcbUploadSource.getEditorComponent().addFocusListener(sourceModelListener); 116 pnl.add(hcbUploadSource, GBC.eol().fill(GBC.HORIZONTAL)); 117 if (obtainSourceAutomatically.isSelected()) { 118 automaticallyAddSource(); 119 } 120 pnl.addAncestorListener(new AncestorListener() { 121 @Override 122 public void ancestorAdded(AncestorEvent event) { 123 if (obtainSourceAutomatically.isSelected()) 124 automaticallyAddSource(); 125 } 126 127 @Override 128 public void ancestorRemoved(AncestorEvent event) { 129 // Do nothing 130 } 131 132 @Override 133 public void ancestorMoved(AncestorEvent event) { 134 // Do nothing 135 } 136 }); 137 return pnl; 138 } 139 140 /** 141 * Add the source tags 142 */ 143 protected void automaticallyAddSource() { 144 final String source = MainApplication.getMap().mapView.getLayerInformationForSourceTag(); 145 hcbUploadSource.setText(Utils.shortenString(source, Changeset.MAX_CHANGESET_TAG_LENGTH)); 146 changesetSourceModel.setComment(hcbUploadSource.getText()); // Fix #9965 147 } 148 149 /** 150 * Refreshes contents of upload history combo boxes from preferences. 151 */ 152 protected void refreshHistoryComboBoxes() { 153 populateHistoryComboBox(hcbUploadComment, HISTORY_KEY, new LinkedList<String>()); 154 populateHistoryComboBox(hcbUploadSource, SOURCE_HISTORY_KEY, getDefaultSources()); 155 } 156 157 private static void populateHistoryComboBox(HistoryComboBox hcb, String historyKey, List<String> defaultValues) { 158 hcb.setPossibleItemsTopDown(Config.getPref().getList(historyKey, defaultValues)); 159 hcb.discardAllUndoableEdits(); 160 } 161 162 /** 163 * Discards undoable edits of upload history combo boxes. 164 */ 165 protected void discardAllUndoableEdits() { 166 hcbUploadComment.discardAllUndoableEdits(); 167 hcbUploadSource.discardAllUndoableEdits(); 168 } 169 170 /** 171 * Returns the default list of sources. 172 * @return the default list of sources 173 */ 174 public static List<String> getDefaultSources() { 175 return Arrays.asList("knowledge", "survey", "Bing"); 176 } 177 178 protected void build() { 179 setLayout(new BorderLayout()); 180 setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); 181 add(buildUploadCommentPanel(), BorderLayout.NORTH); 182 add(pnlUploadParameterSummary, BorderLayout.CENTER); 183 add(cbRequestReview, BorderLayout.SOUTH); 184 cbRequestReview.addItemListener(e -> changesetReviewModel.setReviewRequested(e.getStateChange() == ItemEvent.SELECTED)); 185 } 186 187 /** 188 * Creates the panel 189 * 190 * @param changesetCommentModel the model for the changeset comment. Must not be null 191 * @param changesetSourceModel the model for the changeset source. Must not be null. 192 * @param changesetReviewModel the model for the changeset review. Must not be null. 193 * @throws NullPointerException if a model is null 194 * @since 12719 (signature) 195 */ 196 public BasicUploadSettingsPanel(ChangesetCommentModel changesetCommentModel, ChangesetCommentModel changesetSourceModel, 197 ChangesetReviewModel changesetReviewModel) { 198 this.changesetCommentModel = Objects.requireNonNull(changesetCommentModel, "changesetCommentModel"); 199 this.changesetSourceModel = Objects.requireNonNull(changesetSourceModel, "changesetSourceModel"); 200 this.changesetReviewModel = Objects.requireNonNull(changesetReviewModel, "changesetReviewModel"); 201 changesetCommentModel.addChangeListener(new ChangesetCommentChangeListener(hcbUploadComment)); 202 changesetSourceModel.addChangeListener(new ChangesetCommentChangeListener(hcbUploadSource)); 203 changesetReviewModel.addChangeListener(new ChangesetReviewChangeListener()); 204 build(); 205 } 206 207 void setUploadTagDownFocusTraversalHandlers(final ActionListener handler) { 208 setHistoryComboBoxDownFocusTraversalHandler(handler, hcbUploadComment); 209 setHistoryComboBoxDownFocusTraversalHandler(handler, hcbUploadSource); 210 } 211 212 private static void setHistoryComboBoxDownFocusTraversalHandler(ActionListener handler, HistoryComboBox hcb) { 213 hcb.getEditor().addActionListener(handler); 214 hcb.getEditorComponent().addKeyListener(new HistoryComboBoxKeyAdapter(hcb, handler)); 215 } 216 217 /** 218 * Remembers the user input in the preference settings 219 */ 220 public void rememberUserInput() { 221 // store the history of comments 222 if (getHistoryMaxAgeKey() > 0) { 223 hcbUploadComment.addCurrentItemToHistory(); 224 Config.getPref().putList(HISTORY_KEY, hcbUploadComment.getHistory()); 225 Config.getPref().putLong(HISTORY_LAST_USED_KEY, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); 226 } 227 // store the history of sources 228 hcbUploadSource.addCurrentItemToHistory(); 229 Config.getPref().putList(SOURCE_HISTORY_KEY, hcbUploadSource.getHistory()); 230 } 231 232 /** 233 * Initializes the panel for user input 234 */ 235 public void startUserInput() { 236 hcbUploadComment.requestFocusInWindow(); 237 hcbUploadComment.getEditorComponent().requestFocusInWindow(); 238 } 239 240 /** 241 * Initializes editing of upload comment. 242 */ 243 public void initEditingOfUploadComment() { 244 hcbUploadComment.getEditor().selectAll(); 245 hcbUploadComment.requestFocusInWindow(); 246 } 247 248 /** 249 * Initializes editing of upload source. 250 */ 251 public void initEditingOfUploadSource() { 252 hcbUploadSource.getEditor().selectAll(); 253 hcbUploadSource.requestFocusInWindow(); 254 } 255 256 /** 257 * Returns the panel that displays a summary of data the user is about to upload. 258 * @return the upload parameter summary panel 259 */ 260 public UploadParameterSummaryPanel getUploadParameterSummaryPanel() { 261 return pnlUploadParameterSummary; 262 } 263 264 /** 265 * Forces update of comment/source model if matching text field is focused. 266 * @since 14977 267 */ 268 public void forceUpdateActiveField() { 269 updateModelIfFocused(hcbUploadComment, changesetCommentModel); 270 updateModelIfFocused(hcbUploadSource, changesetSourceModel); 271 } 272 273 private static void updateModelIfFocused(HistoryComboBox hcb, ChangesetCommentModel changesetModel) { 274 if (hcb.getEditorComponent().hasFocus()) { 275 changesetModel.setComment(hcb.getText()); 276 } 277 } 278 279 static long getHistoryMaxAgeKey() { 280 return Config.getPref().getLong(HISTORY_MAX_AGE_KEY, TimeUnit.HOURS.toSeconds(4)); 281 } 282 283 static long getHistoryLastUsedKey() { 284 return Config.getPref().getLong(BasicUploadSettingsPanel.HISTORY_LAST_USED_KEY, 0); 285 } 286 287 static final class HistoryComboBoxKeyAdapter extends KeyAdapter { 288 private final HistoryComboBox hcb; 289 private final ActionListener handler; 290 291 HistoryComboBoxKeyAdapter(HistoryComboBox hcb, ActionListener handler) { 292 this.hcb = hcb; 293 this.handler = handler; 294 } 295 296 @Override 297 public void keyTyped(KeyEvent e) { 298 if (e.getKeyCode() == KeyEvent.VK_TAB) { 299 handler.actionPerformed(new ActionEvent(hcb, 0, "focusDown")); 300 } 301 } 302 } 303 304 /** 305 * Updates the changeset comment model upon changes in the input field. 306 */ 307 static class CommentModelListener extends FocusAdapter implements ActionListener { 308 309 private final HistoryComboBox source; 310 private final ChangesetCommentModel destination; 311 312 CommentModelListener(HistoryComboBox source, ChangesetCommentModel destination) { 313 this.source = source; 314 this.destination = destination; 315 } 316 317 @Override 318 public void actionPerformed(ActionEvent e) { 319 destination.setComment(source.getText()); 320 } 321 322 @Override 323 public void focusLost(FocusEvent e) { 324 destination.setComment(source.getText()); 325 } 326 } 327 328 /** 329 * Observes the changeset comment model and keeps the comment input field 330 * in sync with the current changeset comment 331 */ 332 static class ChangesetCommentChangeListener implements ChangeListener { 333 334 private final HistoryComboBox destination; 335 336 ChangesetCommentChangeListener(HistoryComboBox destination) { 337 this.destination = destination; 338 } 339 340 @Override 341 public void stateChanged(ChangeEvent e) { 342 if (!(e.getSource() instanceof ChangesetCommentModel)) return; 343 String newComment = ((ChangesetCommentModel) e.getSource()).getComment(); 344 if (!destination.getText().equals(newComment)) { 345 destination.setText(newComment); 346 } 347 } 348 } 349 350 /** 351 * Observes the changeset review model and keeps the review checkbox 352 * in sync with the current changeset review request 353 */ 354 class ChangesetReviewChangeListener implements ChangeListener { 355 @Override 356 public void stateChanged(ChangeEvent e) { 357 if (!(e.getSource() instanceof ChangesetReviewModel)) return; 358 boolean newState = ((ChangesetReviewModel) e.getSource()).isReviewRequested(); 359 if (cbRequestReview.isSelected() != newState) { 360 cbRequestReview.setSelected(newState); 361 } 362 } 363 } 364}