/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/UI/tableview.py
ViewVC logotype

Diff of /branches/WIP-pyshapelib-bramz/Thuban/UI/tableview.py

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

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

Legend:
Removed from v.6  
changed lines
  Added in v.1146

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26