001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.event.ActionEvent; 008import java.awt.event.KeyEvent; 009import java.awt.event.MouseAdapter; 010import java.awt.event.MouseEvent; 011import java.text.NumberFormat; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.Collection; 015import java.util.Collections; 016import java.util.HashMap; 017import java.util.HashSet; 018import java.util.Iterator; 019import java.util.LinkedList; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023import java.util.stream.Collectors; 024 025import javax.swing.AbstractAction; 026import javax.swing.JPopupMenu; 027import javax.swing.JTable; 028import javax.swing.ListSelectionModel; 029import javax.swing.event.ListSelectionEvent; 030import javax.swing.event.ListSelectionListener; 031import javax.swing.table.DefaultTableModel; 032 033import org.openstreetmap.josm.actions.AbstractInfoAction; 034import org.openstreetmap.josm.data.osm.DataSelectionListener; 035import org.openstreetmap.josm.data.osm.IPrimitive; 036import org.openstreetmap.josm.data.osm.OsmData; 037import org.openstreetmap.josm.data.osm.OsmPrimitive; 038import org.openstreetmap.josm.data.osm.User; 039import org.openstreetmap.josm.data.osm.event.SelectionEventManager; 040import org.openstreetmap.josm.gui.MainApplication; 041import org.openstreetmap.josm.gui.SideButton; 042import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils; 043import org.openstreetmap.josm.gui.layer.Layer; 044import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 045import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 046import org.openstreetmap.josm.gui.layer.OsmDataLayer; 047import org.openstreetmap.josm.gui.util.GuiHelper; 048import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 049import org.openstreetmap.josm.spi.preferences.Config; 050import org.openstreetmap.josm.tools.ImageProvider; 051import org.openstreetmap.josm.tools.Logging; 052import org.openstreetmap.josm.tools.OpenBrowser; 053import org.openstreetmap.josm.tools.Shortcut; 054import org.openstreetmap.josm.tools.Utils; 055 056/** 057 * Displays a dialog with all users who have last edited something in the 058 * selection area, along with the number of objects. 059 * @since 237 060 */ 061public class UserListDialog extends ToggleDialog implements DataSelectionListener, ActiveLayerChangeListener { 062 063 /** 064 * The display list. 065 */ 066 private JTable userTable; 067 private UserTableModel model; 068 private SelectUsersPrimitivesAction selectionUsersPrimitivesAction; 069 private final JPopupMenu popupMenu = new JPopupMenu(); 070 071 /** 072 * Constructs a new {@code UserListDialog}. 073 */ 074 public UserListDialog() { 075 super(tr("Authors"), "userlist", tr("Open a list of people working on the selected objects."), 076 Shortcut.registerShortcut("subwindow:authors", tr("Toggle: {0}", tr("Authors")), KeyEvent.VK_A, Shortcut.ALT_SHIFT), 150); 077 build(); 078 } 079 080 @Override 081 public void showNotify() { 082 SelectionEventManager.getInstance().addSelectionListenerForEdt(this); 083 MainApplication.getLayerManager().addActiveLayerChangeListener(this); 084 } 085 086 @Override 087 public void hideNotify() { 088 MainApplication.getLayerManager().removeActiveLayerChangeListener(this); 089 SelectionEventManager.getInstance().removeSelectionListener(this); 090 } 091 092 protected void build() { 093 model = new UserTableModel(); 094 userTable = new JTable(model); 095 userTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 096 userTable.addMouseListener(new DoubleClickAdapter()); 097 098 // -- select users primitives action 099 // 100 selectionUsersPrimitivesAction = new SelectUsersPrimitivesAction(); 101 userTable.getSelectionModel().addListSelectionListener(selectionUsersPrimitivesAction); 102 103 // -- info action 104 // 105 ShowUserInfoAction showUserInfoAction = new ShowUserInfoAction(); 106 userTable.getSelectionModel().addListSelectionListener(showUserInfoAction); 107 108 createLayout(userTable, true, Arrays.asList( 109 new SideButton(selectionUsersPrimitivesAction), 110 new SideButton(showUserInfoAction) 111 )); 112 113 // -- popup menu 114 popupMenu.add(new AbstractAction(tr("Copy")) { 115 @Override 116 public void actionPerformed(ActionEvent e) { 117 ClipboardUtils.copyString(getSelectedUsers().stream().map(User::getName).collect(Collectors.joining(", "))); 118 } 119 }); 120 userTable.addMouseListener(new PopupMenuLauncher(popupMenu)); 121 } 122 123 @Override 124 public void selectionChanged(SelectionChangeEvent event) { 125 refresh(event.getSelection()); 126 } 127 128 @Override 129 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 130 Layer activeLayer = e.getSource().getActiveLayer(); 131 refreshForActiveLayer(activeLayer); 132 } 133 134 private void refreshForActiveLayer(Layer activeLayer) { 135 if (activeLayer instanceof OsmDataLayer) { 136 refresh(((OsmDataLayer) activeLayer).data.getAllSelected()); 137 } else { 138 refresh(null); 139 } 140 } 141 142 /** 143 * Refreshes user list from given collection of OSM primitives. 144 * @param fromPrimitives OSM primitives to fetch users from 145 */ 146 public void refresh(Collection<? extends OsmPrimitive> fromPrimitives) { 147 GuiHelper.runInEDT(() -> { 148 model.populate(fromPrimitives); 149 if (model.getRowCount() != 0) { 150 setTitle(trn("{0} Author", "{0} Authors", model.getRowCount(), model.getRowCount())); 151 } else { 152 setTitle(tr("Authors")); 153 } 154 }); 155 } 156 157 @Override 158 public void showDialog() { 159 super.showDialog(); 160 refreshForActiveLayer(MainApplication.getLayerManager().getActiveLayer()); 161 } 162 163 private List<User> getSelectedUsers() { 164 int[] rows = userTable.getSelectedRows(); 165 return rows.length == 0 ? Collections.emptyList() : model.getSelectedUsers(rows); 166 } 167 168 class SelectUsersPrimitivesAction extends AbstractAction implements ListSelectionListener { 169 170 /** 171 * Constructs a new {@code SelectUsersPrimitivesAction}. 172 */ 173 SelectUsersPrimitivesAction() { 174 putValue(NAME, tr("Select")); 175 putValue(SHORT_DESCRIPTION, tr("Select objects submitted by this user")); 176 new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true); 177 updateEnabledState(); 178 } 179 180 public void select() { 181 int[] indexes = userTable.getSelectedRows(); 182 if (indexes.length == 0) 183 return; 184 model.selectPrimitivesOwnedBy(userTable.getSelectedRows()); 185 } 186 187 @Override 188 public void actionPerformed(ActionEvent e) { 189 select(); 190 } 191 192 protected void updateEnabledState() { 193 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0); 194 } 195 196 @Override 197 public void valueChanged(ListSelectionEvent e) { 198 updateEnabledState(); 199 } 200 } 201 202 /** 203 * Action for launching the info page of a user. 204 */ 205 class ShowUserInfoAction extends AbstractInfoAction implements ListSelectionListener { 206 207 ShowUserInfoAction() { 208 super(false); 209 putValue(NAME, tr("Show info")); 210 putValue(SHORT_DESCRIPTION, tr("Launches a browser with information about the user")); 211 new ImageProvider("help/internet").getResource().attachImageIcon(this, true); 212 updateEnabledState(); 213 } 214 215 @Override 216 public void actionPerformed(ActionEvent e) { 217 List<User> users = getSelectedUsers(); 218 if (users.isEmpty()) 219 return; 220 if (users.size() > 10) { 221 Logging.warn(tr("Only launching info browsers for the first {0} of {1} selected users", 10, users.size())); 222 } 223 int num = Math.min(10, users.size()); 224 Iterator<User> it = users.iterator(); 225 while (it.hasNext() && num > 0) { 226 String url = createInfoUrl(it.next()); 227 if (url == null) { 228 break; 229 } 230 OpenBrowser.displayUrl(url); 231 num--; 232 } 233 } 234 235 @Override 236 protected String createInfoUrl(Object infoObject) { 237 if (infoObject instanceof User) { 238 User user = (User) infoObject; 239 return Config.getUrls().getBaseUserUrl() + '/' + Utils.encodeUrl(user.getName()).replaceAll("\\+", "%20"); 240 } else { 241 return null; 242 } 243 } 244 245 @Override 246 protected void updateEnabledState() { 247 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0); 248 } 249 250 @Override 251 public void valueChanged(ListSelectionEvent e) { 252 updateEnabledState(); 253 } 254 } 255 256 class DoubleClickAdapter extends MouseAdapter { 257 @Override 258 public void mouseClicked(MouseEvent e) { 259 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { 260 selectionUsersPrimitivesAction.select(); 261 } 262 } 263 } 264 265 /** 266 * Action for selecting the primitives contributed by the currently selected users. 267 * 268 */ 269 private static class UserInfo implements Comparable<UserInfo> { 270 public final User user; 271 public final int count; 272 public final double percent; 273 274 UserInfo(User user, int count, double percent) { 275 this.user = user; 276 this.count = count; 277 this.percent = percent; 278 } 279 280 @Override 281 public int compareTo(UserInfo o) { 282 if (count < o.count) 283 return 1; 284 if (count > o.count) 285 return -1; 286 if (user == null || user.getName() == null) 287 return 1; 288 if (o.user == null || o.user.getName() == null) 289 return -1; 290 return user.getName().compareTo(o.user.getName()); 291 } 292 293 public String getName() { 294 if (user == null) 295 return tr("<new object>"); 296 return user.getName(); 297 } 298 } 299 300 /** 301 * The table model for the users 302 * 303 */ 304 static class UserTableModel extends DefaultTableModel { 305 private final transient List<UserInfo> data; 306 307 UserTableModel() { 308 setColumnIdentifiers(new String[]{tr("Author"), tr("# Objects"), "%"}); 309 data = new ArrayList<>(); 310 } 311 312 protected Map<User, Integer> computeStatistics(Collection<? extends OsmPrimitive> primitives) { 313 Map<User, Integer> ret = new HashMap<>(); 314 if (primitives == null || primitives.isEmpty()) 315 return ret; 316 for (OsmPrimitive primitive: primitives) { 317 if (ret.containsKey(primitive.getUser())) { 318 ret.put(primitive.getUser(), ret.get(primitive.getUser()) + 1); 319 } else { 320 ret.put(primitive.getUser(), 1); 321 } 322 } 323 return ret; 324 } 325 326 public void populate(Collection<? extends OsmPrimitive> primitives) { 327 GuiHelper.assertCallFromEdt(); 328 Map<User, Integer> statistics = computeStatistics(primitives); 329 data.clear(); 330 if (primitives != null) { 331 for (Map.Entry<User, Integer> entry: statistics.entrySet()) { 332 data.add(new UserInfo(entry.getKey(), entry.getValue(), (double) entry.getValue() / (double) primitives.size())); 333 } 334 } 335 Collections.sort(data); 336 this.fireTableDataChanged(); 337 } 338 339 @Override 340 public int getRowCount() { 341 if (data == null) 342 return 0; 343 return data.size(); 344 } 345 346 @Override 347 public Object getValueAt(int row, int column) { 348 UserInfo info = data.get(row); 349 switch(column) { 350 case 0: /* author */ return info.getName() == null ? "" : info.getName(); 351 case 1: /* count */ return info.count; 352 case 2: /* percent */ return NumberFormat.getPercentInstance().format(info.percent); 353 default: return null; 354 } 355 } 356 357 @Override 358 public boolean isCellEditable(int row, int column) { 359 return false; 360 } 361 362 public void selectPrimitivesOwnedBy(int... rows) { 363 Set<User> users = new HashSet<>(); 364 for (int index: rows) { 365 users.add(data.get(index).user); 366 } 367 OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData(); 368 Collection<? extends IPrimitive> selected = ds.getAllSelected(); 369 Collection<IPrimitive> byUser = new LinkedList<>(); 370 for (IPrimitive p : selected) { 371 if (users.contains(p.getUser())) { 372 byUser.add(p); 373 } 374 } 375 ds.setSelected(byUser); 376 } 377 378 public List<User> getSelectedUsers(int... rows) { 379 List<User> ret = new LinkedList<>(); 380 if (rows == null || rows.length == 0) 381 return ret; 382 for (int row: rows) { 383 if (data.get(row).user == null) { 384 continue; 385 } 386 ret.add(data.get(row).user); 387 } 388 return ret; 389 } 390 } 391}