001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.history; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.BorderLayout; 008import java.awt.Dimension; 009import java.awt.GridBagConstraints; 010import java.awt.GridBagLayout; 011import java.awt.Insets; 012import java.awt.event.ActionEvent; 013import java.text.DateFormat; 014import java.util.Collections; 015import java.util.Date; 016 017import javax.swing.AbstractAction; 018import javax.swing.AbstractButton; 019import javax.swing.JButton; 020import javax.swing.JComponent; 021import javax.swing.JLabel; 022import javax.swing.JPanel; 023import javax.swing.JTextArea; 024import javax.swing.event.ChangeEvent; 025import javax.swing.event.ChangeListener; 026import javax.swing.plaf.basic.BasicArrowButton; 027 028import org.openstreetmap.josm.data.UserIdentityManager; 029import org.openstreetmap.josm.data.osm.Changeset; 030import org.openstreetmap.josm.data.osm.OsmPrimitive; 031import org.openstreetmap.josm.data.osm.PrimitiveId; 032import org.openstreetmap.josm.data.osm.User; 033import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive; 034import org.openstreetmap.josm.gui.MainApplication; 035import org.openstreetmap.josm.gui.dialogs.ChangesetDialog; 036import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetCacheManager; 037import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetDiscussionPanel; 038import org.openstreetmap.josm.gui.layer.OsmDataLayer; 039import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 040import org.openstreetmap.josm.gui.widgets.UrlLabel; 041import org.openstreetmap.josm.spi.preferences.Config; 042import org.openstreetmap.josm.tools.CheckParameterUtil; 043import org.openstreetmap.josm.tools.Destroyable; 044import org.openstreetmap.josm.tools.GBC; 045import org.openstreetmap.josm.tools.ImageProvider; 046import org.openstreetmap.josm.tools.Utils; 047import org.openstreetmap.josm.tools.date.DateUtils; 048 049/** 050 * VersionInfoPanel is an UI component which displays the basic properties of a version 051 * of a {@link OsmPrimitive}. 052 * @since 1709 053 */ 054public class VersionInfoPanel extends JPanel implements ChangeListener, Destroyable { 055 private final PointInTimeType pointInTimeType; 056 private final transient HistoryBrowserModel model; 057 private JMultilineLabel lblInfo; 058 private UrlLabel lblUser; 059 private UrlLabel lblChangeset; 060 private final JButton lblChangesetComments = new JButton(ImageProvider.get("dialogs/notes/note_comment")); 061 private final OpenChangesetDialogAction changesetCommentsDialogAction = new OpenChangesetDialogAction(ChangesetDiscussionPanel.class); 062 private final OpenChangesetDialogAction changesetDialogAction = new OpenChangesetDialogAction(null); 063 private final JButton changesetButton = new JButton(changesetDialogAction); 064 private final BasicArrowButton arrowButton = new BasicArrowButton(BasicArrowButton.SOUTH); 065 private JPanel pnlChangesetSource; 066 private JPanel pnlChangesetImageryUsed; 067 private JLabel lblSource; 068 private JLabel lblImageryUsed; 069 private JTextArea texChangesetComment; 070 private JTextArea texChangesetSource; 071 private JTextArea texChangesetImageryUsed; 072 private PrimitiveId primitiveId; 073 074 protected static JTextArea buildTextArea(String tooltip) { 075 JTextArea lbl = new JTextArea(); 076 lbl.setLineWrap(true); 077 lbl.setWrapStyleWord(true); 078 lbl.setEditable(false); 079 lbl.setOpaque(false); 080 lbl.setToolTipText(tooltip); 081 return lbl; 082 } 083 084 protected static JLabel buildLabel(String text, String tooltip, JTextArea textArea) { 085 // We need text field to be a JTextArea for line wrapping but cannot put HTML code in here, 086 // so create a separate JLabel with same characteristics (margin, font) 087 JLabel lbl = new JLabel("<html><p style='margin-top:"+textArea.getMargin().top+"'>"+text+"</html>"); 088 lbl.setFont(textArea.getFont()); 089 lbl.setToolTipText(tooltip); 090 lbl.setLabelFor(textArea); 091 return lbl; 092 } 093 094 protected static JPanel buildTextPanel(JLabel label, JTextArea textArea) { 095 JPanel pnl = new JPanel(new GridBagLayout()); 096 pnl.add(label, GBC.std().anchor(GBC.NORTHWEST)); 097 pnl.add(textArea, GBC.eol().insets(2, 0, 0, 0).fill()); 098 return pnl; 099 } 100 101 protected void build() { 102 JPanel pnl1 = new JPanel(new BorderLayout()); 103 lblInfo = new JMultilineLabel(""); 104 pnl1.add(lblInfo, BorderLayout.CENTER); 105 106 // +-----------------------+-------------------------------------+ 107 // | User: | lblUser | 108 // +-----------------------+-------------------------------------+ 109 // | changesetButton | lblChangeset | lblChangesetComments | 110 // +-----------------------+-------------------------------------+ 111 JPanel pnlUserAndChangeset = new JPanel(new GridBagLayout()); 112 pnlUserAndChangeset.add(new JLabel(tr("User:")), GBC.std()); 113 114 lblUser = new UrlLabel("", 2); 115 pnlUserAndChangeset.add(lblUser, GBC.eol().insets(5, 0, 0, 0).weight(1, 0)); 116 117 final JPanel changesetPanel = new JPanel(new BorderLayout()); 118 changesetButton.setMargin(new Insets(0, 0, 0, 2)); 119 changesetPanel.add(changesetButton, BorderLayout.CENTER); 120 arrowButton.addActionListener(action -> { 121 if (changesetDialogAction.id != null) { // fix #15444, #16097 122 final OpenChangesetPopupMenu popupMenu = new OpenChangesetPopupMenu(changesetDialogAction.id, primitiveId); 123 popupMenu.insert(changesetDialogAction, 0); 124 ((AbstractButton) popupMenu.getComponent(0)).setText(tr("Open Changeset Manager")); 125 popupMenu.show(arrowButton); 126 } 127 }); 128 changesetPanel.add(arrowButton, BorderLayout.EAST); 129 pnlUserAndChangeset.add(changesetPanel, GBC.std().fill().weight(0, 0)); 130 131 lblChangeset = new UrlLabel("", 2); 132 pnlUserAndChangeset.add(lblChangeset, GBC.std().insets(5, 0, 0, 0).weight(1, 0)); 133 134 lblChangesetComments.setAction(changesetCommentsDialogAction); 135 lblChangesetComments.setMargin(new Insets(0, 0, 0, 0)); 136 lblChangesetComments.setIcon(new ImageProvider("dialogs/notes/note_comment").setMaxSize(12).get()); 137 pnlUserAndChangeset.add(lblChangesetComments, GBC.eol()); 138 139 texChangesetComment = buildTextArea(tr("Changeset comment")); 140 texChangesetSource = buildTextArea(tr("Changeset source")); 141 texChangesetImageryUsed = buildTextArea(tr("Imagery used")); 142 143 lblSource = buildLabel(tr("<b>Source</b>:"), tr("Changeset source"), texChangesetSource); 144 lblImageryUsed = buildLabel(tr("<b>Imagery</b>:"), tr("Imagery used"), texChangesetImageryUsed); 145 pnlChangesetSource = buildTextPanel(lblSource, texChangesetSource); 146 pnlChangesetImageryUsed = buildTextPanel(lblImageryUsed, texChangesetImageryUsed); 147 148 setLayout(new GridBagLayout()); 149 GridBagConstraints gc = new GridBagConstraints(); 150 gc.anchor = GridBagConstraints.NORTHWEST; 151 gc.fill = GridBagConstraints.HORIZONTAL; 152 gc.weightx = 1.0; 153 gc.weighty = 1.0; 154 add(pnl1, gc); 155 gc.gridy = 1; 156 gc.weighty = 0.0; 157 add(pnlUserAndChangeset, gc); 158 gc.gridy = 2; 159 add(texChangesetComment, gc); 160 gc.gridy = 3; 161 add(pnlChangesetSource, gc); 162 gc.gridy = 4; 163 add(pnlChangesetImageryUsed, gc); 164 } 165 166 protected HistoryOsmPrimitive getPrimitive() { 167 if (model == null || pointInTimeType == null) 168 return null; 169 return model.getPointInTime(pointInTimeType); 170 } 171 172 protected String getInfoText(final Date timestamp, final long version, final boolean isLatest) { 173 String text; 174 if (isLatest) { 175 OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer(); 176 text = tr("<html>Version <strong>{0}</strong> currently edited in layer ''{1}''</html>", 177 Long.toString(version), 178 editLayer == null ? tr("unknown") : Utils.escapeReservedCharactersHTML(editLayer.getName()) 179 ); 180 } else { 181 String date = "?"; 182 if (timestamp != null) { 183 date = DateUtils.formatDateTime(timestamp, DateFormat.SHORT, DateFormat.SHORT); 184 } 185 text = tr( 186 "<html>Version <strong>{0}</strong> created on <strong>{1}</strong></html>", 187 Long.toString(version), date); 188 } 189 return text; 190 } 191 192 /** 193 * Constructs a new {@code VersionInfoPanel}. 194 */ 195 public VersionInfoPanel() { 196 pointInTimeType = null; 197 model = null; 198 build(); 199 } 200 201 /** 202 * constructor 203 * 204 * @param model the model (must not be null) 205 * @param pointInTimeType the point in time this panel visualizes (must not be null) 206 * @throws IllegalArgumentException if model is null 207 * @throws IllegalArgumentException if pointInTimeType is null 208 */ 209 public VersionInfoPanel(HistoryBrowserModel model, PointInTimeType pointInTimeType) { 210 CheckParameterUtil.ensureParameterNotNull(pointInTimeType, "pointInTimeType"); 211 CheckParameterUtil.ensureParameterNotNull(model, "model"); 212 213 this.model = model; 214 this.pointInTimeType = pointInTimeType; 215 model.addChangeListener(this); 216 build(); 217 } 218 219 protected static String getUserUrl(String username) { 220 return Config.getUrls().getBaseUserUrl() + '/' + Utils.encodeUrl(username).replaceAll("\\+", "%20"); 221 } 222 223 @Override 224 public void stateChanged(ChangeEvent e) { 225 HistoryOsmPrimitive primitive = getPrimitive(); 226 if (primitive != null) { 227 Changeset cs = primitive.getChangeset(); 228 update(cs, model.isLatest(primitive), primitive.getTimestamp(), primitive.getVersion(), primitive.getPrimitiveId()); 229 } 230 } 231 232 /** 233 * Updates the content of this panel based on the changeset information given by {@code primitive}. 234 * @param primitive the primitive to extract the changeset information from 235 * @param isLatest whether this relates to a not yet committed changeset 236 */ 237 public void update(final OsmPrimitive primitive, final boolean isLatest) { 238 update(Changeset.fromPrimitive(primitive), isLatest, primitive.getTimestamp(), primitive.getVersion(), primitive.getPrimitiveId()); 239 } 240 241 /** 242 * Updates the content of this panel based on the changeset information given by {@code cs}. 243 * @param cs the changeset information 244 * @param isLatest whether this relates to a not yet committed changeset 245 * @param timestamp the timestamp 246 * @param version the version of the primitive 247 * @param id the id and type of the primitive 248 * @since 14432 249 */ 250 public void update(final Changeset cs, final boolean isLatest, final Date timestamp, final long version, final PrimitiveId id) { 251 lblInfo.setText(getInfoText(timestamp, version, isLatest)); 252 primitiveId = id; 253 254 if (!isLatest && cs != null) { 255 User user = cs.getUser(); 256 String url = Config.getUrls().getBaseBrowseUrl() + "/changeset/" + cs.getId(); 257 lblChangeset.setUrl(url); 258 lblChangeset.setDescription(Long.toString(cs.getId())); 259 changesetCommentsDialogAction.setId(cs.getId()); 260 lblChangesetComments.setVisible(cs.getCommentsCount() > 0); 261 lblChangesetComments.setText(String.valueOf(cs.getCommentsCount())); 262 lblChangesetComments.setToolTipText(trn("This changeset has {0} comment", "This changeset has {0} comments", 263 cs.getCommentsCount(), cs.getCommentsCount())); 264 changesetDialogAction.setId(cs.getId()); 265 changesetButton.setEnabled(true); 266 arrowButton.setEnabled(true); 267 268 String username = ""; 269 if (user != null) { 270 username = user.getName(); 271 } 272 lblUser.setDescription(insertWbr(username), false); 273 if (user != null && user != User.getAnonymous()) { 274 lblUser.setUrl(getUserUrl(username)); 275 } else { 276 lblUser.setUrl(null); 277 } 278 } else { 279 String username = UserIdentityManager.getInstance().getUserName(); 280 if (username == null) { 281 lblUser.setDescription(tr("anonymous")); 282 lblUser.setUrl(null); 283 } else { 284 lblUser.setDescription(insertWbr(username), false); 285 lblUser.setUrl(getUserUrl(username)); 286 } 287 lblChangeset.setDescription(tr("none")); 288 lblChangeset.setUrl(null); 289 lblChangesetComments.setVisible(false); 290 changesetDialogAction.setId(null); 291 changesetButton.setEnabled(false); 292 arrowButton.setEnabled(false); 293 } 294 295 final Changeset oppCs = model != null ? model.getPointInTime(pointInTimeType.opposite()).getChangeset() : null; 296 updateText(cs, "comment", texChangesetComment, null, oppCs, texChangesetComment); 297 updateText(cs, "source", texChangesetSource, lblSource, oppCs, pnlChangesetSource); 298 updateText(cs, "imagery_used", texChangesetImageryUsed, lblImageryUsed, oppCs, pnlChangesetImageryUsed); 299 } 300 301 private static String insertWbr(String s) { 302 return Utils.escapeReservedCharactersHTML(s).replace("_", "_<wbr>"); 303 } 304 305 protected static void updateText(Changeset cs, String attr, JTextArea textArea, JLabel label, Changeset oppCs, JComponent container) { 306 final String text = cs != null ? cs.get(attr) : null; 307 // Update text, hide prefixing label if empty 308 if (label != null) { 309 label.setVisible(text != null && !Utils.isStripEmpty(text)); 310 } 311 textArea.setText(text); 312 // Hide container if values of both versions are empty 313 container.setVisible(text != null || (oppCs != null && oppCs.get(attr) != null)); 314 } 315 316 static class OpenChangesetDialogAction extends AbstractAction { 317 private final Class<? extends JComponent> componentToSelect; 318 private Integer id; 319 320 OpenChangesetDialogAction(Class<? extends JComponent> componentToSelect) { 321 super(tr("Changeset")); 322 new ImageProvider("dialogs/changeset", "changesetmanager").resetMaxSize(new Dimension(16, 16)) 323 .getResource().attachImageIcon(this, true); 324 putValue(SHORT_DESCRIPTION, tr("Opens the Changeset Manager window for the selected changesets")); 325 this.componentToSelect = componentToSelect; 326 } 327 328 void setId(Integer id) { 329 this.id = id; 330 } 331 332 @Override 333 public void actionPerformed(ActionEvent e) { 334 if (id != null) { 335 ChangesetDialog.LaunchChangesetManager.displayChangesets(Collections.singleton(id)); 336 } 337 if (componentToSelect != null) { 338 ChangesetCacheManager.getInstance().setSelectedComponentInDetailPanel(componentToSelect); 339 } 340 } 341 } 342 343 @Override 344 public void destroy() { 345 model.removeChangeListener(this); 346 } 347}