Robot Framework Integrated Development Environment (RIDE)
customsourceeditor.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- encoding: utf-8 -*-
3 #
4 # Created by Hélio Guilherme <helioxentric@gmail.com>
5 # Copyright 2016- Robot Framework Foundation
6 #
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
10 #
11 # http://www.apache.org/licenses/LICENSE-2.0
12 #
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18 
19 from __future__ import absolute_import
20 
21 #
22 # from StyledTextCtrl_2 import PythonSTC
23 # -----------------------------------------------------------------
24 # Define Python style
25 import keyword
26 import os
27 import sys
28 
29 import wx
30 import wx.stc as stc
31 from wx import Colour
32 
33 # from images import Smiles
34 #import Smiles # Background, code, SmallDnArrow, SmallUpArrow
35 
36 # ----------------------------------------------------------------------
37 
38 CodeText = """\
39 ## This version of the editor has been set up to edit Python source
40 ## code. Here is a copy of wxPython/Code/Main.py to play with.
41 
42 
43 """
44 
45 # ----------------------------------------------------------------------
46 
47 
48 if wx.Platform == '__WXMSW__':
49  faces = {'times': 'Times New Roman',
50  'mono': 'Courier New',
51  'helv': 'Arial',
52  'other': 'Comic Sans MS',
53  'size': 10,
54  'size2': 8,
55  }
56 elif wx.Platform == '__WXMAC__':
57  faces = {'times': 'Times New Roman',
58  'mono': 'Monaco',
59  'helv': 'Arial',
60  'other': 'Comic Sans MS',
61  'size': 12,
62  'size2': 10,
63  }
64 else:
65  faces = {'times': 'Times',
66  'mono': 'Courier',
67  'helv': 'Helvetica',
68  'other': 'new century schoolbook',
69  'size': 12,
70  'size2': 10,
71  }
72 
73 # ---------------------------------------------------------------------------
74 # This is how you pre-establish a file filter so that the dialog
75 # only shows the extension(s) you want it to.
76 wildcard = "All files (*.*)|*.*|" \
77  "JASON file (*.json)|*.json|" \
78  "Python source (*.py)|*.py|" \
79  "Robot Framework (*.robot)|*.robot|" \
80  "Robot Framework (*.txt)|*.txt|" \
81  "YAML file (*.yaml)|*.yaml"
82 # ----------------------------------------------------------------------
83 
84 
85 class PythonSTC(stc.StyledTextCtrl):
86 
87  fold_symbols = 2
88 
89  def __init__(self, parent, ID,
90  pos=wx.DefaultPosition, size=wx.DefaultSize,
91  style=0):
92  stc.StyledTextCtrl.__init__(self, parent, ID, pos, size, style)
93 
94  self.CmdKeyAssign(ord('B'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
95  self.CmdKeyAssign(ord('N'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)
96 
97  self.SetLexer(stc.STC_LEX_PYTHON)
98  self.SetKeyWords(0, " ".join(keyword.kwlist))
99 
100  self.SetProperty("fold", "1")
101  self.SetProperty("tab.timmy.whinge.level", "1")
102  self.SetMargins(0,0)
103 
104  self.SetViewWhiteSpace(False)
105  #self.SetBufferedDraw(False)
106  #self.SetViewEOL(True)
107  #self.SetEOLMode(stc.STC_EOL_CRLF)
108  #self.SetUseAntiAliasing(True)
109 
110  self.SetEdgeMode(stc.STC_EDGE_BACKGROUND)
111  self.SetEdgeColumn(78)
112 
113  # Setup a margin to hold fold markers
114  #self.SetFoldFlags(16) ### WHAT IS THIS VALUE? WHAT ARE THE OTHER FLAGS? DOES IT MATTER?
115  self.SetMarginType(2, stc.STC_MARGIN_SYMBOL)
116  self.SetMarginMask(2, stc.STC_MASK_FOLDERS)
117  self.SetMarginSensitive(2, True)
118  self.SetMarginWidth(2, 12)
119 
120  if self.fold_symbolsfold_symbolsfold_symbols == 0:
121  # Arrow pointing right for contracted folders, arrow pointing down for expanded
122  self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN, stc.STC_MARK_ARROWDOWN, "black", "black")
123  self.MarkerDefine(stc.STC_MARKNUM_FOLDER, stc.STC_MARK_ARROW, "black", "black")
124  self.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB, stc.STC_MARK_EMPTY, "black", "black")
125  self.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL, stc.STC_MARK_EMPTY, "black", "black")
126  self.MarkerDefine(stc.STC_MARKNUM_FOLDEREND, stc.STC_MARK_EMPTY, "white", "black")
127  self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_EMPTY, "white", "black")
128  self.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_EMPTY, "white", "black")
129 
130  elif self.fold_symbolsfold_symbolsfold_symbols == 1:
131  # Plus for contracted folders, minus for expanded
132  self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN, stc.STC_MARK_MINUS, "white", "black")
133  self.MarkerDefine(stc.STC_MARKNUM_FOLDER, stc.STC_MARK_PLUS, "white", "black")
134  self.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB, stc.STC_MARK_EMPTY, "white", "black")
135  self.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL, stc.STC_MARK_EMPTY, "white", "black")
136  self.MarkerDefine(stc.STC_MARKNUM_FOLDEREND, stc.STC_MARK_EMPTY, "white", "black")
137  self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_EMPTY, "white", "black")
138  self.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_EMPTY, "white", "black")
139 
140  elif self.fold_symbolsfold_symbolsfold_symbols == 2:
141  # Like a flattened tree control using circular headers and curved joins
142  self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN, stc.STC_MARK_CIRCLEMINUS, "white", "#404040")
143  self.MarkerDefine(stc.STC_MARKNUM_FOLDER, stc.STC_MARK_CIRCLEPLUS, "white", "#404040")
144  self.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB, stc.STC_MARK_VLINE, "white", "#404040")
145  self.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL, stc.STC_MARK_LCORNERCURVE, "white", "#404040")
146  self.MarkerDefine(stc.STC_MARKNUM_FOLDEREND, stc.STC_MARK_CIRCLEPLUSCONNECTED, "white", "#404040")
147  self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_CIRCLEMINUSCONNECTED, "white", "#404040")
148  self.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_TCORNERCURVE, "white", "#404040")
149 
150  elif self.fold_symbolsfold_symbolsfold_symbols == 3:
151  # Like a flattened tree control using square headers
152  self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN, stc.STC_MARK_BOXMINUS, "white", "#808080")
153  self.MarkerDefine(stc.STC_MARKNUM_FOLDER, stc.STC_MARK_BOXPLUS, "white", "#808080")
154  self.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB, stc.STC_MARK_VLINE, "white", "#808080")
155  self.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL, stc.STC_MARK_LCORNER, "white", "#808080")
156  self.MarkerDefine(stc.STC_MARKNUM_FOLDEREND, stc.STC_MARK_BOXPLUSCONNECTED, "white", "#808080")
157  self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_BOXMINUSCONNECTED, "white", "#808080")
158  self.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_TCORNER, "white", "#808080")
159 
160  self.Bind(stc.EVT_STC_UPDATEUI, self.OnUpdateUIOnUpdateUI)
161  self.Bind(stc.EVT_STC_MARGINCLICK, self.OnMarginClickOnMarginClick)
162  self.Bind(wx.EVT_KEY_DOWN, self.OnKeyPressedOnKeyPressed)
163 
164  # Make some styles, The lexer defines what each style is used for, we
165  # just have to define what each style looks like. This set is adapted from
166  # Scintilla sample property files.
167 
168  # Global default styles for all languages
169  self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "face:%(helv)s,size:%(size)d" % faces)
170  self.StyleClearAll() # Reset all to be like the default
171 
172  # Global default styles for all languages
173  self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "face:%(helv)s,size:%(size)d" % faces)
174  self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, "back:#C0C0C0,face:%(helv)s,size:%(size2)d" % faces)
175  self.StyleSetSpec(stc.STC_STYLE_CONTROLCHAR, "face:%(other)s" % faces)
176  self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, "fore:#FFFFFF,back:#0000FF,bold")
177  self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, "fore:#000000,back:#FF0000,bold")
178 
179  # Python styles
180  # Default
181  self.StyleSetSpec(stc.STC_P_DEFAULT, "fore:#000000,face:%(helv)s,size:%(size)d" % faces)
182  # Comments
183  self.StyleSetSpec(stc.STC_P_COMMENTLINE, "fore:#007F00,face:%(other)s,size:%(size)d" % faces)
184  # Number
185  self.StyleSetSpec(stc.STC_P_NUMBER, "fore:#007F7F,size:%(size)d" % faces)
186  # String
187  self.StyleSetSpec(stc.STC_P_STRING, "fore:#7F007F,face:%(helv)s,size:%(size)d" % faces)
188  # Single quoted string
189  self.StyleSetSpec(stc.STC_P_CHARACTER, "fore:#7F007F,face:%(helv)s,size:%(size)d" % faces)
190  # Keyword
191  self.StyleSetSpec(stc.STC_P_WORD, "fore:#00007F,bold,size:%(size)d" % faces)
192  # Triple quotes
193  self.StyleSetSpec(stc.STC_P_TRIPLE, "fore:#7F0000,size:%(size)d" % faces)
194  # Triple double quotes
195  self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, "fore:#7F0000,size:%(size)d" % faces)
196  # Class name definition
197  self.StyleSetSpec(stc.STC_P_CLASSNAME, "fore:#0000FF,bold,underline,size:%(size)d" % faces)
198  # Function or method name definition
199  self.StyleSetSpec(stc.STC_P_DEFNAME, "fore:#007F7F,bold,size:%(size)d" % faces)
200  # Operators
201  self.StyleSetSpec(stc.STC_P_OPERATOR, "bold,size:%(size)d" % faces)
202  # Identifiers
203  self.StyleSetSpec(stc.STC_P_IDENTIFIER, "fore:#000000,face:%(helv)s,size:%(size)d" % faces)
204  # Comment-blocks
205  self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, "fore:#7F7F7F,size:%(size)d" % faces)
206  # End of line where string is not closed
207  self.StyleSetSpec(stc.STC_P_STRINGEOL, "fore:#000000,face:%(mono)s,back:#E0C0E0,eol,size:%(size)d" % faces)
208 
209  self.SetCaretForeground("BLUE")
210 
211  # register some images for use in the AutoComplete box.
212 
213  self.RegisterImage(1,
214  wx.ArtProvider.GetBitmap(wx.ART_FLOPPY, size=(16,16)))
215  self.RegisterImage(2,
216  wx.ArtProvider.GetBitmap(wx.ART_NEW, size=(16,16)))
217  self.RegisterImage(3,
218  wx.ArtProvider.GetBitmap(wx.ART_COPY, size=(16,16)))
219 
220  def OnKeyPressed(self, event):
221  if self.CallTipActive():
222  self.CallTipCancel()
223  key = event.GetKeyCode()
224 
225  if key == 32 and event.ControlDown():
226  pos = self.GetCurrentPos()
227 
228  # Tips
229  if event.ShiftDown():
230  self.CallTipSetBackground("yellow")
231  self.CallTipShow(pos, 'lots of of text: blah, blah, blah\n\n'
232  'show some suff, maybe parameters..\n\n'
233  'fubar(param1, param2)')
234  # Code completion
235  else:
236  #lst = []
237  #for x in range(50000):
238  # lst.append('%05d' % x)
239  #st = " ".join(lst)
240  #print(len(st))
241  #self.AutoCompShow(0, st)
242 
243  kw = keyword.kwlist[:]
244  kw.append("zzzzzz?2")
245  kw.append("aaaaa?2")
246  kw.append("__init__?3")
247  kw.append("zzaaaaa?2")
248  kw.append("zzbaaaa?2")
249  kw.append("this_is_a_longer_value")
250  #kw.append("this_is_a_much_much_much_much_much_much_much_longer_value")
251 
252  kw.sort() # Python sorts are case sensitive
253  self.AutoCompSetIgnoreCase(False) # so this needs to match
254 
255  # Images are specified with a appended "?type"
256  for i in range(len(kw)):
257  if kw[i] in keyword.kwlist:
258  kw[i] = kw[i] + "?1"
259 
260  self.AutoCompShow(0, " ".join(kw))
261  else:
262  event.Skip()
263 
264  def OnUpdateUI(self, evt):
265  # check for matching braces
266  braceAtCaret = -1
267  braceOpposite = -1
268  charBefore = None
269  caretPos = self.GetCurrentPos()
270 
271  if caretPos > 0:
272  charBefore = self.GetCharAt(caretPos - 1)
273  styleBefore = self.GetStyleAt(caretPos - 1)
274 
275  # check before
276  if charBefore and chr(charBefore) in "[]{}()" and styleBefore == stc.STC_P_OPERATOR:
277  braceAtCaret = caretPos - 1
278 
279  # check after
280  if braceAtCaret < 0:
281  charAfter = self.GetCharAt(caretPos)
282  styleAfter = self.GetStyleAt(caretPos)
283 
284  if charAfter and chr(charAfter) in "[]{}()" and styleAfter == stc.STC_P_OPERATOR:
285  braceAtCaret = caretPos
286 
287  if braceAtCaret >= 0:
288  braceOpposite = self.BraceMatch(braceAtCaret)
289 
290  if braceAtCaret != -1 and braceOpposite == -1:
291  self.BraceBadLight(braceAtCaret)
292  else:
293  self.BraceHighlight(braceAtCaret, braceOpposite)
294  #pt = self.PointFromPosition(braceOpposite)
295  #self.Refresh(True, wxRect(pt.x, pt.y, 5,5))
296  #print(pt)
297  #self.Refresh(False)
298 
299  def OnMarginClick(self, evt):
300  # fold and unfold as needed
301  if evt.GetMargin() == 2:
302  if evt.GetShift() and evt.GetControl():
303  self.FoldAllFoldAll()
304  else:
305  lineClicked = self.LineFromPosition(evt.GetPosition())
306 
307  if self.GetFoldLevel(lineClicked) & stc.STC_FOLDLEVELHEADERFLAG:
308  if evt.GetShift():
309  self.SetFoldExpanded(lineClicked, True)
310  self.ExpandExpand(lineClicked, True, True, 1)
311  elif evt.GetControl():
312  if self.GetFoldExpanded(lineClicked):
313  self.SetFoldExpanded(lineClicked, False)
314  self.ExpandExpand(lineClicked, False, True, 0)
315  else:
316  self.SetFoldExpanded(lineClicked, True)
317  self.ExpandExpand(lineClicked, True, True, 100)
318  else:
319  self.ToggleFold(lineClicked)
320 
321  def FoldAll(self):
322  lineCount = self.GetLineCount()
323  expanding = True
324 
325  # find out if we are folding or unfolding
326  for lineNum in range(lineCount):
327  if self.GetFoldLevel(lineNum) & stc.STC_FOLDLEVELHEADERFLAG:
328  expanding = not self.GetFoldExpanded(lineNum)
329  break
330 
331  lineNum = 0
332 
333  while lineNum < lineCount:
334  level = self.GetFoldLevel(lineNum)
335  if level & stc.STC_FOLDLEVELHEADERFLAG and \
336  (level & stc.STC_FOLDLEVELNUMBERMASK) == stc.STC_FOLDLEVELBASE:
337 
338  if expanding:
339  self.SetFoldExpanded(lineNum, True)
340  lineNum = self.ExpandExpand(lineNum, True)
341  lineNum = lineNum - 1
342  else:
343  lastChild = self.GetLastChild(lineNum, -1)
344  self.SetFoldExpanded(lineNum, False)
345 
346  if lastChild > lineNum:
347  self.HideLines(lineNum+1, lastChild)
348 
349  lineNum = lineNum + 1
350 
351  def Expand(self, line, doExpand, force=False, visLevels=0, level=-1):
352  lastChild = self.GetLastChild(line, level)
353  line = line + 1
354 
355  while line <= lastChild:
356  if force:
357  if visLevels > 0:
358  self.ShowLines(line, line)
359  else:
360  self.HideLines(line, line)
361  else:
362  if doExpand:
363  self.ShowLines(line, line)
364 
365  if level == -1:
366  level = self.GetFoldLevel(line)
367 
368  if level & stc.STC_FOLDLEVELHEADERFLAG:
369  if force:
370  if visLevels > 1:
371  self.SetFoldExpanded(line, True)
372  else:
373  self.SetFoldExpanded(line, False)
374 
375  line = self.ExpandExpand(line, doExpand, force, visLevels-1)
376 
377  else:
378  if doExpand and self.GetFoldExpanded(line):
379  line = self.ExpandExpand(line, True, force, visLevels-1)
380  else:
381  line = self.ExpandExpand(line, False, force, visLevels-1)
382  else:
383  line = line + 1
384 
385  return line
386 
387 # ----------------------------------------------------------------------
388 
389 
391  def __init__(self, parent, style=wx.BORDER_NONE):
392  PythonSTC.__init__(self, parent, -1, style=style)
393  self.SetUpEditorSetUpEditor()
394 
395  # Some methods to make it compatible with how the wxTextCtrl is used
396  def SetValue(self, value):
397  # if wx.USE_UNICODE:
398  # value = value.decode('iso8859_1')
399  val = self.GetReadOnly()
400  self.SetReadOnly(False)
401  self.SetText(value)
402  self.EmptyUndoBuffer()
403  self.SetSavePoint()
404  self.SetReadOnly(val)
405 
406  def SetEditable(self, val):
407  self.SetReadOnly(not val)
408 
409  def IsModified(self):
410  return self.GetModify()
411 
412  def Clear(self):
413  self.ClearAll()
414 
415  def SetInsertionPoint(self, pos):
416  self.SetCurrentPos(pos)
417  self.SetAnchor(pos)
418 
419  def ShowPosition(self, pos):
420  line = self.LineFromPosition(pos)
421  #self.EnsureVisible(line)
422  self.GotoLine(line)
423 
424  def GetLastPosition(self):
425  return self.GetLength()
426 
427  def GetPositionFromLine(self, line):
428  return self.PositionFromLine(line)
429 
430  def GetRange(self, start, end):
431  return self.GetTextRange(start, end)
432 
433  def GetSelection(self):
434  return self.GetAnchor(), self.GetCurrentPos()
435 
436  def SetSelection(self, start, end):
437  self.SetSelectionStart(start)
438  self.SetSelectionEnd(end)
439 
440  def SelectLine(self, line):
441  start = self.PositionFromLine(line)
442  end = self.GetLineEndPosition(line)
443  self.SetSelectionSetSelection(start, end)
444 
445 
449  def SetUpEditor(self):
450  import keyword
451 
452  self.SetLexer(stc.STC_LEX_PYTHON)
453  self.SetKeyWords(0, " ".join(keyword.kwlist))
454 
455  # Enable folding
456  self.SetProperty("fold", "1")
457 
458  # Highlight tab/space mixing (shouldn't be any)
459  self.SetProperty("tab.timmy.whinge.level", "1")
460 
461  # Set left and right margins
462  self.SetMargins(2,2)
463 
464  # Set up the numbers in the margin for margin #1
465  self.SetMarginType(1, wx.stc.STC_MARGIN_NUMBER)
466  # Reasonable value for, say, 4-5 digits using a mono font (40 pix)
467  self.SetMarginWidth(1, 40)
468 
469  # Indentation and tab stuff
470  self.SetIndent(4) # Proscribed indent size for wx
471  self.SetIndentationGuides(True) # Show indent guides
472  self.SetBackSpaceUnIndents(True) # Backspace unindents rather than
473  # delete 1 space
474  self.SetTabIndents(True) # Tab key indents
475  self.SetTabWidth(4) # Proscribed tab size for wx
476  self.SetUseTabs(False) # Use spaces rather than tabs, or
477  # TabTimmy will complain!
478  # White space
479  self.SetViewWhiteSpace(False) # Don't view white space
480 
481  # EOL: Since we are loading/saving ourselves, and the
482  # strings will always have \n's in them, set the STC to
483  # edit them that way.
484  self.SetEOLMode(wx.stc.STC_EOL_LF)
485  self.SetViewEOL(False)
486 
487  # No right-edge mode indicator
488  self.SetEdgeMode(stc.STC_EDGE_NONE)
489 
490  # Setup a margin to hold fold markers
491  self.SetMarginType(2, stc.STC_MARGIN_SYMBOL)
492  self.SetMarginMask(2, stc.STC_MASK_FOLDERS)
493  self.SetMarginSensitive(2, True)
494  self.SetMarginWidth(2, 12)
495 
496  # Global default style
497  if wx.Platform == '__WXMSW__':
498  # print("DEBUG: Setup on Windows")
499  self.StyleSetSpec(stc.STC_STYLE_DEFAULT,
500  'fore:#000000,back:#FFFFFF,face:Space Mono') # Courier New')
501  elif wx.Platform == '__WXMAC__':
502  # print("DEBUG: Setup on Mac")
503  # TODO: if this looks fine on Linux too, remove the Mac-specific case
504  # and use this whenever OS != MSW.
505  self.StyleSetSpec(stc.STC_STYLE_DEFAULT,
506  'fore:#000000,back:#FFFFFF,face:Monaco')
507  else:
508  # print("DEBUG: Setup on Linux")
509  defsize = wx.SystemSettings.GetFont(wx.SYS_ANSI_FIXED_FONT).GetPointSize()
510  self.StyleSetSpec(stc.STC_STYLE_DEFAULT,
511  'fore:#000000,back:#FFFFFF,face:Hack,size:%d'%defsize) # Courier, Space Mono, Source Pro Mono,
512  """
513  self.StyleSetBackground(stc.STC_STYLE_DEFAULT, Colour(200, 222, 40))
514  self.StyleSetForeground(stc.STC_STYLE_DEFAULT, Colour(7, 0, 70))
515  """
516  # Clear styles and revert to default.
517  self.StyleClearAll()
518 
519  # Following style specs only indicate differences from default.
520  # The rest remains unchanged.
521 
522  # Line numbers in margin
523  self.StyleSetSpec(wx.stc.STC_STYLE_LINENUMBER,'fore:#000000,back:#99A9C2')
524  # Highlighted brace
525  self.StyleSetSpec(wx.stc.STC_STYLE_BRACELIGHT,'fore:#00009D,back:#FFFF00')
526  # Unmatched brace
527  self.StyleSetSpec(wx.stc.STC_STYLE_BRACEBAD,'fore:#00009D,back:#FF0000')
528  # Indentation guide
529  self.StyleSetSpec(wx.stc.STC_STYLE_INDENTGUIDE, "fore:#CDCDCD")
530 
531  # Python styles
532  self.StyleSetSpec(wx.stc.STC_P_DEFAULT, 'fore:#000000')
533  # Comments
534  self.StyleSetSpec(wx.stc.STC_P_COMMENTLINE, 'fore:#008000,back:#F0FFF0')
535  self.StyleSetSpec(wx.stc.STC_P_COMMENTBLOCK, 'fore:#008000,back:#F0FFF0')
536  # Numbers
537  self.StyleSetSpec(wx.stc.STC_P_NUMBER, 'fore:#008080')
538  # Strings and characters
539  self.StyleSetSpec(wx.stc.STC_P_STRING, 'fore:#800080')
540  self.StyleSetSpec(wx.stc.STC_P_CHARACTER, 'fore:#800080')
541  # Keywords
542  self.StyleSetSpec(wx.stc.STC_P_WORD, 'fore:#000080,bold')
543  # Triple quotes
544  self.StyleSetSpec(wx.stc.STC_P_TRIPLE, 'fore:#800080,back:#FFFFEA')
545  self.StyleSetSpec(wx.stc.STC_P_TRIPLEDOUBLE, 'fore:#800080,back:#FFFFEA')
546  # Class names
547  self.StyleSetSpec(wx.stc.STC_P_CLASSNAME, 'fore:#0000FF,bold')
548  # Function names
549  self.StyleSetSpec(wx.stc.STC_P_DEFNAME, 'fore:#008080,bold')
550  # Operators
551  self.StyleSetSpec(wx.stc.STC_P_OPERATOR, 'fore:#800000,bold')
552  # Identifiers. I leave this as not bold because everything seems
553  # to be an identifier if it doesn't match the above criterae
554  self.StyleSetSpec(wx.stc.STC_P_IDENTIFIER, 'fore:#000000')
555 
556  # Caret color
557  self.SetCaretForeground("BLUE")
558  # Selection background
559  # self.SetSelBackground(1, '#66CCFF')
560  """
561  self.SetBackgroundColour(Colour(200, 222, 40))
562  self.SetForegroundColour(Colour(7, 0, 70))
563  """
564 
565  self.SetSelBackground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT))
566  self.SetSelForeground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT))
567 
568  def RegisterModifiedEvent(self, eventHandler):
569  self.Bind(wx.stc.EVT_STC_CHANGE, eventHandler)
570 
571 
572 # ---------------------------------------------------------------------------
573 # Constants for module versions
574 
575 modOriginal = 0
576 modModified = 1
577 modDefault = modOriginal
578 
579 # ---------------------------------------------------------------------------
580 
581 
582 def isUTF8Strict(data):
583  try:
584  decoded = data.decode('UTF-8')
585  except UnicodeDecodeError:
586  return False
587  else:
588  for ch in decoded:
589  if 0xD800 <= ord(ch) <= 0xDFFF:
590  return False
591  return True
592 
593 
594 
595 class CodeEditorPanel(wx.Panel):
596  def __init__(self, parent, mainFrame, path=None):
597  self.loglog = sys.stdout # From FileDialog
598  self.pathpath = path
599  wx.Panel.__init__(self, parent, size=(1,1))
600  self.mainFramemainFrame = mainFrame
601  self.editoreditor = SourceCodeEditor(self)
602  self.editoreditor.RegisterModifiedEvent(self.OnCodeModifiedOnCodeModified)
603 
604  """
605  self.SetBackgroundColour(Colour(200, 222, 40))
606  self.SetOwnBackgroundColour(Colour(200, 222, 40))
607  self.SetForegroundColour(Colour(7, 0, 70))
608  self.SetOwnForegroundColour(Colour(7, 0, 70))
609  """
610 
611  self.btnSavebtnSave = wx.Button(self, -1, "Save Changes")
612  # self.btnRestore = wx.Button(self, -1, "Delete Modified")
613  self.btnSavebtnSave.Enable(False)
614  self.btnSavebtnSave.Bind(wx.EVT_BUTTON, self.OnSaveOnSave)
615  # self.btnRestore.Bind(wx.EVT_BUTTON, self.OnRestore)
616 
617  # From FileDialog
618  self.btnOpenbtnOpen = wx.Button(self, -1, "Open...")
619  self.btnOpenbtnOpen.Bind(wx.EVT_BUTTON, self.OnButtonOnButton)
620 
621  self.btnSaveAsbtnSaveAs = wx.Button(self, -1, "Save as...")
622  self.btnSaveAsbtnSaveAs.Bind(wx.EVT_BUTTON, self.OnButton2OnButton2)
623 
624  self.radioButtonsradioButtons = {modOriginal: wx.RadioButton(self, -1, "Original",
625  style = wx.RB_GROUP),
626  modModified: wx.RadioButton(self, -1, "Modified")}
627 
628  self.controlBoxcontrolBox = wx.BoxSizer(wx.HORIZONTAL)
629  self.controlBoxcontrolBox.Add(wx.StaticText(self, -1, "Active Version:"), 0,
630  wx.RIGHT | wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5)
631  for modID, radioButton in self.radioButtonsradioButtons.items():
632  self.controlBoxcontrolBox.Add(radioButton, 0, wx.EXPAND | wx.RIGHT, 5)
633  radioButton.modID = modID # makes it easier for the event handler
634  radioButton.Bind(wx.EVT_RADIOBUTTON, self.OnRadioButtonOnRadioButton)
635 
636  self.controlBoxcontrolBox.Add(self.btnSavebtnSave, 0, wx.RIGHT, 5)
637  # self.controlBox.Add(self.btnRestore, 0, wx.RIGHT, 5)
638  self.controlBoxcontrolBox.Add(self.btnOpenbtnOpen, 0, wx.RIGHT, 5)
639  self.controlBoxcontrolBox.Add(self.btnSaveAsbtnSaveAs, 0)
640 
641  self.boxbox = wx.BoxSizer(wx.VERTICAL)
642  self.boxbox.Add(self.controlBoxcontrolBox, 0, wx.EXPAND)
643  self.boxbox.Add(wx.StaticLine(self), 0, wx.EXPAND)
644  self.boxbox.Add(self.editoreditor, 1, wx.EXPAND)
645 
646  self.boxbox.Fit(self)
647  self.SetSizer(self.boxbox)
648  if self.pathpath:
649  # print("DEBUG: path is init = %s" % self.path)
650  self.LoadFileLoadFile(self.pathpath)
651 
652  def LoadFile(self, path):
653  # Open
654  f = open(path, "rb")
655  try:
656  source = f.read()
657  finally:
658  f.close()
659  self.LoadSourceLoadSource(source)
660 
662  self.LoadSourceLoadSource(self.CodeModules.GetSource())
663  self.UpdateControlStateUpdateControlState()
664  self.mainFramemainFrame.pnl.Freeze()
665  self.ReloadDemoReloadDemo()
666  self.mainFramemainFrame.pnl.Thaw()
667 
668  def LoadSource(self, source):
669  self.editoreditor.Clear()
670  self.editoreditor.SetTextRaw(source) # DEBUG SetValue
671  self.JumpToLineJumpToLine(0)
672  self.btnSavebtnSave.Enable(False)
673 
674  def JumpToLine(self, line, highlight=False):
675  self.editoreditor.GotoLine(line)
676  self.editoreditor.SetFocus()
677  if highlight:
678  self.editoreditor.SelectLine(line)
679 
681  active = self.CodeModules.GetActiveID()
682  # Update the radio/restore buttons
683  for moduleID in self.radioButtonsradioButtons:
684  btn = self.radioButtonsradioButtons[moduleID]
685  if moduleID == active:
686  btn.SetValue(True)
687  else:
688  btn.SetValue(False)
689 
690  if self.CodeModules.Exists(moduleID):
691  btn.Enable(True)
692  if moduleID == modModified:
693  self.btnRestore.Enable(True)
694  else:
695  btn.Enable(False)
696  if moduleID == modModified:
697  self.btnRestore.Enable(False)
698 
699  def OnRadioButton(self, event):
700  radioSelected = event.GetEventObject()
701  modSelected = radioSelected.modID
702  if modSelected != self.CodeModules.GetActiveID():
703  busy = wx.BusyInfo("Reloading Code module...")
704  self.CodeModules.SetActive(modSelected)
705  self.ActiveModuleChangedActiveModuleChanged()
706 
707  def ReloadDemo(self):
708  if self.CodeModules.name != __name__:
709  self.mainFramemainFrame.RunModule()
710 
711  def OnCodeModified(self, event):
712  self.btnSavebtnSave.Enable(self.editoreditor.IsModified())
713 
714  def OnSave(self, event, path=None):
715  if self.pathpath is None:
716  self.pathpath = "noname"
717  self.OnButton2OnButton2(event)
718  return
719  # print("DEBUG: OnSave path is init = %s passado %s" % (self.path, path))
720  if path:
721  if path != self.pathpath and os.path.isfile(path):
722  overwriteMsg = "You are about to overwrite an existing file\n" + \
723  "Do you want to continue?"
724  dlg = wx.MessageDialog(self, overwriteMsg, "Editor Writer",
725  wx.YES_NO | wx.NO_DEFAULT| wx.ICON_EXCLAMATION)
726  dlg.SetBackgroundColour(Colour(200, 222, 40))
727  dlg.SetForegroundColour(Colour(7, 0, 70))
728  result = dlg.ShowModal()
729  if result == wx.ID_NO:
730  return
731  dlg.Destroy()
732  self.pathpath = path
733 
734  # Save
735  f = open(self.pathpath, "wb")
736  source = self.editoreditor.GetTextRaw()
737  # print("DEBUG: Test is Unicode %s",isUTF8Strict(source))
738  if isUTF8Strict(source):
739  try:
740  f.write(source)
741  # print("DEBUG: Saved as Unicode")
742  finally:
743  f.close()
744  else:
745  # print("DEBUG: there were problems with source not being Unicode.")
746  # Attempt to isolate the problematic bytes
747  bsource = bytearray(source)
748  try:
749  chunksize = 1024
750  for c in range(0, len(source), chunksize):
751  data = [chr(int(x, base=2)) for x in source[c:c + chunksize]]
752  f.write(''.join(data))
753  finally:
754  f.close()
755 
756  # busy = wx.BusyInfo("Reloading Code module...")
757  # self.CodeModules.LoadFromFile(modModified, modifiedFilename)
758  #self.ActiveModuleChanged()
759 
760  #self.mainFrame.SetTreeModified(True)
761 
762  def OnRestore(self, event): # Handles the "Delete Modified" button
763  modifiedFilename = GetModifiedFilename(self.CodeModules.name)
764  self.CodeModules.Delete(modModified)
765  os.unlink(modifiedFilename) # Delete the modified copy
766  busy = wx.BusyInfo("Reloading Code module...")
767 
768  self.ActiveModuleChangedActiveModuleChanged()
769 
770  self.mainFramemainFrame.SetTreeModified(False)
771 
772  def OnButton(self, evt):
773  #self.log.WriteText("CWD: %s\n" % os.getcwd())
774  # self.log.write("CWD: %s\n" % os.getcwd())
775 
776  # Create the dialog. In this case the current directory is forced as the starting
777  # directory for the dialog, and no default file name is forced. This can easilly
778  # be changed in your program. This is an 'open' dialog, and allows multitple
779  # file selections as well.
780  #
781  # Finally, if the directory is changed in the process of getting files, this
782  # dialog is set up to change the current working directory to the path chosen.
783  if self.pathpath:
784  cwd = os.path.dirname(self.pathpath)
785  else:
786  cwd = os.getcwd()
787  dlg = wx.FileDialog(
788  self, message="Choose a file",
789  defaultDir=cwd,
790  defaultFile="",
791  wildcard=wildcard,
792  style=wx.FD_OPEN |
793  wx.FD_CHANGE_DIR | wx.FD_FILE_MUST_EXIST |
794  wx.FD_PREVIEW
795  ) # wx.FD_MULTIPLE |
796 
797  # Show the dialog and retrieve the user response. If it is the OK response,
798  # process the data.
799  if dlg.ShowModal() == wx.ID_OK:
800  # This returns a Python list of files that were selected.
801  paths = dlg.GetPaths()
802 
803  # self.log.WriteText('You selected %d files:' % len(paths))
804  # DEBUG self.log.write('You selected %d files:' % len(paths))
805 
806  #for path in paths:
807  # self.log.WriteText(' %s\n' % path)
808  # self.log.write(' %s\n' % path)
809  path = paths[-1] # just get the last one
810  # Open
811  f = open(path, "rb")
812  try:
813  source = f.read()
814  finally:
815  f.close()
816 
817  # store the new path
818  self.pathpath = path
819  # self.log.write('%s\n' % source)
820  self.LoadSourceLoadSource(source) # Just the last file
821  # Compare this with the debug above; did we change working dirs?
822  # self.log.WriteText("CWD: %s\n" % os.getcwd())
823  # self.log.write("CWD: %s\n" % os.getcwd())
824 
825  # Destroy the dialog. Don't do this until you are done with it!
826  # BAD things can happen otherwise!
827  dlg.Destroy()
828 
829  def OnButton2(self, evt):
830  #self.log.WriteText("CWD: %s\n" % os.getcwd())
831  # self.log.write("CWD: %s\n" % os.getcwd())
832 
833  # Create the dialog. In this case the current directory is forced as the starting
834  # directory for the dialog, and no default file name is forced. This can easilly
835  # be changed in your program. This is an 'save' dialog.
836  #
837  # Unlike the 'open dialog' example found elsewhere, this example does NOT
838  # force the current working directory to change if the user chooses a different
839  # directory than the one initially set.
840  fname = ""
841  if self.pathpath:
842  cwd = os.path.dirname(self.pathpath)
843  fname = os.path.basename(self.pathpath)
844  else:
845  cwd = os.getcwd()
846  self.pathpath = "noname"
847  dlg = wx.FileDialog(
848  self, message="Save file as ...", defaultDir=cwd,
849  defaultFile=fname, wildcard=wildcard, style=wx.FD_SAVE
850  ) # | wx.FD_OVERWRITE_PROMPT
851 
852  # This sets the default filter that the user will initially see. Otherwise,
853  # the first filter in the list will be used by default.
854  # dlg.SetFilterIndex(2)
855 
856  # Show the dialog and retrieve the user response. If it is the OK response,
857  # process the data.
858  if dlg.ShowModal() == wx.ID_OK:
859  path = dlg.GetPath()
860  # self.log.WriteText('You selected "%s"' % path)
861  # self.log.write('You selected "%s"\n' % path)
862 
863  # Normally, at this point you would save your data using the file and path
864  # data that the user provided to you, but since we didn't actually start
865  # with any data to work with, that would be difficult.
866  #
867  # The code to do so would be similar to this, assuming 'data' contains
868  # the data you want to save:
869  #
870  # fp = file(path, 'w') # Create file anew
871  # fp.write(data)
872  # fp.close()
873  #
874  # You might want to add some error checking :-)
875  #
876  # store the new path
877  # self.path = path
878  self.OnSaveOnSave(evt, path)
879  # Note that the current working dir didn't change. This is good since
880  # that's the way we set it up.
881  # self.log.WriteText("CWD: %s\n" % os.getcwd())
882  # self.log.write("CWD: %s\n" % os.getcwd())
883 
884  # Destroy the dialog. Don't do this until you are done with it!
885  # BAD things can happen otherwise!
886  dlg.Destroy()
887 
888 
889 # ---------------------------------------------------------------------------
890 
891 
892 def opj(path):
893  st = os.path.join(*tuple(path.split('/')))
894  # HACK: on Linux, a leading / gets lost...
895  if path.startswith('/'):
896  st = '/' + st
897  return st
898 
899 
900 
904  sp = wx.StandardPaths.Get()
905  return sp.GetUserDataDir()
906 
907 
908 
913  return os.path.join(GetDataDir(), "modified")
914 
915 
916 
920  if not name.endswith(".py"):
921  name = name + ".py"
922  return os.path.join(GetModifiedDirectory(), name)
923 
924 
925 
929  if not name.endswith(".py"):
930  name = name + ".py"
931 
932  if os.path.isfile(name):
933  return name
934 
935  originalDir = os.getcwd()
936  listDir = os.listdir(originalDir)
937  # Loop over the content of the Code directory
938  for item in listDir:
939  if not os.path.isdir(item):
940  # Not a directory, continue
941  continue
942  dirFile = os.listdir(item)
943  # See if a file called "name" is there
944  if name in dirFile:
945  return os.path.join(item, name)
946 
947  # We must return a string...
948  return ""
949 
950 
951 
953  if os.path.exists(GetModifiedFilename(name)):
954  return True
955  else:
956  return False
957 
958 
959 def GetConfig():
960  if not os.path.exists(GetDataDir()):
961  os.makedirs(GetDataDir())
962 
963  config = wx.FileConfig(
964  localFilename=os.path.join(GetDataDir(), "options"))
965  return config
966 
967 
968 
971 _platformNames = ["wxMSW", "wxGTK", "wxMac"]
972 
973 
974 def main(filepath, frame=None):
975  __name__ = 'Editor'
976  app = wx.App()
977  frame = wx.Frame(None)
978  panel = CodeEditorPanel(frame, None, filepath)
979  frame.Show(True)
980  app.MainLoop()
981 # ----------------------------------------------------------------------------
982 # ----------------------------------------------------------------------------
983 # ----------------------------------------------------------------------------
984 
985 
986 if __name__ == '__main__' and __package__ is None:
987  from os import sys, path
988  sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
989  path = None
990  try:
991  if sys.argv[1]:
992  path = sys.argv[1]
993  except IndexError:
994  pass
995  finally:
996  main(path)
997 # ----------------------------------------------------------------------------
def __init__(self, parent, mainFrame, path=None)
def __init__(self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0)
def Expand(self, line, doExpand, force=False, visLevels=0, level=-1)
def SetUpEditor(self)
This method carries out the work of setting up the Code editor.
def __init__(self, parent, style=wx.BORDER_NONE)
def DoesModifiedExist(name)
Returns whether the specified Code has a modified copy.
def GetModifiedDirectory()
Returns the directory where modified versions of the Code files are stored.
def GetOriginalFilename(name)
Returns the filename of the original version of the specified Code.
def opj(path)
Convert paths to the platform-specific separato.
def GetModifiedFilename(name)
Returns the filename of the modified version of the specified Code.
def GetDataDir()
Return the standard location on this platform for application data.