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

Last change on this file since 5889 was 5889, checked in by Don-vip, 6 years ago

fix potential bugs detected by FindBugs

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