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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2032 - (show annotations)
Mon Dec 22 14:54:47 2003 UTC (21 years, 2 months ago) by bh
Original Path: trunk/thuban/Thuban/UI/tableview.py
File MIME type: text/x-python
File size: 22004 byte(s)
(TableGrid.OnDestroy)
(TableGrid.__init__): Handle EVT_WINDOW_DESTROY in the grid to
unsubscribe all subscribers.
(LayerTableFrame.OnDestroy): Do not unsubscribe any messages from
self.grid since it may already have been destroyed.
Fixes RT #2256

1 # Copyright (c) 2001, 2002, 2003 by Intevation GmbH
2 # 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 import os.path
11
12 from Thuban import _
13
14 from wxPython.wx import *
15 from wxPython.grid import *
16
17 from Thuban.Lib.connector import Publisher
18 from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \
19 FIELDTYPE_STRING, table_to_dbf, table_to_csv
20 from dialogs import ThubanFrame
21
22 from messages import SHAPES_SELECTED, SESSION_REPLACED
23 from Thuban.Model.messages import TABLE_REMOVED, MAP_LAYERS_REMOVED
24 from Thuban.UI.common import ThubanBeginBusyCursor, ThubanEndBusyCursor
25
26 wx_value_type_map = {FIELDTYPE_INT: wxGRID_VALUE_NUMBER,
27 FIELDTYPE_DOUBLE: wxGRID_VALUE_FLOAT,
28 FIELDTYPE_STRING: wxGRID_VALUE_STRING}
29
30 ROW_SELECTED = "ROW_SELECTED"
31
32 QUERY_KEY = 'S'
33
34 class DataTable(wxPyGridTableBase):
35
36 """Wrapper around a Thuban table object suitable for a wxGrid"""
37
38 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 self.num_cols = table.NumColumns()
49 self.num_rows = table.NumRows()
50
51 self.columns = []
52 for i in range(self.num_cols):
53 col = table.Column(i)
54 self.columns.append((col.name, wx_value_type_map[col.type]))
55
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 record = self.table.ReadRowAsDict(row, row_is_ordinal = 1)
75 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 #
106 def RowIdToOrdinal(self, rowid):
107 """Return the ordinal of the row given by its id"""
108 return self.table.RowIdToOrdinal(rowid)
109
110 def RowOrdinalToId(self, ordinal):
111 """Return the id of the row given by its ordinal"""
112 return self.table.RowOrdinalToId(ordinal)
113
114
115 class NullRenderer(wxPyGridCellRenderer):
116
117 """Renderer that draws NULL as a gray rectangle
118
119 Other values are delegated to a normal renderer which is given as
120 the parameter to the constructor.
121 """
122
123 def __init__(self, non_null_renderer):
124 wxPyGridCellRenderer.__init__(self)
125 self.non_null_renderer = non_null_renderer
126
127 def Draw(self, grid, attr, dc, rect, row, col, isSelected):
128 value = grid.table.GetValue(row, col)
129 if value is None:
130 dc.SetBackgroundMode(wxSOLID)
131 dc.SetBrush(wxBrush(wxColour(192, 192, 192), wxSOLID))
132 dc.SetPen(wxTRANSPARENT_PEN)
133 dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)
134 else:
135 self.non_null_renderer.Draw(grid, attr, dc, rect, row, col,
136 isSelected)
137
138 def GetBestSize(self, grid, attr, dc, row, col):
139 self.non_null_renderer.GetBestSize(grid, attr, dc, row, col)
140
141 def Clone(self):
142 return NullRenderer(self.non_null_renderer)
143
144
145 class TableGrid(wxGrid, Publisher):
146
147 """A grid view for a Thuban table
148
149 When rows are selected by the user the table issues ROW_SELECTED
150 messages. wx sends selection events even when the selection is
151 manipulated by code (instead of by the user) which usually lead to
152 ROW_SELECTED messages being sent in turn. Therefore sending messages
153 can be switched on and off with the allow_messages and
154 disallow_messages methods.
155 """
156
157 def __init__(self, parent, table = None):
158 wxGrid.__init__(self, parent, -1)
159
160 self.allow_messages_count = 0
161
162 # keep track of which rows are selected.
163 self.rows = {}
164
165 self.table = DataTable(table)
166
167 # The second parameter means that the grid is to take ownership
168 # of the table and will destroy it when done. Otherwise you
169 # would need to keep a reference to it and call its Destroy
170 # method later.
171 self.SetTable(self.table, True)
172
173 #self.SetMargins(0,0)
174
175 # AutoSizeColumns would allow us to make the grid have optimal
176 # column widths automatically but it would cause a traversal of
177 # the entire table which for large .dbf files can take a very
178 # long time.
179 #self.AutoSizeColumns(False)
180
181 self.SetSelectionMode(wxGrid.wxGridSelectRows)
182
183 self.ToggleEventListeners(True)
184 EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)
185 EVT_GRID_SELECT_CELL(self, self.OnSelectCell)
186
187 # Replace the normal renderers with our own versions which
188 # render NULL/None values specially
189 self.RegisterDataType(wxGRID_VALUE_STRING,
190 NullRenderer(wxGridCellStringRenderer()), None)
191 self.RegisterDataType(wxGRID_VALUE_NUMBER,
192 NullRenderer(wxGridCellNumberRenderer()), None)
193 self.RegisterDataType(wxGRID_VALUE_FLOAT,
194 NullRenderer(wxGridCellFloatRenderer()), None)
195
196 EVT_WINDOW_DESTROY(self, self.OnDestroy)
197
198 def OnDestroy(self, event):
199 Publisher.Destroy(self)
200
201 def SetTableObject(self, table):
202 self.table.SetTable(table)
203
204 def OnRangeSelect(self, event):
205 to_id = self.table.RowOrdinalToId
206 if self.handleSelectEvents:
207 self.rows = dict([(to_id(i), 0) for i in self.GetSelectedRows()])
208
209 # if we're selecting we need to include the selected range and
210 # make sure that the current row is also included, which may
211 # not be the case if you just click on a single row!
212 if event.Selecting():
213 for i in range(event.GetTopRow(), event.GetBottomRow() + 1):
214 self.rows[to_id(i)] = 0
215 self.rows[to_id(event.GetTopLeftCoords().GetRow())] = 0
216
217 self.issue(ROW_SELECTED, self.rows.keys())
218
219 event.Skip()
220
221 def OnSelectCell(self, event):
222 to_id = self.table.RowOrdinalToId
223 if self.handleSelectEvents:
224 self.issue(ROW_SELECTED,
225 [to_id(i) for i in self.GetSelectedRows()])
226 event.Skip()
227
228 def ToggleEventListeners(self, on):
229 self.handleSelectEvents = on
230
231 def GetNumberSelected(self):
232 return len(self.rows)
233
234 def disallow_messages(self):
235 """Disallow messages to be send.
236
237 This method only increases a counter so that calls to
238 disallow_messages and allow_messages can be nested. Only the
239 outermost calls will actually switch message sending on and off.
240 """
241 self.allow_messages_count += 1
242
243 def allow_messages(self):
244 """Allow messages to be send.
245
246 This method only decreases a counter so that calls to
247 disallow_messages and allow_messages can be nested. Only the
248 outermost calls will actually switch message sending on and off.
249 """
250 self.allow_messages_count -= 1
251
252 def issue(self, *args):
253 """Issue a message unless disallowed.
254
255 See the allow_messages and disallow_messages methods.
256 """
257 if self.allow_messages_count == 0:
258 Publisher.issue(self, *args)
259
260 def SelectRowById(self, rowid, do_select):
261 """Select row with the id rowid"""
262 self.SelectRow(self.table.RowIdToOrdinal(rowid), do_select)
263
264
265 class LayerTableGrid(TableGrid):
266
267 """Table grid for the layer tables.
268
269 The LayerTableGrid is basically the same as a TableGrid but it's
270 selection is usually coupled to the selected object in the map.
271 """
272
273 def select_shapes(self, layer, shapes):
274 """Select the row corresponding to the specified shape and layer
275
276 If layer is not the layer the table is associated with do
277 nothing. If shape or layer is None also do nothing.
278 """
279 if layer is not None \
280 and layer.ShapeStore().Table() is self.table.table:
281
282 self.disallow_messages()
283 try:
284 self.ClearSelection()
285 if len(shapes) > 0:
286 # keep track of the lowest id so we can make it the
287 # first visible item
288 first = -1
289
290 to_ordinal = self.table.RowIdToOrdinal
291 for shape in shapes:
292 row = to_ordinal(shape)
293 self.SelectRow(row, True)
294 if row < first:
295 first = row
296
297 self.SetGridCursor(first, 0)
298 self.MakeCellVisible(first, 0)
299 finally:
300 self.allow_messages()
301
302
303 class TableFrame(ThubanFrame):
304
305 """Frame that displays a Thuban table in a grid view"""
306
307 def __init__(self, parent, name, title, table):
308 ThubanFrame.__init__(self, parent, name, title)
309 self.panel = wxPanel(self, -1)
310
311 self.table = table
312 self.grid = self.make_grid(self.table)
313 self.app = self.parent.application
314 self.app.Subscribe(SESSION_REPLACED, self.close_on_session_replaced)
315 self.session = self.app.Session()
316 self.session.Subscribe(TABLE_REMOVED, self.close_on_table_removed)
317
318 def OnDestroy(self, event):
319 """Extend inherited method to unsubscribe messages"""
320 self.app.Unsubscribe(SESSION_REPLACED, self.close_on_session_replaced)
321 self.session.Unsubscribe(TABLE_REMOVED, self.close_on_table_removed)
322 ThubanFrame.OnDestroy(self, event)
323
324 def make_grid(self, table):
325 """Return the table grid to use in the frame.
326
327 The default implementation returns a TableGrid instance.
328 Override in derived classes to use different grid classes.
329 """
330 return TableGrid(self, table)
331
332 def close_on_session_replaced(self, *args):
333 """Subscriber for the SESSION_REPLACED messages.
334
335 The table frame is tied to a session so close the window when
336 the session changes.
337 """
338 self.Close()
339
340 def close_on_table_removed(self, table):
341 """Subscriber for the TABLE_REMOVED messages.
342
343 The table frame is tied to a particular table so close the
344 window when the table is removed.
345 """
346 if table is self.table:
347 self.Close()
348
349
350 ID_QUERY = 4001
351 ID_EXPORT = 4002
352 ID_COMBOVALUE = 4003
353 ID_EXPORTSEL = 4004
354
355 class QueryTableFrame(TableFrame):
356
357 """Frame that displays a table in a grid view and offers user actions
358 selection and export
359
360 A QueryTableFrame is TableFrame whose selection is connected to the
361 selected object in a map.
362 """
363
364 def __init__(self, parent, name, title, table):
365 TableFrame.__init__(self, parent, name, title, table)
366
367 self.combo_fields = wxComboBox(self.panel, -1, style=wxCB_READONLY)
368 self.choice_comp = wxChoice(self.panel, -1,
369 choices=["<", "<=", "==", "!=", ">=", ">"])
370 self.combo_value = wxComboBox(self.panel, ID_COMBOVALUE)
371 self.choice_action = wxChoice(self.panel, -1,
372 choices=[_("Replace Selection"),
373 _("Refine Selection"),
374 _("Add to Selection")])
375
376 button_query = wxButton(self.panel, ID_QUERY, _("Query"))
377 button_export = wxButton(self.panel, ID_EXPORT, _("Export"))
378 button_exportSel = wxButton(self.panel, ID_EXPORTSEL, _("Export Selection"))
379 button_close = wxButton(self.panel, wxID_CLOSE, _("Close"))
380
381 self.CreateStatusBar()
382
383 self.grid.SetSize((400, 200))
384
385 self.combo_value.Append("")
386 for i in range(table.NumColumns()):
387 name = table.Column(i).name
388 self.combo_fields.Append(name)
389 self.combo_value.Append(name)
390
391 # assume at least one field?
392 self.combo_fields.SetSelection(0)
393 self.combo_value.SetSelection(0)
394 self.choice_action.SetSelection(0)
395 self.choice_comp.SetSelection(0)
396
397 self.grid.Reparent(self.panel)
398
399 self.UpdateStatusText()
400
401 topBox = wxBoxSizer(wxVERTICAL)
402
403 sizer = wxStaticBoxSizer(wxStaticBox(self.panel, -1,
404 _("Selection")),
405 wxHORIZONTAL)
406 sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)
407 sizer.Add(self.choice_comp, 0, wxALL, 4)
408 sizer.Add(self.combo_value, 1, wxEXPAND|wxALL, 4)
409 sizer.Add(self.choice_action, 0, wxALL, 4)
410 sizer.Add(button_query, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
411 sizer.Add(40, 20, 0, wxALL, 4)
412
413 topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
414 topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
415
416 sizer = wxBoxSizer(wxHORIZONTAL)
417 sizer.Add(button_export, 0, wxALL, 4)
418 sizer.Add(button_exportSel, 0, wxALL, 4)
419 sizer.Add(60, 20, 1, wxALL|wxEXPAND, 4)
420 sizer.Add(button_close, 0, wxALL|wxALIGN_RIGHT, 4)
421 topBox.Add(sizer, 0, wxALL | wxEXPAND, 4)
422
423 self.panel.SetAutoLayout(True)
424 self.panel.SetSizer(topBox)
425 topBox.Fit(self.panel)
426 topBox.SetSizeHints(self.panel)
427
428 panelSizer = wxBoxSizer(wxVERTICAL)
429 panelSizer.Add(self.panel, 1, wxEXPAND, 0)
430 self.SetAutoLayout(True)
431 self.SetSizer(panelSizer)
432 panelSizer.Fit(self)
433 panelSizer.SetSizeHints(self)
434
435 self.grid.SetFocus()
436
437 EVT_BUTTON(self, ID_QUERY, self.OnQuery)
438 EVT_BUTTON(self, ID_EXPORT, self.OnExport)
439 EVT_BUTTON(self, ID_EXPORTSEL, self.OnExportSel)
440 EVT_BUTTON(self, wxID_CLOSE, self.OnClose)
441 EVT_KEY_DOWN(self.grid, self.OnKeyDown)
442
443 self.grid.Subscribe(ROW_SELECTED, self.UpdateStatusText)
444
445 def UpdateStatusText(self, rows=None):
446 self.SetStatusText(_("%i rows (%i selected), %i columns")
447 % (self.grid.GetNumberRows(),
448 self.grid.GetNumberSelected(),
449 self.grid.GetNumberCols()))
450
451 def OnKeyDown(self, event):
452 """Catch query key from grid"""
453 if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
454 self.combo_fields.SetFocus()
455 self.combo_fields.refocus = True
456 else:
457 event.Skip()
458
459 def OnQuery(self, event):
460 ThubanBeginBusyCursor()
461 try:
462
463 text = self.combo_value.GetValue()
464 if self.combo_value.GetSelection() < 1 \
465 or self.combo_value.FindString(text) == -1:
466 value = text
467 else:
468 value = self.table.Column(text)
469
470 ids = self.table.SimpleQuery(
471 self.table.Column(self.combo_fields.GetStringSelection()),
472 self.choice_comp.GetStringSelection(),
473 value)
474
475 choice = self.choice_action.GetSelection()
476
477 #
478 # what used to be nice code got became a bit ugly because
479 # each time we select a row a message is sent to the grid
480 # which we are listening for and then we send further
481 # messages.
482 #
483 # now, we disable those listeners select everything but
484 # the first item, reenable the listeners, and select
485 # the first element, which causes everything to be
486 # updated properly.
487 #
488 if ids:
489 self.grid.ToggleEventListeners(False)
490
491 if choice == 0:
492 # Replace Selection
493 self.grid.ClearSelection()
494 elif choice == 1:
495 # Refine Selection
496 sel = self.get_selected()
497 self.grid.ClearSelection()
498 ids = filter(sel.has_key, ids)
499 elif choice == 2:
500 # Add to Selection
501 pass
502
503 #
504 # select the rows (all but the first)
505 #
506 firsttime = True
507 for id in ids:
508 if firsttime:
509 firsttime = False
510 else:
511 self.grid.SelectRowById(id, True)
512
513 self.grid.ToggleEventListeners(True)
514
515 #
516 # select the first row
517 #
518 if ids:
519 self.grid.SelectRowById(ids[0], True)
520
521 finally:
522 ThubanEndBusyCursor()
523
524 def doExport(self, onlySelected):
525
526 dlg = wxFileDialog(self, _("Export Table To"), ".", "",
527 _("DBF Files (*.dbf)|*.dbf|") +
528 _("CSV Files (*.csv)|*.csv|") +
529 _("All Files (*.*)|*.*"),
530 wxSAVE|wxOVERWRITE_PROMPT)
531 if dlg.ShowModal() == wxID_OK:
532 filename = dlg.GetPath()
533 type = os.path.basename(filename).split('.')[-1:][0]
534 dlg.Destroy()
535
536 if onlySelected:
537 records = self.grid.GetSelectedRows()
538 else:
539 records = None
540
541 if type.upper() == "DBF":
542 table_to_dbf(self.table, filename, records)
543 elif type.upper() == 'CSV':
544 table_to_csv(self.table, filename, records)
545 else:
546 dlg = wxMessageDialog(None, "Unsupported format: %s" % type,
547 "Table Export", wxOK|wxICON_WARNING)
548 dlg.ShowModal()
549 dlg.Destroy()
550 else:
551 dlg.Destroy()
552
553 def OnExport(self, event):
554 self.doExport(False)
555
556 def OnExportSel(self, event):
557 self.doExport(True)
558
559 def OnClose(self, event):
560 TableFrame.OnClose(self, event)
561
562 def get_selected(self):
563 """Return a dictionary of the selected rows.
564
565 The dictionary has the indexes as keys."""
566 to_id = self.table.RowOrdinalToId
567 return dict([(to_id(i), 0) for i in self.grid.GetSelectedRows()])
568
569
570 class LayerTableFrame(QueryTableFrame):
571
572 """Frame that displays a layer table in a grid view
573
574 A LayerTableFrame is a QueryTableFrame whose selection is connected to the
575 selected object in a map.
576 """
577
578 def __init__(self, parent, name, title, layer, table):
579 QueryTableFrame.__init__(self, parent, name, title, table)
580 self.layer = layer
581 self.grid.Subscribe(ROW_SELECTED, self.rows_selected)
582 self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)
583 self.map = self.parent.Map()
584 self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
585
586 # if there is already a selection present, update the grid
587 # accordingly
588 sel = self.get_selected().keys()
589 for i in sel:
590 self.grid.SelectRowById(i, True)
591
592 def OnDestroy(self, event):
593 """Extend inherited method to unsubscribe messages"""
594 # There's no need to unsubscribe from self.grid's messages
595 # because it will get a DESTROY event too (since destroying the
596 # frame basically means that all child windows are also
597 # destroyed) and this it will clear all subscriptions
598 # automatically. It may even have been destroyed already (this
599 # does happen on w2000 for instance) so calling any of its
600 # methods here would be an error.
601 self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
602 self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
603 QueryTableFrame.OnDestroy(self, event)
604
605 def make_grid(self, table):
606 """Override the derived method to return a LayerTableGrid.
607 """
608 return LayerTableGrid(self, table)
609
610 def get_selected(self):
611 """Override the derived method to return a dictionary of the selected
612 rows.
613 """
614 return dict([(i, 0) for i in self.parent.SelectedShapes()])
615
616 def select_shapes(self, layer, shapes):
617 """Subscribed to the SHAPES_SELECTED message.
618
619 If shapes contains exactly one shape id, select that shape in
620 the grid. Otherwise deselect all.
621 """
622 self.grid.select_shapes(layer, shapes)
623
624 def rows_selected(self, rows):
625 """Return the selected rows of the layer as they are returned
626 by Layer.SelectShapes().
627 """
628 if self.layer is not None:
629 self.parent.SelectShapes(self.layer, rows)
630
631 def map_layers_removed(self, *args):
632 """Receiver for the map's MAP_LAYERS_REMOVED message
633
634 Close the dialog if the layer whose table we're showing is not
635 in the map anymore.
636 """
637 if self.layer not in self.map.Layers():
638 self.Close()
639

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26