app.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. # The TkApp class.
  2. from .tklib_cffi import ffi as tkffi, lib as tklib
  3. from . import TclError
  4. from .tclobj import (Tcl_Obj, FromObj, FromTclString, AsObj, TypeCache,
  5. FromBignumObj, FromWideIntObj)
  6. import contextlib
  7. import sys
  8. import threading
  9. import time
  10. import warnings
  11. class _DummyLock(object):
  12. "A lock-like object that does not do anything"
  13. def acquire(self):
  14. pass
  15. def release(self):
  16. pass
  17. def __enter__(self):
  18. pass
  19. def __exit__(self, *exc):
  20. pass
  21. def varname_converter(input):
  22. if isinstance(input, Tcl_Obj):
  23. input = input.string
  24. if isinstance(input, str):
  25. input = input.encode('utf-8')
  26. if b'\0' in input:
  27. raise ValueError("NUL character in string")
  28. return input
  29. def Tcl_AppInit(app):
  30. if tklib.Tcl_Init(app.interp) == tklib.TCL_ERROR:
  31. app.raiseTclError()
  32. skip_tk_init = tklib.Tcl_GetVar(
  33. app.interp, b"_tkinter_skip_tk_init", tklib.TCL_GLOBAL_ONLY)
  34. if skip_tk_init and tkffi.string(skip_tk_init) == b"1":
  35. return
  36. if tklib.Tk_Init(app.interp) == tklib.TCL_ERROR:
  37. app.raiseTclError()
  38. class _CommandData(object):
  39. def __new__(cls, app, name, func):
  40. self = object.__new__(cls)
  41. self.app = app
  42. self.name = name
  43. self.func = func
  44. handle = tkffi.new_handle(self)
  45. app._commands[name] = handle # To keep the command alive
  46. return tkffi.cast("ClientData", handle)
  47. @tkffi.callback("Tcl_CmdProc")
  48. def PythonCmd(clientData, interp, argc, argv):
  49. self = tkffi.from_handle(clientData)
  50. assert self.app.interp == interp
  51. with self.app._tcl_lock_released():
  52. try:
  53. args = [FromTclString(tkffi.string(arg)) for arg in argv[1:argc]]
  54. result = self.func(*args)
  55. obj = AsObj(result)
  56. tklib.Tcl_SetObjResult(interp, obj)
  57. except:
  58. self.app.errorInCmd = True
  59. self.app.exc_info = sys.exc_info()
  60. return tklib.TCL_ERROR
  61. else:
  62. return tklib.TCL_OK
  63. @tkffi.callback("Tcl_CmdDeleteProc")
  64. def PythonCmdDelete(clientData):
  65. self = tkffi.from_handle(clientData)
  66. app = self.app
  67. del app._commands[self.name]
  68. return
  69. class TkApp(object):
  70. _busywaitinterval = 0.02 # 20ms.
  71. def __new__(cls, screenName, className,
  72. interactive, wantobjects, wantTk, sync, use):
  73. if not wantobjects:
  74. raise NotImplementedError("wantobjects=True only")
  75. self = object.__new__(cls)
  76. self.interp = tklib.Tcl_CreateInterp()
  77. self._wantobjects = wantobjects
  78. self.threaded = bool(tklib.Tcl_GetVar2Ex(
  79. self.interp, b"tcl_platform", b"threaded",
  80. tklib.TCL_GLOBAL_ONLY))
  81. self.thread_id = tklib.Tcl_GetCurrentThread()
  82. self.dispatching = False
  83. self.quitMainLoop = False
  84. self.errorInCmd = False
  85. if not self.threaded:
  86. # TCL is not thread-safe, calls needs to be serialized.
  87. self._tcl_lock = threading.RLock()
  88. else:
  89. self._tcl_lock = _DummyLock()
  90. self._typeCache = TypeCache()
  91. self._commands = {}
  92. # Delete the 'exit' command, which can screw things up
  93. tklib.Tcl_DeleteCommand(self.interp, b"exit")
  94. if screenName is not None:
  95. tklib.Tcl_SetVar2(self.interp, b"env", b"DISPLAY",
  96. screenName.encode('utf-8'),
  97. tklib.TCL_GLOBAL_ONLY)
  98. if interactive:
  99. tklib.Tcl_SetVar(self.interp, b"tcl_interactive", b"1",
  100. tklib.TCL_GLOBAL_ONLY)
  101. else:
  102. tklib.Tcl_SetVar(self.interp, b"tcl_interactive", b"0",
  103. tklib.TCL_GLOBAL_ONLY)
  104. # This is used to get the application class for Tk 4.1 and up
  105. argv0 = className.lower().encode('utf-8')
  106. tklib.Tcl_SetVar(self.interp, b"argv0", argv0,
  107. tklib.TCL_GLOBAL_ONLY)
  108. if not wantTk:
  109. tklib.Tcl_SetVar(self.interp, b"_tkinter_skip_tk_init", b"1",
  110. tklib.TCL_GLOBAL_ONLY)
  111. # some initial arguments need to be in argv
  112. if sync or use:
  113. args = ""
  114. if sync:
  115. args += "-sync"
  116. if use:
  117. if sync:
  118. args += " "
  119. args += "-use " + use
  120. tklib.Tcl_SetVar(self.interp, "argv", args,
  121. tklib.TCL_GLOBAL_ONLY)
  122. Tcl_AppInit(self)
  123. # EnableEventHook()
  124. self._typeCache.add_extra_types(self)
  125. return self
  126. def __del__(self):
  127. tklib.Tcl_DeleteInterp(self.interp)
  128. # DisableEventHook()
  129. def raiseTclError(self):
  130. if self.errorInCmd:
  131. self.errorInCmd = False
  132. raise self.exc_info[1].with_traceback(self.exc_info[2])
  133. raise TclError(tkffi.string(
  134. tklib.Tcl_GetStringResult(self.interp)).decode('utf-8'))
  135. def wantobjects(self):
  136. return self._wantobjects
  137. def _check_tcl_appartment(self):
  138. if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread():
  139. raise RuntimeError("Calling Tcl from different appartment")
  140. @contextlib.contextmanager
  141. def _tcl_lock_released(self):
  142. "Context manager to temporarily release the tcl lock."
  143. self._tcl_lock.release()
  144. yield
  145. self._tcl_lock.acquire()
  146. def loadtk(self):
  147. # We want to guard against calling Tk_Init() multiple times
  148. err = tklib.Tcl_Eval(self.interp, b"info exists tk_version")
  149. if err == tklib.TCL_ERROR:
  150. self.raiseTclError()
  151. tk_exists = tklib.Tcl_GetStringResult(self.interp)
  152. if not tk_exists or tkffi.string(tk_exists) != b"1":
  153. err = tklib.Tk_Init(self.interp)
  154. if err == tklib.TCL_ERROR:
  155. self.raiseTclError()
  156. def interpaddr(self):
  157. return int(tkffi.cast('size_t', self.interp))
  158. def _var_invoke(self, func, *args, **kwargs):
  159. if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread():
  160. # The current thread is not the interpreter thread.
  161. # Marshal the call to the interpreter thread, then wait
  162. # for completion.
  163. raise NotImplementedError("Call from another thread")
  164. return func(*args, **kwargs)
  165. def _getvar(self, name1, name2=None, global_only=False):
  166. name1 = varname_converter(name1)
  167. if not name2:
  168. name2 = tkffi.NULL
  169. flags=tklib.TCL_LEAVE_ERR_MSG
  170. if global_only:
  171. flags |= tklib.TCL_GLOBAL_ONLY
  172. with self._tcl_lock:
  173. res = tklib.Tcl_GetVar2Ex(self.interp, name1, name2, flags)
  174. if not res:
  175. self.raiseTclError()
  176. assert self._wantobjects
  177. return FromObj(self, res)
  178. def _setvar(self, name1, value, global_only=False):
  179. name1 = varname_converter(name1)
  180. # XXX Acquire tcl lock???
  181. newval = AsObj(value)
  182. flags=tklib.TCL_LEAVE_ERR_MSG
  183. if global_only:
  184. flags |= tklib.TCL_GLOBAL_ONLY
  185. with self._tcl_lock:
  186. res = tklib.Tcl_SetVar2Ex(self.interp, name1, tkffi.NULL,
  187. newval, flags)
  188. if not res:
  189. self.raiseTclError()
  190. def _unsetvar(self, name1, name2=None, global_only=False):
  191. name1 = varname_converter(name1)
  192. if not name2:
  193. name2 = tkffi.NULL
  194. flags=tklib.TCL_LEAVE_ERR_MSG
  195. if global_only:
  196. flags |= tklib.TCL_GLOBAL_ONLY
  197. with self._tcl_lock:
  198. res = tklib.Tcl_UnsetVar2(self.interp, name1, name2, flags)
  199. if res == tklib.TCL_ERROR:
  200. self.raiseTclError()
  201. def getvar(self, name1, name2=None):
  202. return self._var_invoke(self._getvar, name1, name2)
  203. def globalgetvar(self, name1, name2=None):
  204. return self._var_invoke(self._getvar, name1, name2, global_only=True)
  205. def setvar(self, name1, value):
  206. return self._var_invoke(self._setvar, name1, value)
  207. def globalsetvar(self, name1, value):
  208. return self._var_invoke(self._setvar, name1, value, global_only=True)
  209. def unsetvar(self, name1, name2=None):
  210. return self._var_invoke(self._unsetvar, name1, name2)
  211. def globalunsetvar(self, name1, name2=None):
  212. return self._var_invoke(self._unsetvar, name1, name2, global_only=True)
  213. # COMMANDS
  214. def createcommand(self, cmdName, func):
  215. if not callable(func):
  216. raise TypeError("command not callable")
  217. if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread():
  218. raise NotImplementedError("Call from another thread")
  219. clientData = _CommandData(self, cmdName, func)
  220. if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread():
  221. raise NotImplementedError("Call from another thread")
  222. with self._tcl_lock:
  223. res = tklib.Tcl_CreateCommand(
  224. self.interp, cmdName.encode('utf-8'), _CommandData.PythonCmd,
  225. clientData, _CommandData.PythonCmdDelete)
  226. if not res:
  227. raise TclError("can't create Tcl command")
  228. def deletecommand(self, cmdName):
  229. if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread():
  230. raise NotImplementedError("Call from another thread")
  231. with self._tcl_lock:
  232. res = tklib.Tcl_DeleteCommand(self.interp, cmdName.encode('utf-8'))
  233. if res == -1:
  234. raise TclError("can't delete Tcl command")
  235. def call(self, *args):
  236. flags = tklib.TCL_EVAL_DIRECT | tklib.TCL_EVAL_GLOBAL
  237. # If args is a single tuple, replace with contents of tuple
  238. if len(args) == 1 and isinstance(args[0], tuple):
  239. args = args[0]
  240. if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread():
  241. # We cannot call the command directly. Instead, we must
  242. # marshal the parameters to the interpreter thread.
  243. raise NotImplementedError("Call from another thread")
  244. objects = tkffi.new("Tcl_Obj*[]", len(args))
  245. argc = len(args)
  246. try:
  247. for i, arg in enumerate(args):
  248. if arg is None:
  249. argc = i
  250. break
  251. obj = AsObj(arg)
  252. tklib.Tcl_IncrRefCount(obj)
  253. objects[i] = obj
  254. with self._tcl_lock:
  255. res = tklib.Tcl_EvalObjv(self.interp, argc, objects, flags)
  256. if res == tklib.TCL_ERROR:
  257. self.raiseTclError()
  258. else:
  259. result = self._callResult()
  260. finally:
  261. for obj in objects:
  262. if obj:
  263. tklib.Tcl_DecrRefCount(obj)
  264. return result
  265. def _callResult(self):
  266. assert self._wantobjects
  267. value = tklib.Tcl_GetObjResult(self.interp)
  268. # Not sure whether the IncrRef is necessary, but something
  269. # may overwrite the interpreter result while we are
  270. # converting it.
  271. tklib.Tcl_IncrRefCount(value)
  272. res = FromObj(self, value)
  273. tklib.Tcl_DecrRefCount(value)
  274. return res
  275. def eval(self, script):
  276. self._check_tcl_appartment()
  277. with self._tcl_lock:
  278. res = tklib.Tcl_Eval(self.interp, script.encode('utf-8'))
  279. if res == tklib.TCL_ERROR:
  280. self.raiseTclError()
  281. result = tkffi.string(tklib.Tcl_GetStringResult(self.interp))
  282. return FromTclString(result)
  283. def evalfile(self, filename):
  284. self._check_tcl_appartment()
  285. with self._tcl_lock:
  286. res = tklib.Tcl_EvalFile(self.interp, filename.encode('utf-8'))
  287. if res == tklib.TCL_ERROR:
  288. self.raiseTclError()
  289. result = tkffi.string(tklib.Tcl_GetStringResult(self.interp))
  290. return FromTclString(result)
  291. def split(self, arg):
  292. if isinstance(arg, Tcl_Obj):
  293. objc = tkffi.new("int*")
  294. objv = tkffi.new("Tcl_Obj***")
  295. status = tklib.Tcl_ListObjGetElements(self.interp, arg._value, objc, objv)
  296. if status == tklib.TCL_ERROR:
  297. return FromObj(self, arg._value)
  298. if objc == 0:
  299. return ''
  300. elif objc == 1:
  301. return FromObj(self, objv[0][0])
  302. result = []
  303. for i in range(objc[0]):
  304. result.append(FromObj(self, objv[0][i]))
  305. return tuple(result)
  306. elif isinstance(arg, (tuple, list)):
  307. return self._splitObj(arg)
  308. if isinstance(arg, str):
  309. arg = arg.encode('utf-8')
  310. return self._split(arg)
  311. def splitlist(self, arg):
  312. if isinstance(arg, Tcl_Obj):
  313. objc = tkffi.new("int*")
  314. objv = tkffi.new("Tcl_Obj***")
  315. status = tklib.Tcl_ListObjGetElements(self.interp, arg._value, objc, objv)
  316. if status == tklib.TCL_ERROR:
  317. self.raiseTclError()
  318. result = []
  319. for i in range(objc[0]):
  320. result.append(FromObj(self, objv[0][i]))
  321. return tuple(result)
  322. elif isinstance(arg, tuple):
  323. return arg
  324. elif isinstance(arg, list):
  325. return tuple(arg)
  326. elif isinstance(arg, str):
  327. arg = arg.encode('utf8')
  328. argc = tkffi.new("int*")
  329. argv = tkffi.new("char***")
  330. res = tklib.Tcl_SplitList(self.interp, arg, argc, argv)
  331. if res == tklib.TCL_ERROR:
  332. self.raiseTclError()
  333. result = tuple(FromTclString(tkffi.string(argv[0][i]))
  334. for i in range(argc[0]))
  335. tklib.Tcl_Free(argv[0])
  336. return result
  337. def _splitObj(self, arg):
  338. if isinstance(arg, tuple):
  339. size = len(arg)
  340. result = None
  341. # Recursively invoke SplitObj for all tuple items.
  342. # If this does not return a new object, no action is
  343. # needed.
  344. for i in range(size):
  345. elem = arg[i]
  346. newelem = self._splitObj(elem)
  347. if result is None:
  348. if newelem == elem:
  349. continue
  350. result = [None] * size
  351. for k in range(i):
  352. result[k] = arg[k]
  353. result[i] = newelem
  354. if result is not None:
  355. return tuple(result)
  356. if isinstance(arg, list):
  357. # Recursively invoke SplitObj for all list items.
  358. return tuple(self._splitObj(elem) for elem in arg)
  359. elif isinstance(arg, str):
  360. argc = tkffi.new("int*")
  361. argv = tkffi.new("char***")
  362. list_ = arg.encode('utf-8')
  363. res = tklib.Tcl_SplitList(tkffi.NULL, list_, argc, argv)
  364. if res != tklib.TCL_OK:
  365. return arg
  366. tklib.Tcl_Free(argv[0])
  367. if argc[0] > 1:
  368. return self._split(list_)
  369. elif isinstance(arg, bytes):
  370. argc = tkffi.new("int*")
  371. argv = tkffi.new("char***")
  372. list_ = arg
  373. res = tklib.Tcl_SplitList(tkffi.NULL, list_, argc, argv)
  374. if res != tklib.TCL_OK:
  375. return arg
  376. tklib.Tcl_Free(argv[0])
  377. if argc[0] > 1:
  378. return self._split(list_)
  379. return arg
  380. def _split(self, arg):
  381. argc = tkffi.new("int*")
  382. argv = tkffi.new("char***")
  383. res = tklib.Tcl_SplitList(tkffi.NULL, arg, argc, argv)
  384. if res == tklib.TCL_ERROR:
  385. # Not a list.
  386. # Could be a quoted string containing funnies, e.g. {"}.
  387. # Return the string itself.
  388. return FromTclString(arg)
  389. try:
  390. if argc[0] == 0:
  391. return ""
  392. elif argc[0] == 1:
  393. return FromTclString(tkffi.string(argv[0][0]))
  394. else:
  395. return tuple(self._split(argv[0][i])
  396. for i in range(argc[0]))
  397. finally:
  398. tklib.Tcl_Free(argv[0])
  399. def merge(self, *args):
  400. warnings.warn("merge is deprecated and will be removed in 3.4",
  401. DeprecationWarning)
  402. s = self._merge(args)
  403. return s.decode('utf-8')
  404. def _merge(self, args):
  405. argv = []
  406. for arg in args:
  407. if isinstance(arg, tuple):
  408. argv.append(self._merge(arg))
  409. elif arg is None:
  410. break
  411. elif isinstance(arg, bytes):
  412. argv.append(arg)
  413. else:
  414. argv.append(str(arg).encode('utf-8'))
  415. argv_array = [tkffi.new("char[]", arg) for arg in argv]
  416. res = tklib.Tcl_Merge(len(argv), argv_array)
  417. if not res:
  418. raise TclError("merge failed")
  419. try:
  420. return tkffi.string(res)
  421. finally:
  422. tklib.Tcl_Free(res)
  423. def getboolean(self, s):
  424. if isinstance(s, int):
  425. return bool(s)
  426. try:
  427. s = s.encode('utf-8')
  428. except AttributeError:
  429. raise TypeError
  430. if b'\x00' in s:
  431. raise TypeError
  432. v = tkffi.new("int*")
  433. res = tklib.Tcl_GetBoolean(self.interp, s, v)
  434. if res == tklib.TCL_ERROR:
  435. self.raiseTclError()
  436. return bool(v[0])
  437. def getint(self, s):
  438. if isinstance(s, int):
  439. return s
  440. try:
  441. s = s.encode('utf-8')
  442. except AttributeError:
  443. raise TypeError
  444. if b'\x00' in s:
  445. raise TypeError
  446. if tklib.HAVE_LIBTOMMATH or tklib.HAVE_WIDE_INT_TYPE:
  447. value = tklib.Tcl_NewStringObj(s, -1)
  448. if not value:
  449. self.raiseTclError()
  450. try:
  451. if tklib.HAVE_LIBTOMMATH:
  452. return FromBignumObj(self, value)
  453. else:
  454. return FromWideIntObj(self, value)
  455. finally:
  456. tklib.Tcl_DecrRefCount(value)
  457. else:
  458. v = tkffi.new("int*")
  459. res = tklib.Tcl_GetInt(self.interp, s, v)
  460. if res == tklib.TCL_ERROR:
  461. self.raiseTclError()
  462. return v[0]
  463. def getdouble(self, s):
  464. if isinstance(s, (float, int)):
  465. return float(s)
  466. try:
  467. s = s.encode('utf-8')
  468. except AttributeError:
  469. raise TypeError
  470. if b'\x00' in s:
  471. raise TypeError
  472. v = tkffi.new("double*")
  473. res = tklib.Tcl_GetDouble(self.interp, s, v)
  474. if res == tklib.TCL_ERROR:
  475. self.raiseTclError()
  476. return v[0]
  477. def exprboolean(self, s):
  478. if '\x00' in s:
  479. raise TypeError
  480. s = s.encode('utf-8')
  481. v = tkffi.new("int*")
  482. res = tklib.Tcl_ExprBoolean(self.interp, s, v)
  483. if res == tklib.TCL_ERROR:
  484. self.raiseTclError()
  485. return v[0]
  486. def exprlong(self, s):
  487. if '\x00' in s:
  488. raise TypeError
  489. s = s.encode('utf-8')
  490. v = tkffi.new("long*")
  491. res = tklib.Tcl_ExprLong(self.interp, s, v)
  492. if res == tklib.TCL_ERROR:
  493. self.raiseTclError()
  494. return v[0]
  495. def exprdouble(self, s):
  496. if '\x00' in s:
  497. raise TypeError
  498. s = s.encode('utf-8')
  499. v = tkffi.new("double*")
  500. res = tklib.Tcl_ExprDouble(self.interp, s, v)
  501. if res == tklib.TCL_ERROR:
  502. self.raiseTclError()
  503. return v[0]
  504. def exprstring(self, s):
  505. if '\x00' in s:
  506. raise TypeError
  507. s = s.encode('utf-8')
  508. res = tklib.Tcl_ExprString(self.interp, s)
  509. if res == tklib.TCL_ERROR:
  510. self.raiseTclError()
  511. return FromTclString(tkffi.string(
  512. tklib.Tcl_GetStringResult(self.interp)))
  513. def mainloop(self, threshold):
  514. self._check_tcl_appartment()
  515. self.dispatching = True
  516. while (tklib.Tk_GetNumMainWindows() > threshold and
  517. not self.quitMainLoop and not self.errorInCmd):
  518. if self.threaded:
  519. result = tklib.Tcl_DoOneEvent(0)
  520. else:
  521. with self._tcl_lock:
  522. result = tklib.Tcl_DoOneEvent(tklib.TCL_DONT_WAIT)
  523. if result == 0:
  524. time.sleep(self._busywaitinterval)
  525. if result < 0:
  526. break
  527. self.dispatching = False
  528. self.quitMainLoop = False
  529. if self.errorInCmd:
  530. self.errorInCmd = False
  531. raise self.exc_info[1].with_traceback(self.exc_info[2])
  532. def quit(self):
  533. self.quitMainLoop = True
  534. def _createbytearray(self, buf):
  535. """Convert Python string or any buffer compatible object to Tcl
  536. byte-array object. Use it to pass binary data (e.g. image's
  537. data) to Tcl/Tk commands."""
  538. cdata = tkffi.new("char[]", buf)
  539. res = tklib.Tcl_NewByteArrayObj(cdata, len(buf))
  540. if not res:
  541. self.raiseTclError()
  542. return TclObject(res)