/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/UI/tableview.py
ViewVC logotype

Diff of /branches/WIP-pyshapelib-bramz/Thuban/UI/tableview.py

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 979 by frank, Thu May 22 11:40:59 2003 UTC revision 1276 by jonathan, Fri Jun 20 17:46:34 2003 UTC
# Line 7  Line 7 
7    
8  __version__ = "$Revision$"  __version__ = "$Revision$"
9    
10    import os.path
11    
12  from Thuban import _  from Thuban import _
13    
14  from wxPython.wx import *  from wxPython.wx import *
# Line 14  from wxPython.grid import * Line 16  from wxPython.grid import *
16    
17  from Thuban.Lib.connector import Publisher  from Thuban.Lib.connector import Publisher
18  from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \  from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \
19       FIELDTYPE_STRING       FIELDTYPE_STRING, table_to_dbf, table_to_csv
20  import view  import view
21  from dialogs import NonModalDialog  from dialogs import ThubanFrame
22  from messages import SHAPES_SELECTED  
23    from messages import SHAPES_SELECTED, SESSION_REPLACED
24    from Thuban.Model.messages import TABLE_REMOVED, MAP_LAYERS_REMOVED
25    from Thuban.UI.common import ThubanBeginBusyCursor, ThubanEndBusyCursor
26    
27  wx_value_type_map = {FIELDTYPE_INT: wxGRID_VALUE_NUMBER,  wx_value_type_map = {FIELDTYPE_INT: wxGRID_VALUE_NUMBER,
28                       FIELDTYPE_DOUBLE: wxGRID_VALUE_FLOAT,                       FIELDTYPE_DOUBLE: wxGRID_VALUE_FLOAT,
# Line 98  class DataTable(wxPyGridTableBase): Line 103  class DataTable(wxPyGridTableBase):
103          return self.CanGetValueAs(row, col, typeName)          return self.CanGetValueAs(row, col, typeName)
104    
105    
106    
107    class NullRenderer(wxPyGridCellRenderer):
108    
109        """Renderer that draws NULL as a gray rectangle
110    
111        Other values are delegated to a normal renderer which is given as
112        the parameter to the constructor.
113        """
114    
115        def __init__(self, non_null_renderer):
116            wxPyGridCellRenderer.__init__(self)
117            self.non_null_renderer = non_null_renderer
118    
119        def Draw(self, grid, attr, dc, rect, row, col, isSelected):
120            value = grid.table.GetValue(row, col)
121            if value is None:
122                dc.SetBackgroundMode(wxSOLID)
123                dc.SetBrush(wxBrush(wxColour(192, 192, 192), wxSOLID))
124                dc.SetPen(wxTRANSPARENT_PEN)
125                dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)
126            else:
127                self.non_null_renderer.Draw(grid, attr, dc, rect, row, col,
128                                            isSelected)
129    
130        def GetBestSize(self, grid, attr, dc, row, col):
131            self.non_null_renderer.GetBestSize(grid, attr, dc, row, col)
132    
133        def Clone(self):
134            return NullRenderer(self.non_null_renderer)
135    
136    
137  class TableGrid(wxGrid, Publisher):  class TableGrid(wxGrid, Publisher):
138    
139      """A grid view for a Thuban table      """A grid view for a Thuban table
# Line 115  class TableGrid(wxGrid, Publisher): Line 151  class TableGrid(wxGrid, Publisher):
151    
152          self.allow_messages_count = 0          self.allow_messages_count = 0
153    
154            # keep track of which rows are selected.
155            self.rows = {}
156    
157          self.table = DataTable(table)          self.table = DataTable(table)
158    
159          # The second parameter means that the grid is to take ownership          # The second parameter means that the grid is to take ownership
160          # of the table and will destroy it when done. Otherwise you          # of the table and will destroy it when done. Otherwise you
161          # would need to keep a reference to it and call its Destroy          # would need to keep a reference to it and call its Destroy
162          # method later.          # method later.
163          self.SetTable(self.table, true)          self.SetTable(self.table, True)
164    
165          #self.SetMargins(0,0)          #self.SetMargins(0,0)
166    
# Line 129  class TableGrid(wxGrid, Publisher): Line 168  class TableGrid(wxGrid, Publisher):
168          # column widths automatically but it would cause a traversal of          # column widths automatically but it would cause a traversal of
169          # the entire table which for large .dbf files can take a very          # the entire table which for large .dbf files can take a very
170          # long time.          # long time.
171          #self.AutoSizeColumns(false)          #self.AutoSizeColumns(False)
172    
173          self.SetSelectionMode(wxGrid.wxGridSelectRows)          self.SetSelectionMode(wxGrid.wxGridSelectRows)
174    
         #EVT_GRID_RANGE_SELECT(self, None)  
         #EVT_GRID_SELECT_CELL(self, None)  
         #EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)  
         #EVT_GRID_SELECT_CELL(self, self.OnSelectCell)  
   
175          self.ToggleEventListeners(True)          self.ToggleEventListeners(True)
176            EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)
177            EVT_GRID_SELECT_CELL(self, self.OnSelectCell)
178    
179            # Replace the normal renderers with our own versions which
180            # render NULL/None values specially
181            self.RegisterDataType(wxGRID_VALUE_STRING,
182                                  NullRenderer(wxGridCellStringRenderer()), None)
183            self.RegisterDataType(wxGRID_VALUE_NUMBER,
184                                  NullRenderer(wxGridCellNumberRenderer()), None)
185            self.RegisterDataType(wxGRID_VALUE_FLOAT,
186                                  NullRenderer(wxGridCellFloatRenderer()), None)
187    
188      def SetTableObject(self, table):      def SetTableObject(self, table):
189          self.table.SetTable(table)          self.table.SetTable(table)
190    
191      def OnRangeSelect(self, event):      def OnRangeSelect(self, event):
192          rows = dict([(i, 0) for i in self.GetSelectedRows()])          if self.handleSelectEvents:
193                self.rows = dict([(i, 0) for i in self.GetSelectedRows()])
194    
195          # if we're selecting we need to include the selected range and              # if we're selecting we need to include the selected range and
196          # make sure that the current row is also included, which may              # make sure that the current row is also included, which may
197          # not be the case if you just click on a single row!              # not be the case if you just click on a single row!
198          if event.Selecting():              if event.Selecting():
199              for i in range(event.GetTopRow(), event.GetBottomRow() + 1):                  for i in range(event.GetTopRow(), event.GetBottomRow() + 1):
200                  rows[i] = 0                      self.rows[i] = 0
201              rows[event.GetTopLeftCoords().GetRow()] = 0                  self.rows[event.GetTopLeftCoords().GetRow()] = 0
202        
203                self.issue(ROW_SELECTED, self.rows.keys())
204    
         self.issue(ROW_SELECTED, rows.keys())  
