/[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 1394 - (show annotations)
Thu Jul 10 14:54:58 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: 20726 byte(s)
(QueryTableFrame.__init__): Add an
        Export Selection button and move the export buttons underneath
        the table.
(QueryTableFrame.UpdateStatusText): Added event argument so
        that it can respond to grid selection events. The status text
        is now updated even when the table is not associated with a
        layer as was previously assumed.
(QueryTableFrame.OnGridSelectRange, OnGridSelectCell): Removed.
        UpdateStatusText responds to these events.
(QueryTableFrame.OnSaveAs): Renamed to doExport.
(QueryTableFrame.doExport): Helper function that saves the
        entire table, or selected rows, to a file.
(QueryTableFrame.OnExport, QueryTableFrame.OnExportSel): New.
        Respond to export button events and call doExport.

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)
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.ShapeStore().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 ID_EXPORTSEL = 4004
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_export = wxButton(self.panel, ID_EXPORT, _("Export"))
357 button_exportSel = wxButton(self.panel, ID_EXPORTSEL, _("Export Selection"))
358 button_close = wxButton(self.panel, wxID_CLOSE, _("Close"))
359
360 self.CreateStatusBar()
361
362 self.grid.SetSize((400, 200))
363
364 self.combo_value.Append("")
365 for i in range(table.NumColumns()):
366 name = table.Column(i).name
367 self.combo_fields.Append(name)
368 self.combo_value.Append(name)
369
370 # assume at least one field?
371 self.combo_fields.SetSelection(0)
372 self.combo_value.SetSelection(0)
373 self.choice_action.SetSelection(0)
374 self.choice_comp.SetSelection(0)
375
376 self.grid.Reparent(self.panel)
377
378 self.UpdateStatusText()
379
380 topBox = wxBoxSizer(wxVERTICAL)
381
382 sizer = wxStaticBoxSizer(wxStaticBox(self.panel, -1,
383 _("Selection")),
384 wxHORIZONTAL)
385 sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)
386 sizer.Add(self.choice_comp, 0, wxALL, 4)
387 sizer.Add(self.combo_value, 1, wxEXPAND|wxALL, 4)
388 sizer.Add(self.choice_action, 0, wxALL, 4)
389 sizer.Add(button_query, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
390 sizer.Add(40, 20, 0, wxALL, 4)
391
392 topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
393 topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
394
395 sizer = wxBoxSizer(wxHORIZONTAL)
396 sizer.Add(button_export, 0, wxALL, 4)
397 sizer.Add(button_exportSel, 0, wxALL, 4)
398 sizer.Add(60, 20, 1, wxALL|wxEXPAND, 4)
399 sizer.Add(button_close, 0, wxALL|wxALIGN_RIGHT, 4)
400 topBox.Add(sizer, 0, wxALL | wxEXPAND, 4)
401
402 self.panel.SetAutoLayout(True)
403 self.panel.SetSizer(topBox)
404 topBox.Fit(self.panel)
405 topBox.SetSizeHints(self.panel)
406
407 panelSizer = wxBoxSizer(wxVERTICAL)
408 panelSizer.Add(self.panel, 1, wxEXPAND, 0)
409 self.SetAutoLayout(True)
410 self.SetSizer(panelSizer)
411 panelSizer.Fit(self)
412 panelSizer.SetSizeHints(self)
413
414 self.grid.SetFocus()
415
416 EVT_BUTTON(self, ID_QUERY, self.OnQuery)
417 EVT_BUTTON(self, ID_EXPORT, self.OnExport)
418 EVT_BUTTON(self, ID_EXPORTSEL, self.OnExportSel)
419 EVT_BUTTON(self, wxID_CLOSE, self.OnClose)
420 EVT_KEY_DOWN(self.grid, self.OnKeyDown)
421
422 self.grid.Subscribe(ROW_SELECTED, self.UpdateStatusText)
423
424 def UpdateStatusText(self, rows=None):
425 self.SetStatusText(_("%i rows (%i selected), %i columns")
426 % (self.grid.GetNumberRows(),
427 self.grid.GetNumberSelected(),
428 self.grid.GetNumberCols()))
429
430 def OnKeyDown(self, event):
431 """Catch query key from grid"""
432 if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
433 self.combo_fields.SetFocus()
434 self.combo_fields.refocus = True
435 else:
436 event.Skip()
437
438 def OnQuery(self, event):
439 ThubanBeginBusyCursor()
440 try:
441
442 text = self.combo_value.GetValue()
443 if self.combo_value.GetSelection() < 1 \
444 or self.combo_value.FindString(text) == -1:
445 value = text
446 else:
447 value = self.table.Column(text)
448
449 ids = self.table.SimpleQuery(
450 self.table.Column(self.combo_fields.GetStringSelection()),
451 self.choice_comp.GetStringSelection(),
452 value)
453
454 choice = self.choice_action.GetSelection()
455
456 #
457 # what used to be nice code got became a bit ugly because
458 # each time we select a row a message is sent to the grid
459 # which we are listening for and then we send further
460 # messages.
461 #
462 # now, we disable those listeners select everything but
463 # the first item, reenable the listeners, and select
464 # the first element, which causes everything to be
465 # updated properly.
466 #
467 if ids:
468 self.grid.ToggleEventListeners(False)
469
470 if choice == 0:
471 # Replace Selection
472 self.grid.ClearSelection()
473 elif choice == 1:
474 # Refine Selection
475 sel = self.get_selected()
476 self.grid.ClearSelection()
477 ids = filter(sel.has_key, ids)
478 elif choice == 2:
479 # Add to Selection
480 pass
481
482 #
483 # select the rows (all but the first)
484 #
485 firsttime = True
486 for id in ids:
487 if firsttime:
488 firsttime = False
489 else:
490 self.grid.SelectRow(id, True)
491
492 self.grid.ToggleEventListeners(True)
493
494 #
495 # select the first row
496 #
497 if ids:
498 self.grid.SelectRow(ids[0], True)
499
500 finally:
501 ThubanEndBusyCursor()
502
503 def doExport(self, onlySelected):
504
505 dlg = wxFileDialog(self, _("Export Table To"), ".", "",
506 _("DBF Files (*.dbf)|*.dbf|") +
507 _("CSV Files (*.csv)|*.csv|") +
508 _("All Files (*.*)|*.*"),
509 wxSAVE|wxOVERWRITE_PROMPT)
510 if dlg.ShowModal() == wxID_OK:
511 filename = dlg.GetPath()
512 type = os.path.basename(filename).split('.')[-1:][0]
513 dlg.Destroy()
514
515 if onlySelected:
516 records = self.grid.GetSelectedRows()
517 else:
518 records = None
519
520 if type.upper() == "DBF":
521 table_to_dbf(self.table, filename, records)
522 elif type.upper() == 'CSV':
523 table_to_csv(self.table, filename, records)
524 else:
525 dlg = wxMessageDialog(None, "Unsupported format: %s" % type,
526 "Table Export", wxOK|wxICON_WARNING)
527 dlg.ShowModal()
528 dlg.Destroy()
529 else:
530 dlg.Destroy()
531
532 def OnExport(self, event):
533 self.doExport(False)
534
535 def OnExportSel(self, event):
536 self.doExport(True)
537
538 def OnClose(self, event):
539 TableFrame.OnClose(self, event)
540
541 def get_selected(self):
542 """Return a dictionary of the selected rows.
543
544 The dictionary has the indexes as keys."""
545 return dict([(i, 0) for i in self.grid.GetSelectedRows()])
546
547 class LayerTableFrame(QueryTableFrame):
548
549 """Frame that displays a layer table in a grid view
550
551 A LayerTableFrame is a QueryTableFrame whose selection is connected to the
552 selected object in a map.
553 """
554
555 def __init__(self, parent, name, title, layer, table):
556 QueryTableFrame.__init__(self, parent, name, title, table)
557 self.layer = layer
558 self.grid.Subscribe(ROW_SELECTED, self.rows_selected)
559 self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)
560 self.map = self.parent.Map()
561 self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
562
563 # if there is already a selection present, update the grid
564 # accordingly
565 sel = self.get_selected().keys()
566 for i in sel:
567 self.grid.SelectRow(i, True)
568
569 def make_grid(self, table):
570 """Override the derived method to return a LayerTableGrid.
571 """
572 return LayerTableGrid(self, table)
573
574 def get_selected(self):
575 """Override the derived method to return a dictionary of the selected
576 rows.
577 """
578 return dict([(i, 0) for i in self.parent.SelectedShapes()])
579
580 def OnClose(self, event):
581 """Override the derived method to first unsubscribed."""
582 self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
583 self.grid.Unsubscribe(ROW_SELECTED, self.rows_selected)
584 self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
585 QueryTableFrame.OnClose(self, event)
586
587 def select_shapes(self, layer, shapes):
588 """Subscribed to the SHAPES_SELECTED message.
589
590 If shapes contains exactly one shape id, select that shape in
591 the grid. Otherwise deselect all.
592 """
593 self.grid.select_shapes(layer, shapes)
594
595 def rows_selected(self, rows):
596 """Return the selected rows of the layer as they are returned
597 by Layer.SelectShapes().
598 """
599 if self.layer is not None:
600 self.parent.SelectShapes(self.layer, rows)
601
602 def map_layers_removed(self, *args):
603 """Receiver for the map's MAP_LAYERS_REMOVED message
604
605 Close the dialog if the layer whose table we're showing is not
606 in the map anymore.
607 """
608 if self.layer not in self.map.Layers():
609 self.Close()
610

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26