/[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 1202 - (hide annotations)
Fri Jun 13 16:01:10 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: 20095 byte(s)
(TableFrame.__init__): Add a panel
        object that can be used by derived classes to place any
        controls (including the grid) onto.
(QueryTableFrame.__init__): Use the panel as the parent window
        for all the controls. Reparent the grid so that the panel is
        the parent. Call UpdateStatusText() to correctly initialize
        the status bar.

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 jonathan 1202 self.panel = wxPanel(self, -1)
289    
290 bh 278 self.table = table
291     self.grid = self.make_grid(self.table)
292 bh 1068 self.app = self.parent.application
293     self.app.Subscribe(SESSION_REPLACED, self.close_on_session_replaced)
294     self.session = self.app.Session()
295     self.session.Subscribe(TABLE_REMOVED, self.close_on_table_removed)
296 bh 278
297 jonathan 1202
298 bh 278 def make_grid(self, table):
299     """Return the table grid to use in the frame.
300    
301     The default implementation returns a TableGrid instance.
302     Override in derived classes to use different grid classes.
303     """
304     return TableGrid(self, table)
305    
306 bh 1068 def OnClose(self, event):
307     self.app.Unsubscribe(SESSION_REPLACED, self.close_on_session_replaced)
308     self.session.Unsubscribe(TABLE_REMOVED, self.close_on_table_removed)
309 jonathan 1199 ThubanFrame.OnClose(self, event)
310 bh 278
311 bh 1068 def close_on_session_replaced(self, *args):
312     """Subscriber for the SESSION_REPLACED messages.
313    
314     The table frame is tied to a session so close the window when
315     the session changes.
316     """
317     self.Close()
318    
319     def close_on_table_removed(self, table):
320     """Subscriber for the TABLE_REMOVED messages.
321    
322     The table frame is tied to a particular table so close the
323     window when the table is removed.
324     """
325     if table is self.table:
326     self.Close()
327    
328    
329 jonathan 881 ID_QUERY = 4001
330 jan 1013 ID_EXPORT = 4002
331 jonathan 1199 ID_COMBOVALUE = 4003
332 jonathan 881
333 jan 1013 class QueryTableFrame(TableFrame):
334 bh 278
335 jan 1013 """Frame that displays a table in a grid view and offers user actions
336     selection and export
337 bh 278
338 jan 1096 A QueryTableFrame is TableFrame whose selection is connected to the
339 bh 278 selected object in a map.
340     """
341    
342 jan 1013 def __init__(self, parent, name, title, table):
343 bh 535 TableFrame.__init__(self, parent, name, title, table)
344 bh 30
345 jonathan 1202 self.combo_fields = wxComboBox(self.panel, -1, style=wxCB_READONLY)
346     self.choice_comp = wxChoice(self.panel, -1,
347 jonathan 939 choices=["<", "<=", "==", "!=", ">=", ">"])
348 jonathan 1202 self.combo_value = wxComboBox(self.panel, ID_COMBOVALUE)
349     self.choice_action = wxChoice(self.panel, -1,
350 jonathan 881 choices=[_("Replace Selection"),
351     _("Refine Selection"),
352     _("Add to Selection")])
353    
354 jonathan 1202 button_query = wxButton(self.panel, ID_QUERY, _("Query"))
355     button_saveas = wxButton(self.panel, ID_EXPORT, _("Export"))
356 jonathan 881
357 jonathan 1199 self.CreateStatusBar()
358    
359 jonathan 881 self.grid.SetSize((400, 200))
360    
361     self.combo_value.Append("")
362 jonathan 939 for i in range(table.NumColumns()):
363     name = table.Column(i).name
364 jonathan 881 self.combo_fields.Append(name)
365     self.combo_value.Append(name)
366 jonathan 939
367 jonathan 881 # assume at least one field?
368     self.combo_fields.SetSelection(0)
369     self.combo_value.SetSelection(0)
370 jonathan 1199 self.choice_action.SetSelection(0)
371     self.choice_comp.SetSelection(0)
372 jonathan 881
373 jonathan 1202 self.grid.Reparent(self.panel)
374    
375     self.UpdateStatusText()
376    
377 jonathan 881 topBox = wxBoxSizer(wxVERTICAL)
378    
379 jonathan 1202 sizer = wxStaticBoxSizer(wxStaticBox(self.panel, -1,
380 frank 1066 _("Selection")),
381 jonathan 881 wxHORIZONTAL)
382     sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)
383     sizer.Add(self.choice_comp, 0, wxALL, 4)
384     sizer.Add(self.combo_value, 1, wxEXPAND|wxALL, 4)
385     sizer.Add(self.choice_action, 0, wxALL, 4)
386 frank 979 sizer.Add(button_query, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
387 jonathan 881 sizer.Add(40, 20, 0, wxALL, 4)
388 frank 979 sizer.Add(button_saveas, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
389 jonathan 881
390     topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
391     topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
392    
393 jonathan 1202 self.panel.SetAutoLayout(True)
394     self.panel.SetSizer(topBox)
395     topBox.Fit(self.panel)
396     topBox.SetSizeHints(self.panel)
397    
398     panelSizer = wxBoxSizer(wxVERTICAL)
399     panelSizer.Add(self.panel, 1, wxEXPAND, 0)
400 jonathan 881 self.SetAutoLayout(True)
401 jonathan 1202 self.SetSizer(panelSizer)
402     panelSizer.Fit(self)
403     panelSizer.SetSizeHints(self)
404 jonathan 881
405 frank 979 self.grid.SetFocus()
406 jonathan 1202
407 jonathan 881 EVT_BUTTON(self, ID_QUERY, self.OnQuery)
408 jan 1013 EVT_BUTTON(self, ID_EXPORT, self.OnSaveAs)
409 frank 979 EVT_KEY_DOWN(self.grid, self.OnKeyDown)
410 jonathan 1199 EVT_GRID_RANGE_SELECT(self.grid, self.OnGridSelectRange)
411     EVT_GRID_SELECT_CELL(self.grid, self.OnGridSelectCell)
412 jonathan 881
413 jonathan 1199 def UpdateStatusText(self):
414     self.SetStatusText(_("%i rows (%i selected), %i columns")
415     % (self.grid.GetNumberRows(),
416     self.grid.GetNumberSelected(),
417     self.grid.GetNumberCols()))
418    
419     def OnGridSelectRange(self, event):
420     self.UpdateStatusText()
421     event.Skip()
422    
423     def OnGridSelectCell(self, event):
424     self.UpdateStatusText()
425     event.Skip()
426    
427 frank 979 def OnKeyDown(self, event):
428     """Catch query key from grid"""
429     if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
430     self.combo_fields.SetFocus()
431     self.combo_fields.refocus = True
432     else:
433     event.Skip()
434    
435 jonathan 881 def OnQuery(self, event):
436     wxBeginBusyCursor()
437 jonathan 1104 try:
438 jonathan 881
439 jonathan 1199 text = self.combo_value.GetValue()
440     if self.combo_value.GetSelection() < 1 \
441     or self.combo_value.FindString(text) == -1:
442     value = text
443 jonathan 1104 else:
444 jonathan 1199 value = self.table.Column(text)
445 jonathan 881
446 jonathan 1104 ids = self.table.SimpleQuery(
447     self.table.Column(self.combo_fields.GetStringSelection()),
448     self.choice_comp.GetStringSelection(),
449     value)
450 jonathan 881
451 jonathan 1104 choice = self.choice_action.GetSelection()
452 jonathan 881
453 jonathan 1104 #
454     # what used to be nice code got became a bit ugly because
455     # each time we select a row a message is sent to the grid
456     # which we are listening for and then we send further
457     # messages.
458     #
459     # now, we disable those listeners select everything but
460     # the first item, reenable the listeners, and select
461     # the first element, which causes everything to be
462     # updated properly.
463     #
464 jonathan 1199 if ids:
465     self.grid.ToggleEventListeners(False)
466 jonathan 966
467 jonathan 1104 if choice == 0:
468     # Replace Selection
469     self.grid.ClearSelection()
470     elif choice == 1:
471     # Refine Selection
472     sel = self.get_selected()
473     self.grid.ClearSelection()
474     ids = filter(sel.has_key, ids)
475     elif choice == 2:
476     # Add to Selection
477     pass
478 jonathan 966
479 jonathan 1104 #
480     # select the rows (all but the first)
481     #
482     firsttime = True
483     for id in ids:
484     if firsttime:
485     firsttime = False
486     else:
487     self.grid.SelectRow(id, True)
488 jonathan 881
489 jonathan 1104 self.grid.ToggleEventListeners(True)
490 jonathan 966
491 jonathan 1104 #
492     # select the first row
493     #
494     if ids:
495     self.grid.SelectRow(ids[0], True)
496 jonathan 1199
497 jonathan 1104 finally:
498     wxEndBusyCursor()
499 jonathan 881
500     def OnSaveAs(self, event):
501 jan 1013 dlg = wxFileDialog(self, _("Export Table To"), ".", "",
502     _("DBF Files (*.dbf)|*.dbf|") +
503     _("CSV Files (*.csv)|*.csv|") +
504     _("All Files (*.*)|*.*"),
505 jonathan 881 wxSAVE|wxOVERWRITE_PROMPT)
506     if dlg.ShowModal() == wxID_OK:
507 frank 1027 filename = dlg.GetPath()
508     type = os.path.basename(filename).split('.')[-1:][0]
509     dlg.Destroy()
510     if type.upper() == "DBF":
511     table_to_dbf(self.table, filename)
512     elif type.upper() == 'CSV':
513     table_to_csv(self.table, filename)
514     else:
515     dlg = wxMessageDialog(None, "Unsupported format: %s" % type,
516     "Table Export", wxOK|wxICON_WARNING)
517     dlg.ShowModal()
518     dlg.Destroy()
519     else:
520     dlg.Destroy()
521 jonathan 881
522 jan 1013 def OnClose(self, event):
523     TableFrame.OnClose(self, event)
524    
525     def get_selected(self):
526     """Return a dictionary of the selected rows.
527    
528     The dictionary has sthe indexes as keys."""
529     return dict([(i, 0) for i in self.grid.GetSelectedRows()])
530    
531     class LayerTableFrame(QueryTableFrame):
532    
533     """Frame that displays a layer table in a grid view
534    
535     A LayerTableFrame is a QueryTableFrame whose selection is connected to the
536     selected object in a map.
537     """
538    
539     def __init__(self, parent, name, title, layer, table):
540     QueryTableFrame.__init__(self, parent, name, title, table)
541     self.layer = layer
542     self.grid.Subscribe(ROW_SELECTED, self.rows_selected)
543     self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)
544 bh 1146 self.map = self.parent.Map()
545     self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
546 jan 1013
547 jan 1032 # if there is already a selection present, update the grid
548     # accordingly
549     sel = self.get_selected().keys()
550     for i in sel:
551     self.grid.SelectRow(i, True)
552    
553 bh 278 def make_grid(self, table):
554     """Override the derived method to return a LayerTableGrid.
555     """
556     return LayerTableGrid(self, table)
557    
558 jan 1013 def get_selected(self):
559     """Override the derived method to return a dictionary of the selected
560     rows.
561     """
562     return dict([(i, 0) for i in self.parent.SelectedShapes()])
563    
564 bh 30 def OnClose(self, event):
565 jan 1013 """Override the derived method to first unsubscribed."""
566 jonathan 881 self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
567 jan 1096 self.grid.Unsubscribe(ROW_SELECTED, self.rows_selected)
568 bh 1146 self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
569 jan 1013 QueryTableFrame.OnClose(self, event)
570 bh 30
571 jonathan 881 def select_shapes(self, layer, shapes):
572 bh 535 """Subscribed to the SHAPES_SELECTED message.
573    
574     If shapes contains exactly one shape id, select that shape in
575     the grid. Otherwise deselect all.
576     """
577 jonathan 881 self.grid.select_shapes(layer, shapes)
578 bh 34
579 jonathan 881 def rows_selected(self, rows):
580 jan 1013 """Return the selected rows of the layer as they are returned
581     by Layer.SelectShapes().
582     """
583 bh 34 if self.layer is not None:
584 jonathan 881 self.parent.SelectShapes(self.layer, rows)
585 bh 1146
586     def map_layers_removed(self, *args):
587     """Receiver for the map's MAP_LAYERS_REMOVED message
588    
589     Close the dialog if the layer whose table we're showing is not
590     in the map anymore.
591     """
592     if self.layer not in self.map.Layers():
593     self.Close()
594    

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26