205          event.Skip()          event.Skip()
206    
207      def OnSelectCell(self, event):      def OnSelectCell(self, event):
208          self.issue(ROW_SELECTED, self.GetSelectedRows())          if self.handleSelectEvents:
209                self.issue(ROW_SELECTED, self.GetSelectedRows())
210          event.Skip()          event.Skip()
211    
212      def ToggleEventListeners(self, on):      def ToggleEventListeners(self, on):
213          if on:          self.handleSelectEvents = on
             EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)  
             EVT_GRID_SELECT_CELL(self, self.OnSelectCell)  
         else:  
             EVT_GRID_RANGE_SELECT(self, None)  
             EVT_GRID_SELECT_CELL(self, None)  
214                            
215        def GetNumberSelected(self):
216            return len(self.rows)
217    
218      def disallow_messages(self):      def disallow_messages(self):
219          """Disallow messages to be send.          """Disallow messages to be send.
220    
# Line 211  class LayerTableGrid(TableGrid): Line 257  class LayerTableGrid(TableGrid):
257          nothing. If shape or layer is None also do nothing.          nothing. If shape or layer is None also do nothing.
258          """          """
259          if layer is not None \          if layer is not None \
260              and layer.table is self.table.table:              and layer.ShapeStore().Table() is self.table.table:
261    
262              self.disallow_messages()              self.disallow_messages()
263              try:              try:
# Line 234  class LayerTableGrid(TableGrid): Line 280  class LayerTableGrid(TableGrid):
280                  self.allow_messages()                  self.allow_messages()
281    
282    
283  class TableFrame(NonModalDialog):  class TableFrame(ThubanFrame):
284    
285      """Frame that displays a Thuban table in a grid view"""      """Frame that displays a Thuban table in a grid view"""
286    
287      def __init__(self, parent, name, title, table):      def __init__(self, parent, name, title, table):
288          NonModalDialog.__init__(self, parent, name, title)          ThubanFrame.__init__(self, parent, name, title)
289            self.panel = wxPanel(self, -1)
290    
291          self.table = table          self.table = table
292          self.grid = self.make_grid(self.table)          self.grid = self.make_grid(self.table)
293            self.app = self.parent.application
294            self.app.Subscribe(SESSION_REPLACED, self.close_on_session_replaced)
295            self.session = self.app.Session()
296            self.session.Subscribe(TABLE_REMOVED, self.close_on_table_removed)
297    
298    
299      def make_grid(self, table):      def make_grid(self, table):
300          """Return the table grid to use in the frame.          """Return the table grid to use in the frame.
# Line 251  class TableFrame(NonModalDialog): Line 304  class TableFrame(NonModalDialog):
304          """          """
305          return TableGrid(self, table)          return TableGrid(self, table)
306    
307        def OnClose(self, event):
308            self.app.Unsubscribe(SESSION_REPLACED, self.close_on_session_replaced)
309            self.session.Unsubscribe(TABLE_REMOVED, self.close_on_table_removed)
310            ThubanFrame.OnClose(self, event)
311    
312        def close_on_session_replaced(self, *args):
313            """Subscriber for the SESSION_REPLACED messages.
314    
315            The table frame is tied to a session so close the window when
316            the session changes.
317            """
318            self.Close()
319    
320        def close_on_table_removed(self, table):
321            """Subscriber for the TABLE_REMOVED messages.
322    
323            The table frame is tied to a particular table so close the
324            window when the table is removed.
325            """
326            if table is self.table:
327                self.Close()
328    
329    
330  ID_QUERY = 4001  ID_QUERY = 4001
331  ID_SAVEAS = 4002  ID_EXPORT = 4002
332    ID_COMBOVALUE = 4003
333    
334  class LayerTableFrame(TableFrame):  class QueryTableFrame(TableFrame):
335    
336      """Frame that displays a layer table in a grid view      """Frame that displays a table in a grid view and offers user actions
337        selection and export
338    
339      A LayerTableFrame is TableFrame whose selection is connected to the      A QueryTableFrame is TableFrame whose selection is connected to the
340      selected object in a map.      selected object in a map.
341      """      """
342    
343      def __init__(self, parent, name, title, layer, table):      def __init__(self, parent, name, title, table):
344          TableFrame.__init__(self, parent, name, title, table)          TableFrame.__init__(self, parent, name, title, table)
         self.layer = layer  
         self.grid.Subscribe(ROW_SELECTED, self.rows_selected)  
         self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)  
