/[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 1199 - (show annotations)
Fri Jun 13 15:04:28 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: 19748 byte(s)
(TableGrid.__init__): Create an
        instance variable to keep track of how many rows are selected.
        Subscribe once to the the events we are interested in.
(ThubanGrid.OnRangeSelect): Only handle event if event handling
        hasn't been turned off.
(ThubanGrid.OnSelectCell): Only handle event if event handling
        hasn't been turned off.
(ThubanGrid.ToggleEventListeners): Rather than subscribe None
        as an event listener (which changes the event handler stack)
        simply set an instance variable to False. This is checked in
        the event handlers.
(ThubanGrid.GetNumberSelected): Return the number of currently
        selected rows.
(TableFrame): Inherit from ThubanFrame so we can have a
        status bar and control buttons.
(QueryTableFrame.__init__): Create a status bar. Fixes RTbug #1942.
        Explicitly set which items are selected in the operator choice and
        action choice so there is always a valid selection. Fixes RTbug #1941.
        Subscribe to grid cell selection events so we can update the
        status bar.
(QueryTableFrame.UpdateStatusText): Update the status bar with
        how many rows are in the grid, how many columns, and how many
        rows are selected.
(QueryTableFrame.OnGridSelectRange, QueryTableFrame.OnGridSelectCell):
        Call UpdateStatusText when cells are (de)selected.
(QueryTableFrame.OnQuery): Use the string value in the value
        combo if either the selected item index is 0 or if the string
        cannot be found in the predefined list (this happens if the
        user changes the text). Fixes RTbug #1940.
        Only turn off the grid event listeners if there a query comes
        back with a none empty list of ids. in the case that the list
        is empty this causes a grid.ClearSelection() call to actually
        clear the grid selection which causes the selected items in
        the map to be deselected. Fixes RTbug #1939.

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.table = table
289 self.grid = self.make_grid(self.table)
290 self.app = self.parent.application
291 self.app.Subscribe(SESSION_REPLACED, self.close_on_session_replaced)
292 self.session = self.app.Session()
293 self.session.Subscribe(TABLE_REMOVED, self.close_on_table_removed)
294
295 def make_grid(self, table):
296 """Return the table grid to use in the frame.
297
298 The default implementation returns a TableGrid instance.
299 Override in derived classes to use different grid classes.
300 """
301 return TableGrid(self, table)
302
303 def OnClose(self, event):
304 self.app.Unsubscribe(SESSION_REPLACED, self.close_on_session_replaced)
305 self.session.Unsubscribe(TABLE_REMOVED, self.close_on_table_removed)
306 ThubanFrame.OnClose(self, event)
307
308 def close_on_session_replaced(self, *args):
309 """Subscriber for the SESSION_REPLACED messages.
310
311 The table frame is tied to a session so close the window when
312 the session changes.
313 """
314 self.Close()
315
316 def close_on_table_removed(self, table):
317 """Subscriber for the TABLE_REMOVED messages.
318
319 The table frame is tied to a particular table so close the
320 window when the table is removed.
321 """
322 if table is self.table:
323 self.Close()
324
325
326 ID_QUERY = 4001
327 ID_EXPORT = 4002
328 ID_COMBOVALUE = 4003
329
330 class QueryTableFrame(TableFrame):
331
332 """Frame that displays a table in a grid view and offers user actions
333 selection and export
334
335 A QueryTableFrame is TableFrame whose selection is connected to the
336 selected object in a map.
337 """
338
339 def __init__(self, parent, name, title, table):
340 TableFrame.__init__(self, parent, name, title, table)
341
342 self.combo_fields = wxComboBox(self, -1, style=wxCB_READONLY)
343 self.choice_comp = wxChoice(self, -1,
344 choices=["<", "<=", "==", "!=", ">=", ">"])
345 self.combo_value = wxComboBox(self, ID_COMBOVALUE)
346 self.choice_action = wxChoice(self, -1,
347 choices=[_("Replace Selection"),
348 _("Refine Selection"),
349 _("Add to Selection")])
350
351 button_query = wxButton(self, ID_QUERY, _("Query"))
352 button_saveas = wxButton(self, ID_EXPORT, _("Export"))
353
354 self.CreateStatusBar()
355 self.SetStatusText(_("0 rows (0 selected), 0 columns"))
356
357 self.grid.SetSize((400, 200))
358
359 self.combo_value.Append("")
360 for i in range(table.NumColumns()):
361 name = table.Column(i).name
362 self.combo_fields.Append(name)
363 self.combo_value.Append(name)
364
365 # assume at least one field?
366 self.combo_fields.SetSelection(0)
367 self.combo_value.SetSelection(0)
368 self.choice_action.SetSelection(0)
369 self.choice_comp.SetSelection(0)
370
371 topBox = wxBoxSizer(wxVERTICAL)
372
373 sizer = wxStaticBoxSizer(wxStaticBox(self, -1,
374 _("Selection")),
375 wxHORIZONTAL)
376 sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)
377 sizer.Add(self.choice_comp, 0, wxALL, 4)
378 sizer.Add(self.combo_value, 1, wxEXPAND|wxALL, 4)
379 sizer.Add(self.choice_action, 0, wxALL, 4)
380 sizer.Add(button_query, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
381 sizer.Add(40, 20, 0, wxALL, 4)
382 sizer.Add(button_saveas, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
383
384 topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
385 topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
386
387 self.SetAutoLayout(True)
388 self.SetSizer(topBox)
389 topBox.Fit(self)
390 topBox.SetSizeHints(self)
391
392 self.grid.SetFocus()
393 EVT_BUTTON(self, ID_QUERY, self.OnQuery)
394 EVT_BUTTON(self, ID_EXPORT, self.OnSaveAs)
395 EVT_KEY_DOWN(self.grid, self.OnKeyDown)
396 EVT_GRID_RANGE_SELECT(self.grid, self.OnGridSelectRange)
397 EVT_GRID_SELECT_CELL(self.grid, self.OnGridSelectCell)
398
399 def UpdateStatusText(self):
400 self.SetStatusText(_("%i rows (%i selected), %i columns")
401 % (self.grid.GetNumberRows(),
402 self.grid.GetNumberSelected(),
403 self.grid.GetNumberCols()))
404
405 def OnGridSelectRange(self, event):
406 self.UpdateStatusText()
407 event.Skip()
408
409 def OnGridSelectCell(self, event):
410 self.UpdateStatusText()
411 event.Skip()
412
413 def OnKeyDown(self, event):
414 """Catch query key from grid"""
415 if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
416 self.combo_fields.SetFocus()
417 self.combo_fields.refocus = True
418 else:
419 event.Skip()
420
421 def OnQuery(self, event):
422 wxBeginBusyCursor()
423 try:
424
425 text = self.combo_value.GetValue()
426 if self.combo_value.GetSelection() < 1 \
427 or self.combo_value.FindString(text) == -1:
428 value = text
429 else:
430 value = self.table.Column(text)
431
432 ids = self.table.SimpleQuery(
433 self.table.Column(self.combo_fields.GetStringSelection()),
434 self.choice_comp.GetStringSelection(),
435 value)
436
437 choice = self.choice_action.GetSelection()
438
439 #
440 # what used to be nice code got became a bit ugly because
441 # each time we select a row a message is sent to the grid
442 # which we are listening for and then we send further
443 # messages.
444 #
445 # now, we disable those listeners select everything but
446 # the first item, reenable the listeners, and select
447 # the first element, which causes everything to be
448 # updated properly.
449 #
450 if ids:
451 self.grid.ToggleEventListeners(False)
452
453 if choice == 0:
454 # Replace Selection
455 self.grid.ClearSelection()
456 elif choice == 1:
457 # Refine Selection
458 sel = self.get_selected()
459 self.grid.ClearSelection()
460 ids = filter(sel.has_key, ids)
461 elif choice == 2:
462 # Add to Selection
463 pass
464
465 #
466 # select the rows (all but the first)
467 #
468 firsttime = True
469 for id in ids:
470 if firsttime:
471 firsttime = False
472 else:
473 self.grid.SelectRow(id, True)
474
475 self.grid.ToggleEventListeners(True)
476
477 #
478 # select the first row
479 #
480 if ids:
481 self.grid.SelectRow(ids[0], True)
482
483 finally:
484 wxEndBusyCursor()
485
486 def OnSaveAs(self, event):
487 dlg = wxFileDialog(self, _("Export Table To"), ".", "",
488 _("DBF Files (*.dbf)|*.dbf|") +
489 _("CSV Files (*.csv)|*.csv|") +
490 _("All Files (*.*)|*.*"),
491 wxSAVE|wxOVERWRITE_PROMPT)
492 if dlg.ShowModal() == wxID_OK:
493 filename = dlg.GetPath()
494 type = os.path.basename(filename).split('.')[-1:][0]
495 dlg.Destroy()
496 if type.upper() == "DBF":
497 table_to_dbf(self.table, filename)
498 elif type.upper() == 'CSV':
499 table_to_csv(self.table, filename)
500 else:
501 dlg = wxMessageDialog(None, "Unsupported format: %s" % type,
502 "Table Export", wxOK|wxICON_WARNING)
503 dlg.ShowModal()
504 dlg.Destroy()
505 else:
506 dlg.Destroy()
507
508 def OnClose(self, event):
509 TableFrame.OnClose(self, event)
510
511 def get_selected(self):
512 """Return a dictionary of the selected rows.
513
514 The dictionary has sthe indexes as keys."""
515 return dict([(i, 0) for i in self.grid.GetSelectedRows()])
516
517 class LayerTableFrame(QueryTableFrame):
518
519 """Frame that displays a layer table in a grid view
520
521 A LayerTableFrame is a QueryTableFrame whose selection is connected to the
522 selected object in a map.
523 """
524
525 def __init__(self, parent, name, title, layer, table):
526 QueryTableFrame.__init__(self, parent, name, title, table)
527 self.layer = layer
528 self.grid.Subscribe(ROW_SELECTED, self.rows_selected)
529 self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)
530 self.map = self.parent.Map()
531 self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
532
533 # if there is already a selection present, update the grid
534 # accordingly
535 sel = self.get_selected().keys()
536 for i in sel:
537 self.grid.SelectRow(i, True)
538
539 def make_grid(self, table):
540 """Override the derived method to return a LayerTableGrid.
541 """
542 return LayerTableGrid(self, table)
543
544 def get_selected(self):
545 """Override the derived method to return a dictionary of the selected
546 rows.
547 """
548 return dict([(i, 0) for i in self.parent.SelectedShapes()])
549
550 def OnClose(self, event):
551 """Override the derived method to first unsubscribed."""
552 self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
553 self.grid.Unsubscribe(ROW_SELECTED, self.rows_selected)
554 self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
555 QueryTableFrame.OnClose(self, event)
556
557 def select_shapes(self, layer, shapes):
558 """Subscribed to the SHAPES_SELECTED message.
559
560 If shapes contains exactly one shape id, select that shape in
561 the grid. Otherwise deselect all.
562 """
563 self.grid.select_shapes(layer, shapes)
564
565 def rows_selected(self, rows):
566 """Return the selected rows of the layer as they are returned
567 by Layer.SelectShapes().
568 """
569 if self.layer is not None:
570 self.parent.SelectShapes(self.layer, rows)
571
572 def map_layers_removed(self, *args):
573 """Receiver for the map's MAP_LAYERS_REMOVED message
574
575 Close the dialog if the layer whose table we're showing is not
576 in the map anymore.
577 """
578 if self.layer not in self.map.Layers():
579 self.Close()
580

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26