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