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