root/trunk/ide/ClassDesigner.py

Revision 4673, 125.4 kB (checked in by ed, 3 weeks ago)

Updated these classes to use the revised import statement handling.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
Line 
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 import sys
4 import os
5 import copy
6 import inspect
7 import dabo
8 from dabo.dLocalize import _
9 import dabo.dEvents as dEvents
10 if __name__ == "__main__":
11     dabo.ui.loadUI("wx")
12 # This is because I'm a lazy typist
13 dui = dabo.ui
14 from ClassDesignerFormMixin import ClassDesignerFormMixin as dfm
15 from ClassDesignerPemForm import PemForm
16 from ClassDesignerEditor import EditorForm
17 from ClassDesignerComponents import LayoutPanel
18 from ClassDesignerComponents import LayoutBasePanel
19 from ClassDesignerComponents import LayoutSpacerPanel
20 from ClassDesignerComponents import LayoutSizer
21 from ClassDesignerComponents import LayoutBorderSizer
22 from ClassDesignerComponents import LayoutGridSizer
23 from ClassDesignerComponents import LayoutSaverMixin
24 from ClassDesignerComponents import NoSizerBasePanel
25 from ClassDesignerComponents import szItemDefaults
26 from ClassDesignerComponents import classFlagProp
27 from ClassDesignerControlMixin import ClassDesignerControlMixin as cmix
28 from ClassDesignerCustomPropertyDialog import ClassDesignerCustomPropertyDialog
29 from ClassDesignerSizerPalette import SizerPaletteForm
30 from dabo.lib.DesignerXmlConverter import DesignerXmlConverter
31 from dabo.lib import DesignerUtils
32 import ClassDesignerMenu
33 import dabo.lib.xmltodict as xtd
34 import dabo.ui.dialogs as dlgs
35 from dabo.lib.utils import dictStringify
36 from ClassDesignerExceptions import PropertyUpdateException
37 # Temporary fix for wxPython 2.6 users
38 try:
39     dabo.ui.dDockForm
40     _USE_DOCKFORM = True
41 except:
42     dabo.ui.dDockForm = None
43     _USE_DOCKFORM = False
44
45
46
47 class ClassDesigner(dabo.dApp):
48     # Behaviors which are normal in the framework may need to
49     # be modified when run as the ClassDesigner. This flag will
50     # distinguish between the two states.
51     isDesigner = True
52
53     def __init__(self, clsFile=""):
54         super(ClassDesigner, self).__init__(showSplashScreen=False,
55                 splashTimeout=10)
56
57         self._basePrefKey = "dabo.ide.ClassDesigner"
58         self._desFormClass = None
59         self._selectedClass = dui.dForm
60         self._currentForm = None
61         self._editorForm = None
62         self._pemForm = None
63         self._tree = None
64         self._palette = None
65         self._sizerPalette = None
66         self._selection = []
67         self._editors = []
68         self._srcObj = None
69         self._srcPos = None
70         self._codeDict = {}
71         self._classCodeDict = {}
72         self._classPropDict = {}
73         self._classImportDict = {}
74         self._classDefaultVals = {}
75         self._mixedControlClasses = {}
76         self._superClassInfo = {}
77         self._addingClass = False
78         # Tuple of all paged-control classes
79         self.pagedControls = (dui.dPageFrame, dui.dPageList, dui.dPageSelect,
80                 dui.dPageFrameNoTabs)
81         self.MainFormClass = None
82         self.setAppInfo("appName", "Class Designer")
83         # Some processes need to behave differently when we are
84         # importing a class from a cdxml file; this flag lets them
85         # determine what process is being run.
86         self.openingClassXML = False
87         # Create the clipboard
88         self._clipboard = None
89         # This holds a reference to the target object when
90         # there is a context menu event.
91         self._contextObj = None
92         # When saving classes, we need to note when we are inside
93         # a class definition. The list is used as the class stack.
94         self._classStack = []
95         # We also need to save child class definitions on a stack, when
96         # saving/recreating class components with nested objects.
97         self._classChildDefStack = []
98         # Flag for indicating that all props, not just non-default ones,
99         # are saved in the .cdxml file
100         self.saveAllProps = False
101         # When we set the DefaultBorder for a sizer, should we
102         # resize all its children?
103         self._propagateDefaultBorder = True
104         # Store the name of the custom class menu here instead of
105         # hard-coding it in several places.
106         self._customClassCaption = _("Custom Classes")
107         # Add this to the persistent MRUs
108         self._persistentMRUs[self._customClassCaption] = self.addCustomClass
109         # Save the default atts for sizers. This way we can distinguish
110         # from default sizers that can be replaced from customized
111         # sizers which should remain.
112         self._defBoxSizerAtts = bsa = {}
113         atts = LayoutSizer().getDesignerDict(allProps=True)["attributes"]
114         bsa["DefaultBorder"] = atts["DefaultBorder"]
115         bsa["DefaultBorderTop"] = atts["DefaultBorderTop"]
116         bsa["DefaultBorderBottom"] = atts["DefaultBorderBottom"]
117         bsa["DefaultBorderLeft"] = atts["DefaultBorderLeft"]
118         bsa["DefaultBorderRight"] = atts["DefaultBorderRight"]
119         self._defGridSizerAtts = gsa = {}
120         atts = LayoutGridSizer().getDesignerDict(allProps=True)["attributes"]
121         gsa["HGap"] = atts["HGap"]
122         gsa["VGap"] = atts["VGap"]
123         gsa["MaxDimension"] = atts["MaxDimension"]
124         # Get rid of the update/refresh delays
125         dabo.useUpdateDelays = False
126
127
128         # Define the controls that can be added to the ClassDesigner. The
129         # 'order' value will determine their order in the menu. One plan
130         # is to keep track of the user's choices, and weight the orders
131         # so that their most frequent choices are at the top.
132         self.designerControls = ({"name" : "Box", "class" : dui.dBox, "order" : 0},
133                 {"name" : "Bitmap", "class" : dui.dBitmap, "order" : 10},
134                 {"name" : "BitmapButton", "class" : dui.dBitmapButton, "order" : 20},
135                 {"name" : "Button", "class" : dui.dButton, "order" : 30},
136                 {"name" : "CheckBox", "class" : dui.dCheckBox, "order" : 40},
137                 {"name" : "CodeEditor", "class" : dui.dEditor, "order" : 45},
138                 {"name" : "ComboBox", "class" : dui.dComboBox, "order" : 50},
139                 {"name" : "DateTextBox", "class" : dui.dDateTextBox, "order" : 60},
140                 {"name" : "DropdownList", "class" : dui.dDropdownList, "order" : 70},
141                 {"name" : "EditBox", "class" : dui.dEditBox, "order" : 80},
142                 {"name" : "SlidePanelControl", "class" : dui.dSlidePanelControl, "order" : 82},
143                 {"name" : "HtmlBox", "class" : dui.dHtmlBox, "order" : 85},
144                 {"name" : "Gauge", "class" : dui.dGauge, "order" : 90},
145                 {"name" : "Grid", "class" : dui.dGrid, "order" : 100},
146                 {"name" : "Image", "class" : dui.dImage, "order" : 110},
147                 {"name" : "Label", "class" : dui.dLabel, "order" : 120},
148                 {"name" : "Line", "class" : dui.dLine, "order" : 130},
149                 {"name" : "ListBox", "class" : dui.dListBox, "order" : 140},
150                 {"name" : "ListControl", "class" : dui.dListControl, "order" : 150},
151                 {"name" : "RadioList", "class" : dui.dRadioList, "order" : 160},
152                 {"name" : "Page", "class" : dui.dPage, "order" : 170},
153                 {"name" : "Panel", "class" : dui.dPanel, "order" : 180},
154                 {"name" : "ScrollPanel", "class" : dui.dScrollPanel, "order" : 190},
155                 {"name" : "PageFrame", "class" : dui.dPageFrame, "order" : 200},
156                 {"name" : "PageList", "class" : dui.dPageList, "order" : 210},
157                 {"name" : "PageSelect", "class" : dui.dPageSelect, "order" : 220},
158                 {"name" : "PageFrameNoTabs", "class" : dui.dPageFrameNoTabs, "order" : 230},
159                 {"name" : "Slider", "class" : dui.dSlider, "order" : 240},
160                 {"name" : "Spinner", "class" : dui.dSpinner, "order" : 250},
161                 {"name" : "Splitter", "class" : dui.dSplitter, "order" : 260},
162                 {"name" : "TextBox", "class" : dui.dTextBox, "order" : 270},
163                 {"name" : "ToggleButton", "class" : dui.dToggleButton, "order" : 280},
164                 {"name" : "TreeView", "class" : dui.dTreeView, "order" : 290}
165                 )
166         self._initClassEvents()
167
168         self.setup()
169
170         clsOK = False
171         if clsFile:
172             if not clsFile.endswith(".cdxml"):
173                 clsFile += ".cdxml"
174             try:
175                 frm = self.openClass(clsFile)
176                 clsOK = True
177             except IOError, e:
178                 msg = _("'%s' does not exist. Create it?") % clsFile
179                 if dui.areYouSure(message=msg, title=_("File Not Found"), cancelButton=False):
180                     frm = self.onNewDesign(evt=None, pth=clsFile)
181                     clsOK = True
182
183         if not clsOK:
184             # Define the form class, and instantiate it.
185             frmClass = self.getFormClass()
186
187             # Temp! for development
188 #           useSz = not os.path.exists("/Users/ed/dls")
189 #           frm = frmClass(UseSizers=useSz)
190             frm = frmClass(UseSizers=True)
191
192             frm._setupPanels()
193             # Use this to determine if an empty class should be released
194             frm._initialStateDict = frm.getDesignerDict()
195         else:
196             frm._initialStateDict = {}
197         frm.Controller = self
198         self.MainForm = frm
199         # When more than one ClassDesigner is open, this will
200         # hold the active reference.
201         self.CurrentForm = frm
202         # Create the form the holds the PropSheet, Method listing
203         # and object tree if it hasn't already been created.
204         pf = self._pemForm
205         if pf is None:
206             pf = self._pemForm = PemForm(None)
207         pf.Controller = self
208         pf.Visible = True
209
210         # Create the control palette
211         palette = self.ControlPalette
212         palette.Controller = self
213         palette.Visible = False
214
215         # Create the sizer palette, but make it hidden to start
216         palette = self.SizerPalette
217         palette.Controller = self
218         palette.Visible = False
219
220         # Create the Code Editor
221         ed = self.EditorForm
222         ed.Controller = self
223         ed.Visible = True
224
225         # Set the initial selection to the form
226         self.select(self.CurrentForm)
227
228         frm.Visible = True
229         dui.callAfter(frm.layout)
230         dui.callAfterInterval(100, self.updateLayout)
231         dui.callAfter(frm.bringToFront)
232         dui.callAfter(frm.saveState)
233         self.start()
234
235
236     def _initClassEvents(self):
237         """Create a dict by baseclass of all applicable events."""
238         self._classEvents = {}
239         self._classMethods = {}
240         baseEvents = ("DataEvent", "EditorEvent", "GridEvent", "KeyEvent",
241                 "ListEvent", "MenuEvent", "MouseEvent", "SashEvent",
242                 "CalendarEvent", "TreeEvent")
243         classes = (dui.dBox, dui.dBitmap, dui.dBitmapButton, dui.dButton, dui.dCheckBox, dui.dComboBox,
244                 dui.dDateTextBox, dui.dDialog, dui.dDropdownList, dui.dEditBox, dui.dEditor, dui.dSlidePanelControl,
245                 dui.dForm, dui.dDockForm, dui.dGauge, dui.dGrid, dui.dHtmlBox, dui.dImage, dui.dLabel, dui.dLine,
246                 dui.dListBox, dui.dListControl, dui.dOkCancelDialog, dui.dPanel, dui.dPage, dui.dScrollPanel,
247                 dui.dPage, dui.dPageFrame, dui.dPageList, dui.dPageSelect, dui.dPageFrameNoTabs,
248                 dui.dRadioList, dui.dSlider, dui.dSpinner, dui.dSplitter, dui.dTextBox, dui.dToggleButton,
249                 dui.dTreeView, dlgs.Wizard, dlgs.WizardPage)
250
251         def evtsForClass(cls):
252             def safeApplies(itm, cls):
253                 try:
254                     return itm.appliesToClass(cls)
255                 except (AttributeError, NameError):
256                     return False
257             ret = ["on%s" % k for k,v in dEvents.__dict__.items()
258                     if safeApplies(v,cls)]
259             ret.sort()
260             return ret
261
262         def mthdsForClass(cls):
263             ret = []
264             mthds = inspect.getmembers(cls, inspect.ismethod)
265             ret = [mthd[0] for mthd in mthds
266                     if mthd[0][0] in "abcdefghijklmnopqrstuvwxyz"]
267             return ret
268
269         for cls in classes:
270             self._classEvents[cls] = evtsForClass(cls)
271             self._classMethods[cls] = mthdsForClass(cls)
272
273     def getFormClass(self, filepath=None):
274         """If the selected class is a form/dialog, return a mixed-in
275         subclass of it. Otherwise, return the base ClassDesignerForm.
276         """
277         formIsMain = issubclass(self._selectedClass, (dui.dForm, dui.dDialog))
278         isDialog = issubclass(self._selectedClass, (dui.dDialog, ))
279         isWizard = issubclass(self._selectedClass, (dlgs.Wizard, ))
280         isDockForm = _USE_DOCKFORM and issubclass(self._selectedClass, (dui.dDockForm, ))
281         if formIsMain:
282             if isDockForm:
283                 base = self._selectedClass      ##dui.dForm
284             elif not isDialog and self._desFormClass is not None:
285                 return self._desFormClass
286             else:
287                 base = self._selectedClass
288         else:
289             base = dui.dForm
290         class DesForm(dfm, base):
291             _superBase = base
292             _superMixin = dfm
293             _classFile = filepath
294             def __init__(self, parent=None, *args, **kwargs):
295                 self._isMain = formIsMain
296                 if isDialog:
297                     kwargs["BorderResizable"] = True
298                     kwargs["ShowCloseButton"] = True
299                 if isWizard:
300                     kwargs["Caption"] = "Dabo Wizard Designer"
301                 base.__init__(self, parent=parent, *args, **kwargs)
302                 dfm.__init__(self, parent=parent, *args, **kwargs)
303                 self._basePrefKey = "dabo.ide.ClassDesigner.ClassDesignerForm"
304
305             def _afterInit(self):
306                 self._designerMode = True
307                 self._formMode = True
308                 if isDockForm:
309                     self._configureForDockForm()
310                 super(DesForm, self)._afterInit()
311
312             def addControls(self):
313                 if not isinstance(self, dui.dOkCancelDialog):
314                     # Could be a wizard, or some other object with an 'addControls' method
315                     self._superBase.addControls(self)
316                     return
317                 if self.UseSizers:
318                     self.mainPanel = LayoutBasePanel(self)
319                     self.Sizer.append1x(self.mainPanel)
320                     self.mainPanel.Sizer = LayoutSizer("v")
321                     # Use a Layout Sizer instead of the default sizer.
322                     self.initLayoutPanel = LayoutPanel(self.mainPanel)
323                 else:
324                     self.mainPanel = self.initLayoutPanel = NoSizerBasePanel(self, BackColor=(222,222,255))
325                     self.Sizer.append1x(self.mainPanel)
326                     self.layout()
327                 # We need to 'deactivate' the built-in buttons
328                 self.btnOK.unbindEvent(dEvents.Hit)
329                 self.btnCancel.unbindEvent(dEvents.Hit)
330                 self.btnOK.Enabled = self.btnCancel.Enabled = False
331
332
333             def _setupPanels(self, fromNew=True):
334                 if isinstance(self, dlgs.Wizard):
335                     self.mainPanel = self.pagePanel
336                     if self.UseSizers:
337                         self.mainPanel.Sizer = LayoutSizer("v")
338                     if fromNew:
339                         # Need to ask for number of pages.
340                         numPages = dabo.ui.getInt(_("How many pages?"), caption=_("Wizard Pages"),
341                                 defaultValue=3)
342                         pgCls = self.Controller.getControlClass(dlgs.WizardPage)
343                         pgs = [pgCls] * max(1, numPages)
344                         self.append(pgs)
345                         for num, p in enumerate(self._pages):
346                             # Remove the title and line from the current sizer
347                             p.Caption = _("Page %s Title") % num
348                             if self.UseSizers:
349                                 # This will automatically add itself to the sizer
350                                 LayoutPanel(p)
351                             else:
352                                 p.Sizer.append1x(NoSizerBasePanel(p))
353                         self.initLayoutPanel = self._pages[0].Children[0]
354                     else:
355                         self.initLayoutPanel = self.mainPanel
356                     self.CurrentPage = 0
357                     self.btnCancel.Enabled = False
358                     # Prevent the Finish button from closing the design surface.
359                     self.finish = lambda: False
360                     return
361
362                 if self.UseSizers:
363                     if isinstance(self, dui.dOkCancelDialog):
364                         # already done
365                         return
366                     if _USE_DOCKFORM and isinstance(self, dui.dDockForm):
367                         self.mainPanel = self.CenterPanel
368                     else:
369                         self.Sizer = dui.dSizer("v")
370                         self.mainPanel = LayoutBasePanel(self)
371                         self.Sizer.append1x(self.mainPanel)
372                    
373                     self.mainPanel.Sizer = LayoutSizer("v")
374                     # Use a Layout Sizer instead of the default sizer.
375                     self.initLayoutPanel = LayoutPanel(self.mainPanel)
376                 else:
377                     self.Sizer.release()
378                     self.Sizer = None
379                     self.mainPanel = self.initLayoutPanel = NoSizerBasePanel(self, BackColor=(222,222,255))
380                     self.layout()
381         ret = DesForm
382         if formIsMain and not isDialog and not isDockForm:
383             self._desFormClass = ret
384         return ret
385
386
387     def _reuseMainForm(self, useSizers=False):
388         """Determines if the MainForm for the Class Designer is a blank, unedited
389         form, which can be re-used when the user opens an existing class or
390         creates a new class.
391         """
392         mf = self.MainForm
393         if mf:
394             if mf.UseSizers != useSizers:
395                 return False
396             mfCurrDict = mf.getDesignerDict()
397             # Position and size of the form may have changed; delete those
398             # since they are irrelevant. Also, it seems that on Windows these
399             # atts are set while the object is being created, so we have to
400             # clear them in the _initialStateDict, too.
401             for att in ("Left", "Top", "Width", "Height"):
402                 try:
403                     del mfCurrDict["attributes"][att]
404                 except: pass
405                 try:
406                     del mf._initialStateDict["attributes"][att]
407                 except: pass
408         ret = mf and (mf._initialStateDict == mfCurrDict)
409         return ret
410
411
412     def onEditUndo(self, evt):
413         dabo.infoLog.write(_("Not implemented yet"))
414     def onEditRedo(self, evt):
415         dabo.infoLog.write(_("Not implemented yet"))
416
417
418     def _importClassXML(self, pth):
419         """Read in the XML and associated code file (if any), and
420         return a dict that can be used to re-create the object.
421         """
422         try:
423             if not os.path.exists(pth):
424                 if os.path.exists(os.path.abspath(pth)):
425                     pth = os.path.abspath(pth)
426             dct = xtd.xmltodict(pth, addCodeFile=True)
427         except StandardError, e:
428             if pth.strip().startswith("<?xml") or os.path.exists(pth):
429                 raise IOError, _("This does not appear to be a valid class file.")
430             else:
431                 raise IOError, _("The class file '%s' was not found.") % pth
432            
433
434         # Traverse the dct, looking for superclass information
435         super = xtd.flattenClassDict(dct)
436         if super:
437             # We need to modify the info to incorporate the superclass info
438             xtd.addInheritedInfo(dct, super)
439             # Store the base code so that we can determine if instances have
440             # modified it.
441             self._updateClassCodeRepository(super)
442         return dct
443
444
445     def _updateClassCodeRepository(self, dct):
446         """Take a flattened dict of class IDs and store any code
447         associated with those IDs, so that we can later compare it to
448         an object's code in order to determine if it has been changed.
449         """
450         cds = [(kk, vv["code"]) for kk, vv in dct.items()
451                 if vv["code"]]
452         for cd in cds:
453             self._classCodeDict.update({cd[0]: cd[1]})
454
455
456     def _getClassMethod(self, clsID, mthd):
457         """Given a class ID and a method name, returns the code for that
458         classID/method combination (if any) from self._classCodeDict.
459         """
460         cd = self._classCodeDict.get(clsID, {})
461         return cd.get(mthd, "")
462
463
464     def _findSizerInClassDict(self, clsd):
465         """Recursively search until a child is found with sizer information.
466         If no such child is found, return False.
467         """
468         ret =