/[thuban]/trunk/thuban/Thuban/UI/classifier.py
ViewVC logotype

Contents of /trunk/thuban/Thuban/UI/classifier.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 835 - (show annotations)
Tue May 6 15:52:41 2003 UTC (21 years, 10 months ago) by bh
File MIME type: text/x-python
File size: 44533 byte(s)
(Classifier.__init__)
(Classifier.__init__): Adapt to new table interface

1 # Copyright (c) 2001, 2003 by Intevation GmbH
2 # Authors:
3 # Jonathan Coles <[email protected]>
4 #
5 # This program is free software under the GPL (>=v2)
6 # Read the file COPYING coming with Thuban for details.
7
8 """Dialog for classifying how layers are displayed"""
9
10 __version__ = "$Revision$"
11
12 import copy
13
14 from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \
15 FIELDTYPE_STRING
16
17 from wxPython.wx import *
18 from wxPython.grid import *
19
20 from Thuban import _
21 from Thuban.UI.common import *
22
23 from Thuban.Model.classification import *
24
25 from Thuban.Model.color import Color
26
27 from Thuban.Model.layer import Layer, SHAPETYPE_ARC, SHAPETYPE_POLYGON, SHAPETYPE_POINT
28
29 from Thuban.UI.classgen import ClassGenDialog, ClassGenerator
30
31 from dialogs import NonModalDialog
32
33 ID_CLASS_TABLE = 40011
34
35
36 # table columns
37 COL_VISIBLE = 0
38 COL_SYMBOL = 1
39 COL_VALUE = 2
40 COL_LABEL = 3
41 NUM_COLS = 4
42
43 # indices into the client data lists in Classifier.fields
44 FIELD_CLASS = 0
45 FIELD_TYPE = 1
46 FIELD_NAME = 2
47
48 #
49 # this is a silly work around to ensure that the table that is
50 # passed into SetTable is the same that is returned by GetTable
51 #
52 import weakref
53 class ClassGrid(wxGrid):
54
55
56 def __init__(self, parent, classifier):
57 """Constructor.
58
59 parent -- the parent window
60
61 clazz -- the working classification that this grid should
62 use for display.
63 """
64
65 wxGrid.__init__(self, parent, ID_CLASS_TABLE, style = 0)
66
67 self.classifier = classifier
68
69 self.currentSelection = []
70
71 EVT_GRID_CELL_LEFT_DCLICK(self, self._OnCellDClick)
72 EVT_GRID_RANGE_SELECT(self, self._OnSelectedRange)
73 EVT_GRID_SELECT_CELL(self, self._OnSelectedCell)
74 EVT_GRID_COL_SIZE(self, self._OnCellResize)
75 EVT_GRID_ROW_SIZE(self, self._OnCellResize)
76
77 #def GetCellAttr(self, row, col):
78 #print "GetCellAttr ", row, col
79 #wxGrid.GetCellAttr(self, row, col)
80
81 def CreateTable(self, clazz, shapeType, group = None):
82
83 assert isinstance(clazz, Classification)
84
85 table = self.GetTable()
86 if table is None:
87 w = self.GetDefaultColSize() * NUM_COLS \
88 + self.GetDefaultRowLabelSize()
89 h = self.GetDefaultRowSize() * 4 \
90 + self.GetDefaultColLabelSize()
91
92 self.SetDimensions(-1, -1, w, h)
93 self.SetSizeHints(w, h, -1, -1)
94 table = ClassTable(self)
95 self.SetTable(table, True)
96
97
98 self.SetSelectionMode(wxGrid.wxGridSelectRows)
99 self.ClearSelection()
100
101 table.Reset(clazz, shapeType, group)
102
103 def GetCurrentSelection(self):
104 """Return the currently highlighted rows as an increasing list
105 of row numbers."""
106 sel = copy.copy(self.currentSelection)
107 sel.sort()
108 return sel
109
110 def GetSelectedRows(self):
111 return self.GetCurrentSelection()
112
113 #def SetCellRenderer(self, row, col, renderer):
114 #raise ValueError(_("Must not allow setting of renderer in ClassGrid!"))
115
116 #
117 # [Set|Get]Table is taken from http://wiki.wxpython.org
118 # they are needed as a work around to ensure that the table
119 # that is passed to SetTable is the one that is returned
120 # by GetTable.
121 #
122 def SetTable(self, object, *attributes):
123 self.tableRef = weakref.ref(object)
124 return wxGrid.SetTable(self, object, *attributes)
125
126 def GetTable(self):
127 try:
128 return self.tableRef()
129 except:
130 return None
131
132 def DeleteSelectedRows(self):
133 """Deletes all highlighted rows.
134
135 If only one row is highlighted then after it is deleted the
136 row that was below the deleted row is highlighted."""
137
138 sel = self.GetCurrentSelection()
139
140 # nothing to do
141 if len(sel) == 0: return
142
143 # if only one thing is selected check if it is the default
144 # data row, because we can't remove that
145 if len(sel) == 1:
146 #group = self.GetTable().GetValueAsCustom(sel[0], COL_SYMBOL, None)
147 group = self.GetTable().GetClassGroup(sel[0])
148 if isinstance(group, ClassGroupDefault):
149 wxMessageDialog(self,
150 "The Default group cannot be removed.",
151 style = wxOK | wxICON_EXCLAMATION).ShowModal()
152 return
153
154
155 self.ClearSelection()
156
157 # we need to remove things from the bottom up so we don't
158 # change the indexes of rows that will be deleted next
159 sel.reverse()
160
161 #
162 # actually remove the rows
163 #
164 table = self.GetTable()
165 for row in sel:
166 table.DeleteRows(row)
167
168 #
169 # if there was only one row selected highlight the row
170 # that was directly below it, or move up one if the
171 # deleted row was the last row.
172 #
173 if len(sel) == 1:
174 r = sel[0]
175 if r > self.GetNumberRows() - 1:
176 r = self.GetNumberRows() - 1
177 self.SelectRow(r)
178
179
180 def SelectGroup(self, group, makeVisible = True):
181 if group is None: return
182
183 assert isinstance(group, ClassGroup)
184
185 table = self.GetTable()
186
187 assert table is not None
188
189 for i in range(table.GetNumberRows()):
190 g = table.GetClassGroup(i)
191 if g is group:
192 self.SelectRow(i)
193 if makeVisible:
194 self.MakeCellVisible(i, 0)
195 break
196
197 #
198 # XXX: This isn't working, and there is no way to deselect rows wxPython!
199 #
200 # def DeselectRow(self, row):
201 # self.ProcessEvent(
202 # wxGridRangeSelectEvent(-1,
203 # wxEVT_GRID_RANGE_SELECT,
204 # self,
205 # (row, row), (row, row),
206 # sel = False))
207
208 def _OnCellDClick(self, event):
209 """Handle a double click on a cell."""
210
211 r = event.GetRow()
212 c = event.GetCol()
213
214 if c == COL_SYMBOL:
215 self.classifier.EditSymbol(r)
216 else:
217 event.Skip()
218
219 #
220 # _OnSelectedRange() and _OnSelectedCell() were borrowed
221 # from http://wiki.wxpython.org to keep track of which
222 # cells are currently highlighted
223 #
224 def _OnSelectedRange(self, event):
225 """Internal update to the selection tracking list"""
226 if event.Selecting():
227 for index in range( event.GetTopRow(), event.GetBottomRow()+1):
228 if index not in self.currentSelection:
229 self.currentSelection.append( index )
230 else:
231 for index in range( event.GetTopRow(), event.GetBottomRow()+1):
232 while index in self.currentSelection:
233 self.currentSelection.remove( index )
234 #self.ConfigureForSelection()
235
236 event.Skip()
237
238 def _OnSelectedCell( self, event ):
239 """Internal update to the selection tracking list"""
240 self.currentSelection = [ event.GetRow() ]
241 #self.ConfigureForSelection()
242 event.Skip()
243
244 def _OnCellResize(self, event):
245 self.FitInside()
246
247 class ClassTable(wxPyGridTableBase):
248 """Represents the underlying data structure for the grid."""
249
250 __col_labels = [_("Visible"), _("Symbol"), _("Value"), _("Label")]
251
252
253 def __init__(self, view = None):
254 #def __init__(self, clazz, shapeType, view = None):
255 """Constructor.
256
257 shapeType -- the type of shape that the layer uses
258
259 view -- a wxGrid object that uses this class for its table
260 """
261
262 wxPyGridTableBase.__init__(self)
263
264 assert len(ClassTable.__col_labels) == NUM_COLS
265
266 self.clazz = None
267 self.__colAttr = {}
268
269 self.SetView(view)
270
271 def Reset(self, clazz, shapeType, group = None):
272 """Reset the table with the given data.
273
274 This is necessary because wxWindows does not allow a grid's
275 table to change once it has been intially set and so we
276 need a way of modifying the data.
277
278 clazz -- the working classification that this table should
279 use for display. This may be different from the
280 classification in the layer.
281
282 shapeType -- the type of shape that the layer uses
283 """
284
285 assert isinstance(clazz, Classification)
286
287 self.GetView().BeginBatch()
288
289 self.fieldType = clazz.GetFieldType()
290 self.shapeType = shapeType
291
292 self.SetClassification(clazz, group)
293 self.__Modified(-1)
294
295 self.__colAttr = {}
296
297 attr = wxGridCellAttr()
298 attr.SetEditor(wxGridCellBoolEditor())
299 attr.SetRenderer(wxGridCellBoolRenderer())
300 attr.SetAlignment(wxALIGN_CENTER, wxALIGN_CENTER)
301 self.__colAttr[COL_VISIBLE] = attr
302
303 attr = wxGridCellAttr()
304 attr.SetRenderer(ClassRenderer(self.shapeType))
305 attr.SetReadOnly()
306 self.__colAttr[COL_SYMBOL] = attr
307
308 self.GetView().EndBatch()
309 self.GetView().FitInside()
310
311 def GetClassification(self):
312 return self.clazz
313
314 def SetClassification(self, clazz, group = None):
315
316 self.GetView().BeginBatch()
317
318 old_len = self.GetNumberRows()
319
320 row = -1
321 self.clazz = clazz
322
323 self.__NotifyRowChanges(old_len, self.GetNumberRows())
324
325 #
326 # XXX: this is dead code at the moment
327 #
328 if row > -1:
329 self.GetView().ClearSelection()
330 self.GetView().SelectRow(row)
331 self.GetView().MakeCellVisible(row, 0)
332
333 self.__Modified()
334
335
336 self.GetView().EndBatch()
337 self.GetView().FitInside()
338
339 def __NotifyRowChanges(self, curRows, newRows):
340 #
341 # silly message processing for updates to the number of
342 # rows and columns
343 #
344 if newRows > curRows:
345 msg = wxGridTableMessage(self,
346 wxGRIDTABLE_NOTIFY_ROWS_APPENDED,
347 newRows - curRows) # how many
348 self.GetView().ProcessTableMessage(msg)
349 self.GetView().FitInside()
350 elif newRows < curRows:
351 msg = wxGridTableMessage(self,
352 wxGRIDTABLE_NOTIFY_ROWS_DELETED,
353 curRows, # position
354 curRows - newRows) # how many
355 self.GetView().ProcessTableMessage(msg)
356 self.GetView().FitInside()
357
358
359 def __SetRow(self, row, group):
360 """Set a row's data to that of the group.
361
362 The table is considered modified after this operation.
363
364 row -- if row is < 0 'group' is inserted at the top of the table
365 if row is >= GetNumberRows() or None 'group' is append to
366 the end of the table.
367 otherwise 'group' replaces row 'row'
368 """
369
370 # either append or replace
371 if row is None or row >= self.GetNumberRows():
372 self.clazz.AppendGroup(group)
373 elif row < 0:
374 self.clazz.InsertGroup(0, group)
375 else:
376 if row == 0:
377 self.clazz.SetDefaultGroup(group)
378 else:
379 self.clazz.ReplaceGroup(row - 1, group)
380
381 self.__Modified()
382
383 def GetColLabelValue(self, col):
384 """Return the label for the given column."""
385 return self.__col_labels[col]
386
387 def GetRowLabelValue(self, row):
388 """Return the label for the given row."""
389
390 if row == 0:
391 return _("Default")
392 else:
393 group = self.clazz.GetGroup(row - 1)
394 if isinstance(group, ClassGroupDefault): return _("Default")
395 if isinstance(group, ClassGroupSingleton): return _("Singleton")
396 if isinstance(group, ClassGroupRange): return _("Range")
397 if isinstance(group, ClassGroupMap): return _("Map")
398
399 assert False # shouldn't get here
400 return ""
401
402 def GetNumberRows(self):
403 """Return the number of rows."""
404 if self.clazz is None:
405 return 0
406
407 return self.clazz.GetNumGroups() + 1 # +1 for default group
408
409 def GetNumberCols(self):
410 """Return the number of columns."""
411 return NUM_COLS
412
413 def IsEmptyCell(self, row, col):
414 """Determine if a cell is empty. This is always false."""
415 return False
416
417 def GetValue(self, row, col):
418 """Return the object that is used to represent the given
419 cell coordinates. This may not be a string."""
420 return self.GetValueAsCustom(row, col, None)
421
422 def SetValue(self, row, col, value):
423 """Assign 'value' to the cell specified by 'row' and 'col'.
424
425 The table is considered modified after this operation.
426 """
427
428 self.SetValueAsCustom(row, col, None, value)
429
430 def GetValueAsCustom(self, row, col, typeName):
431 """Return the object that is used to represent the given
432 cell coordinates. This may not be a string.
433
434 typeName -- unused, but needed to overload wxPyGridTableBase
435 """
436
437 if row == 0:
438 group = self.clazz.GetDefaultGroup()
439 else:
440 group = self.clazz.GetGroup(row - 1)
441
442
443 if col == COL_VISIBLE:
444 return group.IsVisible()
445
446 if col == COL_SYMBOL:
447 return group.GetProperties()
448
449 if col == COL_LABEL:
450 return group.GetLabel()
451
452 # col must be COL_VALUE
453 assert col == COL_VALUE
454
455 if isinstance(group, ClassGroupDefault):
456 return _("DEFAULT")
457 elif isinstance(group, ClassGroupSingleton):
458 return group.GetValue()
459 elif isinstance(group, ClassGroupRange):
460 return _("%s - %s") % (group.GetMin(), group.GetMax())
461
462 assert(False) # shouldn't get here
463 return None
464
465 def __ParseInput(self, value):
466 """Try to determine what kind of input value is
467 (string, number, or range)
468
469 Returns a tuple of length one if there is a single
470 value, or of length two if it is a range.
471 """
472
473 type = self.fieldType
474
475 if type == FIELDTYPE_STRING:
476 return (value,)
477 elif type in (FIELDTYPE_INT, FIELDTYPE_DOUBLE):
478
479 if type == FIELDTYPE_INT:
480 # the float call allows the user to enter 1.0 for 1
481 conv = lambda p: int(float(p))
482 else:
483 conv = lambda p: p
484
485 #
486 # first try to take the input as a single number
487 # if there's an exception try to break it into
488 # a range seperated by a '-'. take care to ignore
489 # a leading '-' as that could be for a negative number.
490 # then try to parse the individual parts. if there
491 # is an exception here, let it pass up to the calling
492 # function.
493 #
494 try:
495 return (conv(value),)
496 except ValueError:
497 i = value.find('-')
498 if i == 0:
499 i = value.find('-', 1)
500
501 return (conv(value[:i]), conv(value[i+1:]))
502
503 assert False # shouldn't get here
504 return (0,)
505
506
507 def SetValueAsCustom(self, row, col, typeName, value):
508 """Set the cell specified by 'row' and 'col' to 'value'.
509
510 If column represents the value column, the input is parsed
511 to determine if a string, number, or range was entered.
512 A new ClassGroup may be created if the type of data changes.
513
514 The table is considered modified after this operation.
515
516 typeName -- unused, but needed to overload wxPyGridTableBase
517 """
518
519 assert 0 <= col < self.GetNumberCols()
520 assert 0 <= row < self.GetNumberRows()
521
522 if row == 0:
523 group = self.clazz.GetDefaultGroup()
524 else:
525 group = self.clazz.GetGroup(row - 1)
526
527 mod = True # assume the data will change
528
529 if col == COL_VISIBLE:
530 group.SetVisible(value)
531 elif col == COL_SYMBOL:
532 group.SetProperties(value)
533 elif col == COL_LABEL:
534 group.SetLabel(value)
535 elif col == COL_VALUE:
536 if isinstance(group, ClassGroupDefault):
537 # not allowed to modify the default value
538 pass
539 elif isinstance(group, ClassGroupMap):
540 # something special
541 pass
542 else: # SINGLETON, RANGE
543 try:
544 dataInfo = self.__ParseInput(value)
545 except ValueError:
546 # bad input, ignore the request
547 mod = False
548 else:
549
550 changed = False
551 ngroup = group
552 props = group.GetProperties()
553
554 #
555 # try to update the values, which may include
556 # changing the underlying group type if the
557 # group was a singleton and a range was entered
558 #
559 if len(dataInfo) == 1:
560 if not isinstance(group, ClassGroupSingleton):
561 ngroup = ClassGroupSingleton(props = props)
562 changed = True
563 ngroup.SetValue(dataInfo[0])
564 elif len(dataInfo) == 2:
565 if not isinstance(group, ClassGroupRange):
566 ngroup = ClassGroupRange(props = props)
567 changed = True
568 ngroup.SetRange(dataInfo[0], dataInfo[1])
569 else:
570 assert False
571 pass
572
573 if changed:
574 ngroup.SetLabel(group.GetLabel())
575 self.SetClassGroup(row, ngroup)
576 else:
577 assert False # shouldn't be here
578 pass
579
580 if mod:
581 self.__Modified()
582 self.GetView().Refresh()
583
584 def GetAttr(self, row, col, someExtraParameter):
585 """Returns the cell attributes"""
586
587 return self.__colAttr.get(col, wxGridCellAttr()).Clone()
588
589 def GetClassGroup(self, row):
590 """Return the ClassGroup object representing row 'row'."""
591
592 #return self.GetValueAsCustom(row, COL_SYMBOL, None)
593 if row == 0:
594 return self.clazz.GetDefaultGroup()
595 else:
596 return self.clazz.GetGroup(row - 1)
597
598 def SetClassGroup(self, row, group):
599 self.__SetRow(row, group)
600 self.GetView().Refresh()
601
602 def __Modified(self, mod = True):
603 """Adjust the modified flag.
604
605 mod -- if -1 set the modified flag to False, otherwise perform
606 an 'or' operation with the current value of the flag and
607 'mod'
608 """
609
610 if mod == -1:
611 self.modified = False
612 else:
613 self.modified = mod or self.modified
614
615 def IsModified(self):
616 """True if this table is considered modified."""
617 return self.modified
618
619 def DeleteRows(self, pos, numRows = 1):
620 """Deletes 'numRows' beginning at row 'pos'.
621
622 The row representing the default group is not removed.
623
624 The table is considered modified if any rows are removed.
625 """
626
627 assert pos >= 0
628 old_len = self.GetNumberRows()
629 for row in range(pos, pos - numRows, -1):
630 group = self.GetClassGroup(row)
631 if row != 0:
632 self.clazz.RemoveGroup(row - 1)
633 self.__Modified()
634
635 if self.IsModified():
636 self.__NotifyRowChanges(old_len, self.GetNumberRows())
637
638 def AppendRows(self, numRows = 1):
639 """Append 'numRows' empty rows to the end of the table.
640
641 The table is considered modified if any rows are appended.
642 """
643
644 old_len = self.GetNumberRows()
645 for i in range(numRows):
646 np = ClassGroupSingleton()
647 self.__SetRow(None, np)
648
649 if self.IsModified():
650 self.__NotifyRowChanges(old_len, self.GetNumberRows())
651
652
653 ID_PROPERTY_REVERT = 4002
654 ID_PROPERTY_ADD = 4003
655 ID_PROPERTY_GENCLASS = 4004
656 ID_PROPERTY_REMOVE = 4005
657 ID_PROPERTY_MOVEUP = 4006
658 ID_PROPERTY_MOVEDOWN = 4007
659 ID_PROPERTY_TRY = 4008
660 ID_PROPERTY_EDITSYM = 4009
661 ID_PROPERTY_SELECT = 4011
662 ID_PROPERTY_TITLE = 4012
663 ID_PROPERTY_FIELDTEXT = 4013
664
665 BTN_ADD = 0
666 BTN_EDIT = 1
667 BTN_GEN = 2
668 BTN_UP = 3
669 BTN_DOWN = 4
670 BTN_RM = 5
671
672 EB_LAYER_TITLE = 0
673 EB_SELECT_FIELD = 1
674 EB_GEN_CLASS = 2
675
676 class Classifier(NonModalDialog):
677
678 type2string = {None: _("None"),
679 FIELDTYPE_STRING: _("Text"),
680 FIELDTYPE_INT: _("Integer"),
681 FIELDTYPE_DOUBLE: _("Decimal")}
682
683 def __init__(self, parent, name, layer, group = None):
684 NonModalDialog.__init__(self, parent, name, "")
685
686 self.__SetTitle(layer.Title())
687
688 self.layer = layer
689
690 self.originalClass = self.layer.GetClassification()
691 field = self.originalClass.GetField()
692 fieldType = self.originalClass.GetFieldType()
693
694 self.genDlg = None
695
696 ############################
697 # Create the controls
698 #
699
700 panel = wxPanel(self, -1)
701
702 text_title = wxTextCtrl(panel, ID_PROPERTY_TITLE, layer.Title())
703 #
704 # make field choice box
705 #
706 self.fields = wxChoice(panel, ID_PROPERTY_SELECT,)
707
708 self.num_cols = layer.table.NumColumns()
709 # just assume the first field in case one hasn't been
710 # specified in the file.
711 self.__cur_field = 0
712
713 self.fields.Append("<None>")
714
715 if self.originalClass.GetFieldType() is None:
716 self.fields.SetClientData(0, copy.deepcopy(self.originalClass))
717 else:
718 self.fields.SetClientData(0, None)
719
720 for i in range(self.num_cols):
721 name = layer.table.Column(i).name
722 self.fields.Append(name)
723
724 if name == field:
725 self.__cur_field = i + 1
726 self.fields.SetClientData(i + 1,
727 copy.deepcopy(self.originalClass))
728 else:
729 self.fields.SetClientData(i + 1, None)
730
731
732 self.fieldTypeText = wxStaticText(panel, -1, "")
733
734 button_gen = wxButton(panel, ID_PROPERTY_GENCLASS, _("Generate Class"))
735
736 button_add = wxButton(panel, ID_PROPERTY_ADD, _("Add"))
737 button_moveup = wxButton(panel, ID_PROPERTY_MOVEUP, _("Move Up"))
738 button_movedown = wxButton(panel, ID_PROPERTY_MOVEDOWN, _("Move Down"))
739 button_edit = wxButton(panel, ID_PROPERTY_EDITSYM, _("Edit Symbol"))
740 button_remove = wxButton(panel, ID_PROPERTY_REMOVE, _("Remove"))
741
742
743 button_try = wxButton(panel, ID_PROPERTY_TRY, _("Try"))
744 button_revert = wxButton(panel, ID_PROPERTY_REVERT, _("Revert"))
745 button_ok = wxButton(panel, wxID_OK, _("OK"))
746 button_ok.SetDefault()
747 button_close = wxButton(panel, wxID_CANCEL, _("Close"))
748
749 self.classGrid = ClassGrid(panel, self)
750
751 # calling __SelectField after creating the classGrid fills in the
752 # grid with the correct information
753 self.fields.SetSelection(self.__cur_field)
754 self.__SelectField(self.__cur_field, group = group)
755
756 ############################
757 # Layout the controls
758 #
759
760 topBox = wxBoxSizer(wxVERTICAL)
761 panelBox = wxBoxSizer(wxVERTICAL)
762
763 sizer = wxBoxSizer(wxHORIZONTAL)
764 sizer.Add(wxStaticText(panel, -1, _("Title: ")),
765 0, wxALIGN_LEFT | wxALL | wxALIGN_CENTER_VERTICAL, 4)
766 sizer.Add(text_title, 1, wxGROW, 0)
767
768 panelBox.Add(sizer, 0, wxGROW, 4)
769
770 panelBox.Add(wxStaticText(panel, -1,
771 _("Type: %s") % layer.ShapeType()),
772 0, wxALIGN_LEFT | wxALL, 4)
773
774 classBox = wxStaticBoxSizer(
775 wxStaticBox(panel, -1, _("Classification")), wxVERTICAL)
776
777
778 sizer = wxBoxSizer(wxHORIZONTAL)
779 sizer.Add(wxStaticText(panel, ID_PROPERTY_FIELDTEXT, _("Field: ")),
780 0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxALL, 4)
781 sizer.Add(self.fields, 1, wxGROW | wxALL, 4)
782
783 classBox.Add(sizer, 0, wxGROW, 4)
784
785 classBox.Add(self.fieldTypeText, 0,
786 wxGROW | wxALIGN_LEFT | wxALL | wxADJUST_MINSIZE, 4)
787
788 controlBox = wxBoxSizer(wxHORIZONTAL)
789 controlButtonBox = wxBoxSizer(wxVERTICAL)
790
791 controlButtonBox.Add(button_gen, 0, wxGROW|wxALL, 4)
792 controlButtonBox.Add(button_add, 0, wxGROW|wxALL, 4)
793 controlButtonBox.Add(button_moveup, 0, wxGROW|wxALL, 4)
794 controlButtonBox.Add(button_movedown, 0, wxGROW|wxALL, 4)
795 controlButtonBox.Add(button_edit, 0, wxGROW|wxALL, 4)
796 controlButtonBox.Add(60, 20, 0, wxGROW|wxALL|wxALIGN_BOTTOM, 4)
797 controlButtonBox.Add(button_remove, 0, wxGROW|wxALL|wxALIGN_BOTTOM, 4)
798
799 controlBox.Add(self.classGrid, 1, wxGROW, 0)
800 controlBox.Add(controlButtonBox, 0, wxGROW, 10)
801
802 classBox.Add(controlBox, 1, wxGROW, 10)
803 panelBox.Add(classBox, 1, wxGROW, 0)
804
805
806 buttonBox = wxBoxSizer(wxHORIZONTAL)
807 buttonBox.Add(button_try, 0, wxALL, 4)
808 buttonBox.Add(60, 20, 0, wxALL, 4)
809 buttonBox.Add(button_revert, 0, wxALL, 4)
810 buttonBox.Add(60, 20, 0, wxALL, 4)
811 buttonBox.Add(button_ok, 0, wxALL, 4)
812 buttonBox.Add(60, 20, 0, wxALL, 4)
813 buttonBox.Add(button_close, 0, wxALL, 4)
814 panelBox.Add(buttonBox, 0,
815 wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_BOTTOM, 0)
816
817 panel.SetAutoLayout(True)
818 panel.SetSizer(panelBox)
819 panelBox.Fit(panel)
820 panelBox.SetSizeHints(panel)
821
822 topBox.Add(panel, 1, wxGROW | wxALL, 4)
823
824 self.SetAutoLayout(True)
825 self.SetSizer(topBox)
826 topBox.Fit(self)
827 topBox.SetSizeHints(self)
828 self.Layout()
829
830 ###########
831
832 EVT_CHOICE(self, ID_PROPERTY_SELECT, self._OnFieldSelect)
833 EVT_TEXT(self, ID_PROPERTY_TITLE, self._OnTitleChanged)
834 EVT_BUTTON(self, wxID_OK, self._OnOK)
835 EVT_BUTTON(self, ID_PROPERTY_TRY, self._OnTry)
836 EVT_BUTTON(self, wxID_CANCEL, self._OnCloseBtn)
837 EVT_BUTTON(self, ID_PROPERTY_REVERT, self._OnRevert)
838
839 EVT_BUTTON(self, ID_PROPERTY_ADD, self._OnAdd)
840 EVT_BUTTON(self, ID_PROPERTY_EDITSYM, self._OnEditSymbol)
841 EVT_BUTTON(self, ID_PROPERTY_REMOVE, self._OnRemove)
842 EVT_BUTTON(self, ID_PROPERTY_GENCLASS, self._OnGenClass)
843 EVT_BUTTON(self, ID_PROPERTY_MOVEUP, self._OnMoveUp)
844 EVT_BUTTON(self, ID_PROPERTY_MOVEDOWN, self._OnMoveDown)
845
846 ######################
847
848 self.fields.SetFocus()
849 self.haveApplied = False
850
851 def EditSymbol(self, row):
852 table = self.classGrid.GetTable()
853 prop = table.GetValueAsCustom(row, COL_SYMBOL, None)
854
855 # get a new ClassGroupProperties object and copy the
856 # values over to our current object
857 propDlg = SelectPropertiesDialog(NULL, prop, self.layer.ShapeType())
858
859 self.Enable(False)
860 if propDlg.ShowModal() == wxID_OK:
861 new_prop = propDlg.GetClassGroupProperties()
862 table.SetValueAsCustom(row, COL_SYMBOL, None, new_prop)
863 self.Enable(True)
864 propDlg.Destroy()
865
866 def _SetClassification(self, clazz):
867
868 self.fields.SetClientData(self.__cur_field, clazz)
869 self.classGrid.GetTable().SetClassification(clazz)
870
871 def __BuildClassification(self, fieldIndex, copyClass = False):
872
873 # numRows = self.classGrid.GetNumberRows()
874 # assert numRows > 0 # there should always be a default row
875
876 # clazz = Classification()
877 if fieldIndex == 0:
878 fieldName = None
879 fieldType = None
880 else:
881 fieldName = self.fields.GetString(fieldIndex)
882 fieldType = self.layer.GetFieldType(fieldName)
883
884 clazz = self.classGrid.GetTable().GetClassification()
885
886 if copyClass:
887 clazz = copy.deepcopy(clazz)
888
889 clazz.SetField(fieldName)
890 clazz.SetFieldType(fieldType)
891
892
893 # table = self.classGrid.GetTable()
894 # clazz.SetDefaultGroup(table.GetClassGroup(0))
895
896 # for i in range(1, numRows):
897 # clazz.AppendGroup(table.GetClassGroup(i))
898
899 return clazz
900
901 def __SetGridTable(self, fieldIndex, group = None):
902
903 clazz = self.fields.GetClientData(fieldIndex)
904
905 if clazz is None:
906 clazz = Classification()
907 clazz.SetDefaultGroup(
908 ClassGroupDefault(
909 self.layer.GetClassification().
910 GetDefaultGroup().GetProperties()))
911
912 fieldName = self.fields.GetString(fieldIndex)
913 fieldType = self.layer.GetFieldType(fieldName)
914 clazz.SetFieldType(fieldType)
915
916 self.classGrid.CreateTable(clazz, self.layer.ShapeType(), group)
917
918 def __SetFieldTypeText(self, fieldIndex):
919 fieldName = self.fields.GetString(fieldIndex)
920 fieldType = self.layer.GetFieldType(fieldName)
921
922 assert Classifier.type2string.has_key(fieldType)
923
924 text = Classifier.type2string[fieldType]
925
926 self.fieldTypeText.SetLabel(_("Data Type: %s") % text)
927
928 def __SelectField(self, newIndex, oldIndex = -1, group = None):
929 """This method assumes that the current selection for the
930 combo has already been set by a call to SetSelection().
931 """
932
933 assert oldIndex >= -1
934
935 if oldIndex != -1:
936 clazz = self.__BuildClassification(oldIndex)
937 self.fields.SetClientData(oldIndex, clazz)
938
939 self.__SetGridTable(newIndex, group)
940
941 self.__EnableButtons(EB_SELECT_FIELD, newIndex != 0)
942
943 self.__SetFieldTypeText(newIndex)
944
945 def __SetTitle(self, title):
946 if title != "":
947 title = ": " + title
948
949 self.SetTitle(_("Layer Properties") + title)
950
951 def _OnEditSymbol(self, event):
952 sel = self.classGrid.GetCurrentSelection()
953
954 if len(sel) == 1:
955 self.EditSymbol(sel[0])
956
957 def _OnFieldSelect(self, event):
958 index = self.fields.GetSelection()
959 self.__SelectField(index, self.__cur_field)
960 self.__cur_field = index
961
962 def _OnTry(self, event):
963 """Put the data from the table into a new Classification and hand
964 it to the layer.
965 """
966
967 clazz = self.fields.GetClientData(self.__cur_field)
968
969 #
970 # only build the classification if there wasn't one to
971 # to begin with or it has been modified
972 #
973 if clazz is None or self.classGrid.GetTable().IsModified():
974 clazz = self.__BuildClassification(self.__cur_field, True)
975
976 self.layer.SetClassification(clazz)
977
978 self.haveApplied = True
979
980 def _OnOK(self, event):
981 self._OnTry(event)
982 self.Close()
983
984 def OnClose(self, event):
985 NonModalDialog.OnClose(self, event)
986
987 def _OnCloseBtn(self, event):
988 """Close is similar to Cancel except that any changes that were
989 made and applied remain applied, but the currently displayed
990 classification is discarded.
991 """
992
993 self.Close()
994
995 def _OnRevert(self, event):
996 """The layer's current classification stays the same."""
997 if self.haveApplied:
998 self.layer.SetClassification(self.originalClass)
999
1000 #self.Close()
1001
1002 def _OnAdd(self, event):
1003 self.classGrid.AppendRows()
1004
1005 def _OnRemove(self, event):
1006 self.classGrid.DeleteSelectedRows()
1007
1008 def _OnGenClass(self, event):
1009
1010 self.genDlg = ClassGenDialog(self, self.layer,
1011 self.fields.GetString(self.__cur_field))
1012
1013 EVT_CLOSE(self.genDlg, self._OnGenDialogClose)
1014
1015 self.__EnableButtons(EB_GEN_CLASS, False)
1016
1017 self.genDlg.Show()
1018
1019 def _OnGenDialogClose(self, event):
1020 self.genDlg.Destroy()
1021 self.__EnableButtons(EB_GEN_CLASS, True)
1022
1023 def _OnMoveUp(self, event):
1024 sel = self.classGrid.GetCurrentSelection()
1025
1026 if len(sel) == 1:
1027 i = sel[0]
1028 if i > 1:
1029 table = self.classGrid.GetTable()
1030 x = table.GetClassGroup(i - 1)
1031 y = table.GetClassGroup(i)
1032 table.SetClassGroup(i - 1, y)
1033 table.SetClassGroup(i, x)
1034 self.classGrid.ClearSelection()
1035 self.classGrid.SelectRow(i - 1)
1036 self.classGrid.MakeCellVisible(i - 1, 0)
1037
1038 def _OnMoveDown(self, event):
1039 sel = self.classGrid.GetCurrentSelection()
1040
1041 if len(sel) == 1:
1042 i = sel[0]
1043 table = self.classGrid.GetTable()
1044 if 0 < i < table.GetNumberRows() - 1:
1045 x = table.GetClassGroup(i)
1046 y = table.GetClassGroup(i + 1)
1047 table.SetClassGroup(i, y)
1048 table.SetClassGroup(i + 1, x)
1049 self.classGrid.ClearSelection()
1050 self.classGrid.SelectRow(i + 1)
1051 self.classGrid.MakeCellVisible(i + 1, 0)
1052
1053 def _OnTitleChanged(self, event):
1054 obj = event.GetEventObject()
1055
1056 self.layer.SetTitle(obj.GetValue())
1057 self.__SetTitle(self.layer.Title())
1058
1059 self.__EnableButtons(EB_LAYER_TITLE, self.layer.Title() != "")
1060
1061 def __EnableButtons(self, case, enable):
1062
1063 if case == EB_LAYER_TITLE:
1064 list = (wxID_OK,
1065 wxID_CANCEL)
1066
1067 elif case == EB_SELECT_FIELD:
1068 list = (ID_PROPERTY_GENCLASS,
1069 ID_PROPERTY_ADD,
1070 ID_PROPERTY_MOVEUP,
1071 ID_PROPERTY_MOVEDOWN,
1072 ID_PROPERTY_EDITSYM,
1073 ID_PROPERTY_REMOVE)
1074
1075 elif case == EB_GEN_CLASS:
1076 list = (ID_PROPERTY_SELECT,
1077 ID_PROPERTY_FIELDTEXT,
1078 ID_PROPERTY_GENCLASS,
1079 ID_PROPERTY_EDITSYM)
1080
1081 for id in list:
1082 self.FindWindowById(id).Enable(enable)
1083
1084 ID_SELPROP_SPINCTRL = 4002
1085 ID_SELPROP_PREVIEW = 4003
1086 ID_SELPROP_STROKECLR = 4004
1087 ID_SELPROP_FILLCLR = 4005
1088 ID_SELPROP_STROKECLRTRANS = 4006
1089 ID_SELPROP_FILLCLRTRANS = 4007
1090
1091 class SelectPropertiesDialog(wxDialog):
1092
1093 def __init__(self, parent, prop, shapeType):
1094 wxDialog.__init__(self, parent, -1, _("Select Properties"),
1095 style = wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
1096
1097 self.prop = ClassGroupProperties(prop)
1098
1099 topBox = wxBoxSizer(wxVERTICAL)
1100
1101 itemBox = wxBoxSizer(wxHORIZONTAL)
1102
1103 # preview box
1104 previewBox = wxBoxSizer(wxVERTICAL)
1105 previewBox.Add(wxStaticText(self, -1, _("Preview:")),
1106 0, wxALIGN_LEFT | wxALL, 4)
1107
1108 self.previewWin = ClassGroupPropertiesCtrl(
1109 self, ID_SELPROP_PREVIEW, self.prop, shapeType,
1110 (40, 40), wxSIMPLE_BORDER)
1111
1112 self.previewWin.AllowEdit(False)
1113
1114 previewBox.Add(self.previewWin, 1, wxGROW | wxALL, 4)
1115
1116 itemBox.Add(previewBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)
1117
1118 # control box
1119 ctrlBox = wxBoxSizer(wxVERTICAL)
1120
1121 lineColorBox = wxBoxSizer(wxHORIZONTAL)
1122 button = wxButton(self, ID_SELPROP_STROKECLR, _("Change Line Color"))
1123 button.SetFocus()
1124 lineColorBox.Add(button, 1, wxALL | wxGROW, 4)
1125 EVT_BUTTON(self, ID_SELPROP_STROKECLR, self._OnChangeLineColor)
1126
1127 lineColorBox.Add(
1128 wxButton(self, ID_SELPROP_STROKECLRTRANS, _("Transparent")),
1129 1, wxALL | wxGROW, 4)
1130 EVT_BUTTON(self, ID_SELPROP_STROKECLRTRANS,
1131 self._OnChangeLineColorTrans)
1132
1133 ctrlBox.Add(lineColorBox, 0,
1134 wxALIGN_CENTER_HORIZONTAL | wxALL | wxGROW, 4)
1135
1136 if shapeType != SHAPETYPE_ARC:
1137 fillColorBox = wxBoxSizer(wxHORIZONTAL)
1138 fillColorBox.Add(
1139 wxButton(self, ID_SELPROP_FILLCLR, _("Change Fill Color")),
1140 1, wxALL | wxGROW, 4)
1141 EVT_BUTTON(self, ID_SELPROP_FILLCLR, self._OnChangeFillColor)
1142 fillColorBox.Add(
1143 wxButton(self, ID_SELPROP_FILLCLRTRANS, _("Transparent")),
1144 1, wxALL | wxGROW, 4)
1145 EVT_BUTTON(self, ID_SELPROP_FILLCLRTRANS,
1146 self._OnChangeFillColorTrans)
1147 ctrlBox.Add(fillColorBox, 0,
1148 wxALIGN_CENTER_HORIZONTAL | wxALL | wxGROW, 4)
1149
1150 spinBox = wxBoxSizer(wxHORIZONTAL)
1151 spinBox.Add(wxStaticText(self, -1, _("Line Width: ")),
1152 0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxALL, 4)
1153 self.spinCtrl = wxSpinCtrl(self, ID_SELPROP_SPINCTRL,
1154 min=1, max=10,
1155 value=str(prop.GetLineWidth()),
1156 initial=prop.GetLineWidth())
1157
1158 EVT_SPINCTRL(self, ID_SELPROP_SPINCTRL, self._OnSpin)
1159
1160 spinBox.Add(self.spinCtrl, 0, wxALIGN_LEFT | wxALL, 4)
1161
1162 ctrlBox.Add(spinBox, 0, wxALIGN_RIGHT | wxALL, 0)
1163 itemBox.Add(ctrlBox, 0, wxALIGN_RIGHT | wxALL | wxGROW, 0)
1164 topBox.Add(itemBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)
1165
1166 #
1167 # Control buttons:
1168 #
1169 buttonBox = wxBoxSizer(wxHORIZONTAL)
1170 button_ok = wxButton(self, wxID_OK, _("OK"))
1171 button_ok.SetDefault()
1172 buttonBox.Add(button_ok, 0, wxALL, 4)
1173 buttonBox.Add(wxButton(self, wxID_CANCEL, _("Cancel")),
1174 0, wxALL, 4)
1175 topBox.Add(buttonBox, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_BOTTOM, 10)
1176
1177 #EVT_BUTTON(self, wxID_OK, self._OnOK)
1178 #EVT_BUTTON(self, ID_SELPROP_CANCEL, self._OnCancel)
1179
1180 self.SetAutoLayout(True)
1181 self.SetSizer(topBox)
1182 topBox.Fit(self)
1183 topBox.SetSizeHints(self)
1184
1185 def OnOK(self, event):
1186 self.EndModal(wxID_OK)
1187
1188 def OnCancel(self, event):
1189 self.EndModal(wxID_CANCEL)
1190
1191 def _OnSpin(self, event):
1192 self.prop.SetLineWidth(self.spinCtrl.GetValue())
1193 self.previewWin.Refresh()
1194
1195 def __GetColor(self, cur):
1196 dialog = wxColourDialog(self)
1197 if cur is not Color.Transparent:
1198 dialog.GetColourData().SetColour(Color2wxColour(cur))
1199
1200 ret = None
1201 if dialog.ShowModal() == wxID_OK:
1202 ret = wxColour2Color(dialog.GetColourData().GetColour())
1203
1204 dialog.Destroy()
1205
1206 return ret
1207
1208 def _OnChangeLineColor(self, event):
1209 clr = self.__GetColor(self.prop.GetLineColor())
1210 if clr is not None:
1211 self.prop.SetLineColor(clr)
1212 self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer
1213
1214 def _OnChangeLineColorTrans(self, event):
1215 self.prop.SetLineColor(Color.Transparent)
1216 self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer
1217
1218 def _OnChangeFillColor(self, event):
1219 clr = self.__GetColor(self.prop.GetFill())
1220 if clr is not None:
1221 self.prop.SetFill(clr)
1222 self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer
1223
1224 def _OnChangeFillColorTrans(self, event):
1225 self.prop.SetFill(Color.Transparent)
1226 self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer
1227
1228 def GetClassGroupProperties(self):
1229 return self.prop
1230
1231
1232 class ClassDataPreviewWindow(wxWindow):
1233
1234 def __init__(self, rect, prop, shapeType,
1235 parent = None, id = -1, size = wxDefaultSize):
1236 if parent is not None:
1237 wxWindow.__init__(self, parent, id, (0, 0), size)
1238 EVT_PAINT(self, self._OnPaint)
1239
1240 self.rect = rect
1241
1242 self.prop = prop
1243 self.shapeType = shapeType
1244 self.previewer = ClassDataPreviewer()
1245
1246 def GetProperties():
1247 return self.prop
1248
1249 def _OnPaint(self, event):
1250 dc = wxPaintDC(self)
1251
1252 # XXX: this doesn't seem to be having an effect:
1253 dc.DestroyClippingRegion()
1254
1255 if self.rect is None:
1256 w, h = self.GetSize()
1257 rect = wxRect(0, 0, w, h)
1258 else:
1259 rect = self.rect
1260
1261 self.previewer.Draw(dc, rect, self.prop, self.shapeType)
1262
1263 class ClassDataPreviewer:
1264
1265 def Draw(self, dc, rect, prop, shapeType):
1266
1267 assert dc is not None
1268 assert isinstance(prop, ClassGroupProperties)
1269
1270 if rect is None:
1271 x = 0
1272 y = 0
1273 w, h = dc.GetSize()
1274 else:
1275 x = rect.GetX()
1276 y = rect.GetY()
1277 w = rect.GetWidth()
1278 h = rect.GetHeight()
1279
1280 stroke = prop.GetLineColor()
1281 if stroke is Color.Transparent:
1282 pen = wxTRANSPARENT_PEN
1283 else:
1284 pen = wxPen(Color2wxColour(stroke),
1285 prop.GetLineWidth(),
1286 wxSOLID)
1287
1288 stroke = prop.GetFill()
1289 if stroke is Color.Transparent:
1290 brush = wxTRANSPARENT_BRUSH
1291 else:
1292 brush = wxBrush(Color2wxColour(stroke), wxSOLID)
1293
1294 dc.SetPen(pen)
1295 dc.SetBrush(brush)
1296
1297 if shapeType == SHAPETYPE_ARC:
1298 dc.DrawSpline([wxPoint(x, y + h),
1299 wxPoint(x + w/2, y + h/4),
1300 wxPoint(x + w/2, y + h/4*3),
1301 wxPoint(x + w, y)])
1302
1303 elif shapeType == SHAPETYPE_POINT:
1304
1305 dc.DrawCircle(x + w/2, y + h/2,
1306 (min(w, h) - prop.GetLineWidth())/2)
1307
1308 elif shapeType == SHAPETYPE_POLYGON:
1309 dc.DrawRectangle(x, y, w, h)
1310
1311 class ClassRenderer(wxPyGridCellRenderer):
1312
1313 def __init__(self, shapeType):
1314 wxPyGridCellRenderer.__init__(self)
1315 self.shapeType = shapeType
1316 self.previewer = ClassDataPreviewer()
1317
1318 def Draw(self, grid, attr, dc, rect, row, col, isSelected):
1319 data = grid.GetTable().GetClassGroup(row)
1320
1321 dc.SetClippingRegion(rect.GetX(), rect.GetY(),
1322 rect.GetWidth(), rect.GetHeight())
1323 dc.SetPen(wxPen(wxLIGHT_GREY))
1324 dc.SetBrush(wxBrush(wxLIGHT_GREY, wxSOLID))
1325 dc.DrawRectangle(rect.GetX(), rect.GetY(),
1326 rect.GetWidth(), rect.GetHeight())
1327
1328 if not isinstance(data, ClassGroupMap):
1329 self.previewer.Draw(dc, rect, data.GetProperties(), self.shapeType)
1330
1331 if isSelected:
1332 dc.SetPen(wxPen(wxBLACK, 1, wxSOLID))
1333 dc.SetBrush(wxTRANSPARENT_BRUSH)
1334
1335 dc.DrawRectangle(rect.GetX(), rect.GetY(),
1336 rect.GetWidth(), rect.GetHeight())
1337
1338 dc.DestroyClippingRegion()
1339
1340
1341 class ClassGroupPropertiesCtrl(wxWindow, wxControl):
1342
1343 def __init__(self, parent, id, props, shapeType,
1344 size = wxDefaultSize, style = 0):
1345
1346 wxWindow.__init__(self, parent, id, size = size, style = style)
1347
1348 self.SetProperties(props)
1349 self.SetShapeType(shapeType)
1350 self.AllowEdit(True)
1351
1352 EVT_PAINT(self, self._OnPaint)
1353 EVT_LEFT_DCLICK(self, self._OnLeftDClick)
1354
1355 self.previewer = ClassDataPreviewer()
1356
1357 def _OnPaint(self, event):
1358 dc = wxPaintDC(self)
1359
1360 # XXX: this doesn't seem to be having an effect:
1361 dc.DestroyClippingRegion()
1362
1363 w, h = self.GetClientSize()
1364
1365 self.previewer.Draw(dc,
1366 wxRect(0, 0, w, h),
1367 self.GetProperties(),
1368 self.GetShapeType())
1369
1370
1371 def GetProperties(self):
1372 return self.props
1373
1374 def SetProperties(self, props):
1375 self.props = props
1376 self.Refresh()
1377
1378 def GetShapeType(self):
1379 return self.shapeType
1380
1381 def SetShapeType(self, shapeType):
1382 self.shapeType = shapeType
1383 self.Refresh()
1384
1385 def AllowEdit(self, allow):
1386 self.allowEdit = allow
1387
1388 def DoEdit(self):
1389 if not self.allowEdit: return
1390
1391 propDlg = SelectPropertiesDialog(NULL,
1392 self.GetProperties(),
1393 self.GetShapeType())
1394
1395 if propDlg.ShowModal() == wxID_OK:
1396 new_prop = propDlg.GetClassGroupProperties()
1397 self.SetProperties(new_prop)
1398 self.Refresh()
1399
1400 propDlg.Destroy()
1401
1402 def _OnLeftDClick(self, event):
1403 self.DoEdit()

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26