Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

IDLEenvironment.py 19KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. # Code that allows Pythonwin to pretend it is IDLE
  2. # (at least as far as most IDLE extensions are concerned)
  3. import string
  4. import sys
  5. import win32api
  6. import win32con
  7. import win32ui
  8. from pywin import default_scintilla_encoding
  9. from pywin.mfc.dialog import GetSimpleInput
  10. wordchars = string.ascii_uppercase + string.ascii_lowercase + string.digits
  11. class TextError(Exception): # When a TclError would normally be raised.
  12. pass
  13. class EmptyRange(Exception): # Internally raised.
  14. pass
  15. def GetIDLEModule(module):
  16. try:
  17. # First get it from Pythonwin it is exists.
  18. modname = "pywin.idle." + module
  19. __import__(modname)
  20. except ImportError as details:
  21. msg = (
  22. "The IDLE extension '%s' can not be located.\r\n\r\n"
  23. "Please correct the installation and restart the"
  24. " application.\r\n\r\n%s" % (module, details)
  25. )
  26. win32ui.MessageBox(msg)
  27. return None
  28. mod = sys.modules[modname]
  29. mod.TclError = TextError # A hack that can go soon!
  30. return mod
  31. # A class that is injected into the IDLE auto-indent extension.
  32. # It allows for decent performance when opening a new file,
  33. # as auto-indent uses the tokenizer module to determine indents.
  34. # The default AutoIndent readline method works OK, but it goes through
  35. # this layer of Tk index indirection for every single line. For large files
  36. # without indents (and even small files with indents :-) it was pretty slow!
  37. def fast_readline(self):
  38. if self.finished:
  39. val = ""
  40. else:
  41. if "_scint_lines" not in self.__dict__:
  42. # XXX - note - assumes this is only called once the file is loaded!
  43. self._scint_lines = self.text.edit.GetTextRange().split("\n")
  44. sl = self._scint_lines
  45. i = self.i = self.i + 1
  46. if i >= len(sl):
  47. val = ""
  48. else:
  49. val = sl[i] + "\n"
  50. return val.encode(default_scintilla_encoding)
  51. try:
  52. GetIDLEModule("AutoIndent").IndentSearcher.readline = fast_readline
  53. except AttributeError: # GetIDLEModule may return None
  54. pass
  55. # A class that attempts to emulate an IDLE editor window.
  56. # Construct with a Pythonwin view.
  57. class IDLEEditorWindow:
  58. def __init__(self, edit):
  59. self.edit = edit
  60. self.text = TkText(edit)
  61. self.extensions = {}
  62. self.extension_menus = {}
  63. def close(self):
  64. self.edit = self.text = None
  65. self.extension_menus = None
  66. try:
  67. for ext in self.extensions.values():
  68. closer = getattr(ext, "close", None)
  69. if closer is not None:
  70. closer()
  71. finally:
  72. self.extensions = {}
  73. def IDLEExtension(self, extension):
  74. ext = self.extensions.get(extension)
  75. if ext is not None:
  76. return ext
  77. mod = GetIDLEModule(extension)
  78. if mod is None:
  79. return None
  80. klass = getattr(mod, extension)
  81. ext = self.extensions[extension] = klass(self)
  82. # Find and bind all the events defined in the extension.
  83. events = [item for item in dir(klass) if item[-6:] == "_event"]
  84. for event in events:
  85. name = "<<%s>>" % (event[:-6].replace("_", "-"),)
  86. self.edit.bindings.bind(name, getattr(ext, event))
  87. return ext
  88. def GetMenuItems(self, menu_name):
  89. # Get all menu items for the menu name (eg, "edit")
  90. bindings = self.edit.bindings
  91. ret = []
  92. for ext in self.extensions.values():
  93. menudefs = getattr(ext, "menudefs", [])
  94. for name, items in menudefs:
  95. if name == menu_name:
  96. for text, event in [item for item in items if item is not None]:
  97. text = text.replace("&", "&&")
  98. text = text.replace("_", "&")
  99. ret.append((text, event))
  100. return ret
  101. ######################################################################
  102. # The IDLE "Virtual UI" methods that are exposed to the IDLE extensions.
  103. #
  104. def askinteger(
  105. self, caption, prompt, parent=None, initialvalue=0, minvalue=None, maxvalue=None
  106. ):
  107. while 1:
  108. rc = GetSimpleInput(prompt, str(initialvalue), caption)
  109. if rc is None:
  110. return 0 # Correct "cancel" semantics?
  111. err = None
  112. try:
  113. rc = int(rc)
  114. except ValueError:
  115. err = "Please enter an integer"
  116. if not err and minvalue is not None and rc < minvalue:
  117. err = "Please enter an integer greater then or equal to %s" % (
  118. minvalue,
  119. )
  120. if not err and maxvalue is not None and rc > maxvalue:
  121. err = "Please enter an integer less then or equal to %s" % (maxvalue,)
  122. if err:
  123. win32ui.MessageBox(err, caption, win32con.MB_OK)
  124. continue
  125. return rc
  126. def askyesno(self, caption, prompt, parent=None):
  127. return win32ui.MessageBox(prompt, caption, win32con.MB_YESNO) == win32con.IDYES
  128. ######################################################################
  129. # The IDLE "Virtual Text Widget" methods that are exposed to the IDLE extensions.
  130. #
  131. # Is character at text_index in a Python string? Return 0 for
  132. # "guaranteed no", true for anything else.
  133. def is_char_in_string(self, text_index):
  134. # A helper for the code analyser - we need internal knowledge of
  135. # the colorizer to get this information
  136. # This assumes the colorizer has got to this point!
  137. text_index = self.text._getoffset(text_index)
  138. c = self.text.edit._GetColorizer()
  139. if c and c.GetStringStyle(text_index) is None:
  140. return 0
  141. return 1
  142. # If a selection is defined in the text widget, return
  143. # (start, end) as Tkinter text indices, otherwise return
  144. # (None, None)
  145. def get_selection_indices(self):
  146. try:
  147. first = self.text.index("sel.first")
  148. last = self.text.index("sel.last")
  149. return first, last
  150. except TextError:
  151. return None, None
  152. def set_tabwidth(self, width):
  153. self.edit.SCISetTabWidth(width)
  154. def get_tabwidth(self):
  155. return self.edit.GetTabWidth()
  156. # A class providing the generic "Call Tips" interface
  157. class CallTips:
  158. def __init__(self, edit):
  159. self.edit = edit
  160. def showtip(self, tip_text):
  161. self.edit.SCICallTipShow(tip_text)
  162. def hidetip(self):
  163. self.edit.SCICallTipCancel()
  164. ########################################
  165. #
  166. # Helpers for the TkText emulation.
  167. def TkOffsetToIndex(offset, edit):
  168. lineoff = 0
  169. # May be 1 > actual end if we pretended there was a trailing '\n'
  170. offset = min(offset, edit.GetTextLength())
  171. line = edit.LineFromChar(offset)
  172. lineIndex = edit.LineIndex(line)
  173. return "%d.%d" % (line + 1, offset - lineIndex)
  174. def _NextTok(str, pos):
  175. # Returns (token, endPos)
  176. end = len(str)
  177. if pos >= end:
  178. return None, 0
  179. while pos < end and str[pos] in string.whitespace:
  180. pos = pos + 1
  181. # Special case for +-
  182. if str[pos] in "+-":
  183. return str[pos], pos + 1
  184. # Digits also a special case.
  185. endPos = pos
  186. while endPos < end and str[endPos] in string.digits + ".":
  187. endPos = endPos + 1
  188. if pos != endPos:
  189. return str[pos:endPos], endPos
  190. endPos = pos
  191. while endPos < end and str[endPos] not in string.whitespace + string.digits + "+-":
  192. endPos = endPos + 1
  193. if pos != endPos:
  194. return str[pos:endPos], endPos
  195. return None, 0
  196. def TkIndexToOffset(bm, edit, marks):
  197. base, nextTokPos = _NextTok(bm, 0)
  198. if base is None:
  199. raise ValueError("Empty bookmark ID!")
  200. if base.find(".") > 0:
  201. try:
  202. line, col = base.split(".", 2)
  203. if col == "first" or col == "last":
  204. # Tag name
  205. if line != "sel":
  206. raise ValueError("Tags arent here!")
  207. sel = edit.GetSel()
  208. if sel[0] == sel[1]:
  209. raise EmptyRange
  210. if col == "first":
  211. pos = sel[0]
  212. else:
  213. pos = sel[1]
  214. else:
  215. # Lines are 1 based for tkinter
  216. line = int(line) - 1
  217. if line > edit.GetLineCount():
  218. pos = edit.GetTextLength() + 1
  219. else:
  220. pos = edit.LineIndex(line)
  221. if pos == -1:
  222. pos = edit.GetTextLength()
  223. pos = pos + int(col)
  224. except (ValueError, IndexError):
  225. raise ValueError("Unexpected literal in '%s'" % base)
  226. elif base == "insert":
  227. pos = edit.GetSel()[0]
  228. elif base == "end":
  229. pos = edit.GetTextLength()
  230. # Pretend there is a trailing '\n' if necessary
  231. if pos and edit.SCIGetCharAt(pos - 1) != "\n":
  232. pos = pos + 1
  233. else:
  234. try:
  235. pos = marks[base]
  236. except KeyError:
  237. raise ValueError("Unsupported base offset or undefined mark '%s'" % base)
  238. while 1:
  239. word, nextTokPos = _NextTok(bm, nextTokPos)
  240. if word is None:
  241. break
  242. if word in ("+", "-"):
  243. num, nextTokPos = _NextTok(bm, nextTokPos)
  244. if num is None:
  245. raise ValueError("+/- operator needs 2 args")
  246. what, nextTokPos = _NextTok(bm, nextTokPos)
  247. if what is None:
  248. raise ValueError("+/- operator needs 2 args")
  249. if what[0] != "c":
  250. raise ValueError("+/- only supports chars")
  251. if word == "+":
  252. pos = pos + int(num)
  253. else:
  254. pos = pos - int(num)
  255. elif word == "wordstart":
  256. while pos > 0 and edit.SCIGetCharAt(pos - 1) in wordchars:
  257. pos = pos - 1
  258. elif word == "wordend":
  259. end = edit.GetTextLength()
  260. while pos < end and edit.SCIGetCharAt(pos) in wordchars:
  261. pos = pos + 1
  262. elif word == "linestart":
  263. while pos > 0 and edit.SCIGetCharAt(pos - 1) not in "\n\r":
  264. pos = pos - 1
  265. elif word == "lineend":
  266. end = edit.GetTextLength()
  267. while pos < end and edit.SCIGetCharAt(pos) not in "\n\r":
  268. pos = pos + 1
  269. else:
  270. raise ValueError("Unsupported relative offset '%s'" % word)
  271. return max(pos, 0) # Tkinter is tollerant of -ve indexes - we aren't
  272. # A class that resembles an IDLE (ie, a Tk) text widget.
  273. # Construct with an edit object (eg, an editor view)
  274. class TkText:
  275. def __init__(self, edit):
  276. self.calltips = None
  277. self.edit = edit
  278. self.marks = {}
  279. ## def __getattr__(self, attr):
  280. ## if attr=="tk": return self # So text.tk.call works.
  281. ## if attr=="master": return None # ditto!
  282. ## raise AttributeError, attr
  283. ## def __getitem__(self, item):
  284. ## if item=="tabs":
  285. ## size = self.edit.GetTabWidth()
  286. ## if size==8: return "" # Tk default
  287. ## return size # correct semantics?
  288. ## elif item=="font": # Used for measurements we dont need to do!
  289. ## return "Dont know the font"
  290. ## raise IndexError, "Invalid index '%s'" % item
  291. def make_calltip_window(self):
  292. if self.calltips is None:
  293. self.calltips = CallTips(self.edit)
  294. return self.calltips
  295. def _getoffset(self, index):
  296. return TkIndexToOffset(index, self.edit, self.marks)
  297. def _getindex(self, off):
  298. return TkOffsetToIndex(off, self.edit)
  299. def _fix_indexes(self, start, end):
  300. # first some magic to handle skipping over utf8 extended chars.
  301. while start > 0 and ord(self.edit.SCIGetCharAt(start)) & 0xC0 == 0x80:
  302. start -= 1
  303. while (
  304. end < self.edit.GetTextLength()
  305. and ord(self.edit.SCIGetCharAt(end)) & 0xC0 == 0x80
  306. ):
  307. end += 1
  308. # now handling fixing \r\n->\n disparities...
  309. if (
  310. start > 0
  311. and self.edit.SCIGetCharAt(start) == "\n"
  312. and self.edit.SCIGetCharAt(start - 1) == "\r"
  313. ):
  314. start = start - 1
  315. if (
  316. end < self.edit.GetTextLength()
  317. and self.edit.SCIGetCharAt(end - 1) == "\r"
  318. and self.edit.SCIGetCharAt(end) == "\n"
  319. ):
  320. end = end + 1
  321. return start, end
  322. ## def get_tab_width(self):
  323. ## return self.edit.GetTabWidth()
  324. ## def call(self, *rest):
  325. ## # Crap to support Tk measurement hacks for tab widths
  326. ## if rest[0] != "font" or rest[1] != "measure":
  327. ## raise ValueError, "Unsupport call type"
  328. ## return len(rest[5])
  329. ## def configure(self, **kw):
  330. ## for name, val in kw.items():
  331. ## if name=="tabs":
  332. ## self.edit.SCISetTabWidth(int(val))
  333. ## else:
  334. ## raise ValueError, "Unsupported configuration item %s" % kw
  335. def bind(self, binding, handler):
  336. self.edit.bindings.bind(binding, handler)
  337. def get(self, start, end=None):
  338. try:
  339. start = self._getoffset(start)
  340. if end is None:
  341. end = start + 1
  342. else:
  343. end = self._getoffset(end)
  344. except EmptyRange:
  345. return ""
  346. # Simple semantic checks to conform to the Tk text interface
  347. if end <= start:
  348. return ""
  349. max = self.edit.GetTextLength()
  350. checkEnd = 0
  351. if end > max:
  352. end = max
  353. checkEnd = 1
  354. start, end = self._fix_indexes(start, end)
  355. ret = self.edit.GetTextRange(start, end)
  356. # pretend a trailing '\n' exists if necessary.
  357. if checkEnd and (not ret or ret[-1] != "\n"):
  358. ret = ret + "\n"
  359. return ret.replace("\r", "")
  360. def index(self, spec):
  361. try:
  362. return self._getindex(self._getoffset(spec))
  363. except EmptyRange:
  364. return ""
  365. def insert(self, pos, text):
  366. try:
  367. pos = self._getoffset(pos)
  368. except EmptyRange:
  369. raise TextError("Empty range")
  370. self.edit.SetSel((pos, pos))
  371. # IDLE only deals with "\n" - we will be nicer
  372. bits = text.split("\n")
  373. self.edit.SCIAddText(bits[0])
  374. for bit in bits[1:]:
  375. self.edit.SCINewline()
  376. self.edit.SCIAddText(bit)
  377. def delete(self, start, end=None):
  378. try:
  379. start = self._getoffset(start)
  380. if end is not None:
  381. end = self._getoffset(end)
  382. except EmptyRange:
  383. raise TextError("Empty range")
  384. # If end is specified and == start, then we must delete nothing.
  385. if start == end:
  386. return
  387. # If end is not specified, delete one char
  388. if end is None:
  389. end = start + 1
  390. else:
  391. # Tk says not to delete in this case, but our control would.
  392. if end < start:
  393. return
  394. if start == self.edit.GetTextLength():
  395. return # Nothing to delete.
  396. old = self.edit.GetSel()[0] # Lose a selection
  397. # Hack for partial '\r\n' and UTF-8 char removal
  398. start, end = self._fix_indexes(start, end)
  399. self.edit.SetSel((start, end))
  400. self.edit.Clear()
  401. if old >= start and old < end:
  402. old = start
  403. elif old >= end:
  404. old = old - (end - start)
  405. self.edit.SetSel(old)
  406. def bell(self):
  407. win32api.MessageBeep()
  408. def see(self, pos):
  409. # Most commands we use in Scintilla actually force the selection
  410. # to be seen, making this unnecessary.
  411. pass
  412. def mark_set(self, name, pos):
  413. try:
  414. pos = self._getoffset(pos)
  415. except EmptyRange:
  416. raise TextError("Empty range '%s'" % pos)
  417. if name == "insert":
  418. self.edit.SetSel(pos)
  419. else:
  420. self.marks[name] = pos
  421. def tag_add(self, name, start, end):
  422. if name != "sel":
  423. raise ValueError("Only sel tag is supported")
  424. try:
  425. start = self._getoffset(start)
  426. end = self._getoffset(end)
  427. except EmptyRange:
  428. raise TextError("Empty range")
  429. self.edit.SetSel(start, end)
  430. def tag_remove(self, name, start, end):
  431. if name != "sel" or start != "1.0" or end != "end":
  432. raise ValueError("Cant remove this tag")
  433. # Turn the sel into a cursor
  434. self.edit.SetSel(self.edit.GetSel()[0])
  435. def compare(self, i1, op, i2):
  436. try:
  437. i1 = self._getoffset(i1)
  438. except EmptyRange:
  439. i1 = ""
  440. try:
  441. i2 = self._getoffset(i2)
  442. except EmptyRange:
  443. i2 = ""
  444. return eval("%d%s%d" % (i1, op, i2))
  445. def undo_block_start(self):
  446. self.edit.SCIBeginUndoAction()
  447. def undo_block_stop(self):
  448. self.edit.SCIEndUndoAction()
  449. ######################################################################
  450. #
  451. # Test related code.
  452. #
  453. ######################################################################
  454. def TestCheck(index, edit, expected=None):
  455. rc = TkIndexToOffset(index, edit, {})
  456. if rc != expected:
  457. print("ERROR: Index", index, ", expected", expected, "but got", rc)
  458. def TestGet(fr, to, t, expected):
  459. got = t.get(fr, to)
  460. if got != expected:
  461. print(
  462. "ERROR: get(%s, %s) expected %s, but got %s"
  463. % (repr(fr), repr(to), repr(expected), repr(got))
  464. )
  465. def test():
  466. import pywin.framework.editor
  467. d = pywin.framework.editor.editorTemplate.OpenDocumentFile(None)
  468. e = d.GetFirstView()
  469. t = TkText(e)
  470. e.SCIAddText("hi there how\nare you today\r\nI hope you are well")
  471. e.SetSel((4, 4))
  472. skip = """
  473. TestCheck("insert", e, 4)
  474. TestCheck("insert wordstart", e, 3)
  475. TestCheck("insert wordend", e, 8)
  476. TestCheck("insert linestart", e, 0)
  477. TestCheck("insert lineend", e, 12)
  478. TestCheck("insert + 4 chars", e, 8)
  479. TestCheck("insert +4c", e, 8)
  480. TestCheck("insert - 2 chars", e, 2)
  481. TestCheck("insert -2c", e, 2)
  482. TestCheck("insert-2c", e, 2)
  483. TestCheck("insert-2 c", e, 2)
  484. TestCheck("insert- 2c", e, 2)
  485. TestCheck("1.1", e, 1)
  486. TestCheck("1.0", e, 0)
  487. TestCheck("2.0", e, 13)
  488. try:
  489. TestCheck("sel.first", e, 0)
  490. print "*** sel.first worked with an empty selection"
  491. except TextError:
  492. pass
  493. e.SetSel((4,5))
  494. TestCheck("sel.first- 2c", e, 2)
  495. TestCheck("sel.last- 2c", e, 3)
  496. """
  497. # Check EOL semantics
  498. e.SetSel((4, 4))
  499. TestGet("insert lineend", "insert lineend +1c", t, "\n")
  500. e.SetSel((20, 20))
  501. TestGet("insert lineend", "insert lineend +1c", t, "\n")
  502. e.SetSel((35, 35))
  503. TestGet("insert lineend", "insert lineend +1c", t, "\n")
  504. class IDLEWrapper:
  505. def __init__(self, control):
  506. self.text = control
  507. def IDLETest(extension):
  508. import os
  509. import sys
  510. modname = "pywin.idle." + extension
  511. __import__(modname)
  512. mod = sys.modules[modname]
  513. mod.TclError = TextError
  514. klass = getattr(mod, extension)
  515. # Create a new Scintilla Window.
  516. import pywin.framework.editor
  517. d = pywin.framework.editor.editorTemplate.OpenDocumentFile(None)
  518. v = d.GetFirstView()
  519. fname = os.path.splitext(__file__)[0] + ".py"
  520. v.SCIAddText(open(fname).read())
  521. d.SetModifiedFlag(0)
  522. r = klass(IDLEWrapper(TkText(v)))
  523. return r
  524. if __name__ == "__main__":
  525. test()