/[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 1202 - (show annotations)
Fri Jun 13 16:01:10 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: 20095 byte(s)
(TableFrame.__init__): Add a panel
        object that can be used by derived classes to place any
        controls (including the grid) onto.
(QueryTableFrame.__init__): Use the panel as the parent window
        for all the controls. Reparent the grid so that the panel is
        the parent. Call UpdateStatusText() to correctly initialize
        the status bar.

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26