/[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 1276 - (hide annotations)
Fri Jun 20 17:46:34 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: 20190 byte(s)
Use Thuban[Begin|End]BusyCursor()
        instead of a direct call to wx[Begin|End]CusyCursor().

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 jonathan 1276 from Thuban.UI.common import ThubanBeginBusyCursor, ThubanEndBusyCursor
26 bh 1068
27 bh 6 wx_value_type_map = {FIELDTYPE_INT: wxGRID_VALUE_NUMBER,
28     FIELDTYPE_DOUBLE: wxGRID_VALUE_FLOAT,
29     FIELDTYPE_STRING: wxGRID_VALUE_STRING}
30    
31 bh 34 ROW_SELECTED = "ROW_SELECTED"
32    
33 frank 979 QUERY_KEY = 'S'
34 bh 34
35 bh 6 class DataTable(wxPyGridTableBase):
36    
37 bh 34 """Wrapper around a Thuban table object suitable for a wxGrid"""
38    
39 bh 6 def __init__(self, table = None):
40     wxPyGridTableBase.__init__(self)
41     self.num_cols = 0
42     self.num_rows = 0
43     self.columns = []
44     self.table = None
45     self.SetTable(table)
46    
47     def SetTable(self, table):
48     self.table = table
49 bh 838 self.num_cols = table.NumColumns()
50     self.num_rows = table.NumRows()
51 bh 6
52     self.columns = []
53     for i in range(self.num_cols):
54 bh 838 col = table.Column(i)
55     self.columns.append((col.name, wx_value_type_map[col.type]))
56 bh 6
57     #
58     # required methods for the wxPyGridTableBase interface
59     #
60    
61     def GetNumberRows(self):
62     return self.num_rows
63    
64     def GetNumberCols(self):
65     return self.num_cols
66    
67     def IsEmptyCell(self, row, col):
68     return 0
69    
70     # Get/Set values in the table. The Python version of these
71     # methods can handle any data-type, (as long as the Editor and
72     # Renderer understands the type too,) not just strings as in the
73     # C++ version.
74     def GetValue(self, row, col):
75 bh 838 record = self.table.ReadRowAsDict(row)
76 bh 6 return record[self.columns[col][0]]
77    
78     def SetValue(self, row, col, value):
79     pass
80    
81     #
82     # Some optional methods
83     #
84    
85     # Called when the grid needs to display labels
86     def GetColLabelValue(self, col):
87     return self.columns[col][0]
88    
89     # Called to determine the kind of editor/renderer to use by
90     # default, doesn't necessarily have to be the same type used
91     # nativly by the editor/renderer if they know how to convert.
92     def GetTypeName(self, row, col):
93     return self.columns[col][1]
94    
95     # Called to determine how the data can be fetched and stored by the
96     # editor and renderer. This allows you to enforce some type-safety
97     # in the grid.
98     def CanGetValueAs(self, row, col, typeName):
99     # perhaps we should allow conversion int->double?
100     return self.GetTypeName(row, col) == typeName
101    
102     def CanSetValueAs(self, row, col, typeName):
103     return self.CanGetValueAs(row, col, typeName)
104    
105    
106 bh 1092
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 bh 34 class TableGrid(wxGrid, Publisher):
138 bh 6
139 bh 808 """A grid view for a Thuban table
140 bh 6
141 bh 808 When rows are selected by the user the table issues ROW_SELECTED
142     messages. wx sends selection events even when the selection is
143     manipulated by code (instead of by the user) which usually lead to
144     ROW_SELECTED messages being sent in turn. Therefore sending messages
145     can be switched on and off with the allow_messages and
146     disallow_messages methods.
147     """
148    
149 bh 6 def __init__(self, parent, table = None):
150     wxGrid.__init__(self, parent, -1)
151    
152 bh 808 self.allow_messages_count = 0
153    
154 jonathan 1199 # keep track of which rows are selected.
155     self.rows = {}
156    
157 bh 6 self.table = DataTable(table)
158    
159     # The second parameter means that the grid is to take ownership
160     # of the table and will destroy it when done. Otherwise you
161 bh 77 # would need to keep a reference to it and call its Destroy
162 bh 6 # method later.
163 jan 1035 self.SetTable(self.table, True)
164 bh 6
165     #self.SetMargins(0,0)
166    
167 bh 173 # AutoSizeColumns would allow us to make the grid have optimal
168     # column widths automatically but it would cause a traversal of
169     # the entire table which for large .dbf files can take a very
170     # long time.
171 jan 1035 #self.AutoSizeColumns(False)
172 bh 173
173 bh 6 self.SetSelectionMode(wxGrid.wxGridSelectRows)
174 bh 278
175 jonathan 966 self.ToggleEventListeners(True)
176 jonathan 1199 EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)
177     EVT_GRID_SELECT_CELL(self, self.OnSelectCell)
178 jonathan 966
179 bh 1092 # 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 bh 6 def SetTableObject(self, table):
189     self.table.SetTable(table)
190    
191     def OnRangeSelect(self, event):
192 jonathan 1199 if self.handleSelectEvents:
193     self.rows = dict([(i, 0) for i in self.GetSelectedRows()])
194 jonathan 881
195 jonathan 1199 # if we're selecting we need to include the selected range and
196     # make sure that the current row is also included, which may
197     # not be the case if you just click on a single row!
198     if event.Selecting():
199     for i in range(event.GetTopRow(), event.GetBottomRow() + 1):
200     self.rows[i] = 0
201     self.rows[event.GetTopLeftCoords().GetRow()] = 0
202    
203     self.issue(ROW_SELECTED, self.rows.keys())
204 bh 6
205 jonathan 881 event.Skip()
206    
207 bh 6 def OnSelectCell(self, event):
208 jonathan 1199 if self.handleSelectEvents:
209     self.issue(ROW_SELECTED, self.GetSelectedRows())
210 jonathan 881 event.Skip()
211 bh 6
212 jonathan 966 def ToggleEventListeners(self, on):
213 jonathan 1199 self.handleSelectEvents = on
214 jonathan 966
215 jonathan 1199 def GetNumberSelected(self):
216     return len(self.rows)
217    
218 bh 808 def disallow_messages(self):
219     """Disallow messages to be send.
220 bh 278
221 bh 808 This method only increases a counter so that calls to
222     disallow_messages and allow_messages can be nested. Only the
223     outermost calls will actually switch message sending on and off.
224     """
225     self.allow_messages_count += 1
226    
227     def allow_messages(self):
228     """Allow messages to be send.
229    
230     This method only decreases a counter so that calls to
231     disallow_messages and allow_messages can be nested. Only the
232     outermost calls will actually switch message sending on and off.
233     """
234     self.allow_messages_count -= 1
235    
236     def issue(self, *args):
237     """Issue a message unless disallowed.
238    
239     See the allow_messages and disallow_messages methods.
240     """
241     if self.allow_messages_count == 0:
242     Publisher.issue(self, *args)
243    
244    
245 bh 278 class LayerTableGrid(TableGrid):
246    
247     """Table grid for the layer tables.
248    
249     The LayerTableGrid is basically the same as a TableGrid but it's
250     selection is usually coupled to the selected object in the map.
251     """
252    
253 jonathan 881 def select_shapes(self, layer, shapes):
254 bh 278 """Select the row corresponding to the specified shape and layer
255    
256     If layer is not the layer the table is associated with do
257     nothing. If shape or layer is None also do nothing.
258     """
259 jonathan 881 if layer is not None \
260 bh 1219 and layer.ShapeStore().Table() is self.table.table:
261 jonathan 881
262 bh 808 self.disallow_messages()
263     try:
264 jonathan 881 self.ClearSelection()
265     if len(shapes) > 0:
266     #
267     # keep track of the lowest id so we can make it
268     # the first visible item
269     #
270     first = shapes[0]
271    
272     for shape in shapes:
273     self.SelectRow(shape, True)
274     if shape < first:
275     first = shape
276    
277     self.SetGridCursor(first, 0)
278     self.MakeCellVisible(first, 0)
279 bh 808 finally:
280     self.allow_messages()
281 bh 6
282    
283 jonathan 1199 class TableFrame(ThubanFrame):
284 bh 6
285 bh 34 """Frame that displays a Thuban table in a grid view"""
286    
287 bh 535 def __init__(self, parent, name, title, table):
288 jonathan 1199 ThubanFrame.__init__(self, parent, name, title)
289 jonathan 1202 self.panel = wxPanel(self, -1)
290    
291 bh 278 self.table = table
292     self.grid = self.make_grid(self.table)
293 bh 1068 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 bh 278
298 jonathan 1202
299 bh 278 def make_grid(self, table):
300     """Return the table grid to use in the frame.
301    
302     The default implementation returns a TableGrid instance.
303     Override in derived classes to use different grid classes.
304     """
305     return TableGrid(self, table)
306    
307 bh 1068 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 jonathan 1199 ThubanFrame.OnClose(self, event)
311 bh 278
312 bh 1068 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 jonathan 881 ID_QUERY = 4001
331 jan 1013 ID_EXPORT = 4002
332 jonathan 1199 ID_COMBOVALUE = 4003
333 jonathan 881
334 jan 1013 class QueryTableFrame(TableFrame):
335 bh 278
336 jan 1013 """Frame that displays a table in a grid view and offers user actions
337     selection and export
338 bh 278
339 jan 1096 A QueryTableFrame is TableFrame whose selection is connected to the
340 bh 278 selected object in a map.
341     """
342    
343 jan 1013 def __init__(self, parent, name, title, table):
344 bh 535 TableFrame.__init__(self, parent, name, title, table)
345 bh 30
346 jonathan 1202 self.combo_fields = wxComboBox(self.panel, -1, style=wxCB_READONLY)
347     self.choice_comp = wxChoice(self.panel, -1,
348 jonathan 939 choices=["<", "<=", "==", "!=", ">=", ">"])
349 jonathan 1202 self.combo_value = wxComboBox(self.panel, ID_COMBOVALUE)
350     self.choice_action = wxChoice(self.panel, -1,
351 jonathan 881 choices=[_("Replace Selection"),
352     _("Refine Selection"),
353     _("Add to Selection")])
354    
355 jonathan 1202 button_query = wxButton(self.panel, ID_QUERY, _("Query"))
356     button_saveas = wxButton(self.panel, ID_EXPORT, _("Export"))
357 jonathan 881
358 jonathan 1199 self.CreateStatusBar()
359    
360 jonathan 881 self.grid.SetSize((400, 200))
361    
362     self.combo_value.Append("")
363 jonathan 939 for i in range(table.NumColumns()):
364     name = table.Column(i).name
365 jonathan 881 self.combo_fields.Append(name)
366     self.combo_value.Append(name)
367 jonathan 939
368 jonathan 881 # assume at least one field?
369     self.combo_fields.SetSelection(0)
370     self.combo_value.SetSelection(0)
371 jonathan 1199 self.choice_action.SetSelection(0)
372     self.choice_comp.SetSelection(0)
373 jonathan 881
374 jonathan 1202 self.grid.Reparent(self.panel)
375    
376     self.UpdateStatusText()
377    
378 jonathan 881 topBox = wxBoxSizer(wxVERTICAL)
379    
380 jonathan 1202 sizer = wxStaticBoxSizer(wxStaticBox(self.panel, -1,
381 frank 1066 _("Selection")),
382 jonathan 881 wxHORIZONTAL)
383     sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)
384     sizer.Add(self.choice_comp, 0, wxALL, 4)
385     sizer.Add(self.combo_value, 1, wxEXPAND|wxALL, 4)
386     sizer.Add(self.choice_action, 0, wxALL, 4)
387 frank 979 sizer.Add(button_query, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
388 jonathan 881 sizer.Add(40, 20, 0, wxALL, 4)
389 frank 979 sizer.Add(button_saveas, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
390 jonathan 881
391     topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
392     topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
393    
394 jonathan 1202 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 jonathan 881 self.SetAutoLayout(True)
402 jonathan 1202 self.SetSizer(panelSizer)
403     panelSizer.Fit(self)
404     panelSizer.SetSizeHints(self)
405 jonathan 881
406 frank 979 self.grid.SetFocus()
407 jonathan 1202
408 jonathan 881 EVT_BUTTON(self, ID_QUERY, self.OnQuery)
409 jan 1013 EVT_BUTTON(self, ID_EXPORT, self.OnSaveAs)
410 frank 979 EVT_KEY_DOWN(self.grid, self.OnKeyDown)
411 jonathan 1199 EVT_GRID_RANGE_SELECT(self.grid, self.OnGridSelectRange)
412     EVT_GRID_SELECT_CELL(self.grid, self.OnGridSelectCell)
413 jonathan 881
414 jonathan 1199 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 frank 979 def OnKeyDown(self, event):
429     """Catch query key from grid"""
430     if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
431     self.combo_fields.SetFocus()
432     self.combo_fields.refocus = True
433     else:
434     event.Skip()
435    
436 jonathan 881 def OnQuery(self, event):
437 jonathan 1276 ThubanBeginBusyCursor()
438 jonathan 1104 try:
439 jonathan 881
440 jonathan 1199 text = self.combo_value.GetValue()
441     if self.combo_value.GetSelection() < 1 \
442     or self.combo_value.FindString(text) == -1:
443     value = text
444 jonathan 1104 else:
445 jonathan 1199 value = self.table.Column(text)
446 jonathan 881
447 jonathan 1104 ids = self.table.SimpleQuery(
448     self.table.Column(self.combo_fields.GetStringSelection()),
449     self.choice_comp.GetStringSelection(),
450     value)
451 jonathan 881
452 jonathan 1104 choice = self.choice_action.GetSelection()
453 jonathan 881
454 jonathan 1104 #
455     # what used to be nice code got became a bit ugly because
456     # 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 jonathan 1199 if ids:
466     self.grid.ToggleEventListeners(False)
467 jonathan 966
468 jonathan 1104 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 jonathan 966
480 jonathan 1104 #
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 jonathan 881
490 jonathan 1104 self.grid.ToggleEventListeners(True)
491 jonathan 966
492 jonathan 1104 #
493     # select the first row
494     #
495     if ids:
496     self.grid.SelectRow(ids[0], True)
497 jonathan 1199
498 jonathan 1104 finally:
499 jonathan 1276 ThubanEndBusyCursor()
500 jonathan 881
501     def OnSaveAs(self, event):
502 jan 1013 dlg = wxFileDialog(self, _("Export Table To"), ".", "",
503     _("DBF Files (*.dbf)|*.dbf|") +
504     _("CSV Files (*.csv)|*.csv|") +
505     _("All Files (*.*)|*.*"),
506 jonathan 881 wxSAVE|wxOVERWRITE_PROMPT)
507     if dlg.ShowModal() == wxID_OK:
508 frank 1027 filename = dlg.GetPath()
509     type = os.path.basename(filename).split('.')[-1:][0]
510     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 jonathan 881
523 jan 1013 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 bh 1146 self.map = self.parent.Map()
546     self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
547 jan 1013
548 jan 1032 # 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 bh 278 def make_grid(self, table):
555     """Override the derived method to return a LayerTableGrid.
556     """
557     return LayerTableGrid(self, table)
558    
559 jan 1013 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 bh 30 def OnClose(self, event):
566 jan 1013 """Override the derived method to first unsubscribed."""
567 jonathan 881 self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
568 jan 1096 self.grid.Unsubscribe(ROW_SELECTED, self.rows_selected)
569 bh 1146 self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
570 jan 1013 QueryTableFrame.OnClose(self, event)
571 bh 30
572 jonathan 881 def select_shapes(self, layer, shapes):
573 bh 535 """Subscribed to the SHAPES_SELECTED message.
574    
575     If shapes contains exactly one shape id, select that shape in
576     the grid. Otherwise deselect all.
577     """
578 jonathan 881 self.grid.select_shapes(layer, shapes)
579 bh 34
580 jonathan 881 def rows_selected(self, rows):
581 jan 1013 """Return the selected rows of the layer as they are returned
582     by Layer.SelectShapes().
583     """
584 bh 34 if self.layer is not None:
585 jonathan 881 self.parent.SelectShapes(self.layer, rows)
586 bh 1146
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    

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26