345    
346          self.combo_fields = wxComboBox(self, -1, style=wxCB_READONLY)          self.combo_fields = wxComboBox(self.panel, -1, style=wxCB_READONLY)
347          self.choice_comp = wxChoice(self, -1,          self.choice_comp = wxChoice(self.panel, -1,
348                                choices=["<", "<=", "==", "!=", ">=", ">"])                                choices=["<", "<=", "==", "!=", ">=", ">"])
349          self.combo_value = wxComboBox(self, -1)          self.combo_value = wxComboBox(self.panel, ID_COMBOVALUE)
350          self.choice_action = wxChoice(self, -1,          self.choice_action = wxChoice(self.panel, -1,
351                                  choices=[_("Replace Selection"),                                  choices=[_("Replace Selection"),
352                                          _("Refine Selection"),                                          _("Refine Selection"),
353                                          _("Add to Selection")])                                          _("Add to Selection")])
354    
355          button_query = wxButton(self, ID_QUERY, _("Query"))          button_query = wxButton(self.panel, ID_QUERY, _("Query"))
356          button_saveas = wxButton(self, ID_SAVEAS, _("Export"))          button_saveas = wxButton(self.panel, ID_EXPORT, _("Export"))
357    
358            self.CreateStatusBar()
359    
360          self.grid.SetSize((400, 200))          self.grid.SetSize((400, 200))
361    
# Line 292  class LayerTableFrame(TableFrame): Line 368  class LayerTableFrame(TableFrame):
368          # assume at least one field?          # assume at least one field?
369          self.combo_fields.SetSelection(0)          self.combo_fields.SetSelection(0)
370          self.combo_value.SetSelection(0)          self.combo_value.SetSelection(0)
371            self.choice_action.SetSelection(0)
372            self.choice_comp.SetSelection(0)
373    
374            self.grid.Reparent(self.panel)
375    
376            self.UpdateStatusText()
377    
378          topBox = wxBoxSizer(wxVERTICAL)          topBox = wxBoxSizer(wxVERTICAL)
379    
380          sizer = wxStaticBoxSizer(wxStaticBox(self, -1, _("Selections")),          sizer = wxStaticBoxSizer(wxStaticBox(self.panel, -1,
381                                      _("Selection")),
382                                    wxHORIZONTAL)                                    wxHORIZONTAL)
383          sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)          sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)
384          sizer.Add(self.choice_comp, 0, wxALL, 4)          sizer.Add(self.choice_comp, 0, wxALL, 4)
# Line 308  class LayerTableFrame(TableFrame): Line 391  class LayerTableFrame(TableFrame):
391          topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)          topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
392          topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)          topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
393    
394            self.panel.SetAutoLayout(True)
395            self.panel.SetSizer(topBox)
396            topBox.Fit(self.panel)
397            topBox.SetSizeHints(self.panel)
398    
399            panelSizer = wxBoxSizer(wxVERTICAL)
400            panelSizer.Add(self.panel, 1, wxEXPAND, 0)
401          self.SetAutoLayout(True)          self.SetAutoLayout(True)
402          self.SetSizer(topBox)          self.SetSizer(panelSizer)
403          topBox.Fit(self)          panelSizer.Fit(self)
404          topBox.SetSizeHints(self)          panelSizer.SetSizeHints(self)
405    
406          self.grid.SetFocus()          self.grid.SetFocus()
407    
408          EVT_BUTTON(self, ID_QUERY, self.OnQuery)          EVT_BUTTON(self, ID_QUERY, self.OnQuery)
409          EVT_BUTTON(self, ID_SAVEAS, self.OnSaveAs)          EVT_BUTTON(self, ID_EXPORT, self.OnSaveAs)
410          EVT_KEY_DOWN(self.grid, self.OnKeyDown)          EVT_KEY_DOWN(self.grid, self.OnKeyDown)
411            EVT_GRID_RANGE_SELECT(self.grid, self.OnGridSelectRange)
412            EVT_GRID_SELECT_CELL(self.grid, self.OnGridSelectCell)
413    
414        def UpdateStatusText(self):
415            self.SetStatusText(_("%i rows (%i selected), %i columns")
416                % (self.grid.GetNumberRows(),
417                   self.grid.GetNumberSelected(),
418                   self.grid.GetNumberCols()))
419    
420        def OnGridSelectRange(self, event):
421            self.UpdateStatusText()
422            event.Skip()
423    
424        def OnGridSelectCell(self, event):
425            self.UpdateStatusText()
426            event.Skip()
427            
428      def OnKeyDown(self, event):      def OnKeyDown(self, event):
429          """Catch query key from grid"""          """Catch query key from grid"""
         print "In OnKeyDown"  
