/[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 173 by bh, Wed May 15 13:29:34 2002 UTC revision 1219 by bh, Mon Jun 16 17:42:54 2003 UTC
# Line 1  Line 1 
1  # Copyright (c) 2001, 2002 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  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 dialogs import NonModalDialog  from dialogs import ThubanFrame
22  from messages import SELECTED_SHAPE  
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,
# Line 23  wx_value_type_map = {FIELDTYPE_INT: wxGR Line 29  wx_value_type_map = {FIELDTYPE_INT: wxGR
29    
30  ROW_SELECTED = "ROW_SELECTED"  ROW_SELECTED = "ROW_SELECTED"
31    
32    QUERY_KEY = 'S'
33    
34  class DataTable(wxPyGridTableBase):  class DataTable(wxPyGridTableBase):
35    
# Line 38  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 64  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 95  class DataTable(wxPyGridTableBase): Line 102  class DataTable(wxPyGridTableBase):
102          return self.CanGetValueAs(row, col, typeName)          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):  class TableGrid(wxGrid, Publisher):
137    
138      """A grid view for a Thuban table"""      """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            # keep track of which rows are selected.
154            self.rows = {}
155    
156          self.table = DataTable(table)          self.table = DataTable(table)
157    
158          # The second parameter means that the grid is to take ownership          # The second parameter means that the grid is to take ownership
159          # of the table and will destroy it when done. Otherwise you          # of the table and will destroy it when done. Otherwise you
160          # would need to keep a reference to it and call its Destroy          # would need to keep a reference to it and call its Destroy
161          # method later.          # method later.
162          self.SetTable(self.table, true)          self.SetTable(self.table, True)
163    
164          #self.SetMargins(0,0)          #self.SetMargins(0,0)
165    
# Line 116  class TableGrid(wxGrid, Publisher): Line 167  class TableGrid(wxGrid, Publisher):
167          # column widths automatically but it would cause a traversal of          # column widths automatically but it would cause a traversal of
168          # the entire table which for large .dbf files can take a very          # the entire table which for large .dbf files can take a very
169          # long time.          # long time.
170          #self.AutoSizeColumns(false)          #self.AutoSizeColumns(False)
171    
172          self.SetSelectionMode(wxGrid.wxGridSelectRows)          self.SetSelectionMode(wxGrid.wxGridSelectRows)
173            
174            self.ToggleEventListeners(True)
175          EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)          EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)
176          EVT_GRID_SELECT_CELL(self, self.OnSelectCell)          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):      def SetTableObject(self, table):
188          self.table.SetTable(table)          self.table.SetTable(table)
189    
190      def OnRangeSelect(self, event):      def OnRangeSelect(self, event):
191          if event.Selecting():          if self.handleSelectEvents:
192              self.issue(ROW_SELECTED, event.GetTopLeftCoords().GetRow())              self.rows = dict([(i, 0) for i in self.GetSelectedRows()])
193    
194      def OnSelectCell(self, event):              # if we're selecting we need to include the selected range and
195          self.issue(ROW_SELECTED, event.GetRow())              # 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      def select_shape(self, layer, shape):          event.Skip()
205          if layer is not None and layer.table is self.table.table \  
206             and shape is not None:      def OnSelectCell(self, event):
207              self.SelectRow(shape)          if self.handleSelectEvents:
208              self.SetGridCursor(shape, 0)              self.issue(ROW_SELECTED, self.GetSelectedRows())
209              self.MakeCellVisible(shape, 0)          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(NonModalDialog):  class TableFrame(ThubanFrame):
283    
284      """Frame that displays a Thuban table in a grid view"""      """Frame that displays a Thuban table in a grid view"""
285    
286      def __init__(self, parent, interactor, name, title, layer = None,      def __init__(self, parent, name, title, table):
287                   table = None):          ThubanFrame.__init__(self, parent, name, title)
288          NonModalDialog.__init__(self, parent, interactor, name, title)          self.panel = wxPanel(self, -1)
289          self.layer = layer  
290          self.table = table          self.table = table
291          self.grid = TableGrid(self, table)          self.grid = self.make_grid(self.table)
292          self.grid.Subscribe(ROW_SELECTED, self.row_selected)          self.app = self.parent.application
293          self.interactor.Subscribe(SELECTED_SHAPE, self.select_shape)          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):      def OnClose(self, event):
523          self.interactor.Unsubscribe(SELECTED_SHAPE, self.select_shape)          TableFrame.OnClose(self, event)
         NonModalDialog.OnClose(self, event)  
524    
525      def select_shape(self, layer, shape):      def get_selected(self):
526          self.grid.select_shape(layer, shape)          """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      def row_selected(self, row):  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:          if self.layer is not None:
584              self.interactor.SelectLayerAndShape(self.layer, row)              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    

Legend:
Removed from v.173  
changed lines
  Added in v.1219

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26