/[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 34 by bh, Thu Sep 6 15:32:56 2001 UTC revision 1199 by jonathan, Fri Jun 13 15:04:28 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  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 it's 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          self.AutoSizeColumns(false)  
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)          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.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)
         NonModalDialog.__init__(self, parent, interactor, name, title)  
         self.layer = layer  
288          self.table = table          self.table = table
289          self.grid = TableGrid(self, table)          self.grid = self.make_grid(self.table)
290          self.grid.Subscribe(ROW_SELECTED, self.row_selected)          self.app = self.parent.application
291          self.interactor.Subscribe(SELECTED_SHAPE, self.select_shape)          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):      def OnClose(self, event):
304          self.interactor.Unsubscribe(SELECTED_SHAPE, self.select_shape)          self.app.Unsubscribe(SESSION_REPLACED, self.close_on_session_replaced)
305          NonModalDialog.OnClose(self, event)          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      def select_shape(self, layer, shape):      A LayerTableFrame is a QueryTableFrame whose selection is connected to the
522          self.grid.select_shape(layer, shape)      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 row_selected(self, row):      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:          if self.layer is not None:
570              self.interactor.SelectLayerAndShape(self.layer, row)              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    

Legend:
Removed from v.34  
changed lines
  Added in v.1199

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26