430          if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):          if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
             print "Got the Key!"  
431              self.combo_fields.SetFocus()              self.combo_fields.SetFocus()
432              self.combo_fields.refocus = True              self.combo_fields.refocus = True
433          else:          else:
434              event.Skip()              event.Skip()
435    
   
436      def OnQuery(self, event):      def OnQuery(self, event):
437          wxBeginBusyCursor()          ThubanBeginBusyCursor()
438            try:
         if self.combo_value.GetSelection() < 1:  
             value = self.combo_value.GetValue()  
         else:  
             value = self.table.Column(self.combo_value.GetValue())  
439    
440          ids = self.table.SimpleQuery(              text = self.combo_value.GetValue()
441                  self.table.Column(self.combo_fields.GetStringSelection()),              if self.combo_value.GetSelection() < 1 \
442                  self.choice_comp.GetStringSelection(),                  or self.combo_value.FindString(text) == -1:
443                  value)                  value = text
   
         choice = self.choice_action.GetSelection()  
               
         #  
         # what used to be nice code got became a bit ugly because  
         # each time we select a row a message is sent to the grid  
         # which we are listening for and then we send further  
         # messages.  
         #  
         # now, we disable those listeners select everything but  
         # the first item, reenable the listeners, and select  
         # the first element, which causes everything to be  
         # updated properly.  
         #  
         self.grid.ToggleEventListeners(False)  
   
         if choice == 0:  
             # Replace Selection  
             self.grid.ClearSelection()  
         elif choice == 1:  
             # Refine Selection  
             sel = dict([(i, 0) for i in self.parent.SelectedShapes()])  
             self.grid.ClearSelection()  
             ids = filter(sel.has_key, ids)  
         elif choice == 2:  
             # Add to Selection  
             pass  
   
         #  
         # select the rows (all but the first)  
         #  
         firsttime = True  
         for id in ids:  
             if firsttime:  
                 firsttime = False  
