/[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 1276 - (show 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 # 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 ThubanFrame
22
23 from messages import SHAPES_SELECTED, SESSION_REPLACED
24 from Thuban.Model.messages import TABLE_REMOVED, MAP_LAYERS_REMOVED
25 from Thuban.UI.common import ThubanBeginBusyCursor, ThubanEndBusyCursor
26
27 wx_value_type_map = {FIELDTYPE_INT: wxGRID_VALUE_NUMBER,
28 FIELDTYPE_DOUBLE: wxGRID_VALUE_FLOAT,
29 FIELDTYPE_STRING: wxGRID_VALUE_STRING}
30
31 ROW_SELECTED = "ROW_SELECTED"
32
33 QUERY_KEY = 'S'
34
35 class DataTable(wxPyGridTableBase):
36
37 """Wrapper around a Thuban table object suitable for a wxGrid"""
38
39 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 self.num_cols = table.NumColumns()
50 self.num_rows = table.NumRows()
51
52 self.columns = []
53 for i in range(self.num_cols):
54 col = table.Column(i)
55 self.columns.append((col.name, wx_value_type_map[col.type]))
56
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 record = self.table.ReadRowAsDict(row)
76 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
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 class TableGrid(wxGrid, Publisher):
138
139 """A grid view for a Thuban table
140
141 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 def __init__(self, parent, table = None):
150 wxGrid.__init__(self, parent, -1)
151
152 self.allow_messages_count = 0
153
154 # keep track of which rows are selected.
155 self.rows = {}
156
157 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 # would need to keep a reference to it and call its Destroy
162 # method later.
163 self.SetTable(self.table, True)
164
165 #self.SetMargins(0,0)
166
167 # 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 #self.AutoSizeColumns(False)
172
173 self.SetSelectionMode(wxGrid.wxGridSelectRows)
174
175 self.ToggleEventListeners(True)
176 EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)
177 EVT_GRID_SELECT_CELL(self, self.OnSelectCell)
178
179 # 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 def SetTableObject(self, table):
189 self.table.SetTable(table)
190
191 def OnRangeSelect(self, event):
192 if self.handleSelectEvents:
193 self.rows = dict([(i, 0) for i in self.GetSelectedRows()])
194
195 # 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
205 event.Skip()
206
207 def OnSelectCell(self, event):
208 if self.handleSelectEvents:
209 self.issue(ROW_SELECTED, self.GetSelectedRows())
210 event.Skip()
211
212 def ToggleEventListeners(self, on):
213 self.handleSelectEvents = on
214
215 def GetNumberSelected(self):
216 return len(self.rows)
217
218 def disallow_messages(self):
219 """Disallow messages to be send.
220
221 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 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 def select_shapes(self, layer, shapes):
254 """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 if layer is not None \
260 and layer.ShapeStore().Table() is self.table.table:
261
262 self.disallow_messages()
263 try:
264 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 finally:
280 self.allow_messages()
281
282
283 class TableFrame(ThubanFrame):
284
285 """Frame that displays a Thuban table in a grid view"""
286
287 def __init__(self, parent, name, title, table):
288 ThubanFrame.__init__(self, parent, name, title)
289 self.panel = wxPanel(self, -1)
290
291 self.table = table
292 self.grid = self.make_grid(self.table)
293 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
298
299 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 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 ThubanFrame.OnClose(self, event)
311
312 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 ID_QUERY = 4001
331 ID_EXPORT = 4002
332 ID_COMBOVALUE = 4003
333
334 class QueryTableFrame(TableFrame):
335
336 """Frame that displays a table in a grid view and offers user actions
337 selection and export
338
339 A QueryTableFrame is TableFrame whose selection is connected to the
340 selected object in a map.
341 """
342
343 def __init__(self, parent, name, title, table):
344 TableFrame.__init__(self, parent, name, title, table)
345
346 self.combo_fields = wxComboBox(self.panel, -1, style=wxCB_READONLY)
347 self.choice_comp = wxChoice(self.panel, -1,
348 choices=["<", "<=", "==", "!=", ">=", ">"])
349 self.combo_value = wxComboBox(self.panel, ID_COMBOVALUE)
350 self.choice_action = wxChoice(self.panel, -1,
351 choices=[_("Replace Selection"),
352 _("Refine Selection"),
353 _("Add to Selection")])
354
355 button_query = wxButton(self.panel, ID_QUERY, _("Query"))
356 button_saveas = wxButton(self.panel, ID_EXPORT, _("Export"))
357
358 self.CreateStatusBar()
359
360 self.grid.SetSize((400, 200))
361
362 self.combo_value.Append("")
363 for i in range(table.NumColumns()):
364 name = table.Column(i).name
365 self.combo_fields.Append(name)
366 self.combo_value.Append(name)
367
368 # assume at least one field?
369 self.combo_fields.SetSelection(0)
370 self.combo_value.SetSelection(0)
371 self.choice_action.SetSelection(0)
372 self.choice_comp.SetSelection(0)
373
374 self.grid.Reparent(self.panel)
375
376 self.UpdateStatusText()
377
378 topBox = wxBoxSizer(wxVERTICAL)
379
380 sizer = wxStaticBoxSizer(wxStaticBox(self.panel, -1,
381 _("Selection")),
382 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 sizer.Add(button_query, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
388 sizer.Add(40, 20, 0, wxALL, 4)
389 sizer.Add(button_saveas, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
390
391 topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
392 topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
393
394 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 self.SetAutoLayout(True)
402 self.SetSizer(panelSizer)
403 panelSizer.Fit(self)
404 panelSizer.SetSizeHints(self)
405
406 self.grid.SetFocus()
407
408 EVT_BUTTON(self, ID_QUERY, self.OnQuery)
409 EVT_BUTTON(self, ID_EXPORT, self.OnSaveAs)
410 EVT_KEY_DOWN(self.grid, self.OnKeyDown)
411 EVT_GRID_RANGE_SELECT(self.grid, self.OnGridSelectRange)
412 EVT_GRID_SELECT_CELL(self.grid, self.OnGridSelectCell)
413
414 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 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 def OnQuery(self, event):
437 ThubanBeginBusyCursor()
438 try:
439
440 text = self.combo_value.GetValue()
441 if self.combo_value.GetSelection() < 1 \
442 or self.combo_value.FindString(text) == -1:
443 value = text
444 else:
445 value = self.table.Column(text)
446
447 ids = self.table.SimpleQuery(
448 self.table.Column(self.combo_fields.GetStringSelection()),
449 self.choice_comp.GetStringSelection(),
450 value)
451
452 choice = self.choice_action.GetSelection()
453
454 #
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 if ids:
466 self.grid.ToggleEventListeners(False)
467
468 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
480 #
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
490 self.grid.ToggleEventListeners(True)
491
492 #
493 # select the first row
494 #
495 if ids:
496 self.grid.SelectRow(ids[0], True)
497
498 finally:
499 ThubanEndBusyCursor()
500
501 def OnSaveAs(self, event):
502 dlg = wxFileDialog(self, _("Export Table To"), ".", "",
503 _("DBF Files (*.dbf)|*.dbf|") +
504 _("CSV Files (*.csv)|*.csv|") +
505 _("All Files (*.*)|*.*"),
506 wxSAVE|wxOVERWRITE_PROMPT)
507 if dlg.ShowModal() == wxID_OK:
508 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
523 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 self.map = self.parent.Map()
546 self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
547
548 # 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 def make_grid(self, table):
555 """Override the derived method to return a LayerTableGrid.
556 """
557 return LayerTableGrid(self, table)
558
559 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 def OnClose(self, event):
566 """Override the derived method to first unsubscribed."""
567 self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
568 self.grid.Unsubscribe(ROW_SELECTED, self.rows_selected)
569 self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
570 QueryTableFrame.OnClose(self, event)
571
572 def select_shapes(self, layer, shapes):
573 """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 self.grid.select_shapes(layer, shapes)
579
580 def rows_selected(self, rows):
581 """Return the selected rows of the layer as they are returned
582 by Layer.SelectShapes().
583 """
584 if self.layer is not None:
585 self.parent.SelectShapes(self.layer, rows)
586
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