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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1199 - (hide annotations)
Fri Jun 13 15:04:28 2003 UTC (21 years, 8 months ago) by jonathan
Original Path: trunk/thuban/Thuban/UI/tableview.py
File MIME type: text/x-python
File size: 19748 byte(s)
(TableGrid.__init__): Create an
        instance variable to keep track of how many rows are selected.
        Subscribe once to the the events we are interested in.
(ThubanGrid.OnRangeSelect): Only handle event if event handling
        hasn't been turned off.
(ThubanGrid.OnSelectCell): Only handle event if event handling
        hasn't been turned off.
(ThubanGrid.ToggleEventListeners): Rather than subscribe None
        as an event listener (which changes the event handler stack)
        simply set an instance variable to False. This is checked in
        the event handlers.
(ThubanGrid.GetNumberSelected): Return the number of currently
        selected rows.
(TableFrame): Inherit from ThubanFrame so we can have a
        status bar and control buttons.
(QueryTableFrame.__init__): Create a status bar. Fixes RTbug #1942.
        Explicitly set which items are selected in the operator choice and
        action choice so there is always a valid selection. Fixes RTbug #1941.
        Subscribe to grid cell selection events so we can update the
        status bar.
(QueryTableFrame.UpdateStatusText): Update the status bar with
        how many rows are in the grid, how many columns, and how many
        rows are selected.
(QueryTableFrame.OnGridSelectRange, QueryTableFrame.OnGridSelectCell):
        Call UpdateStatusText when cells are (de)selected.
(QueryTableFrame.OnQuery): Use the string value in the value
        combo if either the selected item index is 0 or if the string
        cannot be found in the predefined list (this happens if the
        user changes the text). Fixes RTbug #1940.
        Only turn off the grid event listeners if there a query comes
        back with a none empty list of ids. in the case that the list
        is empty this causes a grid.ClearSelection() call to actually
        clear the grid selection which causes the selected items in
        the map to be deselected. Fixes RTbug #1939.

