/[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 1892 - (show annotations)
Thu Oct 30 18:16:53 2003 UTC (21 years, 4 months ago) by bh
Original Path: trunk/thuban/Thuban/UI/tableview.py
File MIME type: text/x-python
File size: 21507 byte(s)
(TableFrame.OnDestroy, LayerTableFrame.OnDestroy): New.
Unsubscribe the messages here not in OnClose because that might
get called multiple times. Fixes RT #2196
(TableFrame.OnClose, LayerTableFrame.OnClose): Removed. Not needed
anymore.

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 def SetTableObject(self, table):
197 self.table.SetTable(table)
198
199 def OnRangeSelect(self, event):
200 to_id = self.table.RowOrdinalToId
201 if self.handleSelectEvents:
202 self.rows = dict([(to_id(i), 0) for i in self.GetSelectedRows()])
203
204 # if we're selecting we need to include the selected range and
205 # make sure that the current row is also included, which may
206 # not be the case if you just click on a single row!
207 if event.Selecting():
208 for i in range(event.GetTopRow(), event.GetBottomRow() + 1):
209 self.rows[to_id(i)] = 0
210 self.rows[to_id(event.GetTopLeftCoords().GetRow())] = 0
211
212 self.issue(ROW_SELECTED, self.rows.keys())
213
214 event.Skip()
215
216 def OnSelectCell(self, event):
217 to_id = self.table.RowOrdinalToId
218 if self.handleSelectEvents:
219 self.issue(ROW_SELECTED,
220 [to_id(i) for i in self.GetSelectedRows()])
221 event.Skip()
222
223 def ToggleEventListeners(self, on):
224 self.handleSelectEvents = on
225
226 def GetNumberSelected(self):
227 return len(self.rows)
228
229 def disallow_messages(self):
230 """Disallow messages to be send.
231
232 This method only increases a counter so that calls to
233 disallow_messages and allow_messages can be nested. Only the
234 outermost calls will actually switch message sending on and off.
235 """
236 self.allow_messages_count += 1
237
238 def allow_messages(self):
239 """Allow messages to be send.
240
241 This method only decreases a counter so that calls to
242 disallow_messages and allow_messages can be nested. Only the
243 outermost calls will actually switch message sending on and off.
244 """
245 self.allow_messages_count -= 1
246
247 def issue(self, *args):
248 """Issue a message unless disallowed.
249
250 See the allow_messages and disallow_messages methods.
251 """
252 if self.allow_messages_count == 0:
253 Publisher.issue(self, *args)
254
255 def SelectRowById(self, rowid, do_select):
256 """Select row with the id rowid"""
257 self.SelectRow(self.table.RowIdToOrdinal(rowid), do_select)
258
259
260 class LayerTableGrid(TableGrid):
261
262 """Table grid for the layer tables.
263
264 The LayerTableGrid is basically the same as a TableGrid but it's
265 selection is usually coupled to the selected object in the map.
266 """
267
268 def select_shapes(self, layer, shapes):
269 """Select the row corresponding to the specified shape and layer
270
271 If layer is not the layer the table is associated with do
272 nothing. If shape or layer is None also do nothing.
273 """
274 if layer is not None \
275 and layer.ShapeStore().Table() is self.table.table:
276
277 self.disallow_messages()
278 try:
279 self.ClearSelection()
280 if len(shapes) > 0:
281 # keep track of the lowest id so we can make it the
282 # first visible item
283 first = -1
284
285 to_ordinal = self.table.RowIdToOrdinal
286 for shape in shapes:
287 row = to_ordinal(shape)
288 self.SelectRow(row, True)
289 if row < first:
290 first = row
291
292 self.SetGridCursor(first, 0)
293 self.MakeCellVisible(first, 0)
294 finally:
295 self.allow_messages()
296
297
298 class TableFrame(ThubanFrame):
299
300 """Frame that displays a Thuban table in a grid view"""
301
302 def __init__(self, parent, name, title, table):
303 ThubanFrame.__init__(self, parent, name, title)
304 self.panel = wxPanel(self, -1)
305
306 self.table = table
307 self.grid = self.make_grid(self.table)
308 self.app = self.parent.application
309 self.app.Subscribe(SESSION_REPLACED, self.close_on_session_replaced)
310 self.session = self.app.Session()
311 self.session.Subscribe(TABLE_REMOVED, self.close_on_table_removed)
312
313 def OnDestroy(self, event):
314 """Extend inherited method to unsubscribe messages"""
315 self.app.Unsubscribe(SESSION_REPLACED, self.close_on_session_replaced)
316 self.session.Unsubscribe(TABLE_REMOVED, self.close_on_table_removed)
317 ThubanFrame.OnDestroy(self, event)
318
319 def make_grid(self, table):
320 """Return the table grid to use in the frame.
321
322 The default implementation returns a TableGrid instance.
323 Override in derived classes to use different grid classes.
324 """
325 return TableGrid(self, table)
326
327 def close_on_session_replaced(self, *args):
328 """Subscriber for the SESSION_REPLACED messages.
329
330 The table frame is tied to a session so close the window when
331 the session changes.
332 """
333 self.Close()
334
335 def close_on_table_removed(self, table):
336 """Subscriber for the TABLE_REMOVED messages.
337
338 The table frame is tied to a particular table so close the
339 window when the table is removed.
340 """
341 if table is self.table:
342 self.Close()
343
344
345 ID_QUERY = 4001
346 ID_EXPORT = 4002
347 ID_COMBOVALUE = 4003
348 ID_EXPORTSEL = 4004
349
350 class QueryTableFrame(TableFrame):
351
352 """Frame that displays a table in a grid view and offers user actions
353 selection and export
354
355 A QueryTableFrame is TableFrame whose selection is connected to the
356 selected object in a map.
357 """
358
359 def __init__(self, parent, name, title, table):
360 TableFrame.__init__(self, parent, name, title, table)
361
362 self.combo_fields = wxComboBox(self.panel, -1, style=wxCB_READONLY)
363 self.choice_comp = wxChoice(self.panel, -1,
364 choices=["<", "<=", "==", "!=", ">=", ">"])
365 self.combo_value = wxComboBox(self.panel, ID_COMBOVALUE)
366 self.choice_action = wxChoice(self.panel, -1,
367 choices=[_("Replace Selection"),
368 _("Refine Selection"),
369 _("Add to Selection")])
370
371 button_query = wxButton(self.panel, ID_QUERY, _("Query"))
372 button_export = wxButton(self.panel, ID_EXPORT, _("Export"))
373 button_exportSel = wxButton(self.panel, ID_EXPORTSEL, _("Export Selection"))
374 button_close = wxButton(self.panel, wxID_CLOSE, _("Close"))
375
376 self.CreateStatusBar()
377
378 self.grid.SetSize((400, 200))
379
380 self.combo_value.Append("")
381 for i in range(table.NumColumns()):
382 name = table.Column(i).name
383 self.combo_fields.Append(name)
384 self.combo_value.Append(name)
385
386 # assume at least one field?
387 self.combo_fields.SetSelection(0)
388 self.combo_value.SetSelection(0)
389 self.choice_action.SetSelection(0)
390 self.choice_comp.SetSelection(0)
391
392 self.grid.Reparent(self.panel)
393
394 self.UpdateStatusText()
395
396 topBox = wxBoxSizer(wxVERTICAL)
397
398 sizer = wxStaticBoxSizer(wxStaticBox(self.panel, -1,
399 _("Selection")),
400 wxHORIZONTAL)
401 sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)
402 sizer.Add(self.choice_comp, 0, wxALL, 4)
403 sizer.Add(self.combo_value, 1, wxEXPAND|wxALL, 4)
404 sizer.Add(self.choice_action, 0, wxALL, 4)
405 sizer.Add(button_query, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
406 sizer.Add(40, 20, 0, wxALL, 4)
407
408 topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
409 topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
410
411 sizer = wxBoxSizer(wxHORIZONTAL)
412 sizer.Add(button_export, 0, wxALL, 4)
413 sizer.Add(button_exportSel, 0, wxALL, 4)
414 sizer.Add(60, 20, 1, wxALL|wxEXPAND, 4)
415 sizer.Add(button_close, 0, wxALL|wxALIGN_RIGHT, 4)
416 topBox.Add(sizer, 0, wxALL | wxEXPAND, 4)
417
418 self.panel.SetAutoLayout(True)
419 self.panel.SetSizer(topBox)
420 topBox.Fit(self.panel)
421 topBox.SetSizeHints(self.panel)
422
423 panelSizer = wxBoxSizer(wxVERTICAL)
424 panelSizer.Add(self.panel, 1, wxEXPAND, 0)
425 self.SetAutoLayout(True)
426 self.SetSizer(panelSizer)
427 panelSizer.Fit(self)
428 panelSizer.SetSizeHints(self)
429
430 self.grid.SetFocus()
431
432 EVT_BUTTON(self, ID_QUERY, self.OnQuery)
433 EVT_BUTTON(self, ID_EXPORT, self.OnExport)
434 EVT_BUTTON(self, ID_EXPORTSEL, self.OnExportSel)
435 EVT_BUTTON(self, wxID_CLOSE, self.OnClose)
436 EVT_KEY_DOWN(self.grid, self.OnKeyDown)
437
438 self.grid.Subscribe(ROW_SELECTED, self.UpdateStatusText)
439
440 def UpdateStatusText(self, rows=None):
441 self.SetStatusText(_("%i rows (%i selected), %i columns")
442 % (self.grid.GetNumberRows(),
443 self.grid.GetNumberSelected(),
444 self.grid.GetNumberCols()))
445
446 def OnKeyDown(self, event):
447 """Catch query key from grid"""
448 if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
449 self.combo_fields.SetFocus()
450 self.combo_fields.refocus = True
451 else:
452 event.Skip()
453
454 def OnQuery(self, event):
455 ThubanBeginBusyCursor()
456 try:
457
458 text = self.combo_value.GetValue()
459 if self.combo_value.GetSelection() < 1 \
460 or self.combo_value.FindString(text) == -1:
461 value = text
462 else:
463 value = self.table.Column(text)
464
465 ids = self.table.SimpleQuery(
466 self.table.Column(self.combo_fields.GetStringSelection()),
467 self.choice_comp.GetStringSelection(),
468 value)
469
470 choice = self.choice_action.GetSelection()
471
472 #
473 # what used to be nice code got became a bit ugly because
474 # each time we select a row a message is sent to the grid
475 # which we are listening for and then we send further
476 # messages.
477 #
478 # now, we disable those listeners select everything but
479 # the first item, reenable the listeners, and select
480 # the first element, which causes everything to be
481 # updated properly.
482 #
483 if ids:
484 self.grid.ToggleEventListeners(False)
485
486 if choice == 0:
487 # Replace Selection
488 self.grid.ClearSelection()
489 elif choice == 1:
490 # Refine Selection
491 sel = self.get_selected()
492 self.grid.ClearSelection()
493 ids = filter(sel.has_key, ids)
494 elif choice == 2:
495 # Add to Selection
496 pass
497
498 #
499 # select the rows (all but the first)
500 #
501 firsttime = True
502 for id in ids:
503 if firsttime:
504 firsttime = False
505 else:
506 self.grid.SelectRowById(id, True)
507
508 self.grid.ToggleEventListeners(True)
509
510 #
511 # select the first row
512 #
513 if ids:
514 self.grid.SelectRowById(ids[0], True)
515
516 finally:
517 ThubanEndBusyCursor()
518
519 def doExport(self, onlySelected):
520
521 dlg = wxFileDialog(self, _("Export Table To"), ".", "",
522 _("DBF Files (*.dbf)|*.dbf|") +
523 _("CSV Files (*.csv)|*.csv|") +
524 _("All Files (*.*)|*.*"),
525 wxSAVE|wxOVERWRITE_PROMPT)
526 if dlg.ShowModal() == wxID_OK:
527 filename = dlg.GetPath()
528 type = os.path.basename(filename).split('.')[-1:][0]
529 dlg.Destroy()
530
531 if onlySelected:
532 records = self.grid.GetSelectedRows()
533 else:
534 records = None
535
536 if type.upper() == "DBF":
537 table_to_dbf(self.table, filename, records)
538 elif type.upper() == 'CSV':
539 table_to_csv(self.table, filename, records)
540 else:
541 dlg = wxMessageDialog(None, "Unsupported format: %s" % type,
542 "Table Export", wxOK|wxICON_WARNING)
543 dlg.ShowModal()
544 dlg.Destroy()
545 else:
546 dlg.Destroy()
547
548 def OnExport(self, event):
549 self.doExport(False)
550
551 def OnExportSel(self, event):
552 self.doExport(True)
553
554 def OnClose(self, event):
555 TableFrame.OnClose(self, event)
556
557 def get_selected(self):
558 """Return a dictionary of the selected rows.
559
560 The dictionary has the indexes as keys."""
561 to_id = self.table.RowOrdinalToId
562 return dict([(to_id(i), 0) for i in self.grid.GetSelectedRows()])
563
564
565 class LayerTableFrame(QueryTableFrame):
566
567 """Frame that displays a layer table in a grid view
568
569 A LayerTableFrame is a QueryTableFrame whose selection is connected to the
570 selected object in a map.
571 """
572
573 def __init__(self, parent, name, title, layer, table):
574 QueryTableFrame.__init__(self, parent, name, title, table)
575 self.layer = layer
576 self.grid.Subscribe(ROW_SELECTED, self.rows_selected)
577 self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)
578 self.map = self.parent.Map()
579 self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
580
581 # if there is already a selection present, update the grid
582 # accordingly
583 sel = self.get_selected().keys()
584 for i in sel:
585 self.grid.SelectRowById(i, True)
586
587 def OnDestroy(self, event):
588 """Extend inherited method to unsubscribe messages"""
589 self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
590 self.grid.Unsubscribe(ROW_SELECTED, self.rows_selected)
591 self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
592 QueryTableFrame.OnDestroy(self, event)
593
594 def make_grid(self, table):
595 """Override the derived method to return a LayerTableGrid.
596 """
597 return LayerTableGrid(self, table)
598
599 def get_selected(self):
600 """Override the derived method to return a dictionary of the selected
601 rows.
602 """
603 return dict([(i, 0) for i in self.parent.SelectedShapes()])
604
605 def select_shapes(self, layer, shapes):
606 """Subscribed to the SHAPES_SELECTED message.
607
608 If shapes contains exactly one shape id, select that shape in
609 the grid. Otherwise deselect all.
610 """
611 self.grid.select_shapes(layer, shapes)
612
613 def rows_selected(self, rows):
614 """Return the selected rows of the layer as they are returned
615 by Layer.SelectShapes().
616 """
617 if self.layer is not None:
618 self.parent.SelectShapes(self.layer, rows)
619
620 def map_layers_removed(self, *args):
621 """Receiver for the map's MAP_LAYERS_REMOVED message
622
623 Close the dialog if the layer whose table we're showing is not
624 in the map anymore.
625 """
626 if self.layer not in self.map.Layers():
627 self.Close()
628

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26