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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26