1 bh 535 # Copyright (c) 2001, 2002, 2003 by Intevation GmbH
2 bh 6 # Authors:
3     # Bernhard Herzog <[email protected]>
4     #
5     # This program is free software under the GPL (>=v2)
6     # Read the file COPYING coming with Thuban for details.
7    
8     __version__ = "$Revision$"
9    
10 frank 1027 import os.path
11    
12 jonathan 881 from Thuban import _
13    
14 bh 6 from wxPython.wx import *
15     from wxPython.grid import *
16    
17 bh 34 from Thuban.Lib.connector import Publisher
18 bh 6 from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \
19 frank 1027 FIELDTYPE_STRING, table_to_dbf, table_to_csv
20 bh 6 import view
21 jonathan 1199 from dialogs import ThubanFrame
22 bh 6
23 bh 1068 from messages import SHAPES_SELECTED, SESSION_REPLACED
24 bh 1146 from Thuban.Model.messages import TABLE_REMOVED, MAP_LAYERS_REMOVED
25 bh 1068
26 bh 6 wx_value_type_map = {FIELDTYPE_INT: wxGRID_VALUE_NUMBER,
27     FIELDTYPE_DOUBLE: wxGRID_VALUE_FLOAT,
28     FIELDTYPE_STRING: wxGRID_VALUE_STRING}
29    
30 bh 34 ROW_SELECTED = "ROW_SELECTED"
31    
32 frank 979 QUERY_KEY = 'S'
33 bh 34
34 bh 6 class DataTable(wxPyGridTableBase):
35    
36 bh 34 """Wrapper around a Thuban table object suitable for a wxGrid"""
37    
38 bh 6 def __init__(self, table = None):
39     wxPyGridTableBase.__init__(self)
40     self.num_cols = 0
41     self.num_rows = 0
42     self.columns = []
43     self.table = None
44     self.SetTable(table)
45    
46     def SetTable(self, table):
47     self.table = table
48 bh 838 self.num_cols = table.NumColumns()
49     self.num_rows = table.NumRows()
50 bh 6
51     self.columns = []
52     for i in range(self.num_cols):
53 bh 838 col = table.Column(i)
54     self.columns.append((col.name, wx_value_type_map[col.type]))
55 bh 6
56     #
57     # required methods for the wxPyGridTableBase interface
58     #
59    
60     def GetNumberRows(self):
61     return self.num_rows
62    
63     def GetNumberCols(self):
64     return self.num_cols
65    
66     def IsEmptyCell(self, row, col):
67     return 0
68    
69     # Get/Set values in the table. The Python version of these
70     # methods can handle any data-type, (as long as the Editor and
71     # Renderer understands the type too,) not just strings as in the
72     # C++ version.
73     def GetValue(self, row, col):
74 bh 838 record = self.table.ReadRowAsDict(row)
75 bh 6 return record[self.columns[col][0]]
76    
77     def SetValue(self, row, col, value):
78     pass
79    
80     #
81     # Some optional methods
82     #
83    
84     # Called when the grid needs to display labels
85     def GetColLabelValue(self, col):
86     return self.columns[col][0]
87    
88     # Called to determine the kind of editor/renderer to use by
89     # default, doesn't necessarily have to be the same type used
90     # nativly by the editor/renderer if they know how to convert.
91     def GetTypeName(self, row, col):
92     return self.columns[col][1]
93    
94     # Called to determine how the data can be fetched and stored by the
95     # editor and renderer. This allows you to enforce some type-safety
96     # in the grid.
97     def CanGetValueAs(self, row, col, typeName):
98     # perhaps we should allow conversion int->double?
99     return self.GetTypeName(row, col) == typeName
100    
101     def CanSetValueAs(self, row, col, typeName):
102     return self.CanGetValueAs(row, col, typeName)
103    
104    
105 bh 1092
106     class NullRenderer(wxPyGridCellRenderer):
107    
108     """Renderer that draws NULL as a gray rectangle
109    
110     Other values are delegated to a normal renderer which is given as
111     the parameter to the constructor.
112     """
113    
114     def __init__(self, non_null_renderer):
115     wxPyGridCellRenderer.__init__(self)
116     self.non_null_renderer = non_null_renderer
117    
118     def Draw(self, grid, attr, dc, rect, row, col, isSelected):
119     value = grid.table.GetValue(row, col)
120     if value is None:
121     dc.SetBackgroundMode(wxSOLID)
122     dc.SetBrush(wxBrush(wxColour(192, 192, 192), wxSOLID))
123     dc.SetPen(wxTRANSPARENT_PEN)
124     dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)
125     else:
126     self.non_null_renderer.Draw(grid, attr, dc, rect, row, col,
127     isSelected)
128    
129     def GetBestSize(self, grid, attr, dc, row, col):
130     self.non_null_renderer.GetBestSize(grid, attr, dc, row, col)
131    
132     def Clone(self):
133     return NullRenderer(self.non_null_renderer)
134    
135    
136 bh 34 class TableGrid(wxGrid, Publisher):
137 bh 6
138 bh 808 """A grid view for a Thuban table
139 bh 6
140 bh 808 When rows are selected by the user the table issues ROW_SELECTED
141     messages. wx sends selection events even when the selection is
142     manipulated by code (instead of by the user) which usually lead to
143     ROW_SELECTED messages being sent in turn. Therefore sending messages
144     can be switched on and off with the allow_messages and
145     disallow_messages methods.
146     """
147    
148 bh 6 def __init__(self, parent, table = None):
149     wxGrid.__init__(self, parent, -1)
150    
151 bh 808 self.allow_messages_count = 0
152    
153 jonathan 1199 # keep track of which rows are selected.
154     self.rows = {}
155    
156 bh 6 self.table = DataTable(table)
157    
158     # The second parameter means that the grid is to take ownership
159     # of the table and will destroy it when done. Otherwise you
160 bh 77 # would need to keep a reference to it and call its Destroy
161 bh 6 # method later.
162 jan 1035 self.SetTable(self.table, True)
163 bh 6
164     #self.SetMargins(0,0)
165    
166 bh 173 # AutoSizeColumns would allow us to make the grid have optimal
167     # column widths automatically but it would cause a traversal of
168     # the entire table which for large .dbf files can take a very
169     # long time.
170 jan 1035 #self.AutoSizeColumns(False)
171 bh 173
172 bh 6 self.SetSelectionMode(wxGrid.wxGridSelectRows)
173 bh 278
174 jonathan 966 self.ToggleEventListeners(True)
175 jonathan 1199 EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)
176     EVT_GRID_SELECT_CELL(self, self.OnSelectCell)
177 jonathan 966
178 bh 1092 # Replace the normal renderers with our own versions which
179     # render NULL/None values specially
180     self.RegisterDataType(wxGRID_VALUE_STRING,
181     NullRenderer(wxGridCellStringRenderer()), None)
182     self.RegisterDataType(wxGRID_VALUE_NUMBER,
183     NullRenderer(wxGridCellNumberRenderer()), None)
184     self.RegisterDataType(wxGRID_VALUE_FLOAT,
185     NullRenderer(wxGridCellFloatRenderer()), None)
186    
187 bh 6 def SetTableObject(self, table):
188     self.table.SetTable(table)
189    
190     def OnRangeSelect(self, event):
191 jonathan 1199 if self.handleSelectEvents:
192     self.rows = dict([(i, 0) for i in self.GetSelectedRows()])
193 jonathan 881
194 jonathan 1199 # if we're selecting we need to include the selected range and
195     # make sure that the current row is also included, which may
196     # not be the case if you just click on a single row!
197     if event.Selecting():
198     for i in range(event.GetTopRow(), event.GetBottomRow() + 1):
199     self.rows[i] = 0
200     self.rows[event.GetTopLeftCoords().GetRow()] = 0
201    
202     self.issue(ROW_SELECTED, self.rows.keys())
203 bh 6
204 jonathan 881 event.Skip()
205    
206 bh 6 def OnSelectCell(self, event):
207 jonathan 1199 if self.handleSelectEvents:
208     self.issue(ROW_SELECTED, self.GetSelectedRows())
209 jonathan 881 event.Skip()
210 bh 6
211 jonathan 966 def ToggleEventListeners(self, on):
212 jonathan 1199 self.handleSelectEvents = on
213 jonathan 966
214 jonathan 1199 def GetNumberSelected(self):
215     return len(self.rows)
216    
217 bh 808 def disallow_messages(self):
218     """Disallow messages to be send.
219 bh 278
220 bh 808 This method only increases a counter so that calls to
221     disallow_messages and allow_messages can be nested. Only the
222     outermost calls will actually switch message sending on and off.
223     """
224     self.allow_messages_count += 1
225    
226     def allow_messages(self):
227     """Allow messages to be send.
228    
229     This method only decreases a counter so that calls to
230     disallow_messages and allow_messages can be nested. Only the
231     outermost calls will actually switch message sending on and off.
232     """
233     self.allow_messages_count -= 1
234    
235     def issue(self, *args):
236     """Issue a message unless disallowed.
237    
238     See the allow_messages and disallow_messages methods.
239     """
240     if self.allow_messages_count == 0:
241     Publisher.issue(self, *args)
242    
243    
244 bh 278 class LayerTableGrid(TableGrid):
245    
246     """Table grid for the layer tables.
247    
248     The LayerTableGrid is basically the same as a TableGrid but it's
249     selection is usually coupled to the selected object in the map.
250     """
251    
252 jonathan 881 def select_shapes(self, layer, shapes):
253 bh 278 """Select the row corresponding to the specified shape and layer
254    
255     If layer is not the layer the table is associated with do
256     nothing. If shape or layer is None also do nothing.
257     """
258 jonathan 881 if layer is not None \
259     and layer.table is self.table.table:
260    
261 bh 808 self.disallow_messages()
262     try:
263 jonathan 881 self.ClearSelection()
264     if len(shapes) > 0:
265     #
266     # keep track of the lowest id so we can make it
267     # the first visible item
268     #
269     first = shapes[0]
270    
271     for shape in shapes:
272     self.SelectRow(shape, True)
273     if shape < first:
274     first = shape
275    
276     self.SetGridCursor(first, 0)
277     self.MakeCellVisible(first, 0)
278 bh 808 finally:
279     self.allow_messages()
280 bh 6
281    
282 jonathan 1199 class TableFrame(ThubanFrame):
283 bh 6
284 bh 34 """Frame that displays a Thuban table in a grid view"""
285    
286 bh 535 def __init__(self, parent, name, title, table):
287 jonathan 1199 ThubanFrame.__init__(self, parent, name, title)
288 bh 278 self.table = table
289     self.grid = self.make_grid(self.table)
290 bh 1068 self.app = self.parent.application
291     self.app.Subscribe(SESSION_REPLACED, self.close_on_session_replaced)
292     self.session = self.app.Session()
293     self.session.Subscribe(TABLE_REMOVED, self.close_on_table_removed)
294 bh 278
295     def make_grid(self, table):
296     """Return the table grid to use in the frame.
297    
298     The default implementation returns a TableGrid instance.
299     Override in derived classes to use different grid classes.
300     """
301     return TableGrid(self, table)
302    
303 bh 1068 def OnClose(self, event):
304     self.app.Unsubscribe(SESSION_REPLACED, self.close_on_session_replaced)
305     self.session.Unsubscribe(TABLE_REMOVED, self.close_on_table_removed)
306 jonathan 1199 ThubanFrame.OnClose(self, event)
307 bh 278
308 bh 1068 def close_on_session_replaced(self, *args):
309     """Subscriber for the SESSION_REPLACED messages.
310    
311     The table frame is tied to a session so close the window when
312     the session changes.
313     """
314     self.Close()
315    
316     def close_on_table_removed(self, table):
317     """Subscriber for the TABLE_REMOVED messages.
318    
319     The table frame is tied to a particular table so close the
320     window when the table is removed.
321     """
322     if table is self.table:
323     self.Close()
324    
325    
326 jonathan 881 ID_QUERY = 4001
327 jan 1013 ID_EXPORT = 4002
328 jonathan 1199 ID_COMBOVALUE = 4003
329 jonathan 881
330 jan 1013 class QueryTableFrame(TableFrame):
331 bh 278
332 jan 1013 """Frame that displays a table in a grid view and offers user actions
333     selection and export
334 bh 278
335 jan 1096 A QueryTableFrame is TableFrame whose selection is connected to the
336 bh 278 selected object in a map.
337     """
338    
339 jan 1013 def __init__(self, parent, name, title, table):
340 bh 535 TableFrame.__init__(self, parent, name, title, table)
341 bh 30
342 jonathan 881 self.combo_fields = wxComboBox(self, -1, style=wxCB_READONLY)
343     self.choice_comp = wxChoice(self, -1,
344 jonathan 939 choices=["<", "<=", "==", "!=", ">=", ">"])
345 jonathan 1199 self.combo_value = wxComboBox(self, ID_COMBOVALUE)
346 jonathan 881 self.choice_action = wxChoice(self, -1,
347     choices=[_("Replace Selection"),
348     _("Refine Selection"),
349     _("Add to Selection")])
350    
351     button_query = wxButton(self, ID_QUERY, _("Query"))
352 jan 1013 button_saveas = wxButton(self, ID_EXPORT, _("Export"))
353 jonathan 881
354 jonathan 1199 self.CreateStatusBar()
355     self.SetStatusText(_("0 rows (0 selected), 0 columns"))
356    
357 jonathan 881 self.grid.SetSize((400, 200))
358    
359     self.combo_value.Append("")
360 jonathan 939 for i in range(table.NumColumns()):
361     name = table.Column(i).name
362 jonathan 881 self.combo_fields.Append(name)
363     self.combo_value.Append(name)
364 jonathan 939
365 jonathan 881 # assume at least one field?
366     self.combo_fields.SetSelection(0)
367     self.combo_value.SetSelection(0)
368 jonathan 1199 self.choice_action.SetSelection(0)
369     self.choice_comp.SetSelection(0)
370 jonathan 881
371     topBox = wxBoxSizer(wxVERTICAL)
372    
373 frank 1045 sizer = wxStaticBoxSizer(wxStaticBox(self, -1,
374 frank 1066 _("Selection")),
375 jonathan 881 wxHORIZONTAL)
376     sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)
377     sizer.Add(self.choice_comp, 0, wxALL, 4)
378     sizer.Add(self.combo_value, 1, wxEXPAND|wxALL, 4)
379     sizer.Add(self.choice_action, 0, wxALL, 4)
380 frank 979 sizer.Add(button_query, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
381 jonathan 881 sizer.Add(40, 20, 0, wxALL, 4)
382 frank 979 sizer.Add(button_saveas, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
383 jonathan 881
384     topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
385     topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
386    
387     self.SetAutoLayout(True)
388     self.SetSizer(topBox)
389     topBox.Fit(self)
390     topBox.SetSizeHints(self)
391    
392 frank 979 self.grid.SetFocus()
393 jonathan 881 EVT_BUTTON(self, ID_QUERY, self.OnQuery)
394 jan 1013 EVT_BUTTON(self, ID_EXPORT, self.OnSaveAs)
395 frank 979 EVT_KEY_DOWN(self.grid, self.OnKeyDown)
396 jonathan 1199 EVT_GRID_RANGE_SELECT(self.grid, self.OnGridSelectRange)
397     EVT_GRID_SELECT_CELL(self.grid, self.OnGridSelectCell)
398 jonathan 881
399 jonathan 1199 def UpdateStatusText(self):
400     self.SetStatusText(_("%i rows (%i selected), %i columns")
401     % (self.grid.GetNumberRows(),
402     self.grid.GetNumberSelected(),
403     self.grid.GetNumberCols()))
404    
405     def OnGridSelectRange(self, event):
406     self.UpdateStatusText()
407     event.Skip()
408    
409     def OnGridSelectCell(self, event):
410     self.UpdateStatusText()
411     event.Skip()
412    
413 frank 979 def OnKeyDown(self, event):
414     """Catch query key from grid"""
415     if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
416     self.combo_fields.SetFocus()
417     self.combo_fields.refocus = True
418     else:
419     event.Skip()
420    
421 jonathan 881 def OnQuery(self, event):
422     wxBeginBusyCursor()
423 jonathan 1104 try:
424 jonathan 881
425 jonathan 1199 text = self.combo_value.GetValue()
426     if self.combo_value.GetSelection() < 1 \
427     or self.combo_value.FindString(text) == -1:
428     value = text
429 jonathan 1104 else:
430 jonathan 1199 value = self.table.Column(text)
431 jonathan 881
432 jonathan 1104 ids = self.table.SimpleQuery(
433     self.table.Column(self.combo_fields.GetStringSelection()),
434     self.choice_comp.GetStringSelection(),
435     value)
436 jonathan 881
437 jonathan 1104 choice = self.choice_action.GetSelection()
438 jonathan 881
439 jonathan 1104 #
440     # what used to be nice code got became a bit ugly because
441     # each time we select a row a message is sent to the grid
442     # which we are listening for and then we send further
443     # messages.
444     #
445     # now, we disable those listeners select everything but
446     # the first item, reenable the listeners, and select
447     # the first element, which causes everything to be
448     # updated properly.
449     #
450 jonathan 1199 if ids:
451     self.grid.ToggleEventListeners(False)
452 jonathan 966
453 jonathan 1104 if choice == 0:
454     # Replace Selection
455     self.grid.ClearSelection()
456     elif choice == 1:
457     # Refine Selection
458     sel = self.get_selected()
459     self.grid.ClearSelection()
460     ids = filter(sel.has_key, ids)
461     elif choice == 2:
462     # Add to Selection
463     pass
464 jonathan 966
465 jonathan 1104 #
466     # select the rows (all but the first)
467     #
468     firsttime = True
469     for id in ids:
470     if firsttime:
471     firsttime = False
472     else:
473     self.grid.SelectRow(id, True)
474 jonathan 881
475 jonathan 1104 self.grid.ToggleEventListeners(True)
476 jonathan 966
477 jonathan 1104 #
478     # select the first row
479     #
480     if ids:
481     self.grid.SelectRow(ids[0], True)
482 jonathan 1199
483 jonathan 1104 finally:
484     wxEndBusyCursor()
485 jonathan 881
486     def OnSaveAs(self, event):
487 jan 1013 dlg = wxFileDialog(self, _("Export Table To"), ".", "",
488     _("DBF Files (*.dbf)|*.dbf|") +
489     _("CSV Files (*.csv)|*.csv|") +
490     _("All Files (*.*)|*.*"),
491 jonathan 881 wxSAVE|wxOVERWRITE_PROMPT)
492     if dlg.ShowModal() == wxID_OK:
493 frank 1027 filename = dlg.GetPath()
494     type = os.path.basename(filename).split('.')[-1:][0]
495     dlg.Destroy()
496     if type.upper() == "DBF":
497     table_to_dbf(self.table, filename)
498     elif type.upper() == 'CSV':
499     table_to_csv(self.table, filename)
500     else:
501     dlg = wxMessageDialog(None, "Unsupported format: %s" % type,
502     "Table Export", wxOK|wxICON_WARNING)
503     dlg.ShowModal()
504     dlg.Destroy()
505     else:
506     dlg.Destroy()
507 jonathan 881
508 jan 1013 def OnClose(self, event):
509     TableFrame.OnClose(self, event)
510    
511     def get_selected(self):
512     """Return a dictionary of the selected rows.
513    
514     The dictionary has sthe indexes as keys."""
515     return dict([(i, 0) for i in self.grid.GetSelectedRows()])
516    
517     class LayerTableFrame(QueryTableFrame):
518    
519     """Frame that displays a layer table in a grid view
520    
521     A LayerTableFrame is a QueryTableFrame whose selection is connected to the
522     selected object in a map.
523     """
524    
525     def __init__(self, parent, name, title, layer, table):
526     QueryTableFrame.__init__(self, parent, name, title, table)
527     self.layer = layer
528     self.grid.Subscribe(ROW_SELECTED, self.rows_selected)
529     self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)
530 bh 1146 self.map = self.parent.Map()
531     self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
532 jan 1013
533 jan 1032 # if there is already a selection present, update the grid
534     # accordingly
535     sel = self.get_selected().keys()
536     for i in sel:
537     self.grid.SelectRow(i, True)
538    
539 bh 278 def make_grid(self, table):
540     """Override the derived method to return a LayerTableGrid.
541     """
542     return LayerTableGrid(self, table)
543    
544 jan 1013 def get_selected(self):
545     """Override the derived method to return a dictionary of the selected
546     rows.
547     """
548     return dict([(i, 0) for i in self.parent.SelectedShapes()])
549    
550 bh 30 def OnClose(self, event):
551 jan 1013 """Override the derived method to first unsubscribed."""
552 jonathan 881 self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
553 jan 1096 self.grid.Unsubscribe(ROW_SELECTED, self.rows_selected)
554 bh 1146 self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
555 jan 1013 QueryTableFrame.OnClose(self, event)
556 bh 30
557 jonathan 881 def select_shapes(self, layer, shapes):
558 bh 535 """Subscribed to the SHAPES_SELECTED message.
559    
560     If shapes contains exactly one shape id, select that shape in
561     the grid. Otherwise deselect all.
562     """
563 jonathan 881 self.grid.select_shapes(layer, shapes)
564 bh 34
565 jonathan 881 def rows_selected(self, rows):
566 jan 1013 """Return the selected rows of the layer as they are returned
567     by Layer.SelectShapes().
568     """
569 bh 34 if self.layer is not None:
570 jonathan 881 self.parent.SelectShapes(self.layer, rows)
571 bh 1146
572     def map_layers_removed(self, *args):
573     """Receiver for the map's MAP_LAYERS_REMOVED message
574    
575     Close the dialog if the layer whose table we're showing is not
576     in the map anymore.
577     """
578     if self.layer not in self.map.Layers():
579     self.Close()
580    

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26