source: sasview/src/sas/sasgui/perspectives/fitting/fitpanel.py @ 728b291

Last change on this file since 728b291 was 69363c7, checked in by Paul Kienzle <pkienzle@…>, 7 years ago

Merge branch 'master' into ticket-853-fit-gui-to-calc

  • Property mode set to 100644
File size: 22.4 KB
RevLine 
[fa09d62]1"""
2FitPanel class contains fields allowing to fit  models and  data
3
4:note: For Fit to be performed the user should check at least one parameter
5    on fit Panel window.
[2f4b430]6
[fa09d62]7"""
8import wx
9from wx.aui import AuiNotebook as nb
10
[65f3930]11from sas.sascalc.fit.models import ModelManager
12
[d85c194]13from sas.sasgui.guiframe.panel_base import PanelBase
[e6de6b8]14from sas.sasgui.guiframe.events import PanelOnFocusEvent, StatusEvent
[d85c194]15from sas.sasgui.guiframe.dataFitting import check_data_validity
[fa09d62]16
[65f3930]17
18from . import basepage
19from .fitpage import FitPage
20from .simfitpage import SimultaneousFitPage
21from .batchfitpage import BatchFitPage
22from .fitting_widgets import BatchDataDialog
23
[fa09d62]24_BOX_WIDTH = 80
25
[a95ae9a]26
[fa09d62]27class FitPanel(nb, PanelBase):
28    """
29    FitPanel class contains fields allowing to fit  models and  data
[2f4b430]30
[fa09d62]31    :note: For Fit to be performed the user should check at least one parameter
32        on fit Panel window.
[2f4b430]33
[fa09d62]34    """
[a95ae9a]35    # Internal name for the AUI manager
[fa09d62]36    window_name = "Fit panel"
[a95ae9a]37    # Title to appear on top of the window
[fa09d62]38    window_caption = "Fit Panel "
39    CENTER_PANE = True
[2f4b430]40
[fa09d62]41    def __init__(self, parent, manager=None, *args, **kwargs):
42        """
43        """
[6f16e25]44        nb.__init__(self, parent, wx.ID_ANY,
[2f4b430]45                    style=wx.aui.AUI_NB_WINDOWLIST_BUTTON |
46                    wx.aui.AUI_NB_DEFAULT_STYLE |
[fa09d62]47                    wx.CLIP_CHILDREN)
48        PanelBase.__init__(self, parent)
[a95ae9a]49        # self.SetWindowStyleFlag(style=nb.FNB_FANCY_TABS)
[fa09d62]50        self._manager = manager
51        self.parent = parent
52        self.event_owner = None
[a95ae9a]53        # dictionary of miodel {model class name, model class}
[65f3930]54        self.menu_mng = ModelManager()
[fa09d62]55        self.model_list_box = self.menu_mng.get_model_list()
[a95ae9a]56        # pageClosedEvent = nb.EVT_FLATNOTEBOOK_PAGE_CLOSING
[fa09d62]57        self.model_dictionary = self.menu_mng.get_model_dictionary()
58        self.pageClosedEvent = wx.aui.EVT_AUINOTEBOOK_PAGE_CLOSE
[2f4b430]59
[fa09d62]60        self.Bind(self.pageClosedEvent, self.on_close_page)
[a95ae9a]61        # save the title of the last page tab added
[fa09d62]62        self.fit_page_name = {}
[a95ae9a]63        # list of existing fit page
[fa09d62]64        self.opened_pages = {}
[a95ae9a]65        # index of fit page
[fa09d62]66        self.fit_page_index = 0
[a95ae9a]67        # index of batch page
[fa09d62]68        self.batch_page_index = 0
[a95ae9a]69        # page of simultaneous fit
[fa09d62]70        self.sim_page = None
71        self.batch_page = None
[a95ae9a]72        # get the state of a page
[fa09d62]73        self.Bind(basepage.EVT_PAGE_INFO, self._onGetstate)
74        self.Bind(basepage.EVT_PREVIOUS_STATE, self._onUndo)
75        self.Bind(basepage.EVT_NEXT_STATE, self._onRedo)
76        self.Bind(wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED, self.on_page_changing)
77        self.Bind(wx.aui.EVT_AUINOTEBOOK_PAGE_CLOSED, self.on_closed)
[2f4b430]78
[fa09d62]79    def on_closed(self, event):
80        """
81        """
82        if self.GetPageCount() == 0:
83            self.add_empty_page()
84            self.enable_close_button()
[2f4b430]85
[fa09d62]86    def save_project(self, doc=None):
87        """
88        return an xml node containing state of the panel
[a95ae9a]89        that guiframe can write to file
[fa09d62]90        """
[a95ae9a]91        # Iterate through all pages and check for batch fitting
92        batch_state = None
93        if self.sim_page is not None:
94            batch_state = self.sim_page.set_state()
95
[fa09d62]96        for uid, page in self.opened_pages.iteritems():
[a95ae9a]97            data = page.get_data()
98            # state must be cloned
99            state = page.get_state().clone()
[d79feea]100            # data_list only populated with real data
101            # Fake object in data from page.get_data() if model is selected
102            if len(page.data_list) is not 0 and page.model is not None:
[a95ae9a]103                new_doc = self._manager.state_reader.write_toXML(data,
104                                                                 state,
105                                                                 batch_state)
[d79feea]106                # Fit #2 through #n are append to first fit
[a95ae9a]107                if doc is not None and hasattr(doc, "firstChild"):
[d79feea]108                    # Only append if properly formed new_doc
109                    if new_doc is not None and hasattr(new_doc, "firstChild"):
110                        child = new_doc.firstChild.firstChild
111                        doc.firstChild.appendChild(child)
112                # First fit defines the main document
[a95ae9a]113                else:
114                    doc = new_doc
115
[fa09d62]116        return doc
[2f4b430]117
[fa09d62]118    def update_model_list(self):
119        """
120        """
121        temp = self.menu_mng.update()
[277257f]122        if temp:
[fa09d62]123            self.model_list_box = temp
124        return temp
[2f4b430]125
[fa09d62]126    def reset_pmodel_list(self):
127        """
128        """
[277257f]129        self.model_list_box = self.menu_mng.plugins_reset()
130        return self.model_list_box
[2f4b430]131
[fa09d62]132    def get_page_by_id(self, uid):
133        """
134        """
135        if uid not in self.opened_pages:
136            msg = "Fitpanel cannot find ID: %s in self.opened_pages" % str(uid)
137            raise ValueError, msg
138        else:
139            return self.opened_pages[uid]
[2f4b430]140
[fa09d62]141    def on_page_changing(self, event):
142        """
143        calls the function when the current event handler has exited. avoiding
144        to call panel on focus on a panel that is currently deleted
145        """
146        wx.CallAfter(self.helper_on_page_change)
[2f4b430]147
[fa09d62]148    def helper_on_page_change(self):
149        """
150        """
151        pos = self.GetSelection()
152        if pos != -1:
153            selected_page = self.GetPage(pos)
[2f4b430]154            wx.PostEvent(self._manager.parent,
[fa09d62]155                         PanelOnFocusEvent(panel=selected_page))
156        self.enable_close_button()
[2f4b430]157
[fa09d62]158    def on_set_focus(self, event):
159        """
160        """
161        pos = self.GetSelection()
162        if pos != -1:
163            selected_page = self.GetPage(pos)
[2f4b430]164            wx.PostEvent(self._manager.parent,
[fa09d62]165                         PanelOnFocusEvent(panel=selected_page))
[2f4b430]166
[fa09d62]167    def get_data(self):
168        """
169        get the data in the current page
170        """
171        pos = self.GetSelection()
172        if pos != -1:
173            selected_page = self.GetPage(pos)
174            return selected_page.get_data()
[2f4b430]175
[fa09d62]176    def set_model_state(self, state):
177        """
178        receive a state to reset the model in the current page
179        """
180        pos = self.GetSelection()
181        if pos != -1:
182            selected_page = self.GetPage(pos)
183            selected_page.set_model_state(state)
[2f4b430]184
[fa09d62]185    def get_state(self):
186        """
[e6de6b8]187        return the state of the current selected page
[fa09d62]188        """
189        pos = self.GetSelection()
190        if pos != -1:
191            selected_page = self.GetPage(pos)
192            return selected_page.get_state()
[2f4b430]193
[fa09d62]194    def close_all(self):
195        """
196        remove all pages, used when a svs file is opened
197        """
[2f4b430]198
[e6de6b8]199        # use while-loop, for-loop will not do the job well.
[c8e1996]200        while (self.GetPageCount() > 0):
[67b0a99]201            page = self.GetPage(self.GetPageCount() - 1)
[fa09d62]202            if self._manager.parent.panel_on_focus == page:
203                self._manager.parent.panel_on_focus = None
204            self._close_helper(selected_page=page)
[67b0a99]205            self.DeletePage(self.GetPageCount() - 1)
[e6de6b8]206        # Clear list of names
[fa09d62]207        self.fit_page_name = {}
[e6de6b8]208        # Clear list of opened pages
[fa09d62]209        self.opened_pages = {}
[998ca90]210        self.fit_page_index = 0
211        self.batch_page_index = 0
[2f4b430]212
[fa09d62]213    def set_state(self, state):
214        """
215        Restore state of the panel
216        """
217        page_is_opened = False
218        if state is not None:
219            for uid, panel in self.opened_pages.iteritems():
[e6de6b8]220                # Don't return any panel is the exact same page is created
[fa09d62]221                if uid == panel.uid and panel.data == state.data:
222                    # the page is still opened
223                    panel.reset_page(state=state)
224                    panel.save_current_state()
225                    page_is_opened = True
226            if not page_is_opened:
227                if state.data.__class__.__name__ != 'list':
[e6de6b8]228                    # To support older state file format
[fa09d62]229                    list_data = [state.data]
230                else:
[e6de6b8]231                    # Todo: need new file format for the list
[fa09d62]232                    list_data = state.data
233                panel = self._manager.add_fit_page(data=list_data)
234                # add data associated to the page created
235                if panel is not None:
236                    self._manager.store_data(uid=panel.uid,
237                                             data_list=list_data,
238                                             caption=panel.window_caption)
239                    panel.reset_page(state=state)
240                    panel.save_current_state()
[2f4b430]241
[fa09d62]242    def clear_panel(self):
243        """
244        Clear and close all panels, used by guimanager
245        """
[e6de6b8]246        # close all panels only when svs file opened
[fa09d62]247        self.close_all()
[998ca90]248        self.sim_page = None
249        self.batch_page = None
[2f4b430]250
[fa09d62]251    def on_close_page(self, event=None):
252        """
253        close page and remove all references to the closed page
254        """
255        selected_page = self.GetPage(self.GetSelection())
[c8e1996]256        if self.GetPageCount() == 1:
[e6de6b8]257            if selected_page.get_data() is not None:
[fa09d62]258                if event is not None:
259                    event.Veto()
260                return
261        self._close_helper(selected_page=selected_page)
[2f4b430]262
[fa09d62]263    def close_page_with_data(self, deleted_data):
264        """
265        close a fit page when its data is completely remove from the graph
266        """
267        if deleted_data is None:
268            return
269        for index in range(self.GetPageCount()):
270            selected_page = self.GetPage(index)
271            if hasattr(selected_page, "get_data"):
272                data = selected_page.get_data()
[2f4b430]273
[fa09d62]274                if data is None:
[e6de6b8]275                    # the fitpanel exists and only the initial fit page is open
276                    # with no selected data
[fa09d62]277                    return
278                if data.id == deleted_data.id:
279                    self._close_helper(selected_page)
280                    self.DeletePage(index)
281                    break
[2f4b430]282
[fa09d62]283    def set_manager(self, manager):
284        """
285        set panel manager
[2f4b430]286
[fa09d62]287        :param manager: instance of plugin fitting
288        """
289        self._manager = manager
290        for pos in range(self.GetPageCount()):
291            page = self.GetPage(pos)
292            if page is not None:
293                page.set_manager(self._manager)
294
295    def set_model_list(self, dict):
296        """
297        copy a dictionary of model into its own dictionary
[2f4b430]298
[c8e1996]299        :param dict: dictionnary made of model name as key and model class
[ac7be54]300            as value
[fa09d62]301        """
302        self.model_list_box = dict
[2f4b430]303
[277257f]304    def set_model_dictionary(self, model_dictionary):
[fa09d62]305        """
306        copy a dictionary of model name -> model object
307
[277257f]308        :param model_dictionary: dictionary linking model name -> model object
[fa09d62]309        """
310
311    def get_current_page(self):
312        """
313        :return: the current page selected
[2f4b430]314
[fa09d62]315        """
316        return self.GetPage(self.GetSelection())
[2f4b430]317
[fa09d62]318    def add_sim_page(self, caption="Const & Simul Fit"):
319        """
320        Add the simultaneous fit page
321        """
322        page_finder = self._manager.get_page_finder()
323        if caption == "Const & Simul Fit":
324            self.sim_page = SimultaneousFitPage(self, page_finder=page_finder,
[c8e1996]325                                                 id=wx.ID_ANY, batch_on=False)
[fa09d62]326            self.sim_page.window_caption = caption
327            self.sim_page.window_name = caption
328            self.sim_page.uid = wx.NewId()
329            self.AddPage(self.sim_page, caption, True)
330            self.sim_page.set_manager(self._manager)
331            self.enable_close_button()
332            return self.sim_page
333        else:
334            self.batch_page = SimultaneousFitPage(self, batch_on=True,
[e6de6b8]335                                                  page_finder=page_finder)
[fa09d62]336            self.batch_page.window_caption = caption
337            self.batch_page.window_name = caption
338            self.batch_page.uid = wx.NewId()
339            self.AddPage(self.batch_page, caption, True)
340            self.batch_page.set_manager(self._manager)
341            self.enable_close_button()
342            return self.batch_page
[2f4b430]343
[fa09d62]344    def add_empty_page(self):
345        """
346        add an empty page
347        """
348        if self.batch_on:
349            panel = BatchFitPage(parent=self)
350            self.batch_page_index += 1
351            caption = "BatchPage" + str(self.batch_page_index)
352            panel.set_index_model(self.batch_page_index)
353        else:
[a95ae9a]354            # Increment index of fit page
[fa09d62]355            panel = FitPage(parent=self)
356            self.fit_page_index += 1
357            caption = "FitPage" + str(self.fit_page_index)
358            panel.set_index_model(self.fit_page_index)
359        panel.batch_on = self.batch_on
360        panel._set_save_flag(not panel.batch_on)
361        panel.set_model_dictionary(self.model_dictionary)
[277257f]362        panel.populate_box(model_list_box=self.model_list_box)
[fa09d62]363        panel.formfactor_combo_init()
364        panel.set_manager(self._manager)
365        panel.window_caption = caption
366        panel.window_name = caption
367        self.AddPage(panel, caption, select=True)
368        self.opened_pages[panel.uid] = panel
369        self._manager.create_fit_problem(panel.uid)
370        self._manager.page_finder[panel.uid].add_data(panel.get_data())
371        self.enable_close_button()
372        panel.on_set_focus(None)
373        return panel
[2f4b430]374
[fa09d62]375    def enable_close_button(self):
376        """
377        display the close button on tab for more than 1 tabs else remove the
378        close button
379        """
380        if self.GetPageCount() <= 1:
381            style = self.GetWindowStyleFlag()
382            flag = wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
383            if style & wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB == flag:
384                style = style & ~wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
385                self.SetWindowStyle(style)
386        else:
387            style = self.GetWindowStyleFlag()
388            flag = wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
389            if style & wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB != flag:
390                style |= wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
391                self.SetWindowStyle(style)
[2f4b430]392
[fa09d62]393    def delete_data(self, data):
394        """
395        Delete the given data
396        """
397        if data.__class__.__name__ != "list":
398            raise ValueError, "Fitpanel delete_data expect list of id"
399        else:
400            for page in self.opened_pages.values():
401                pos = self.GetPageIndex(page)
402                temp_data = page.get_data()
403                if temp_data is not None and temp_data.id in data:
[dab94f6]404                    self.close_page_with_data(temp_data)
[67b0a99]405            if self.sim_page is not None:
406                if len(self.sim_page.model_list) == 0:
407                    pos = self.GetPageIndex(self.sim_page)
408                    self.SetSelection(pos)
409                    self.on_close_page(event=None)
[dab94f6]410                    self.DeletePage(pos)
[67b0a99]411                    self.sim_page = None
412                    self.batch_on = False
[fa09d62]413            if self.GetPageCount() == 0:
414                self._manager.on_add_new_page(event=None)
[2f4b430]415
[fa09d62]416    def set_data_on_batch_mode(self, data_list):
417        """
418        Add all data to a single tab when the application is on Batch mode.
[2f4b430]419        However all data in the set of data must be either 1D or 2D type.
420        This method presents option to select the data type before creating a
[fa09d62]421        tab.
422        """
423        data_1d_list = []
424        data_2d_list = []
425        group_id_1d = wx.NewId()
426        # separate data into data1d and data2d list
427        for data in data_list:
428            if data.__class__.__name__ == "Data1D":
429                data.group_id = group_id_1d
430                data_1d_list.append(data)
431            if data.__class__.__name__ == "Data2D":
432                data.group_id = wx.NewId()
433                data_2d_list.append(data)
434        page = None
435        for p in self.opened_pages.values():
[e6de6b8]436            # check if there is an empty page to fill up
[fa09d62]437            if not check_data_validity(p.get_data()) and p.batch_on:
[2f4b430]438
[e6de6b8]439                # make sure data get placed in 1D empty tab if data is 1D
440                # else data get place on 2D tab empty tab
[fa09d62]441                enable2D = p.get_view_mode()
442                if (data.__class__.__name__ == "Data2D" and enable2D)\
443                or (data.__class__.__name__ == "Data1D" and not enable2D):
444                    page = p
445                    break
446        if data_1d_list and data_2d_list:
447            # need to warning the user that this batch is a special case
448            dlg = BatchDataDialog(self)
449            if dlg.ShowModal() == wx.ID_OK:
450                data_type = dlg.get_data()
451                dlg.Destroy()
[e6de6b8]452                if page is None:
[fa09d62]453                    page = self.add_empty_page()
454                if data_type == 1:
[e6de6b8]455                    # user has selected only data1D
[fa09d62]456                    page.fill_data_combobox(data_1d_list)
457                elif data_type == 2:
458                    page.fill_data_combobox(data_2d_list)
459            else:
[e6de6b8]460                # the batch analysis is canceled
[fa09d62]461                dlg.Destroy()
462                return None
463        else:
464            if page is None:
465                page = self.add_empty_page()
466            if data_1d_list and not data_2d_list:
[c8e1996]467                # only on type of data
[fa09d62]468                page.fill_data_combobox(data_1d_list)
469            elif not data_1d_list and data_2d_list:
470                page.fill_data_combobox(data_2d_list)
[2f4b430]471
[fa09d62]472        pos = self.GetPageIndex(page)
473        page.batch_on = self.batch_on
474        page._set_save_flag(not page.batch_on)
475        self.SetSelection(pos)
476        self.opened_pages[page.uid] = page
477        return page
[2f4b430]478
[fa09d62]479    def set_data(self, data_list):
480        """
481        Add a fitting page on the notebook contained by fitpanel
[2f4b430]482
[c8e1996]483        :param data_list: data to fit
[2f4b430]484
[fa09d62]485        :return panel : page just added for further used.
486        is used by fitting module
[2f4b430]487
[fa09d62]488        """
489        if not data_list:
490            return None
491        if self.batch_on:
492            return self.set_data_on_batch_mode(data_list)
493        else:
494            data = None
495            try:
496                data = data_list[0]
[e6de6b8]497            except Exception:
[fa09d62]498                # for 'fitv' files
499                data_list = [data]
500                data = data_list[0]
[2f4b430]501
[fa09d62]502            if data is None:
503                return None
[6f9abd3]504        focused_page = self.GetPage(self.GetSelection())
[fa09d62]505        for page in self.opened_pages.values():
[e6de6b8]506            # check if the selected data existing in the fitpanel
[fa09d62]507            pos = self.GetPageIndex(page)
508            if not check_data_validity(page.get_data()) and not page.batch_on:
[6f9abd3]509                if page.model is not None and page != focused_page:
510                    # Page has an active theory and is in background - don't
511                    # send data here.
512                    continue
[e6de6b8]513                # make sure data get placed in 1D empty tab if data is 1D
514                # else data get place on 2D tab empty tab
[fa09d62]515                enable2D = page.get_view_mode()
516                if (data.__class__.__name__ == "Data2D" and enable2D)\
[e6de6b8]517                   or (data.__class__.__name__ == "Data1D" and not enable2D):
[fa09d62]518                    page.batch_on = self.batch_on
519                    page._set_save_flag(not page.batch_on)
520                    page.fill_data_combobox(data_list)
[e6de6b8]521                    # caption = "FitPage" + str(self.fit_page_index)
[fa09d62]522                    self.SetPageText(pos, page.window_caption)
523                    self.SetSelection(pos)
524                    return page
[a95ae9a]525        # create new page and add data
[fa09d62]526        page = self.add_empty_page()
527        pos = self.GetPageIndex(page)
528        page.fill_data_combobox(data_list)
529        self.opened_pages[page.uid] = page
530        self.SetSelection(pos)
531        return page
[2f4b430]532
[fa09d62]533    def _onGetstate(self, event):
534        """
535        copy the state of a page
536        """
537        page = event.page
538        if page.uid in self.fit_page_name:
539            self.fit_page_name[page.uid].appendItem(page.createMemento())
[2f4b430]540
[fa09d62]541    def _onUndo(self, event):
542        """
543        return the previous state of a given page is available
544        """
545        page = event.page
546        if page.uid in self.fit_page_name:
547            if self.fit_page_name[page.uid].getCurrentPosition() == 0:
548                state = None
549            else:
550                state = self.fit_page_name[page.uid].getPreviousItem()
551                page._redo.Enable(True)
552            page.reset_page(state)
[2f4b430]553
[fa09d62]554    def _onRedo(self, event):
555        """
556        return the next state available
557        """
558        page = event.page
559        if page.uid in self.fit_page_name:
560            length = len(self.fit_page_name[page.uid])
561            if self.fit_page_name[page.uid].getCurrentPosition() == length - 1:
562                state = None
563                page._redo.Enable(False)
564                page._redo.Enable(True)
565            else:
566                state = self.fit_page_name[page.uid].getNextItem()
567            page.reset_page(state)
[2f4b430]568
[fa09d62]569    def _close_helper(self, selected_page):
570        """
571        Delete the given page from the notebook
572        """
[e6de6b8]573        # remove hint page
574        # if selected_page == self.hint_page:
[fa09d62]575        #    return
[e6de6b8]576        # removing sim_page
[fa09d62]577        if selected_page == self.sim_page:
578            self._manager.sim_page = None
579            return
580        if selected_page == self.batch_page:
581            self._manager.batch_page = None
582            return
[e6de6b8]583        # closing other pages
[fa09d62]584        state = selected_page.createMemento()
585        page_finder = self._manager.get_page_finder()
[e6de6b8]586        # removing fit page
[fa09d62]587        data = selected_page.get_data()
[e6de6b8]588        # Don' t remove plot for 2D
[fa09d62]589        flag = True
590        if data.__class__.__name__ == 'Data2D':
591            flag = False
592        if selected_page in page_finder:
[e6de6b8]593            # Delete the name of the page into the list of open page
[fa09d62]594            for uid, list in self.opened_pages.iteritems():
[e6de6b8]595                # Don't return any panel is the exact same page is created
[fa09d62]596                if flag and selected_page.uid == uid:
597                    self._manager.remove_plot(uid, theory=False)
598                    break
599            del page_finder[selected_page]
[2f4b430]600
[e6de6b8]601        # Delete the name of the page into the list of open page
[fa09d62]602        for uid, list in self.opened_pages.iteritems():
[e6de6b8]603            # Don't return any panel is the exact same page is created
[fa09d62]604            if selected_page.uid == uid:
605                del self.opened_pages[selected_page.uid]
606                break
[e6de6b8]607        # remove the check box link to the model name of the selected_page
[fa09d62]608        try:
609            self.sim_page.draw_page()
610        except:
[e6de6b8]611            # that page is already deleted no need to remove check box on
612            # non existing page
[fa09d62]613            pass
614        try:
615            self.batch_page.draw_page()
616        except:
[e6de6b8]617            # that page is already deleted no need to remove check box on
618            # non existing page
[fa09d62]619            pass
Note: See TracBrowser for help on using the repository browser.