/[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 77 by bh, Mon Feb 4 19:27:13 2002 UTC revision 1276 by jonathan, Fri Jun 20 17:46:34 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    from Thuban.UI.common import ThubanBeginBusyCursor, ThubanEndBusyCursor
26    
27  wx_value_type_map = {FIELDTYPE_INT: wxGRID_VALUE_NUMBER,  wx_value_type_map = {FIELDTYPE_INT: wxGRID_VALUE_NUMBER,
28                       FIELDTYPE_DOUBLE: wxGRID_VALUE_FLOAT,                       FIELDTYPE_DOUBLE: wxGRID_VALUE_FLOAT,
# Line 23  wx_value_type_map = {FIELDTYPE_INT: wxGR Line 30  wx_value_type_map = {FIELDTYPE_INT: wxGR
30    
31  ROW_SELECTED = "ROW_SELECTED"  ROW_SELECTED = "ROW_SELECTED"
32    
33    QUERY_KEY = 'S'
34    
35  class DataTable(wxPyGridTableBase):  class DataTable(wxPyGridTableBase):
36    
# Line 38  class DataTable(wxPyGridTableBase): Line 46  class DataTable(wxPyGridTableBase):
46    
47      def SetTable(self, table):      def SetTable(self, table):
48          self.table = table          self.table = table
49          self.num_cols = table.field_count()          self.num_cols = table.NumColumns()
50          self.num_rows = table.record_count()          self.num_rows = table.NumRows()
51    
52          self.columns = []          self.columns = []
53          for i in range(self.num_cols):          for i in range(self.num_cols):
54              type, name, len, decc = table.field_info(i)              col = table.Column(i)
55              self.columns.append((name, wx_value_type_map[type], len, decc))              self.columns.append((col.name, wx_value_type_map[col.type]))
56    
57      #      #
58      # required methods for the wxPyGridTableBase interface      # required methods for the wxPyGridTableBase interface
# Line 64  class DataTable(wxPyGridTableBase): Line 72  class DataTable(wxPyGridTableBase):
72      # Renderer understands the type too,) not just strings as in the      # Renderer understands the type too,) not just strings as in the
73      # C++ version.      # C++ version.
74      def GetValue(self, row, col):      def GetValue(self, row, col):
75          record = self.table.read_record(row)          record = self.table.ReadRowAsDict(row)
76          return record[self.columns[col][0]]          return record[self.columns[col][0]]
77    
78      def SetValue(self, row, col, value):      def SetValue(self, row, col, value):
# Line 95  class DataTable(wxPyGridTableBase): Line 103  class DataTable(wxPyGridTableBase):
103          return self.CanGetValueAs(row, col, typeName)          return self.CanGetValueAs(row, col, typeName)
104    
105    
106    
107    class NullRenderer(wxPyGridCellRenderer):
108    
109        """Renderer that draws NULL as a gray rectangle
110    
111        Other values are delegated to a normal renderer which is given as
112        the parameter to the constructor.
113        """
114    
115        def __init__(self, non_null_renderer):
116            wxPyGridCellRenderer.__init__(self)
117            self.non_null_renderer = non_null_renderer
118    
119        def Draw(self, grid, attr, dc, rect, row, col, isSelected):
120            value = grid.table.GetValue(row, col)
121            if value is None:
122                dc.SetBackgroundMode(wxSOLID)
123                dc.SetBrush(wxBrush(wxColour(192, 192, 192), wxSOLID))
124                dc.SetPen(wxTRANSPARENT_PEN)
125                dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)
126            else:
127                self.non_null_renderer.Draw(grid, attr, dc, rect, row, col,
128                                            isSelected)
129    
130        def GetBestSize(self, grid, attr, dc, row, col):
131            self.non_null_renderer.GetBestSize(grid, attr, dc, row, col)
132    
133        def Clone(self):
134            return NullRenderer(self.non_null_renderer)
135    
136    
137  class TableGrid(wxGrid, Publisher):  class TableGrid(wxGrid, Publisher):
138    
139      """A grid view for a Thuban table"""      """A grid view for a Thuban table
140    
141        When rows are selected by the user the table issues ROW_SELECTED
142        messages. wx sends selection events even when the selection is
143        manipulated by code (instead of by the user) which usually lead to
144        ROW_SELECTED messages being sent in turn. Therefore sending messages
145        can be switched on and off with the allow_messages and
146        disallow_messages methods.
147        """
148    
149      def __init__(self, parent, table = None):      def __init__(self, parent, table = None):
150          wxGrid.__init__(self, parent, -1)          wxGrid.__init__(self, parent, -1)
151    
152            self.allow_messages_count = 0
153    
154            # keep track of which rows are selected.
155            self.rows = {}
156    
157          self.table = DataTable(table)          self.table = DataTable(table)
158    
159          # The second parameter means that the grid is to take ownership          # The second parameter means that the grid is to take ownership
160          # of the table and will destroy it when done. Otherwise you          # of the table and will destroy it when done. Otherwise you
161          # would need to keep a reference to it and call its Destroy          # would need to keep a reference to it and call its Destroy
162          # method later.          # method later.
163          self.SetTable(self.table, true)          self.SetTable(self.table, True)
164    
165          #self.SetMargins(0,0)          #self.SetMargins(0,0)
166          self.AutoSizeColumns(false)  
167            # AutoSizeColumns would allow us to make the grid have optimal
168            # column widths automatically but it would cause a traversal of
169            # the entire table which for large .dbf files can take a very
170            # long time.
171            #self.AutoSizeColumns(False)
172    
173          self.SetSelectionMode(wxGrid.wxGridSelectRows)          self.SetSelectionMode(wxGrid.wxGridSelectRows)
174            
175            self.ToggleEventListeners(True)
176          EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)          EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect)
177          EVT_GRID_SELECT_CELL(self, self.OnSelectCell)          EVT_GRID_SELECT_CELL(self, self.OnSelectCell)
178    
179            # Replace the normal renderers with our own versions which
180            # render NULL/None values specially
181            self.RegisterDataType(wxGRID_VALUE_STRING,
182                                  NullRenderer(wxGridCellStringRenderer()), None)
183            self.RegisterDataType(wxGRID_VALUE_NUMBER,
184                                  NullRenderer(wxGridCellNumberRenderer()), None)
185            self.RegisterDataType(wxGRID_VALUE_FLOAT,
186                                  NullRenderer(wxGridCellFloatRenderer()), None)
187    
188      def SetTableObject(self, table):      def SetTableObject(self, table):
189          self.table.SetTable(table)          self.table.SetTable(table)
190    
191      def OnRangeSelect(self, event):      def OnRangeSelect(self, event):
192          if event.Selecting():          if self.handleSelectEvents:
193              self.issue(ROW_SELECTED, event.GetTopLeftCoords().GetRow())              self.rows = dict([(i, 0) for i in self.GetSelectedRows()])
194    
195      def OnSelectCell(self, event):              # if we're selecting we need to include the selected range and
196          self.issue(ROW_SELECTED, event.GetRow())              # make sure that the current row is also included, which may
197                # not be the case if you just click on a single row!
198                if event.Selecting():
199                    for i in range(event.GetTopRow(), event.GetBottomRow() + 1):
200                        self.rows[i] = 0
201                    self.rows[event.GetTopLeftCoords().GetRow()] = 0
202        
203                self.issue(ROW_SELECTED, self.rows.keys())
204    
205      def select_shape(self, layer, shape):          event.Skip()
206          if layer is not None and layer.table is self.table.table \  
207             and shape is not None:      def OnSelectCell(self, event):
208              self.SelectRow(shape)          if self.handleSelectEvents:
209              self.SetGridCursor(shape, 0)              self.issue(ROW_SELECTED, self.GetSelectedRows())
210              self.MakeCellVisible(shape, 0)          event.Skip()
211    
212        def ToggleEventListeners(self, on):
213            self.handleSelectEvents = on
214                
215        def GetNumberSelected(self):
216            return len(self.rows)
217    
218        def disallow_messages(self):
219            """Disallow messages to be send.
220    
221            This method only increases a counter so that calls to
222            disallow_messages and allow_messages can be nested. Only the
223            outermost calls will actually switch message sending on and off.
224            """
225            self.allow_messages_count += 1
226    
227        def allow_messages(self):
228            """Allow messages to be send.
229    
230            This method only decreases a counter so that calls to
231            disallow_messages and allow_messages can be nested. Only the
232            outermost calls will actually switch message sending on and off.
233            """
234            self.allow_messages_count -= 1
235    
236        def issue(self, *args):
237            """Issue a message unless disallowed.
238    
239            See the allow_messages and disallow_messages methods.
240            """
241            if self.allow_messages_count == 0:
242                Publisher.issue(self, *args)
243    
244    
245    class LayerTableGrid(TableGrid):
246    
247        """Table grid for the layer tables.
248    
249        The LayerTableGrid is basically the same as a TableGrid but it's
250        selection is usually coupled to the selected object in the map.
251        """
252    
253        def select_shapes(self, layer, shapes):
254            """Select the row corresponding to the specified shape and layer
255    
256            If layer is not the layer the table is associated with do
257            nothing. If shape or layer is None also do nothing.
258            """
259            if layer is not None \
260                and layer.ShapeStore().Table() is self.table.table:
261    
262                self.disallow_messages()
263                try:
264                    self.ClearSelection()
265                    if len(shapes) > 0:
266                        #
267                        # keep track of the lowest id so we can make it
268                        # the first visible item
269                        #
270                        first = shapes[0]
271    
272                        for shape in shapes:
273                            self.SelectRow(shape, True)
274                            if shape < first:
275                                first = shape
276    
277                        self.SetGridCursor(first, 0)
278                        self.MakeCellVisible(first, 0)
279                finally:
280                    self.allow_messages()
281    
282    
283  class TableFrame(NonModalDialog):  class TableFrame(ThubanFrame):
284    
285      """Frame that displays a Thuban table in a grid view"""      """Frame that displays a Thuban table in a grid view"""
286    
287      def __init__(self, parent, interactor, name, title, layer = None,      def __init__(self, parent, name, title, table):
288                   table = None):          ThubanFrame.__init__(self, parent, name, title)
289          NonModalDialog.__init__(self, parent, interactor, name, title)          self.panel = wxPanel(self, -1)
290          self.layer = layer  
291          self.table = table          self.table = table
292          self.grid = TableGrid(self, table)          self.grid = self.make_grid(self.table)
293          self.grid.Subscribe(ROW_SELECTED, self.row_selected)          self.app = self.parent.application
294          self.interactor.Subscribe(SELECTED_SHAPE, self.select_shape)          self.app.Subscribe(SESSION_REPLACED, self.close_on_session_replaced)
295            self.session = self.app.Session()
296            self.session.Subscribe(TABLE_REMOVED, self.close_on_table_removed)
297    
298    
299        def make_grid(self, table):
300            """Return the table grid to use in the frame.
301    
302            The default implementation returns a TableGrid instance.
303            Override in derived classes to use different grid classes.
304            """
305            return TableGrid(self, table)
306    
307        def OnClose(self, event):
308            self.app.Unsubscribe(SESSION_REPLACED, self.close_on_session_replaced)
309            self.session.Unsubscribe(TABLE_REMOVED, self.close_on_table_removed)
310            ThubanFrame.OnClose(self, event)
311    
312        def close_on_session_replaced(self, *args):
313            """Subscriber for the SESSION_REPLACED messages.
314    
315            The table frame is tied to a session so close the window when
316            the session changes.
317            """
318            self.Close()
319    
320        def close_on_table_removed(self, table):
321            """Subscriber for the TABLE_REMOVED messages.
322    
323            The table frame is tied to a particular table so close the
324            window when the table is removed.
325            """
326            if table is self.table:
327                self.Close()
328    
329    
330    ID_QUERY = 4001
331    ID_EXPORT = 4002
332    ID_COMBOVALUE = 4003
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_saveas = wxButton(self.panel, ID_EXPORT, _("Export"))
357    
358            self.CreateStatusBar()
359    
360            self.grid.SetSize((400, 200))
361    
362            self.combo_value.Append("")
363            for i in range(table.NumColumns()):
364                name = table.Column(i).name
365                self.combo_fields.Append(name)
366                self.combo_value.Append(name)
367    
368            # assume at least one field?
369            self.combo_fields.SetSelection(0)
370            self.combo_value.SetSelection(0)
371            self.choice_action.SetSelection(0)
372            self.choice_comp.SetSelection(0)
373    
374            self.grid.Reparent(self.panel)
375    
376            self.UpdateStatusText()
377    
378            topBox = wxBoxSizer(wxVERTICAL)
379    
380            sizer = wxStaticBoxSizer(wxStaticBox(self.panel, -1,
381                                      _("Selection")),
382                                      wxHORIZONTAL)
383            sizer.Add(self.combo_fields, 1, wxEXPAND|wxALL, 4)
384            sizer.Add(self.choice_comp, 0, wxALL, 4)
385            sizer.Add(self.combo_value, 1, wxEXPAND|wxALL, 4)
386            sizer.Add(self.choice_action, 0, wxALL, 4)
387            sizer.Add(button_query, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
388            sizer.Add(40, 20, 0, wxALL, 4)
389            sizer.Add(button_saveas, 0, wxALL | wxALIGN_CENTER_VERTICAL, 4)
390    
391            topBox.Add(sizer, 0, wxEXPAND|wxALL, 4)
392            topBox.Add(self.grid, 1, wxEXPAND|wxALL, 0)
393    
394            self.panel.SetAutoLayout(True)
395            self.panel.SetSizer(topBox)
396            topBox.Fit(self.panel)
397            topBox.SetSizeHints(self.panel)
398    
399            panelSizer = wxBoxSizer(wxVERTICAL)
400            panelSizer.Add(self.panel, 1, wxEXPAND, 0)
401            self.SetAutoLayout(True)
402            self.SetSizer(panelSizer)
403            panelSizer.Fit(self)
404            panelSizer.SetSizeHints(self)
405    
406            self.grid.SetFocus()
407    
408            EVT_BUTTON(self, ID_QUERY, self.OnQuery)
409            EVT_BUTTON(self, ID_EXPORT, self.OnSaveAs)
410            EVT_KEY_DOWN(self.grid, self.OnKeyDown)
411            EVT_GRID_RANGE_SELECT(self.grid, self.OnGridSelectRange)
412            EVT_GRID_SELECT_CELL(self.grid, self.OnGridSelectCell)
413    
414        def UpdateStatusText(self):
415            self.SetStatusText(_("%i rows (%i selected), %i columns")
416                % (self.grid.GetNumberRows(),
417                   self.grid.GetNumberSelected(),
418                   self.grid.GetNumberCols()))
419    
420        def OnGridSelectRange(self, event):
421            self.UpdateStatusText()
422            event.Skip()
423    
424        def OnGridSelectCell(self, event):
425            self.UpdateStatusText()
426            event.Skip()
427            
428        def OnKeyDown(self, event):
429            """Catch query key from grid"""
430            if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
431                self.combo_fields.SetFocus()
432                self.combo_fields.refocus = True
433            else:
434                event.Skip()
435    
436        def OnQuery(self, event):
437            ThubanBeginBusyCursor()
438            try:
439    
440                text = self.combo_value.GetValue()
441                if self.combo_value.GetSelection() < 1 \
442                    or self.combo_value.FindString(text) == -1:
443                    value = text
444                else:
445                    value = self.table.Column(text)
446    
447                ids = self.table.SimpleQuery(
448                        self.table.Column(self.combo_fields.GetStringSelection()),
449                        self.choice_comp.GetStringSelection(),
450                        value)
451    
452                choice = self.choice_action.GetSelection()
453                
454                #
455                # what used to be nice code got became a bit ugly because
456                # each time we select a row a message is sent to the grid
457                # which we are listening for and then we send further
458                # messages.
459                #
460                # now, we disable those listeners select everything but
461                # the first item, reenable the listeners, and select
462                # the first element, which causes everything to be
463                # updated properly.
464                #
465                if ids:
466                    self.grid.ToggleEventListeners(False)
467    
468                if choice == 0:
469                    # Replace Selection
470                    self.grid.ClearSelection()
471                elif choice == 1:
472                    # Refine Selection
473                    sel = self.get_selected()
474                    self.grid.ClearSelection()
475                    ids = filter(sel.has_key, ids)
476                elif choice == 2:
477                    # Add to Selection
478                    pass
479    
480                #
481                # select the rows (all but the first)
482                #
483                firsttime = True
484                for id in ids:
485                    if firsttime:
486                        firsttime = False
487                    else:
488                        self.grid.SelectRow(id, True)
489    
490                self.grid.ToggleEventListeners(True)
491    
492                #
493                # select the first row
494                #
495                if ids:
496                    self.grid.SelectRow(ids[0], True)
497    
498            finally:
499                ThubanEndBusyCursor()
500            
501        def OnSaveAs(self, event):
502            dlg = wxFileDialog(self, _("Export Table To"), ".", "",
503                               _("DBF Files (*.dbf)|*.dbf|") +
504                               _("CSV Files (*.csv)|*.csv|") +
505                               _("All Files (*.*)|*.*"),
506                               wxSAVE|wxOVERWRITE_PROMPT)
507            if dlg.ShowModal() == wxID_OK:
508                filename = dlg.GetPath()
509                type = os.path.basename(filename).split('.')[-1:][0]
510                dlg.Destroy()
511                if type.upper() == "DBF":
512                    table_to_dbf(self.table, filename)
513                elif type.upper() == 'CSV':
514                    table_to_csv(self.table, filename)
515                else:
516                    dlg = wxMessageDialog(None, "Unsupported format: %s" % type,
517                                          "Table Export", wxOK|wxICON_WARNING)
518                    dlg.ShowModal()
519                    dlg.Destroy()
520            else:
521                dlg.Destroy()
522    
523      def OnClose(self, event):      def OnClose(self, event):
524          self.interactor.Unsubscribe(SELECTED_SHAPE, self.select_shape)          TableFrame.OnClose(self, event)
         NonModalDialog.OnClose(self, event)  
525    
526      def select_shape(self, layer, shape):      def get_selected(self):
527          self.grid.select_shape(layer, shape)          """Return a dictionary of the selected rows.
528            
529            The dictionary has sthe indexes as keys."""
530            return dict([(i, 0) for i in self.grid.GetSelectedRows()])
531    
532      def row_selected(self, row):  class LayerTableFrame(QueryTableFrame):
533    
534        """Frame that displays a layer table in a grid view
535    
536        A LayerTableFrame is a QueryTableFrame whose selection is connected to the
537        selected object in a map.
538        """
539    
540        def __init__(self, parent, name, title, layer, table):
541            QueryTableFrame.__init__(self, parent, name, title, table)
542            self.layer = layer
543            self.grid.Subscribe(ROW_SELECTED, self.rows_selected)
544            self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)
545            self.map = self.parent.Map()
546            self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
547    
548            # if there is already a selection present, update the grid
549            # accordingly
550            sel = self.get_selected().keys()
551            for i in sel:
552                self.grid.SelectRow(i, True)
553    
554        def make_grid(self, table):
555            """Override the derived method to return a LayerTableGrid.
556            """
557            return LayerTableGrid(self, table)
558    
559        def get_selected(self):
560            """Override the derived method to return a dictionary of the selected
561            rows.
562            """
563            return dict([(i, 0) for i in self.parent.SelectedShapes()])
564    
565        def OnClose(self, event):
566            """Override the derived method to first unsubscribed."""
567            self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
568            self.grid.Unsubscribe(ROW_SELECTED, self.rows_selected)
569            self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
570            QueryTableFrame.OnClose(self, event)
571    
572        def select_shapes(self, layer, shapes):
573            """Subscribed to the SHAPES_SELECTED message.
574    
575            If shapes contains exactly one shape id, select that shape in
576            the grid. Otherwise deselect all.
577            """
578            self.grid.select_shapes(layer, shapes)
579    
580        def rows_selected(self, rows):
581            """Return the selected rows of the layer as they are returned
582            by Layer.SelectShapes().
583            """
584          if self.layer is not None:          if self.layer is not None:
585              self.interactor.SelectLayerAndShape(self.layer, row)              self.parent.SelectShapes(self.layer, rows)
586    
587        def map_layers_removed(self, *args):
588            """Receiver for the map's MAP_LAYERS_REMOVED message
589    
590            Close the dialog if the layer whose table we're showing is not
591            in the map anymore.
592            """
593            if self.layer not in self.map.Layers():
594                self.Close()
595    

Legend:
Removed from v.77  
changed lines
  Added in v.1276

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26