/[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 1096 - (show annotations)
Thu May 29 10:03:07 2003 UTC (21 years, 9 months ago) by jan
Original Path: trunk/thuban/Thuban/UI/tableview.py
File MIME type: text/x-python
File size: 18047 byte(s)
(LayerTableFrame.OnClose): Bug-fix: Now
unsubscribes all that is subcribed in __init__.

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 import view
21 from dialogs import NonModalNonParentDialog
22
23 from messages import SHAPES_SELECTED, SESSION_REPLACED
24 from Thuban.Model.messages import TABLE_REMOVED
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)
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 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 class TableGrid(wxGrid, Publisher):
137
138 """A grid view for a Thuban table
139
140 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 def __init__(self, parent, table = None):
149 wxGrid.__init__(self, parent, -1)
150
151 self.allow_messages_count = 0
152
153 self.table = DataTable(table)
154
155 # The second parameter means that the grid is to take ownership
156 # of the table and will destroy it when done. Otherwise you
157 # would need to keep a reference to it and call its Destroy
158 # method later.
159 self.SetTable(self.table, True)
160
161 #self.SetMargins(0,0)
162
163 # AutoSizeColumns would allow us to make the grid have optimal
164 # column widths automatically but it would cause a traversal of
165 # the entire table which for large .dbf files can take a very
166 # long time.
167 #self.AutoSizeColumns(False)
168
169 self.SetSelectionMode(wxGrid.wxGridSelectRows)
170
171 self.ToggleEventListeners(True)
172
173 # Replace the normal renderers with our own versions which
174 # render NULL/None values specially
175 self.RegisterDataType(wxGRID_VALUE_STRING,
176 NullRenderer(wxGridCellStringRenderer()), None)
177 self.RegisterDataType(wxGRID_VALUE_NUMBER,
178 NullRenderer(wxGridCellNumberRenderer()), None)
179 self.RegisterDataType(wxGRID_VALUE_FLOAT,
180 NullRenderer(wxGridCellFloatRenderer()), None)
181
182 def SetTableObject(self, table):
183 self.table.SetTable(table)
184
185 def OnRangeSelect(self, event):
186 rows = dict([(i, 0) for i in self.GetSelectedRows()])
187
188 # if we're selecting we need to include the selected range and
189 # make sure that the current row is also included, which may
190 # not be the case if you just click on a single row!
191 if event.Selecting():
192 for i in range(event.GetTopRow(), event.GetBottomRow() + 1):
193 rows[i] = 0
194 rows[event.GetTopLeftCoords().GetRow()] = 0
195
196 self.issue(ROW_SELECTED, rows.keys())
197 event.Skip()
198
199 def OnSelectCell(self, event):
200 self.issue(ROW_SELECTED, self.GetSelectedRows())
201 event.Skip()
202
203 def ToggleEventListeners(self, on):
204 if on:
205 EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)
206 EVT_GRID_SELECT_CELL(self, self.OnSelectCell)
207 else:
208 EVT_GRID_RANGE_SELECT(self, None)
209 EVT_GRID_SELECT_CELL(self, None)
210
211 def disallow_messages(self):
212 """Disallow messages to be send.
213
214 This method only increases a counter so that calls to
215 disallow_messages and allow_messages can be nested. Only the
216 outermost calls will actually switch message sending on and off.
217 """
218 self.allow_messages_count += 1
219
220 def allow_messages(self):
221 """Allow messages to be send.
222
223 This method only decreases a counter so that calls to
224 disallow_messages and allow_messages can be nested. Only the
225 outermost calls will actually switch message sending on and off.
226 """
227 self.allow_messages_count -= 1
228
229 def issue(self, *args):
230 """Issue a message unless disallowed.
231
232 See the allow_messages and disallow_messages methods.
233 """
234 if self.allow_messages_count == 0:
235 Publisher.issue(self, *args)
236
237
238 class LayerTableGrid(TableGrid):
239
240 """Table grid for the layer tables.
241
242 The LayerTableGrid is basically the same as a TableGrid but it's
243 selection is usually coupled to the selected object in the map.
244 """
245
246 def select_shapes(self, layer, shapes):
247 """Select the row corresponding to the specified shape and layer
248
249 If layer is not the layer the table is associated with do
250 nothing. If shape or layer is None also do nothing.
251 """
252 if layer is not None \
253 and layer.table is self.table.table:
254
255 self.disallow_messages()
256 try:
257 self.ClearSelection()
258 if len(shapes) > 0:
259 #
260 # keep track of the lowest id so we can make it
261 # the first visible item
262 #
263 first = shapes[0]
264
265 for shape in shapes:
266 self.SelectRow(shape, True)
267 if shape < first:
268 first = shape
269
270 self.SetGridCursor(first, 0)
271 self.MakeCellVisible(first, 0)
272 finally:
273 self.allow_messages()
274
275
276 class TableFrame(NonModalNonParentDialog):
277
278 """Frame that displays a Thuban table in a grid view"""
279
280 def __init__(self, parent, name, title, table):
281 NonModalNonParentDialog.__init__(self, parent, name, title)
282 self.table = table
283 self.grid = self.make_grid(self.table)
284 self.app = self.parent.application
285 self.app.Subscribe(SESSION_REPLACED, self.close_on_session_replaced)
286 self.session = self.app.Session()
287 self.session.Subscribe(TABLE_REMOVED, self.close_on_table_removed)
288
289 def make_grid(self, table):
290 """Return the table grid to use in the frame.
291
292 The default implementation returns a TableGrid instance.
293 Override in derived classes to use different grid classes.
294 """
295 return TableGrid(self, table)
296
297 def OnClose(self, event):
298 self.app.Unsubscribe(SESSION_REPLACED, self.close_on_session_replaced)
299 self.session.Unsubscribe(TABLE_REMOVED, self.close_on_table_removed)
300 NonModalNonParentDialog.OnClose(self, event)
301
302 def close_on_session_replaced(self, *args):
303 """Subscriber for the SESSION_REPLACED messages.
304
305 The table frame is tied to a session so close the window when
306 the session changes.
307 """
308 self.Close()
309
310 def close_on_table_removed(self, table):
311 """Subscriber for the TABLE_REMOVED messages.
312
313 The table frame is tied to a particular table so close the
314 window when the table is removed.
315 """
316 if table is self.table:
317 self.Close()
318
319
320 ID_QUERY = 4001
321 ID_EXPORT = 4002
322
323 class QueryTableFrame(TableFrame):
324
325 """Frame that displays a table in a grid view and offers user actions
326 selection and export
327
328 A QueryTableFrame is TableFrame whose selection is connected to the
329 selected object in a map.
330 """
331
332 def __init__(self, parent, name, title, table):
333 TableFrame.__init__(self, parent, name, title, table)
334
335 self.combo_fields = wxComboBox(self, -1, style=wxCB_READONLY)
336 self.choice_comp = wxChoice(self, -1,
337 choices=["<", "<=", "==", "!=", ">=", ">"])
338 self.combo_value = wxComboBox(self, -1)
339 self.choice_action = wxChoice(self, -1,
340 choices=[_("Replace Selection"),
341 _("Refine Selection"),
342 _("Add to Selection")])
343
344 button_query = wxButton(self, ID_QUERY, _("Query"))
345 button_saveas = wxButton(self, ID_EXPORT, _("Export"))
346
347 self.grid.SetSize((400, 200))
348
349 self.combo_value.Append("")
350 for i in range(table.NumColumns()):
351 name = table.Column(i).name
352 self.combo_fields.Append(name)
353 self.combo_value.Append(name)
354
355 # assume at least one field?
356 self.combo_fields.SetSelection(0)
357 self.combo_value.SetSelection(0)
358
359 topBox = wxBoxSizer(wxVERTICAL)
360
361 sizer = wxStaticBoxSizer(wxStaticBox(self, -1,
362 _("Selection")),
363 wxHORIZONTAL)
364 sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)
365 sizer.Add(self.choice_comp, 0, wxALL, 4)
366 sizer.Add(self.combo_value, 1, wxEXPAND|wxALL, 4)
367 sizer.Add(self.choice_action, 0, wxALL, 4)
368 sizer.Add(button_query, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
369 sizer.Add(40, 20, 0, wxALL, 4)
370 sizer.Add(button_saveas, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
371
372 topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
373 topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
374
375 self.SetAutoLayout(True)
376 self.SetSizer(topBox)
377 topBox.Fit(self)
378 topBox.SetSizeHints(self)
379
380 self.grid.SetFocus()
381 EVT_BUTTON(self, ID_QUERY, self.OnQuery)
382 EVT_BUTTON(self, ID_EXPORT, self.OnSaveAs)
383 EVT_KEY_DOWN(self.grid, self.OnKeyDown)
384
385 def OnKeyDown(self, event):
386 """Catch query key from grid"""
387 if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
388 self.combo_fields.SetFocus()
389 self.combo_fields.refocus = True
390 else:
391 event.Skip()
392
393
394 def OnQuery(self, event):
395 wxBeginBusyCursor()
396
397 if self.combo_value.GetSelection() < 1:
398 value = self.combo_value.GetValue()
399 else:
400 value = self.table.Column(self.combo_value.GetValue())
401
402 ids = self.table.SimpleQuery(
403 self.table.Column(self.combo_fields.GetStringSelection()),
404 self.choice_comp.GetStringSelection(),
405 value)
406
407 choice = self.choice_action.GetSelection()
408
409 #
410 # what used to be nice code got became a bit ugly because
411 # each time we select a row a message is sent to the grid
412 # which we are listening for and then we send further
413 # messages.
414 #
415 # now, we disable those listeners select everything but
416 # the first item, reenable the listeners, and select
417 # the first element, which causes everything to be
418 # updated properly.
419 #
420 self.grid.ToggleEventListeners(False)
421
422 if choice == 0:
423 # Replace Selection
424 self.grid.ClearSelection()
425 elif choice == 1:
426 # Refine Selection
427 sel = self.get_selected()
428 self.grid.ClearSelection()
429 ids = filter(sel.has_key, ids)
430 elif choice == 2:
431 # Add to Selection
432 pass
433
434 #
435 # select the rows (all but the first)
436 #
437 firsttime = True
438 for id in ids:
439 if firsttime:
440 firsttime = False
441 else:
442 self.grid.SelectRow(id, True)
443
444 self.grid.ToggleEventListeners(True)
445
446 #
447 # select the first row
448 #
449 if ids:
450 self.grid.SelectRow(ids[0], True)
451
452 wxEndBusyCursor()
453
454 def OnSaveAs(self, event):
455 dlg = wxFileDialog(self, _("Export Table To"), ".", "",
456 _("DBF Files (*.dbf)|*.dbf|") +
457 _("CSV Files (*.csv)|*.csv|") +
458 _("All Files (*.*)|*.*"),
459 wxSAVE|wxOVERWRITE_PROMPT)
460 if dlg.ShowModal() == wxID_OK:
461 filename = dlg.GetPath()
462 type = os.path.basename(filename).split('.')[-1:][0]
463 dlg.Destroy()
464 if type.upper() == "DBF":
465 table_to_dbf(self.table, filename)
466 elif type.upper() == 'CSV':
467 table_to_csv(self.table, filename)
468 else:
469 dlg = wxMessageDialog(None, "Unsupported format: %s" % type,
470 "Table Export", wxOK|wxICON_WARNING)
471 dlg.ShowModal()
472 dlg.Destroy()
473 else:
474 dlg.Destroy()
475
476 def OnClose(self, event):
477 TableFrame.OnClose(self, event)
478
479 def get_selected(self):
480 """Return a dictionary of the selected rows.
481
482 The dictionary has sthe indexes as keys."""
483 return dict([(i, 0) for i in self.grid.GetSelectedRows()])
484
485 class LayerTableFrame(QueryTableFrame):
486
487 """Frame that displays a layer table in a grid view
488
489 A LayerTableFrame is a QueryTableFrame whose selection is connected to the
490 selected object in a map.
491 """
492
493 def __init__(self, parent, name, title, layer, table):
494 QueryTableFrame.__init__(self, parent, name, title, table)
495 self.layer = layer
496 self.grid.Subscribe(ROW_SELECTED, self.rows_selected)
497 self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)
498
499 # if there is already a selection present, update the grid
500 # accordingly
501 sel = self.get_selected().keys()
502 for i in sel:
503 self.grid.SelectRow(i, True)
504
505 def make_grid(self, table):
506 """Override the derived method to return a LayerTableGrid.
507 """
508 return LayerTableGrid(self, table)
509
510 def get_selected(self):
511 """Override the derived method to return a dictionary of the selected
512 rows.
513 """
514 return dict([(i, 0) for i in self.parent.SelectedShapes()])
515
516 def OnClose(self, event):
517 """Override the derived method to first unsubscribed."""
518 self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
519 self.grid.Unsubscribe(ROW_SELECTED, self.rows_selected)
520 QueryTableFrame.OnClose(self, event)
521
522 def select_shapes(self, layer, shapes):
523 """Subscribed to the SHAPES_SELECTED message.
524
525 If shapes contains exactly one shape id, select that shape in
526 the grid. Otherwise deselect all.
527 """
528 self.grid.select_shapes(layer, shapes)
529
530 def rows_selected(self, rows):
531 """Return the selected rows of the layer as they are returned
532 by Layer.SelectShapes().
533 """
534 if self.layer is not None:
535 self.parent.SelectShapes(self.layer, rows)

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26