source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/changeset/query/AdvancedChangesetQueryPanel.java@ 4043

Last change on this file since 4043 was 4043, checked in by stoecker, 13 years ago

fix #6153 - spelling fixes

  • Property svn:eol-style set to native
File size: 46.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.changeset.query;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Color;
8import java.awt.GridBagConstraints;
9import java.awt.GridBagLayout;
10import java.awt.Insets;
11import java.awt.event.ItemEvent;
12import java.awt.event.ItemListener;
13import java.text.DateFormat;
14import java.text.ParseException;
15import java.util.Date;
16import java.util.GregorianCalendar;
17import java.util.Locale;
18
19import javax.swing.BorderFactory;
20import javax.swing.ButtonGroup;
21import javax.swing.JCheckBox;
22import javax.swing.JLabel;
23import javax.swing.JOptionPane;
24import javax.swing.JPanel;
25import javax.swing.JRadioButton;
26import javax.swing.JScrollPane;
27import javax.swing.JTextField;
28import javax.swing.text.JTextComponent;
29
30import org.openstreetmap.josm.Main;
31import org.openstreetmap.josm.gui.HelpAwareOptionPane;
32import org.openstreetmap.josm.gui.JMultilineLabel;
33import org.openstreetmap.josm.gui.JosmUserIdentityManager;
34import org.openstreetmap.josm.gui.help.HelpUtil;
35import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
36import org.openstreetmap.josm.gui.widgets.BoundingBoxSelectionPanel;
37import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
38import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
39import org.openstreetmap.josm.io.ChangesetQuery;
40import org.openstreetmap.josm.tools.CheckParameterUtil;
41
42/**
43 * This panel allows to specify a changeset query
44 *
45 */
46public class AdvancedChangesetQueryPanel extends JPanel {
47
48 private JCheckBox cbUserRestriction;
49 private JCheckBox cbOpenAndCloseRestrictions;
50 private JCheckBox cbTimeRestrictions;
51 private JCheckBox cbBoundingBoxRestriction;
52 private UserRestrictionPanel pnlUserRestriction;
53 private OpenAndCloseStateRestrictionPanel pnlOpenAndCloseRestriction;
54 private TimeRestrictionPanel pnlTimeRestriction;
55 private BBoxRestrictionPanel pnlBoundingBoxRestriction;
56
57 protected JPanel buildQueryPanel() {
58 ItemListener stateChangeHandler = new RestrictionGroupStateChangeHandler();
59 JPanel pnl = new VerticallyScrollablePanel();
60 pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
61 pnl.setLayout(new GridBagLayout());
62 GridBagConstraints gc = new GridBagConstraints();
63
64 // -- select changesets by a specific user
65 //
66 gc.anchor = GridBagConstraints.NORTHWEST;
67 gc.weightx = 0.0;
68 gc.fill = GridBagConstraints.HORIZONTAL;
69 pnl.add(cbUserRestriction = new JCheckBox(), gc);
70 cbUserRestriction.addItemListener(stateChangeHandler);
71
72 gc.gridx = 1;
73 gc.weightx = 1.0;
74 pnl.add(new JMultilineLabel(tr("Select changesets owned by specific users")),gc);
75
76 gc.gridy = 1;
77 gc.gridx = 1;
78 gc.weightx = 1.0;
79 pnl.add(pnlUserRestriction = new UserRestrictionPanel(), gc);
80
81 // -- restricting the query to open and closed changesets
82 //
83 gc.gridy = 2;
84 gc.gridx = 0;
85 gc.anchor = GridBagConstraints.NORTHWEST;
86 gc.weightx = 0.0;
87 gc.fill = GridBagConstraints.HORIZONTAL;
88 pnl.add(cbOpenAndCloseRestrictions = new JCheckBox(), gc);
89 cbOpenAndCloseRestrictions.addItemListener(stateChangeHandler);
90
91 gc.gridx = 1;
92 gc.weightx = 1.0;
93 pnl.add(new JMultilineLabel(tr("Select changesets depending on whether they are open or closed")),gc);
94
95 gc.gridy = 3;
96 gc.gridx = 1;
97 gc.weightx = 1.0;
98 pnl.add(pnlOpenAndCloseRestriction = new OpenAndCloseStateRestrictionPanel(), gc);
99
100 // -- restricting the query to a specific time
101 //
102 gc.gridy = 4;
103 gc.gridx = 0;
104 gc.anchor = GridBagConstraints.NORTHWEST;
105 gc.weightx = 0.0;
106 gc.fill = GridBagConstraints.HORIZONTAL;
107 pnl.add(cbTimeRestrictions = new JCheckBox(), gc);
108 cbTimeRestrictions.addItemListener(stateChangeHandler);
109
110 gc.gridx = 1;
111 gc.weightx = 1.0;
112 pnl.add(new JMultilineLabel(tr("Select changesets based on the date/time they have been created or closed")),gc);
113
114 gc.gridy = 5;
115 gc.gridx = 1;
116 gc.weightx = 1.0;
117 pnl.add(pnlTimeRestriction = new TimeRestrictionPanel(), gc);
118
119
120 // -- restricting the query to a specific bounding box
121 //
122 gc.gridy = 6;
123 gc.gridx = 0;
124 gc.anchor = GridBagConstraints.NORTHWEST;
125 gc.weightx = 0.0;
126 gc.fill = GridBagConstraints.HORIZONTAL;
127 pnl.add(cbBoundingBoxRestriction = new JCheckBox(), gc);
128 cbBoundingBoxRestriction.addItemListener(stateChangeHandler);
129
130 gc.gridx = 1;
131 gc.weightx = 1.0;
132 pnl.add(new JMultilineLabel(tr("Select only changesets related to a specific bounding box")),gc);
133
134 gc.gridy = 7;
135 gc.gridx = 1;
136 gc.weightx = 1.0;
137 pnl.add(pnlBoundingBoxRestriction = new BBoxRestrictionPanel(), gc);
138
139
140 gc.gridy = 8;
141 gc.gridx = 0;
142 gc.gridwidth = 2;
143 gc.fill =GridBagConstraints.BOTH;
144 gc.weightx = 1.0;
145 gc.weighty = 1.0;
146 pnl.add(new JPanel(), gc);
147
148 return pnl;
149 }
150
151 protected void build() {
152 setLayout(new BorderLayout());
153 JScrollPane spQueryPanel = new JScrollPane(buildQueryPanel());
154 add(spQueryPanel, BorderLayout.CENTER);
155 spQueryPanel.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
156 spQueryPanel.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
157
158 }
159
160 public AdvancedChangesetQueryPanel() {
161 build();
162 }
163
164 public void startUserInput() {
165 restoreFromSettings();
166 pnlBoundingBoxRestriction.setVisible(cbBoundingBoxRestriction.isSelected());
167 pnlOpenAndCloseRestriction.setVisible(cbOpenAndCloseRestrictions.isSelected());
168 pnlTimeRestriction.setVisible(cbTimeRestrictions.isSelected());
169 pnlUserRestriction.setVisible(cbUserRestriction.isSelected());
170 pnlOpenAndCloseRestriction.startUserInput();
171 pnlUserRestriction.startUserInput();
172 pnlTimeRestriction.startUserInput();
173 }
174
175 public void displayMessageIfInvalid() {
176 if (cbUserRestriction.isSelected()) {
177 if (! pnlUserRestriction.isValidChangesetQuery()) {
178 pnlUserRestriction.displayMessageIfInvalid();
179 }
180 } else if (cbTimeRestrictions.isSelected()) {
181 if (!pnlTimeRestriction.isValidChangesetQuery()) {
182 pnlTimeRestriction.displayMessageIfInvalid();
183 }
184 } else if (cbBoundingBoxRestriction.isSelected()) {
185 if (!pnlBoundingBoxRestriction.isValidChangesetQuery()) {
186 pnlBoundingBoxRestriction.displayMessageIfInvalid();
187 }
188 }
189 }
190
191 /**
192 * Builds the changeset query based on the data entered in the form.
193 *
194 * @return the changeset query. null, if the data entered doesn't represent
195 * a valid changeset query.
196 */
197 public ChangesetQuery buildChangesetQuery() {
198 ChangesetQuery query = new ChangesetQuery();
199 if (cbUserRestriction.isSelected()) {
200 if (! pnlUserRestriction.isValidChangesetQuery())
201 return null;
202 pnlUserRestriction.fillInQuery(query);
203 }
204 if (cbOpenAndCloseRestrictions.isSelected()) {
205 // don't have to check whether it's valid. It always is.
206 pnlOpenAndCloseRestriction.fillInQuery(query);
207 }
208 if (cbBoundingBoxRestriction.isSelected()) {
209 if (!pnlBoundingBoxRestriction.isValidChangesetQuery())
210 return null;
211 pnlBoundingBoxRestriction.fillInQuery(query);
212 }
213 if (cbTimeRestrictions.isSelected()) {
214 if (!pnlTimeRestriction.isValidChangesetQuery())
215 return null;
216 pnlTimeRestriction.fillInQuery(query);
217 }
218 return query;
219 }
220
221 public void rememberSettings() {
222 Main.pref.put("changeset-query.advanced.user-restrictions", cbUserRestriction.isSelected());
223 Main.pref.put("changeset-query.advanced.open-restrictions", cbOpenAndCloseRestrictions.isSelected());
224 Main.pref.put("changeset-query.advanced.time-restrictions", cbTimeRestrictions.isSelected());
225 Main.pref.put("changeset-query.advanced.bbox-restrictions", cbBoundingBoxRestriction.isSelected());
226
227 pnlUserRestriction.rememberSettings();
228 pnlOpenAndCloseRestriction.rememberSettings();
229 pnlTimeRestriction.rememberSettings();
230 }
231
232 public void restoreFromSettings() {
233 cbUserRestriction.setSelected(Main.pref.getBoolean("changeset-query.advanced.user-restrictions", false));
234 cbOpenAndCloseRestrictions.setSelected(Main.pref.getBoolean("changeset-query.advanced.open-restrictions", false));
235 cbTimeRestrictions.setSelected(Main.pref.getBoolean("changeset-query.advanced.time-restrictions", false));
236 cbBoundingBoxRestriction.setSelected(Main.pref.getBoolean("changeset-query.advanced.bbox-restrictions", false));
237 }
238
239 class RestrictionGroupStateChangeHandler implements ItemListener {
240 protected void userRestrictionStateChanged() {
241 if (pnlUserRestriction == null) return;
242 pnlUserRestriction.setVisible(cbUserRestriction.isSelected());
243 }
244
245 protected void openCloseRestrictionStateChanged() {
246 if (pnlOpenAndCloseRestriction == null) return;
247 pnlOpenAndCloseRestriction.setVisible(cbOpenAndCloseRestrictions.isSelected());
248 }
249
250 protected void timeRestrictionsStateChanged() {
251 if (pnlTimeRestriction == null) return;
252 pnlTimeRestriction.setVisible(cbTimeRestrictions.isSelected());
253 }
254
255 protected void boundingBoxRestrictionChanged() {
256 if (pnlBoundingBoxRestriction == null) return;
257 pnlBoundingBoxRestriction.setVisible(cbBoundingBoxRestriction.isSelected());
258 }
259
260 public void itemStateChanged(ItemEvent e) {
261 if (e.getSource() == cbUserRestriction) {
262 userRestrictionStateChanged();
263 } else if (e.getSource() == cbOpenAndCloseRestrictions) {
264 openCloseRestrictionStateChanged();
265 } else if (e.getSource() == cbTimeRestrictions) {
266 timeRestrictionsStateChanged();
267 } else if (e.getSource() == cbBoundingBoxRestriction) {
268 boundingBoxRestrictionChanged();
269 }
270 validate();
271 repaint();
272 }
273 }
274
275 /**
276 * This is the panel for selecting whether the changeset query should be restricted to
277 * open or closed changesets
278 */
279 static private class OpenAndCloseStateRestrictionPanel extends JPanel {
280
281 private JRadioButton rbOpenOnly;
282 private JRadioButton rbClosedOnly;
283 private JRadioButton rbBoth;
284
285 protected void build() {
286 setLayout(new GridBagLayout());
287 setBorder(BorderFactory.createCompoundBorder(
288 BorderFactory.createEmptyBorder(3,3,3,3),
289 BorderFactory.createCompoundBorder(
290 BorderFactory.createLineBorder(Color.GRAY),
291 BorderFactory.createEmptyBorder(5,5,5,5)
292 )
293 ));
294 GridBagConstraints gc = new GridBagConstraints();
295 gc.anchor = GridBagConstraints.NORTHWEST;
296 gc.fill = GridBagConstraints.HORIZONTAL;
297 gc.weightx = 0.0;
298 add(rbOpenOnly = new JRadioButton(), gc);
299
300 gc.gridx = 1;
301 gc.weightx = 1.0;
302 add(new JMultilineLabel(tr("Query open changesets only")), gc);
303
304 gc.gridy = 1;
305 gc.gridx = 0;
306 gc.weightx = 0.0;
307 add(rbClosedOnly = new JRadioButton(), gc);
308
309 gc.gridx = 1;
310 gc.weightx = 1.0;
311 add(new JMultilineLabel(tr("Query closed changesets only")), gc);
312
313 gc.gridy = 2;
314 gc.gridx = 0;
315 gc.weightx = 0.0;
316 add(rbBoth = new JRadioButton(), gc);
317
318 gc.gridx = 1;
319 gc.weightx = 1.0;
320 add(new JMultilineLabel(tr("Query both open and closed changesets")), gc);
321
322 ButtonGroup bgRestrictions = new ButtonGroup();
323 bgRestrictions.add(rbBoth);
324 bgRestrictions.add(rbClosedOnly);
325 bgRestrictions.add(rbOpenOnly);
326 }
327
328 public OpenAndCloseStateRestrictionPanel() {
329 build();
330 }
331
332 public void startUserInput() {
333 restoreFromSettings();
334 }
335
336 public void fillInQuery(ChangesetQuery query) {
337 if (rbBoth.isSelected()) {
338 query.beingClosed(true);
339 query.beingOpen(true);
340 } else if (rbOpenOnly.isSelected()) {
341 query.beingOpen(true);
342 } else if (rbClosedOnly.isSelected()) {
343 query.beingClosed(true);
344 }
345 }
346
347 public void rememberSettings() {
348 String prefRoot = "changeset-query.advanced.open-restrictions";
349 if (rbBoth.isSelected()) {
350 Main.pref.put(prefRoot + ".query-type", "both");
351 } else if (rbOpenOnly.isSelected()) {
352 Main.pref.put(prefRoot + ".query-type", "open");
353 } else if (rbClosedOnly.isSelected()) {
354 Main.pref.put(prefRoot + ".query-type", "closed");
355 }
356 }
357
358 public void restoreFromSettings() {
359 String prefRoot = "changeset-query.advanced.open-restrictions";
360 String v = Main.pref.get(prefRoot + ".query-type", "open");
361 rbBoth.setSelected(v.equals("both"));
362 rbOpenOnly.setSelected(v.equals("open"));
363 rbClosedOnly.setSelected(v.equals("closed"));
364 }
365 }
366
367 /**
368 * This is the panel for selecting whether the query should be restricted to a specific
369 * user
370 *
371 */
372 static private class UserRestrictionPanel extends JPanel {
373 private ButtonGroup bgUserRestrictions;
374 private JRadioButton rbRestrictToMyself;
375 private JRadioButton rbRestrictToUid;
376 private JRadioButton rbRestrictToUserName;
377 private JTextField tfUid;
378 private UidInputFieldValidator valUid;
379 private JTextField tfUserName;
380 private UserNameInputValidator valUserName;
381 private JMultilineLabel lblRestrictedToMyself;
382
383 protected JPanel buildUidInputPanel() {
384 JPanel pnl = new JPanel(new GridBagLayout());
385 GridBagConstraints gc = new GridBagConstraints();
386 gc.fill = GridBagConstraints.HORIZONTAL;
387 gc.weightx = 0.0;
388 gc.insets = new Insets(0,0,0,3);
389 pnl.add(new JLabel(tr("User ID:")), gc);
390
391 gc.gridx = 1;
392 pnl.add(tfUid = new JTextField(10),gc);
393 SelectAllOnFocusGainedDecorator.decorate(tfUid);
394 valUid = UidInputFieldValidator.decorate(tfUid);
395
396 // grab remaining space
397 gc.gridx = 2;
398 gc.weightx = 1.0;
399 pnl.add(new JPanel(), gc);
400 return pnl;
401 }
402
403 protected JPanel buildUserNameInputPanel() {
404 JPanel pnl = new JPanel(new GridBagLayout());
405 GridBagConstraints gc = new GridBagConstraints();
406 gc.fill = GridBagConstraints.HORIZONTAL;
407 gc.weightx = 0.0;
408 gc.insets = new Insets(0,0,0,3);
409 pnl.add(new JLabel(tr("User name:")), gc);
410
411 gc.gridx = 1;
412 pnl.add(tfUserName = new JTextField(10),gc);
413 SelectAllOnFocusGainedDecorator.decorate(tfUserName);
414 valUserName = UserNameInputValidator.decorate(tfUserName);
415
416 // grab remaining space
417 gc.gridx = 2;
418 gc.weightx = 1.0;
419 pnl.add(new JPanel(), gc);
420 return pnl;
421 }
422
423 protected void build() {
424 setLayout(new GridBagLayout());
425 setBorder(BorderFactory.createCompoundBorder(
426 BorderFactory.createEmptyBorder(3,3,3,3),
427 BorderFactory.createCompoundBorder(
428 BorderFactory.createLineBorder(Color.GRAY),
429 BorderFactory.createEmptyBorder(5,5,5,5)
430 )
431 ));
432
433 ItemListener userRestrictionChangeHandler = new UserRestrictionChangedHandler();
434 GridBagConstraints gc = new GridBagConstraints();
435 gc.anchor = GridBagConstraints.NORTHWEST;
436 gc.gridx = 0;
437 gc.fill= GridBagConstraints.HORIZONTAL;
438 gc.weightx = 0.0;
439 add(rbRestrictToMyself = new JRadioButton(), gc);
440 rbRestrictToMyself.addItemListener(userRestrictionChangeHandler);
441
442 gc.gridx = 1;
443 gc.fill = GridBagConstraints.HORIZONTAL;
444 gc.weightx = 1.0;
445 add(lblRestrictedToMyself = new JMultilineLabel(tr("Only changesets owned by myself")), gc);
446
447 gc.gridx = 0;
448 gc.gridy = 1;
449 gc.fill= GridBagConstraints.HORIZONTAL;
450 gc.weightx = 0.0;
451 add(rbRestrictToUid = new JRadioButton(), gc);
452 rbRestrictToUid.addItemListener(userRestrictionChangeHandler);
453
454 gc.gridx = 1;
455 gc.fill = GridBagConstraints.HORIZONTAL;
456 gc.weightx = 1.0;
457 add(new JMultilineLabel(tr("Only changesets owned by the user with the following user ID")),gc);
458
459 gc.gridx = 1;
460 gc.gridy = 2;
461 gc.fill = GridBagConstraints.HORIZONTAL;
462 gc.weightx = 1.0;
463 add(buildUidInputPanel(),gc);
464
465 gc.gridx = 0;
466 gc.gridy = 3;
467 gc.fill= GridBagConstraints.HORIZONTAL;
468 gc.weightx = 0.0;
469 add(rbRestrictToUserName = new JRadioButton(), gc);
470 rbRestrictToUserName.addItemListener(userRestrictionChangeHandler);
471
472 gc.gridx = 1;
473 gc.fill = GridBagConstraints.HORIZONTAL;
474 gc.weightx = 1.0;
475 add(new JMultilineLabel(tr("Only changesets owned by the user with the following user name")),gc);
476
477 gc.gridx = 1;
478 gc.gridy = 4;
479 gc.fill = GridBagConstraints.HORIZONTAL;
480 gc.weightx = 1.0;
481 add(buildUserNameInputPanel(),gc);
482
483 bgUserRestrictions = new ButtonGroup();
484 bgUserRestrictions.add(rbRestrictToMyself);
485 bgUserRestrictions.add(rbRestrictToUid);
486 bgUserRestrictions.add(rbRestrictToUserName);
487 }
488
489 public UserRestrictionPanel() {
490 build();
491 }
492
493 public void startUserInput() {
494 if (JosmUserIdentityManager.getInstance().isAnonymous()) {
495 lblRestrictedToMyself.setText("Only changesets owned by myself (disabled. JOSM is currently run by an anonymous user)");
496 rbRestrictToMyself.setEnabled(false);
497 if (rbRestrictToMyself.isSelected()) {
498 rbRestrictToUid.setSelected(true);
499 }
500 } else {
501 lblRestrictedToMyself.setText("Only changesets owned by myself");
502 rbRestrictToMyself.setEnabled(true);
503 rbRestrictToMyself.setSelected(true);
504 }
505 restoreFromSettings();
506 }
507
508 /**
509 * Sets the query restrictions on <code>query</code> for changeset owner based
510 * restrictions.
511 *
512 * @param query the query. Must not be null.
513 * @throws IllegalArgumentException thrown if query is null
514 * @throws IllegalStateException thrown if one of the available values for query parameters in
515 * this panel isn't valid
516 *
517 */
518 public void fillInQuery(ChangesetQuery query) throws IllegalStateException, IllegalArgumentException {
519 CheckParameterUtil.ensureParameterNotNull(query, "query");
520 if (rbRestrictToMyself.isSelected()) {
521 JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
522 if (im.isPartiallyIdentified()) {
523 query.forUser(im.getUserName());
524 } else if (im.isFullyIdentified()) {
525 query.forUser(im.getUserId());
526 } else
527 throw new IllegalStateException(tr("Cannot restrict changeset query to the current user because the current user is anonymous"));
528 } else if (rbRestrictToUid.isSelected()) {
529 int uid = valUid.getUid();
530 if (uid > 0) {
531 query.forUser(uid);
532 } else
533 throw new IllegalStateException(tr("Current value ''{0}'' for user ID is not valid", tfUid.getText()));
534 } else if (rbRestrictToUserName.isSelected()) {
535 if (! valUserName.isValid())
536 throw new IllegalStateException(tr("Cannot restrict the changeset query to the user name ''{0}''", tfUserName.getText()));
537 query.forUser(tfUserName.getText());
538 }
539 }
540
541
542 public boolean isValidChangesetQuery() {
543 if (rbRestrictToUid.isSelected())
544 return valUid.isValid();
545 else if (rbRestrictToUserName.isSelected())
546 return valUserName.isValid();
547 return true;
548 }
549
550 protected void alertInvalidUid() {
551 HelpAwareOptionPane.showOptionDialog(
552 this,
553 tr("Please enter a valid user ID"),
554 tr("Invalid user ID"),
555 JOptionPane.ERROR_MESSAGE,
556 HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidUserId")
557 );
558 }
559
560 protected void alertInvalidUserName() {
561 HelpAwareOptionPane.showOptionDialog(
562 this,
563 tr("Please enter a non-empty user name"),
564 tr("Invalid user name"),
565 JOptionPane.ERROR_MESSAGE,
566 HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidUserName")
567 );
568 }
569
570 public void displayMessageIfInvalid() {
571 if (rbRestrictToUid.isSelected()) {
572 if (!valUid.isValid()) {
573 alertInvalidUid();
574 }
575 } else if (rbRestrictToUserName.isSelected()) {
576 if (!valUserName.isValid()) {
577 alertInvalidUserName();
578 }
579 }
580 }
581
582 public void rememberSettings() {
583 String prefRoot = "changeset-query.advanced.user-restrictions";
584 if (rbRestrictToMyself.isSelected()) {
585 Main.pref.put(prefRoot + ".query-type", "mine");
586 } else if (rbRestrictToUid.isSelected()) {
587 Main.pref.put(prefRoot + ".query-type", "uid");
588 } else if (rbRestrictToUserName.isSelected()) {
589 Main.pref.put(prefRoot + ".query-type", "username");
590 }
591 Main.pref.put(prefRoot + ".uid", tfUid.getText());
592 Main.pref.put(prefRoot + ".username", tfUserName.getText());
593 }
594
595 public void restoreFromSettings() {
596 String prefRoot = "changeset-query.advanced.user-restrictions";
597 String v = Main.pref.get(prefRoot + ".query-type", "mine");
598 if (v.equals("mine")) {
599 JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
600 if (im.isAnonymous()) {
601 rbRestrictToUid.setSelected(true);
602 } else {
603 rbRestrictToMyself.setSelected(true);
604 }
605 } else if (v.equals("uid")) {
606 rbRestrictToUid.setSelected(true);
607 } else if (v.equals("username")) {
608 rbRestrictToUserName.setSelected(true);
609 }
610 tfUid.setText(Main.pref.get(prefRoot + ".uid", ""));
611 if (!valUid.isValid()) {
612 tfUid.setText("");
613 }
614 tfUserName.setText(Main.pref.get(prefRoot + ".username", ""));
615 }
616
617 class UserRestrictionChangedHandler implements ItemListener {
618 public void itemStateChanged(ItemEvent e) {
619 tfUid.setEnabled(rbRestrictToUid.isSelected());
620 tfUserName.setEnabled(rbRestrictToUserName.isSelected());
621 if (rbRestrictToUid.isSelected()) {
622 tfUid.requestFocusInWindow();
623 } else if (rbRestrictToUserName.isSelected()) {
624 tfUserName.requestFocusInWindow();
625 }
626 }
627 }
628 }
629
630 /**
631 * This is the panel to apply a time restriction to the changeset query
632 */
633 static private class TimeRestrictionPanel extends JPanel {
634
635 private JRadioButton rbClosedAfter;
636 private JRadioButton rbClosedAfterAndCreatedBefore;
637 private JTextField tfClosedAfterDate1;
638 private DateValidator valClosedAfterDate1;
639 private JTextField tfClosedAfterTime1;
640 private TimeValidator valClosedAfterTime1;
641 private JTextField tfClosedAfterDate2;
642 private DateValidator valClosedAfterDate2;
643 private JTextField tfClosedAfterTime2;
644 private TimeValidator valClosedAfterTime2;
645 private JTextField tfCreatedBeforeDate;
646 private DateValidator valCreatedBeforeDate;
647 private JTextField tfCreatedBeforeTime;
648 private TimeValidator valCreatedBeforeTime;
649
650 protected JPanel buildClosedAfterInputPanel() {
651 JPanel pnl = new JPanel(new GridBagLayout());
652 GridBagConstraints gc = new GridBagConstraints();
653 gc.fill = GridBagConstraints.HORIZONTAL;
654 gc.weightx = 0.0;
655 gc.insets = new Insets(0,0,0,3);
656 pnl.add(new JLabel(tr("Date: ")), gc);
657
658 gc.gridx = 1;
659 gc.weightx = 0.7;
660 pnl.add(tfClosedAfterDate1 = new JTextField(),gc);
661 SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterDate1);
662 valClosedAfterDate1 = DateValidator.decorate(tfClosedAfterDate1);
663 tfClosedAfterDate1.setToolTipText(valClosedAfterDate1.getStandardTooltipTextAsHtml());
664
665 gc.gridx = 2;
666 gc.weightx = 0.0;
667 pnl.add(new JLabel(tr("Time:")),gc);
668
669 gc.gridx = 3;
670 gc.weightx = 0.3;
671 pnl.add(tfClosedAfterTime1 = new JTextField(),gc);
672 SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterTime1);
673 valClosedAfterTime1 = TimeValidator.decorate(tfClosedAfterTime1);
674 tfClosedAfterTime1.setToolTipText(valClosedAfterTime1.getStandardTooltipTextAsHtml());
675 return pnl;
676 }
677
678 protected JPanel buildClosedAfterAndCreatedBeforeInputPanel() {
679 JPanel pnl = new JPanel(new GridBagLayout());
680 GridBagConstraints gc = new GridBagConstraints();
681 gc.fill = GridBagConstraints.HORIZONTAL;
682 gc.weightx = 0.0;
683 gc.insets = new Insets(0,0,0,3);
684 pnl.add(new JLabel(tr("Closed after - ")), gc);
685
686 gc.gridx = 1;
687 gc.fill = GridBagConstraints.HORIZONTAL;
688 gc.weightx = 0.0;
689 gc.insets = new Insets(0,0,0,3);
690 pnl.add(new JLabel(tr("Date:")), gc);
691
692 gc.gridx = 2;
693 gc.weightx = 0.7;
694 pnl.add(tfClosedAfterDate2 = new JTextField(),gc);
695 SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterDate2);
696 valClosedAfterDate2 = DateValidator.decorate(tfClosedAfterDate2);
697 tfClosedAfterDate2.setToolTipText(valClosedAfterDate2.getStandardTooltipTextAsHtml());
698 gc.gridx = 3;
699 gc.weightx = 0.0;
700 pnl.add(new JLabel(tr("Time:")),gc);
701
702 gc.gridx = 4;
703 gc.weightx = 0.3;
704 pnl.add(tfClosedAfterTime2 = new JTextField(),gc);
705 SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterTime2);
706 valClosedAfterTime2 = TimeValidator.decorate(tfClosedAfterTime2);
707 tfClosedAfterTime2.setToolTipText(valClosedAfterTime2.getStandardTooltipTextAsHtml());
708
709 gc.gridy = 1;
710 gc.gridx = 0;
711 gc.fill = GridBagConstraints.HORIZONTAL;
712 gc.weightx = 0.0;
713 gc.insets = new Insets(0,0,0,3);
714 pnl.add(new JLabel(tr("Created before - ")), gc);
715
716 gc.gridx = 1;
717 gc.fill = GridBagConstraints.HORIZONTAL;
718 gc.weightx = 0.0;
719 gc.insets = new Insets(0,0,0,3);
720 pnl.add(new JLabel(tr("Date:")), gc);
721
722 gc.gridx = 2;
723 gc.weightx = 0.7;
724 pnl.add(tfCreatedBeforeDate = new JTextField(),gc);
725 SelectAllOnFocusGainedDecorator.decorate(tfCreatedBeforeDate);
726 valCreatedBeforeDate = DateValidator.decorate(tfCreatedBeforeDate);
727 tfCreatedBeforeDate.setToolTipText(valCreatedBeforeDate.getStandardTooltipTextAsHtml());
728
729 gc.gridx = 3;
730 gc.weightx = 0.0;
731 pnl.add(new JLabel(tr("Time:")),gc);
732
733 gc.gridx = 4;
734 gc.weightx = 0.3;
735 pnl.add(tfCreatedBeforeTime = new JTextField(),gc);
736 SelectAllOnFocusGainedDecorator.decorate(tfCreatedBeforeTime);
737 valCreatedBeforeTime = TimeValidator.decorate(tfCreatedBeforeTime);
738 tfCreatedBeforeTime.setToolTipText(valCreatedBeforeDate.getStandardTooltipTextAsHtml());
739
740 return pnl;
741 }
742
743 protected void build() {
744 setLayout(new GridBagLayout());
745 setBorder(BorderFactory.createCompoundBorder(
746 BorderFactory.createEmptyBorder(3,3,3,3),
747 BorderFactory.createCompoundBorder(
748 BorderFactory.createLineBorder(Color.GRAY),
749 BorderFactory.createEmptyBorder(5,5,5,5)
750 )
751 ));
752
753 // -- changesets closed after a specific date/time
754 //
755 GridBagConstraints gc = new GridBagConstraints();
756 gc.anchor = GridBagConstraints.NORTHWEST;
757 gc.gridx = 0;
758 gc.fill= GridBagConstraints.HORIZONTAL;
759 gc.weightx = 0.0;
760 add(rbClosedAfter = new JRadioButton(), gc);
761
762 gc.gridx = 1;
763 gc.fill = GridBagConstraints.HORIZONTAL;
764 gc.weightx = 1.0;
765 add(new JMultilineLabel(tr("Only changesets closed after the following date/time")), gc);
766
767 gc.gridx = 1;
768 gc.gridy = 1;
769 gc.fill = GridBagConstraints.HORIZONTAL;
770 gc.weightx = 1.0;
771 add(buildClosedAfterInputPanel(),gc);
772
773 // -- changesets closed after a specific date/time and created before a specific date time
774 //
775 gc = new GridBagConstraints();
776 gc.anchor = GridBagConstraints.NORTHWEST;
777 gc.gridy = 2;
778 gc.gridx = 0;
779 gc.fill= GridBagConstraints.HORIZONTAL;
780 gc.weightx = 0.0;
781 add(rbClosedAfterAndCreatedBefore = new JRadioButton(), gc);
782
783 gc.gridx = 1;
784 gc.fill = GridBagConstraints.HORIZONTAL;
785 gc.weightx = 1.0;
786 add(new JMultilineLabel(tr("Only changesets closed after and created before a specific date/time")), gc);
787
788 gc.gridx = 1;
789 gc.gridy = 3;
790 gc.fill = GridBagConstraints.HORIZONTAL;
791 gc.weightx = 1.0;
792 add(buildClosedAfterAndCreatedBeforeInputPanel(),gc);
793
794 ButtonGroup bg = new ButtonGroup();
795 bg.add(rbClosedAfter);
796 bg.add(rbClosedAfterAndCreatedBefore);
797
798 ItemListener restrictionChangeHandler = new TimeRestrictionChangedHandler();
799 rbClosedAfter.addItemListener(restrictionChangeHandler);
800 rbClosedAfterAndCreatedBefore.addItemListener(restrictionChangeHandler);
801
802 rbClosedAfter.setSelected(true);
803 }
804
805 public TimeRestrictionPanel() {
806 build();
807 }
808
809 public boolean isValidChangesetQuery() {
810 if (rbClosedAfter.isSelected())
811 return valClosedAfterDate1.isValid() && valClosedAfterTime1.isValid();
812 else if (rbClosedAfterAndCreatedBefore.isSelected())
813 return valClosedAfterDate2.isValid() && valClosedAfterTime2.isValid()
814 && valCreatedBeforeDate.isValid() && valCreatedBeforeTime.isValid();
815 // should not happen
816 return true;
817 }
818
819 class TimeRestrictionChangedHandler implements ItemListener {
820 public void itemStateChanged(ItemEvent e) {
821 tfClosedAfterDate1.setEnabled(rbClosedAfter.isSelected());
822 tfClosedAfterTime1.setEnabled(rbClosedAfter.isSelected());
823
824 tfClosedAfterDate2.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
825 tfClosedAfterTime2.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
826 tfCreatedBeforeDate.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
827 tfCreatedBeforeTime.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
828 }
829 }
830
831 public void startUserInput() {
832 restoreFromSettings();
833 }
834
835 public void fillInQuery(ChangesetQuery query) throws IllegalStateException{
836 if (!isValidChangesetQuery())
837 throw new IllegalStateException(tr("Cannot build changeset query with time based restrictions. Input is not valid."));
838 if (rbClosedAfter.isSelected()) {
839 GregorianCalendar cal = new GregorianCalendar();
840 Date d1 = valClosedAfterDate1.getDate();
841 Date d2 = valClosedAfterTime1.getDate();
842 cal.setTimeInMillis(d1.getTime() + (d2 == null ? 0 : d2.getTime()));
843 query.closedAfter(cal.getTime());
844 } else if (rbClosedAfterAndCreatedBefore.isSelected()) {
845 GregorianCalendar cal = new GregorianCalendar();
846 Date d1 = valClosedAfterDate2.getDate();
847 Date d2 = valClosedAfterTime2.getDate();
848 cal.setTimeInMillis(d1.getTime() + (d2 == null ? 0 : d2.getTime()));
849 Date d3 = cal.getTime();
850
851 d1 = valCreatedBeforeDate.getDate();
852 d2 = valCreatedBeforeTime.getDate();
853 cal.setTimeInMillis(d1.getTime() + (d2 == null ? 0 : d2.getTime()));
854 Date d4 = cal.getTime();
855
856 query.closedAfterAndCreatedBefore(d3, d4);
857 }
858 }
859
860 public void displayMessageIfInvalid() {
861 if (isValidChangesetQuery()) return;
862 HelpAwareOptionPane.showOptionDialog(
863 this,
864 tr(
865 "<html>Please enter valid date/time values to restrict<br>"
866 + "the query to a specific time range.</html>"
867 ),
868 tr("Invalid date/time values"),
869 JOptionPane.ERROR_MESSAGE,
870 HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidDateTimeValues")
871 );
872 }
873
874
875 public void rememberSettings() {
876 String prefRoot = "changeset-query.advanced.time-restrictions";
877 if (rbClosedAfter.isSelected()) {
878 Main.pref.put(prefRoot + ".query-type", "closed-after");
879 } else if (rbClosedAfterAndCreatedBefore.isSelected()) {
880 Main.pref.put(prefRoot + ".query-type", "closed-after-created-before");
881 }
882 Main.pref.put(prefRoot + ".closed-after.date", tfClosedAfterDate1.getText());
883 Main.pref.put(prefRoot + ".closed-after.time", tfClosedAfterTime1.getText());
884 Main.pref.put(prefRoot + ".closed-created.closed.date", tfClosedAfterDate2.getText());
885 Main.pref.put(prefRoot + ".closed-created.closed.time", tfClosedAfterTime2.getText());
886 Main.pref.put(prefRoot + ".closed-created.created.date", tfCreatedBeforeDate.getText());
887 Main.pref.put(prefRoot + ".closed-created.created.time", tfCreatedBeforeTime.getText());
888 }
889
890 public void restoreFromSettings() {
891 String prefRoot = "changeset-query.advanced.open-restrictions";
892 String v = Main.pref.get(prefRoot + ".query-type", "closed-after");
893 rbClosedAfter.setSelected(v.equals("closed-after"));
894 rbClosedAfterAndCreatedBefore.setSelected(v.equals("closed-after-created-before"));
895 if (!rbClosedAfter.isSelected() && !rbClosedAfterAndCreatedBefore.isSelected()) {
896 rbClosedAfter.setSelected(true);
897 }
898 tfClosedAfterDate1.setText(Main.pref.get(prefRoot + ".closed-after.date", ""));
899 tfClosedAfterTime1.setText(Main.pref.get(prefRoot + ".closed-after.time", ""));
900 tfClosedAfterDate2.setText(Main.pref.get(prefRoot + ".closed-created.closed.date", ""));
901 tfClosedAfterTime2.setText(Main.pref.get(prefRoot + ".closed-created.closed.time", ""));
902 tfCreatedBeforeDate.setText(Main.pref.get(prefRoot + ".closed-created.created.date", ""));
903 tfCreatedBeforeTime.setText(Main.pref.get(prefRoot + ".closed-created.created.time", ""));
904 if (!valClosedAfterDate1.isValid()) {
905 tfClosedAfterDate1.setText("");
906 }
907 if (!valClosedAfterTime1.isValid()) {
908 tfClosedAfterTime1.setText("");
909 }
910 if (!valClosedAfterDate2.isValid()) {
911 tfClosedAfterDate2.setText("");
912 }
913 if (!valClosedAfterTime2.isValid()) {
914 tfClosedAfterTime2.setText("");
915 }
916 if (!valCreatedBeforeDate.isValid()) {
917 tfCreatedBeforeDate.setText("");
918 }
919 if (!valCreatedBeforeTime.isValid()) {
920 tfCreatedBeforeTime.setText("");
921 }
922 }
923 }
924
925 static private class BBoxRestrictionPanel extends BoundingBoxSelectionPanel {
926 public BBoxRestrictionPanel() {
927 setBorder(BorderFactory.createCompoundBorder(
928 BorderFactory.createEmptyBorder(3,3,3,3),
929 BorderFactory.createCompoundBorder(
930 BorderFactory.createLineBorder(Color.GRAY),
931 BorderFactory.createEmptyBorder(5,5,5,5)
932 )
933 ));
934 }
935
936 public boolean isValidChangesetQuery() {
937 return getBoundingBox() != null;
938 }
939
940 public void fillInQuery(ChangesetQuery query) {
941 if (!isValidChangesetQuery())
942 throw new IllegalStateException(tr("Cannot restrict the changeset query to a specific bounding box. The input is invalid."));
943 query.inBbox(getBoundingBox());
944 }
945
946 public void displayMessageIfInvalid() {
947 if (isValidChangesetQuery()) return;
948 HelpAwareOptionPane.showOptionDialog(
949 this,
950 tr(
951 "<html>Please enter valid longitude/latitude values to restrict<br>" +
952 "the changeset query to a specific bounding box.</html>"
953 ),
954 tr("Invalid bounding box"),
955 JOptionPane.ERROR_MESSAGE,
956 HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidBoundingBox")
957 );
958 }
959 }
960
961 /**
962 * Validator for user ids entered in in a {@see JTextComponent}.
963 *
964 */
965 static private class UidInputFieldValidator extends AbstractTextComponentValidator {
966 static public UidInputFieldValidator decorate(JTextComponent tc) {
967 return new UidInputFieldValidator(tc);
968 }
969
970 public UidInputFieldValidator(JTextComponent tc) {
971 super(tc);
972 }
973
974 @Override
975 public boolean isValid() {
976 return getUid() > 0;
977 }
978
979 @Override
980 public void validate() {
981 String value = getComponent().getText();
982 if (value == null || value.trim().length() == 0) {
983 feedbackInvalid("");
984 return;
985 }
986 try {
987 int uid = Integer.parseInt(value);
988 if (uid <= 0) {
989 feedbackInvalid(tr("The current value is not a valid user ID. Please enter an integer value > 0"));
990 return;
991 }
992 } catch(NumberFormatException e) {
993 feedbackInvalid(tr("The current value is not a valid user ID. Please enter an integer value > 0"));
994 return;
995 }
996 feedbackValid(tr("Please enter an integer value > 0"));
997 }
998
999 public int getUid() {
1000 String value = getComponent().getText();
1001 if (value == null || value.trim().length() == 0) return 0;
1002 try {
1003 int uid = Integer.parseInt(value.trim());
1004 if (uid > 0) return uid;
1005 return 0;
1006 } catch(NumberFormatException e) {
1007 return 0;
1008 }
1009 }
1010 }
1011
1012 static private class UserNameInputValidator extends AbstractTextComponentValidator {
1013 static public UserNameInputValidator decorate(JTextComponent tc) {
1014 return new UserNameInputValidator(tc);
1015 }
1016
1017 public UserNameInputValidator(JTextComponent tc) {
1018 super(tc);
1019 }
1020
1021 @Override
1022 public boolean isValid() {
1023 return getComponent().getText().trim().length() > 0;
1024 }
1025
1026 @Override
1027 public void validate() {
1028 String value = getComponent().getText();
1029 if (value.trim().length() == 0) {
1030 feedbackInvalid(tr("<html>The current value is not a valid user name.<br>Please enter an non-empty user name.</html>"));
1031 return;
1032 }
1033 feedbackValid(tr("Please enter an non-empty user name"));
1034 }
1035 }
1036
1037 /**
1038 * Validates dates entered as text in in a {@see JTextComponent}. Validates the input
1039 * on the fly and gives feedback about whether the date is valid or not.
1040 *
1041 * Dates can be entered in one of four standard formats defined for the current locale.
1042 */
1043 static private class DateValidator extends AbstractTextComponentValidator {
1044 static public DateValidator decorate(JTextComponent tc) {
1045 return new DateValidator(tc);
1046 }
1047
1048 public DateValidator(JTextComponent tc) {
1049 super(tc);
1050 }
1051
1052 @Override
1053 public boolean isValid() {
1054 return getDate() != null;
1055 }
1056
1057 public String getStandardTooltipTextAsHtml() {
1058 return "<html>" + getStandardTooltipText() + "</html>";
1059 }
1060
1061 public String getStandardTooltipText() {
1062 return tr(
1063 "Please enter a date in the usual format for your locale.<br>"
1064 + "Example: {0}<br>"
1065 + "Example: {1}<br>"
1066 + "Example: {2}<br>"
1067 + "Example: {3}<br>",
1068 DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()).format(new Date()),
1069 DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()).format(new Date()),
1070 DateFormat.getDateInstance(DateFormat.LONG, Locale.getDefault()).format(new Date()),
1071 DateFormat.getDateInstance(DateFormat.FULL, Locale.getDefault()).format(new Date())
1072 );
1073 }
1074
1075 @Override
1076 public void validate() {
1077 if (!isValid()) {
1078 String msg = "<html>The current value isn't a valid date.<br>" + getStandardTooltipText()+ "</html>";
1079 feedbackInvalid(msg);
1080 return;
1081 } else {
1082 String msg = "<html>" + getStandardTooltipText() + "</html>";
1083 feedbackValid(msg);
1084 }
1085 }
1086
1087 public Date getDate() {
1088 for (int format: new int[] {DateFormat.SHORT, DateFormat.MEDIUM, DateFormat.LONG, DateFormat.FULL}) {
1089 DateFormat df = DateFormat.getDateInstance(format);
1090 try {
1091 return df.parse(getComponent().getText());
1092 } catch (ParseException e) {
1093 // Try next format
1094 }
1095 }
1096 return null;
1097 }
1098 }
1099
1100 /**
1101 * Validates time values entered as text in in a {@see JTextComponent}. Validates the input
1102 * on the fly and gives feedback about whether the time value is valid or not.
1103 *
1104 * Time values can be entered in one of four standard formats defined for the current locale.
1105 */
1106 static private class TimeValidator extends AbstractTextComponentValidator {
1107 static public TimeValidator decorate(JTextComponent tc) {
1108 return new TimeValidator(tc);
1109 }
1110
1111 public TimeValidator(JTextComponent tc) {
1112 super(tc);
1113 }
1114
1115 @Override
1116 public boolean isValid() {
1117 if (getComponent().getText().trim().length() == 0) return true;
1118 return getDate() != null;
1119 }
1120
1121 public String getStandardTooltipTextAsHtml() {
1122 return "<html>" + getStandardTooltipText() + "</html>";
1123 }
1124
1125 public String getStandardTooltipText() {
1126 return tr(
1127 "Please enter a valid time in the usual format for your locale.<br>"
1128 + "Example: {0}<br>"
1129 + "Example: {1}<br>"
1130 + "Example: {2}<br>"
1131 + "Example: {3}<br>",
1132 DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(new Date()),
1133 DateFormat.getTimeInstance(DateFormat.MEDIUM, Locale.getDefault()).format(new Date()),
1134 DateFormat.getTimeInstance(DateFormat.LONG, Locale.getDefault()).format(new Date()),
1135 DateFormat.getTimeInstance(DateFormat.FULL, Locale.getDefault()).format(new Date())
1136 );
1137 }
1138
1139 @Override
1140 public void validate() {
1141
1142 if (!isValid()) {
1143 String msg = "<html>The current value isn't a valid time.<br>" + getStandardTooltipText() + "</html>";
1144 feedbackInvalid(msg);
1145 return;
1146 } else {
1147 String msg = "<html>" + getStandardTooltipText() + "</html>";
1148 feedbackValid(msg);
1149 }
1150 }
1151
1152 public Date getDate() {
1153 if (getComponent().getText().trim().length() == 0)
1154 return null;
1155
1156 for (int i = 0; i< 4; i++) {
1157 try {
1158 DateFormat df = null;
1159 switch(i) {
1160 case 0: df = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()); break;
1161 case 1: df = DateFormat.getTimeInstance(DateFormat.MEDIUM, Locale.getDefault()); break;
1162 case 2: df = DateFormat.getTimeInstance(DateFormat.LONG, Locale.getDefault()); break;
1163 case 3: df = DateFormat.getTimeInstance(DateFormat.FULL,Locale.getDefault()); break;
1164 }
1165 Date d = df.parse(getComponent().getText());
1166 return d;
1167 } catch(ParseException e) {
1168 continue;
1169 }
1170 }
1171 return null;
1172 }
1173 }
1174}
Note: See TracBrowser for help on using the repository browser.