/[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 1394 by jonathan, Thu Jul 10 14:54:58 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  from dialogs import ThubanFrame
21  from messages import SELECTED_SHAPE  
22    from messages import SHAPES_SELECTED, SESSION_REPLACED
23    from Thuban.Model.messages import TABLE_REMOVED, MAP_LAYERS_REMOVED
24    from Thuban.UI.common import ThubanBeginBusyCursor, ThubanEndBusyCursor
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            # 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          import main  
178          main.app.interactor.Subscribe(SELECTED_SHAPE, self.select_shape)          # 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              if not self.table.SelectRow(event.GetTopLeftCoords().GetRow()):              self.rows = dict([(i, 0) for i in self.GetSelectedRows()])
193                  event.Skip()  
194                # if we're selecting we need to include the selected range and
195                # make sure that the current row is also included, which may
196                # not be the case if you just click on a single row!
197                if event.Selecting():
198                    for i in range(event.GetTopRow(), event.GetBottomRow() + 1):
199                        self.rows[i] = 0
200                    self.rows[event.GetTopLeftCoords().GetRow()] = 0
201        
202                self.issue(ROW_SELECTED, self.rows.keys())
203    
204            event.Skip()
205    
206      def OnSelectCell(self, event):      def OnSelectCell(self, event):
207          if not self.table.SelectRow(event.GetRow()):          if self.handleSelectEvents:
208                self.issue(ROW_SELECTED, self.GetSelectedRows())
209            event.Skip()
210    
211        def ToggleEventListeners(self, on):
212            self.handleSelectEvents = on
213                
214        def GetNumberSelected(self):
215            return len(self.rows)
216    
217        def disallow_messages(self):
218            """Disallow messages to be send.
219    
220            This method only increases a counter so that calls to
221            disallow_messages and allow_messages can be nested. Only the
222            outermost calls will actually switch message sending on and off.
223            """
224            self.allow_messages_count += 1
225    
226        def allow_messages(self):
227            """Allow messages to be send.
228    
229            This method only decreases a counter so that calls to
230            disallow_messages and allow_messages can be nested. Only the
231            outermost calls will actually switch message sending on and off.
232            """
233            self.allow_messages_count -= 1
234    
235        def issue(self, *args):
236            """Issue a message unless disallowed.
237    
238            See the allow_messages and disallow_messages methods.
239            """
240            if self.allow_messages_count == 0:
241                Publisher.issue(self, *args)
242    
243    
244    class LayerTableGrid(TableGrid):
245    
246        """Table grid for the layer tables.
247    
248        The LayerTableGrid is basically the same as a TableGrid but it's
249        selection is usually coupled to the selected object in the map.
250        """
251    
252        def select_shapes(self, layer, shapes):
253            """Select the row corresponding to the specified shape and layer
254    
255            If layer is not the layer the table is associated with do
256            nothing. If shape or layer is None also do nothing.
257            """
258            if layer is not None \
259                and layer.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(ThubanFrame):
283    
284        """Frame that displays a Thuban table in a grid view"""
285    
286        def __init__(self, parent, name, title, table):
287            ThubanFrame.__init__(self, parent, name, title)
288            self.panel = wxPanel(self, -1)
289    
290            self.table = table
291            self.grid = self.make_grid(self.table)
292            self.app = self.parent.application
293            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    ID_EXPORTSEL = 4004
333    
334    class QueryTableFrame(TableFrame):
335    
336        """Frame that displays a table in a grid view and offers user actions
337        selection and export
338    
339        A QueryTableFrame is TableFrame whose selection is connected to the
340        selected object in a map.
341        """
342    
343        def __init__(self, parent, name, title, table):
344            TableFrame.__init__(self, parent, name, title, table)
345    
346            self.combo_fields = wxComboBox(self.panel, -1, style=wxCB_READONLY)
347            self.choice_comp = wxChoice(self.panel, -1,
348                                  choices=["<", "<=", "==", "!=", ">=", ">"])
349            self.combo_value = wxComboBox(self.panel, ID_COMBOVALUE)
350            self.choice_action = wxChoice(self.panel, -1,
351                                    choices=[_("Replace Selection"),
352                                            _("Refine Selection"),
353                                            _("Add to Selection")])
354    
355            button_query = wxButton(self.panel, ID_QUERY, _("Query"))
356            button_export = wxButton(self.panel, ID_EXPORT, _("Export"))
357            button_exportSel = wxButton(self.panel, ID_EXPORTSEL, _("Export Selection"))
358            button_close = wxButton(self.panel, wxID_CLOSE, _("Close"))
359    
360            self.CreateStatusBar()
361    
362            self.grid.SetSize((400, 200))
363    
364            self.combo_value.Append("")
365            for i in range(table.NumColumns()):
366                name = table.Column(i).name
367                self.combo_fields.Append(name)
368                self.combo_value.Append(name)
369    
370            # assume at least one field?
371            self.combo_fields.SetSelection(0)
372            self.combo_value.SetSelection(0)
373            self.choice_action.SetSelection(0)
374            self.choice_comp.SetSelection(0)
375    
376            self.grid.Reparent(self.panel)
377    
378            self.UpdateStatusText()
379    
380            topBox = wxBoxSizer(wxVERTICAL)
381    
382            sizer = wxStaticBoxSizer(wxStaticBox(self.panel, -1,
383                                      _("Selection")),
384                                      wxHORIZONTAL)
385            sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)
386            sizer.Add(self.choice_comp, 0, wxALL, 4)
387            sizer.Add(self.combo_value, 1, wxEXPAND|wxALL, 4)
388            sizer.Add(self.choice_action, 0, wxALL, 4)
389            sizer.Add(button_query, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
390            sizer.Add(40, 20, 0, wxALL, 4)
391    
392            topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
393            topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
394    
395            sizer = wxBoxSizer(wxHORIZONTAL)
396            sizer.Add(button_export, 0, wxALL, 4)
397            sizer.Add(button_exportSel, 0, wxALL, 4)
398            sizer.Add(60, 20, 1, wxALL|wxEXPAND, 4)
399            sizer.Add(button_close, 0, wxALL|wxALIGN_RIGHT, 4)
400            topBox.Add(sizer, 0, wxALL | wxEXPAND, 4)
401    
402            self.panel.SetAutoLayout(True)
403            self.panel.SetSizer(topBox)
404            topBox.Fit(self.panel)
405            topBox.SetSizeHints(self.panel)
406    
407            panelSizer = wxBoxSizer(wxVERTICAL)
408            panelSizer.Add(self.panel, 1, wxEXPAND, 0)
409            self.SetAutoLayout(True)
410            self.SetSizer(panelSizer)
411            panelSizer.Fit(self)
412            panelSizer.SetSizeHints(self)
413    
414            self.grid.SetFocus()
415    
416            EVT_BUTTON(self, ID_QUERY, self.OnQuery)
417            EVT_BUTTON(self, ID_EXPORT, self.OnExport)
418            EVT_BUTTON(self, ID_EXPORTSEL, self.OnExportSel)
419            EVT_BUTTON(self, wxID_CLOSE, self.OnClose)
420            EVT_KEY_DOWN(self.grid, self.OnKeyDown)
421    
422            self.grid.Subscribe(ROW_SELECTED, self.UpdateStatusText)
423    
424        def UpdateStatusText(self, rows=None):
425            self.SetStatusText(_("%i rows (%i selected), %i columns")
426                % (self.grid.GetNumberRows(),
427                   self.grid.GetNumberSelected(),
428                   self.grid.GetNumberCols()))
429    
430        def OnKeyDown(self, event):
431            """Catch query key from grid"""
432            if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
433                self.combo_fields.SetFocus()
434                self.combo_fields.refocus = True
435            else:
436              event.Skip()              event.Skip()
437    
438      def select_shape(self, layer, shape):      def OnQuery(self, event):
439          if layer is not None and layer.table is self.table.table:          ThubanBeginBusyCursor()
440              self.SelectRow(shape)          try:
441              self.SetGridCursor(shape, 0)  
442              self.MakeCellVisible(shape, 0)              text = self.combo_value.GetValue()
443                if self.combo_value.GetSelection() < 1 \
444                    or self.combo_value.FindString(text) == -1:
445                    value = text
446                else:
447                    value = self.table.Column(text)
448    
449                ids = self.table.SimpleQuery(
450                        self.table.Column(self.combo_fields.GetStringSelection()),
451                        self.choice_comp.GetStringSelection(),
452                        value)
453    
454                choice = self.choice_action.GetSelection()
455                
456                #
457                # what used to be nice code got became a bit ugly because
458                # each time we select a row a message is sent to the grid
459                # which we are listening for and then we send further
460                # messages.
461                #
462                # now, we disable those listeners select everything but
463                # the first item, reenable the listeners, and select
464                # the first element, which causes everything to be
465                # updated properly.
466                #
467                if ids:
468                    self.grid.ToggleEventListeners(False)
469    
470                if choice == 0:
471                    # Replace Selection
472                    self.grid.ClearSelection()
473                elif choice == 1:
474                    # Refine Selection
475                    sel = self.get_selected()
476                    self.grid.ClearSelection()
477                    ids = filter(sel.has_key, ids)
478                elif choice == 2:
479                    # Add to Selection
480                    pass
481    
482                #
483                # select the rows (all but the first)
484                #
485                firsttime = True
486                for id in ids:
487                    if firsttime:
488                        firsttime = False
489                    else:
490                        self.grid.SelectRow(id, True)
491    
492                self.grid.ToggleEventListeners(True)
493    
494                #
495                # select the first row
496                #
497                if ids:
498                    self.grid.SelectRow(ids[0], True)
499    
500            finally:
501                ThubanEndBusyCursor()
502            
503        def doExport(self, onlySelected):
504            
505            dlg = wxFileDialog(self, _("Export Table To"), ".", "",
506                               _("DBF Files (*.dbf)|*.dbf|") +
507                               _("CSV Files (*.csv)|*.csv|") +
508                               _("All Files (*.*)|*.*"),
509                               wxSAVE|wxOVERWRITE_PROMPT)
510            if dlg.ShowModal() == wxID_OK:
511                filename = dlg.GetPath()
512                type = os.path.basename(filename).split('.')[-1:][0]
513                dlg.Destroy()
514    
515                if onlySelected:
516                    records = self.grid.GetSelectedRows()
517                else:
518                    records = None
519    
520                if type.upper() == "DBF":
521                    table_to_dbf(self.table, filename, records)
522                elif type.upper() == 'CSV':
523                    table_to_csv(self.table, filename, records)
524                else:
525                    dlg = wxMessageDialog(None, "Unsupported format: %s" % type,
526                                          "Table Export", wxOK|wxICON_WARNING)
527                    dlg.ShowModal()
528                    dlg.Destroy()
529            else:
530                dlg.Destroy()
531    
532        def OnExport(self, event):
533            self.doExport(False)
534    
535  class TableFrame(wxFrame):      def OnExportSel(self, event):
536            self.doExport(True)
537    
538        def OnClose(self, event):
539            TableFrame.OnClose(self, event)
540    
541        def get_selected(self):
542            """Return a dictionary of the selected rows.
543            
544            The dictionary has the indexes as keys."""
545            return dict([(i, 0) for i in self.grid.GetSelectedRows()])
546    
547    class LayerTableFrame(QueryTableFrame):
548    
549        """Frame that displays a layer table in a grid view
550    
551        A LayerTableFrame is a QueryTableFrame whose selection is connected to the
552        selected object in a map.
553        """
554    
555        def __init__(self, parent, name, title, layer, table):
556            QueryTableFrame.__init__(self, parent, name, title, table)
557            self.layer = layer
558            self.grid.Subscribe(ROW_SELECTED, self.rows_selected)
559            self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)
560            self.map = self.parent.Map()
561            self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
562    
563            # if there is already a selection present, update the grid
564            # accordingly
565            sel = self.get_selected().keys()
566            for i in sel:
567                self.grid.SelectRow(i, True)
568    
569        def make_grid(self, table):
570            """Override the derived method to return a LayerTableGrid.
571            """
572            return LayerTableGrid(self, table)
573    
574        def get_selected(self):
575            """Override the derived method to return a dictionary of the selected
576            rows.
577            """
578            return dict([(i, 0) for i in self.parent.SelectedShapes()])
579    
580        def OnClose(self, event):
581            """Override the derived method to first unsubscribed."""
582            self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
583            self.grid.Unsubscribe(ROW_SELECTED, self.rows_selected)
584            self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
585            QueryTableFrame.OnClose(self, event)
586    
587        def select_shapes(self, layer, shapes):
588            """Subscribed to the SHAPES_SELECTED message.
589    
590            If shapes contains exactly one shape id, select that shape in
591            the grid. Otherwise deselect all.
592            """
593            self.grid.select_shapes(layer, shapes)
594    
595        def rows_selected(self, rows):
596            """Return the selected rows of the layer as they are returned
597            by Layer.SelectShapes().
598            """
599            if self.layer is not None:
600                self.parent.SelectShapes(self.layer, rows)
601    
602        def map_layers_removed(self, *args):
603            """Receiver for the map's MAP_LAYERS_REMOVED message
604    
605            Close the dialog if the layer whose table we're showing is not
606            in the map anymore.
607            """
608            if self.layer not in self.map.Layers():
609                self.Close()
610    
     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.1394

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26