/[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 1068 - (show annotations)
Tue May 27 15:02:37 2003 UTC (21 years, 9 months ago) by bh
Original Path: trunk/thuban/Thuban/UI/tableview.py
File MIME type: text/x-python
File size: 16638 byte(s)
* Thuban/Model/messages.py (TABLE_REMOVED): New message.

* Thuban/Model/session.py (Session.UnreferencedTables): New method
to return tables that are not referenced by other tables or shape
stores and can be removed.
(Session.RemoveTable): Issue a TABLE_REMOVED message after
removing the table

* Thuban/UI/mainwindow.py: Remove unused imports
(MainWindow.TableClose): Implement.

* Thuban/UI/tableview.py (TableFrame.__init__): Subscribe to some
messages so that the frame will be automatically closed when a new
session is opened or the table is removed.
(TableFrame.OnClose): Unsubscribe the Subscriptions made in
__init__
(TableFrame.close_on_session_replaced)
(TableFrame.close_on_table_removed): New. Subscribers that close
the window

* test/test_session.py (TestSessionMessages.test_remove_table)
(TestSessionSimple.test_remove_table): Move the test to
TestSessionSimple and add test for the TABLE_REMOVED message
(TestSessionBase.setUp): Also subscribe to TABLE_REMOVED
(TestSessionSimple.test_unreferenced_tables) New. Test for the
UnreferencedTables method.
(UnreferencedTablesTests): New. Class with some more sophisticated
tests for UnreferencedTables.

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