444              else:              else:
445                  self.grid.SelectRow(id, True)                  value = self.table.Column(text)
446    
447          self.grid.ToggleEventListeners(True)              ids = self.table.SimpleQuery(
448                        self.table.Column(self.combo_fields.GetStringSelection()),
449                        self.choice_comp.GetStringSelection(),
450                        value)
451    
452          #              choice = self.choice_action.GetSelection()
453          # select the first row              
454          #              #
455          if ids:              # what used to be nice code got became a bit ugly because
456              self.grid.SelectRow(ids[0], True)              # each time we select a row a message is sent to the grid
457                # which we are listening for and then we send further
458                # messages.
459                #
460                # now, we disable those listeners select everything but
461                # the first item, reenable the listeners, and select
462                # the first element, which causes everything to be
463                # updated properly.
464                #
465                if ids:
466                    self.grid.ToggleEventListeners(False)
467    
468                if choice == 0:
469                    # Replace Selection
470                    self.grid.ClearSelection()
471                elif choice == 1:
472                    # Refine Selection
473                    sel = self.get_selected()
474                    self.grid.ClearSelection()
475                    ids = filter(sel.has_key, ids)
476                elif choice == 2:
477                    # Add to Selection
478                    pass
479    
480                #
481                # select the rows (all but the first)
482                #
483                firsttime = True
484                for id in ids:
485                    if firsttime:
486                        firsttime = False
487                    else:
488                        self.grid.SelectRow(id, True)
489    
490                self.grid.ToggleEventListeners(True)
491    
492                #
493                # select the first row
494                #
495                if ids:
496                    self.grid.SelectRow(ids[0], True)
497    
498          wxEndBusyCursor()          finally:
499                ThubanEndBusyCursor()
500                    
501      def OnSaveAs(self, event):      def OnSaveAs(self, event):
502          dlg = wxFileDialog(self, _("Save Table As"), ".", "",          dlg = wxFileDialog(self, _("Export Table To"), ".", "",
503                             "DBF Files (*.dbf)|*.dbf|" +                             _("DBF Files (*.dbf)|*.dbf|") +
504                             "CSV Files (*.csv)|*.csv|" +                             _("CSV Files (*.csv)|*.csv|") +
505                             "All Files (*.*)|*.*",                             _("All Files (*.*)|*.*"),
506                             wxSAVE|wxOVERWRITE_PROMPT)                             wxSAVE|wxOVERWRITE_PROMPT)
507          if dlg.ShowModal() == wxID_OK:          if dlg.ShowModal() == wxID_OK:
508              pass              filename = dlg.GetPath()
509                                                                                                type = os.path.basename(filename).split('.')[-1:][0]
510          dlg.Destroy()              dlg.Destroy()
511                if type.upper() == "DBF":
512                    table_to_dbf(self.table, filename)
513                elif type.upper() == 'CSV':
514                    table_to_csv(self.table, filename)
515                else:
516                    dlg = wxMessageDialog(None, "Unsupported format: %s" % type,
517                                          "Table Export", wxOK|wxICON_WARNING)
518                    dlg.ShowModal()
519                    dlg.Destroy()
520            else:
521                dlg.Destroy()
522    
523        def OnClose(self, event):
524            TableFrame.OnClose(self, event)
525    
526        def get_selected(self):
527            """Return a dictionary of the selected rows.
528            
529            The dictionary has sthe indexes as keys."""
530            return dict([(i, 0) for i in self.grid.GetSelectedRows()])
531    
532    class LayerTableFrame(QueryTableFrame):
533    
534        """Frame that displays a layer table in a grid view
535    
536        A LayerTableFrame is a QueryTableFrame whose selection is connected to the
537        selected object in a map.
538        """
539    
540        def __init__(self, parent, name, title, layer, table):
541            QueryTableFrame.__init__(self, parent, name, title, table)
542            self.layer = layer
543            self.grid.Subscribe(ROW_SELECTED, self.rows_selected)
544            self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)
545            self.map = self.parent.Map()
546            self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
547    
548            # if there is already a selection present, update the grid
549            # accordingly
550            sel = self.get_selected().keys()
551            for i in sel:
552                self.grid.SelectRow(i, True)
553    
554      def make_grid(self, table):      def make_grid(self, table):
555          """Override the derived method to return a LayerTableGrid.          """Override the derived method to return a LayerTableGrid.
556          """          """
557          return LayerTableGrid(self, table)          return LayerTableGrid(self, table)
558    
559        def get_selected(self):
560            """Override the derived method to return a dictionary of the selected
561            rows.
562            """
563            return dict([(i, 0) for i in self.parent.SelectedShapes()])
564    
565      def OnClose(self, event):      def OnClose(self, event):
566            """Override the derived method to first unsubscribed."""
567          self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)          self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
568          TableFrame.OnClose(self, event)          self.grid.Unsubscribe(ROW_SELECTED, self.rows_selected)
569            self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
570            QueryTableFrame.OnClose(self, event)
571    
572      def select_shapes(self, layer, shapes):      def select_shapes(self, layer, shapes):
573          """Subscribed to the SHAPES_SELECTED message.          """Subscribed to the SHAPES_SELECTED message.
# Line 418  class LayerTableFrame(TableFrame): Line 578  class LayerTableFrame(TableFrame):
578          self.grid.select_shapes(layer, shapes)          self.grid.select_shapes(layer, shapes)
579    
580      def rows_selected(self, rows):      def rows_selected(self, rows):
581            """Return the selected rows of the layer as they are returned
582            by Layer.SelectShapes().
583            """
584          if self.layer is not None:          if self.layer is not None:
585              self.parent.SelectShapes(self.layer, rows)              self.parent.SelectShapes(self.layer, rows)
586    
587        def map_layers_removed(self, *args):
588            """Receiver for the map's MAP_LAYERS_REMOVED message
589    
590            Close the dialog if the layer whose table we're showing is not
591            in the map anymore.
592            """
593            if self.layer not in self.map.Layers():
594                self.Close()
595    

Legend:
Removed from v.979  
changed lines
  Added in v.1276

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26