psychopy 2024.1.3__py3-none-any.whl → 2024.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of psychopy might be problematic. Click here for more details.

Files changed (331) hide show
  1. psychopy/.DS_Store +0 -0
  2. psychopy/CHANGELOG.txt +206 -0
  3. psychopy/GIT_SHA +1 -0
  4. psychopy/VERSION +1 -0
  5. psychopy/__init__.py +77 -15
  6. psychopy/app/Resources/classic/plugin16.png +0 -0
  7. psychopy/app/Resources/classic/plugin16@2x.png +0 -0
  8. psychopy/app/Resources/dark/plugin16.png +0 -0
  9. psychopy/app/Resources/dark/plugin16@2x.png +0 -0
  10. psychopy/app/Resources/light/plugin16.png +0 -0
  11. psychopy/app/Resources/light/plugin16@2x.png +0 -0
  12. psychopy/app/__init__.py +76 -2
  13. psychopy/app/_psychopyApp.py +126 -101
  14. psychopy/app/builder/builder.py +14 -10
  15. psychopy/app/builder/dialogs/__init__.py +8 -8
  16. psychopy/app/builder/dialogs/dlgsConditions.py +12 -13
  17. psychopy/app/builder/dialogs/paramCtrls.py +24 -57
  18. psychopy/app/builder/localizedStrings.py +11 -9
  19. psychopy/app/builder/validators.py +2 -2
  20. psychopy/app/coder/codeEditorBase.py +8 -8
  21. psychopy/app/coder/coder.py +4 -4
  22. psychopy/app/connections/sendusage.py +2 -2
  23. psychopy/app/connections/updates.py +9 -9
  24. psychopy/app/dialogs.py +34 -2
  25. psychopy/app/idle.py +31 -0
  26. psychopy/app/jobs.py +21 -3
  27. psychopy/app/linuxconfig/__init__.py +9 -0
  28. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  29. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +4602 -2540
  30. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  31. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +56 -54
  32. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +53 -43
  33. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  34. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +56 -54
  35. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  36. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1258 -1176
  37. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +9415 -5
  38. psychopy/app/pavlovia_ui/_base.py +33 -3
  39. psychopy/app/pavlovia_ui/search.py +0 -1
  40. psychopy/app/plugin_manager/dialog.py +104 -51
  41. psychopy/app/plugin_manager/packages.py +5 -0
  42. psychopy/app/plugin_manager/plugins.py +152 -67
  43. psychopy/app/preferencesDlg.py +8 -8
  44. psychopy/app/psychopyApp.py +11 -5
  45. psychopy/app/ribbon.py +124 -14
  46. psychopy/app/runner/runner.py +6 -1
  47. psychopy/app/stdout/stdOutRich.py +27 -11
  48. psychopy/app/themes/icons.py +52 -2
  49. psychopy/assets/__init__.py +0 -0
  50. psychopy/assets/click.png +0 -0
  51. psychopy/assets/clicknext.png +0 -0
  52. psychopy/assets/next.png +0 -0
  53. psychopy/assets/psychopy.ico +0 -0
  54. psychopy/assets/psychopy.png +0 -0
  55. psychopy/assets/templates/__init__.py +0 -0
  56. psychopy/assets/touch.png +0 -0
  57. psychopy/assets/touchnext.png +0 -0
  58. psychopy/assets/window.ico +0 -0
  59. psychopy/changes/2023.1.0.md +9 -0
  60. psychopy/changes/2024.1.0.md +16 -0
  61. psychopy/changes/__init__.py +0 -0
  62. psychopy/clock.py +2 -2
  63. psychopy/colors.py +2 -1
  64. psychopy/compatibility.py +53 -1
  65. psychopy/contrib/.DS_Store +0 -0
  66. psychopy/contrib/configobj/__init__.py +10 -8
  67. psychopy/data/__init__.py +3 -2
  68. psychopy/data/base.py +5 -5
  69. psychopy/data/experiment.py +130 -4
  70. psychopy/data/routine.py +56 -0
  71. psychopy/data/staircase.py +2 -2
  72. psychopy/data/trial.py +559 -97
  73. psychopy/data/utils.py +56 -21
  74. psychopy/demos/.DS_Store +0 -0
  75. psychopy/demos/builder/.DS_Store +0 -0
  76. psychopy/demos/builder/Design Templates/.DS_Store +0 -0
  77. psychopy/demos/builder/Experiments/.DS_Store +0 -0
  78. psychopy/demos/builder/Feature Demos/.DS_Store +0 -0
  79. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +375 -0
  80. psychopy/demos/builder/Feature Demos/buttonBox/readme.md +5 -0
  81. psychopy/demos/builder/Feature Demos/pilotMode/pilotMode.psyexp +433 -0
  82. psychopy/demos/builder/Feature Demos/pilotMode/readme.md +7 -0
  83. psychopy/demos/builder/Feature Demos/progress/progressBar.psyexp +4 -4
  84. psychopy/demos/builder/Hardware/.DS_Store +0 -0
  85. psychopy/demos/builder/Helper Tools/.DS_Store +0 -0
  86. psychopy/demos/coder/.DS_Store +0 -0
  87. psychopy/demos/coder/hardware/testSoundLatency.py +2 -2
  88. psychopy/demos/coder/iohub/.DS_Store +0 -0
  89. psychopy/demos/coder/misc/hdf5_2_csv +33 -0
  90. psychopy/event.py +30 -29
  91. psychopy/experiment/.DS_Store +0 -0
  92. psychopy/experiment/_experiment.py +6 -6
  93. psychopy/experiment/components/.DS_Store +0 -0
  94. psychopy/experiment/components/__init__.py +6 -3
  95. psychopy/experiment/components/_base.py +286 -131
  96. psychopy/experiment/components/aperture/.DS_Store +0 -0
  97. psychopy/experiment/components/brush/.DS_Store +0 -0
  98. psychopy/experiment/components/button/.DS_Store +0 -0
  99. psychopy/experiment/components/button/__init__.py +5 -1
  100. psychopy/experiment/components/buttonBox/.DS_Store +0 -0
  101. psychopy/experiment/components/buttonBox/__init__.py +21 -12
  102. psychopy/experiment/components/camera/.DS_Store +0 -0
  103. psychopy/experiment/components/code/.DS_Store +0 -0
  104. psychopy/experiment/components/dots/.DS_Store +0 -0
  105. psychopy/experiment/components/eyetracker_record/.DS_Store +0 -0
  106. psychopy/experiment/components/eyetracker_record/__init__.py +92 -30
  107. psychopy/experiment/components/form/.DS_Store +0 -0
  108. psychopy/experiment/components/form/__init__.py +6 -2
  109. psychopy/experiment/components/grating/.DS_Store +0 -0
  110. psychopy/experiment/components/grating/__init__.py +14 -3
  111. psychopy/experiment/components/image/.DS_Store +0 -0
  112. psychopy/experiment/components/image/__init__.py +14 -3
  113. psychopy/experiment/components/joyButtons/.DS_Store +0 -0
  114. psychopy/experiment/components/joystick/.DS_Store +0 -0
  115. psychopy/experiment/components/keyboard/.DS_Store +0 -0
  116. psychopy/experiment/components/keyboard/__init__.py +22 -10
  117. psychopy/experiment/components/microphone/.DS_Store +0 -0
  118. psychopy/experiment/components/microphone/__init__.py +59 -39
  119. psychopy/experiment/components/mouse/.DS_Store +0 -0
  120. psychopy/experiment/components/mouse/__init__.py +44 -29
  121. psychopy/experiment/components/movie/.DS_Store +0 -0
  122. psychopy/experiment/components/movie/__init__.py +1 -1
  123. psychopy/experiment/components/panorama/.DS_Store +0 -0
  124. psychopy/experiment/components/parallelOut/.DS_Store +0 -0
  125. psychopy/experiment/components/patch/.DS_Store +0 -0
  126. psychopy/experiment/components/polygon/.DS_Store +0 -0
  127. psychopy/experiment/components/polygon/__init__.py +26 -6
  128. psychopy/experiment/components/progress/.DS_Store +0 -0
  129. psychopy/experiment/components/progress/__init__.py +1 -1
  130. psychopy/experiment/components/ratingScale/.DS_Store +0 -0
  131. psychopy/experiment/components/resourceManager/.DS_Store +0 -0
  132. psychopy/experiment/components/roi/.DS_Store +0 -0
  133. psychopy/experiment/components/roi/__init__.py +5 -0
  134. psychopy/experiment/components/routineSettings/.DS_Store +0 -0
  135. psychopy/experiment/components/routineSettings/__init__.py +57 -10
  136. psychopy/experiment/components/serialOut/.DS_Store +0 -0
  137. psychopy/experiment/components/settings/.DS_Store +0 -0
  138. psychopy/experiment/components/settings/__init__.py +117 -42
  139. psychopy/experiment/components/slider/.DS_Store +0 -0
  140. psychopy/experiment/components/sound/.DS_Store +0 -0
  141. psychopy/experiment/components/sound/__init__.py +54 -19
  142. psychopy/experiment/components/static/.DS_Store +0 -0
  143. psychopy/experiment/components/static/__init__.py +1 -1
  144. psychopy/experiment/components/text/.DS_Store +0 -0
  145. psychopy/experiment/components/text/__init__.py +28 -3
  146. psychopy/experiment/components/textbox/.DS_Store +0 -0
  147. psychopy/experiment/components/textbox/__init__.py +12 -2
  148. psychopy/experiment/components/unknown/.DS_Store +0 -0
  149. psychopy/experiment/components/unknown/__init__.py +1 -2
  150. psychopy/experiment/components/unknownPlugin/.DS_Store +0 -0
  151. psychopy/experiment/components/unknownPlugin/__init__.py +2 -2
  152. psychopy/experiment/components/variable/.DS_Store +0 -0
  153. psychopy/experiment/flow.py +11 -4
  154. psychopy/experiment/loops.py +85 -37
  155. psychopy/experiment/params.py +74 -32
  156. psychopy/experiment/py2js_transpiler.py +8 -1
  157. psychopy/experiment/routines/.DS_Store +0 -0
  158. psychopy/experiment/routines/_base.py +102 -22
  159. psychopy/experiment/routines/counterbalance/.DS_Store +0 -0
  160. psychopy/experiment/routines/counterbalance/__init__.py +5 -1
  161. psychopy/experiment/routines/eyetracker_calibrate/.DS_Store +0 -0
  162. psychopy/experiment/routines/eyetracker_validate/.DS_Store +0 -0
  163. psychopy/experiment/routines/pavlovia_survey/.DS_Store +0 -0
  164. psychopy/experiment/routines/photodiodeValidator/.DS_Store +0 -0
  165. psychopy/experiment/routines/photodiodeValidator/__init__.py +7 -6
  166. psychopy/experiment/routines/unknown/.DS_Store +0 -0
  167. psychopy/gui/wxgui.py +4 -4
  168. psychopy/hardware/.DS_Store +0 -0
  169. psychopy/hardware/__init__.py +1 -1
  170. psychopy/hardware/base.py +12 -0
  171. psychopy/hardware/camera/__init__.py +1 -15
  172. psychopy/hardware/cedrus.py +10 -11
  173. psychopy/hardware/crs/colorcal.py +13 -22
  174. psychopy/hardware/crs/optical.py +10 -20
  175. psychopy/hardware/emulator.py +17 -14
  176. psychopy/hardware/eyetracker.py +42 -118
  177. psychopy/hardware/gammasci.py +4 -15
  178. psychopy/hardware/keyboard.py +102 -11
  179. psychopy/hardware/listener.py +3 -0
  180. psychopy/hardware/microphone.py +148 -18
  181. psychopy/hardware/minolta.py +8 -15
  182. psychopy/hardware/photodiode.py +191 -16
  183. psychopy/hardware/photometer/__init__.py +11 -19
  184. psychopy/hardware/pr.py +8 -15
  185. psychopy/hardware/speaker.py +39 -4
  186. psychopy/info.py +0 -71
  187. psychopy/iohub/.DS_Store +0 -0
  188. psychopy/iohub/__init__.py +1 -1
  189. psychopy/iohub/client/__init__.py +30 -20
  190. psychopy/iohub/client/keyboard.py +24 -24
  191. psychopy/iohub/datastore/__init__.py +2 -2
  192. psychopy/iohub/datastore/util.py +2 -2
  193. psychopy/iohub/default_config.yaml +1 -1
  194. psychopy/iohub/devices/.DS_Store +0 -0
  195. psychopy/iohub/devices/__init__.py +112 -25
  196. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  197. psychopy/iohub/devices/experiment/default_experiment.yaml +12 -1
  198. psychopy/iohub/devices/experiment/supported_config_settings.yaml +5 -1
  199. psychopy/iohub/devices/eyetracker/.DS_Store +0 -0
  200. psychopy/iohub/devices/eyetracker/__init__.py +46 -0
  201. psychopy/iohub/devices/eyetracker/calibration/procedure.py +2 -2
  202. psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +14 -2
  203. psychopy/iohub/devices/eyetracker/hw/mouse/eyetracker.py +3 -4
  204. psychopy/iohub/server.py +2 -2
  205. psychopy/iohub/start_iohub_process.py +3 -0
  206. psychopy/iohub/util/__init__.py +62 -70
  207. psychopy/layout.py +5 -5
  208. psychopy/logging.py +8 -1
  209. psychopy/microphone.py +10 -37
  210. psychopy/platform_specific/__init__.py +0 -2
  211. psychopy/platform_specific/darwin.py +1 -3
  212. psychopy/platform_specific/linux.py +31 -33
  213. psychopy/platform_specific/win32.py +38 -13
  214. psychopy/plugins/__init__.py +148 -116
  215. psychopy/plugins/util.py +39 -0
  216. psychopy/preferences/Darwin.spec +4 -2
  217. psychopy/preferences/FreeBSD.spec +4 -2
  218. psychopy/preferences/Linux.spec +4 -2
  219. psychopy/preferences/Windows.spec +4 -2
  220. psychopy/preferences/baseNoArch.spec +4 -2
  221. psychopy/preferences/preferences.py +47 -24
  222. psychopy/projects/pavlovia.py +47 -4
  223. psychopy/scripts/psyexpCompile.py +0 -4
  224. psychopy/session.py +153 -21
  225. psychopy/sound/__init__.py +31 -21
  226. psychopy/sound/_base.py +20 -3
  227. psychopy/sound/audioclip.py +320 -33
  228. psychopy/sound/backend_ptb.py +47 -58
  229. psychopy/sound/backend_pygame.py +1 -1
  230. psychopy/sound/backend_pysound.py +6 -15
  231. psychopy/sound/transcribe.py +53 -0
  232. psychopy/tests/.DS_Store +0 -0
  233. psychopy/tests/data/.DS_Store +0 -0
  234. psychopy/tests/data/TestUnknownPluginComponent_load_resave.psyexp +135 -0
  235. psychopy/tests/data/Test_textbox/test_ori_0_bottom right.png +0 -0
  236. psychopy/tests/data/Test_textbox/test_ori_0_center.png +0 -0
  237. psychopy/tests/data/Test_textbox/test_ori_0_top left.png +0 -0
  238. psychopy/tests/data/Test_textbox/test_ori_120_bottom right.png +0 -0
  239. psychopy/tests/data/Test_textbox/test_ori_120_center.png +0 -0
  240. psychopy/tests/data/Test_textbox/test_ori_120_top left.png +0 -0
  241. psychopy/tests/data/Test_textbox/test_ori_180_bottom right.png +0 -0
  242. psychopy/tests/data/Test_textbox/test_ori_180_center.png +0 -0
  243. psychopy/tests/data/Test_textbox/test_ori_180_top left.png +0 -0
  244. psychopy/tests/data/Test_textbox/test_ori_240_bottom right.png +0 -0
  245. psychopy/tests/data/Test_textbox/test_ori_240_center.png +0 -0
  246. psychopy/tests/data/Test_textbox/test_ori_240_top left.png +0 -0
  247. psychopy/tests/data/correctScript/.DS_Store +0 -0
  248. psychopy/tests/data/test_components/testClearKeyboard/testClearKeyboard.psyexp +200 -0
  249. psychopy/tests/data/test_session/.DS_Store +0 -0
  250. psychopy/tests/data/test_session/root/testFutureTrials/testFutureTrials.psyexp +155 -0
  251. psychopy/tests/data/test_session/root/testTrialNav/trialNav.psyexp +158 -0
  252. psychopy/tests/test_app/.DS_Store +0 -0
  253. psychopy/tests/test_app/conftest.py +2 -2
  254. psychopy/tests/test_app/test_speed.py +4 -1
  255. psychopy/tests/test_data/test_TrialHandler2.py +146 -1
  256. psychopy/tests/test_experiment/.DS_Store +0 -0
  257. psychopy/tests/test_experiment/needs_wx/genComponsTemplate.py +3 -3
  258. psychopy/tests/test_experiment/needs_wx/test_components.py +2 -2
  259. psychopy/tests/test_experiment/test_components/test_KeyboardComponent.py +28 -0
  260. psychopy/tests/test_experiment/test_components/test_UnknownPluginComponent.py +27 -0
  261. psychopy/tests/test_experiment/test_components/test_base_components.py +58 -0
  262. psychopy/tests/test_experiment/test_py2js.py +1 -1
  263. psychopy/tests/test_hardware/test_keyboard.py +184 -16
  264. psychopy/tests/test_hardware/test_ports.py +1 -11
  265. psychopy/tests/test_liaison/test_Liaison.py +47 -0
  266. psychopy/tests/test_misc/test_core.py +5 -0
  267. psychopy/tests/test_session/test_Session.py +5 -1
  268. psychopy/tests/test_tools/test_versionchooser.py +39 -8
  269. psychopy/tests/test_visual/test_all_stimuli.py +0 -97
  270. psychopy/tests/test_visual/test_image.py +6 -5
  271. psychopy/tests/test_visual/test_textbox.py +36 -0
  272. psychopy/tests/utils.py +4 -0
  273. psychopy/tools/filetools.py +1 -1
  274. psychopy/tools/pkgtools.py +160 -137
  275. psychopy/tools/versionchooser.py +10 -10
  276. psychopy/tools/wizard.py +3 -3
  277. psychopy/visual/.DS_Store +0 -0
  278. psychopy/visual/backends/pygletbackend.py +24 -13
  279. psychopy/visual/basevisual.py +5 -11
  280. psychopy/visual/button.py +2 -14
  281. psychopy/visual/helpers.py +5 -5
  282. psychopy/visual/line.py +1 -2
  283. psychopy/visual/movie2.py +7 -816
  284. psychopy/visual/movie3.py +7 -589
  285. psychopy/visual/movies/__init__.py +8 -11
  286. psychopy/visual/movies/frame.py +5 -2
  287. psychopy/visual/movies/players/ffpyplayer_player.py +5 -2
  288. psychopy/visual/noise.py +8 -7
  289. psychopy/visual/patch.py +7 -16
  290. psychopy/visual/progress.py +1 -1
  291. psychopy/visual/radial.py +9 -7
  292. psychopy/visual/ratingscale.py +8 -1415
  293. psychopy/visual/secondorder.py +10 -9
  294. psychopy/visual/shape.py +7 -2
  295. psychopy/visual/text.py +1 -1
  296. psychopy/visual/textbox2/textbox2.py +28 -5
  297. psychopy/web.py +5 -2
  298. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/METADATA +8 -13
  299. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/RECORD +313 -219
  300. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/WHEEL +1 -1
  301. psychopy/app/Resources/click.png +0 -0
  302. psychopy/app/Resources/next.png +0 -0
  303. psychopy/experiment/components/patch/__init__.py +0 -121
  304. psychopy/experiment/components/patch/classic/patch.png +0 -0
  305. psychopy/experiment/components/patch/dark/patch.png +0 -0
  306. psychopy/experiment/components/patch/dark/patch@2x.png +0 -0
  307. psychopy/experiment/components/patch/light/patch.png +0 -0
  308. psychopy/experiment/components/patch/light/patch@2x.png +0 -0
  309. psychopy/experiment/components/ratingScale/__init__.py +0 -337
  310. psychopy/experiment/components/ratingScale/classic/ratingscale.png +0 -0
  311. psychopy/experiment/components/ratingScale/classic/ratingscale@2x.png +0 -0
  312. psychopy/experiment/components/ratingScale/dark/ratingScale@2x.png +0 -0
  313. psychopy/experiment/components/ratingScale/dark/ratingscale.png +0 -0
  314. psychopy/experiment/components/ratingScale/light/ratingScale@2x.png +0 -0
  315. psychopy/experiment/components/ratingScale/light/ratingscale.png +0 -0
  316. psychopy/platform_specific/posix.py +0 -16
  317. psychopy/tests/test_sound/test_microphone.py +0 -217
  318. psychopy/tests/test_visual/test_ratingScale.py +0 -299
  319. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@16w.png +0 -0
  320. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@32w.png +0 -0
  321. /psychopy/{app/Resources → assets}/USB-C.png +0 -0
  322. /psychopy/{app/Resources → assets}/USB.png +0 -0
  323. /psychopy/{app/Resources → assets}/creditCard.png +0 -0
  324. /psychopy/{app/Resources → assets}/default.mp3 +0 -0
  325. /psychopy/{app/Resources → assets}/default.mp4 +0 -0
  326. /psychopy/{app/Resources → assets}/default.png +0 -0
  327. /psychopy/{app/Resources → assets/templates}/instruct1.png +0 -0
  328. /psychopy/{app/Resources → assets/templates}/instruct2.png +0 -0
  329. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/entry_points.txt +0 -0
  330. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/AUTHORS.md +0 -0
  331. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/LICENSE +0 -0
psychopy/gui/wxgui.py CHANGED
@@ -13,11 +13,11 @@ import wx
13
13
  import numpy
14
14
  import os
15
15
  from psychopy.localization import _translate
16
- from pkg_resources import parse_version
16
+ from packaging.version import Version
17
17
 
18
18
  OK = wx.ID_OK
19
19
 
20
- thisVer = parse_version(wx.__version__)
20
+ thisVer = Version(wx.__version__)
21
21
 
22
22
  def ensureWxApp():
23
23
  # make sure there's a wxApp prior to showing a gui, e.g., for expInfo
@@ -26,9 +26,9 @@ def ensureWxApp():
26
26
  wx.Dialog(None, -1) # not shown; FileDialog gives same exception
27
27
  return True
28
28
  except wx._core.PyNoAppError:
29
- if thisVer < parse_version('2.9'):
29
+ if thisVer < Version('2.9'):
30
30
  return wx.PySimpleApp()
31
- elif thisVer >= parse_version('4.0') and thisVer < parse_version('4.1'):
31
+ elif thisVer >= Version('4.0') and thisVer < Version('4.1'):
32
32
  raise Exception(
33
33
  "wx>=4.0 clashes with pyglet and making it unsafe "
34
34
  "as a PsychoPy gui helper. Please install PyQt (4 or 5)"
Binary file
@@ -7,7 +7,7 @@ from itertools import chain
7
7
  from psychopy import logging
8
8
  from . import eyetracker, listener
9
9
  from .manager import DeviceManager, deviceManager
10
- from .base import BaseDevice
10
+ from .base import BaseDevice, BaseResponse, BaseResponseDevice
11
11
 
12
12
  try:
13
13
  from collections.abc import Iterable
psychopy/hardware/base.py CHANGED
@@ -14,6 +14,7 @@ __all__ = [
14
14
 
15
15
  import json
16
16
  import inspect
17
+ import time
17
18
 
18
19
 
19
20
  class BaseResponse:
@@ -170,6 +171,17 @@ class BaseResponseDevice(BaseDevice):
170
171
  """
171
172
  pass
172
173
 
174
+ def hasUnfinishedMessage(self):
175
+ """
176
+ If there is a message which have been partially received but not finished (e.g.
177
+ getting the start of a message from a serial device but no end of line character
178
+ yet), this will return True.
179
+
180
+ If not implemented or not relevant on a given device (e.g. Keyboard, which only
181
+ sends full messages), this will always return False.
182
+ """
183
+ return False
184
+
173
185
  def parseMessage(self, message):
174
186
  raise NotImplementedError(
175
187
  "All subclasses of BaseDevice must implement the method `parseMessage`"
@@ -1725,7 +1725,7 @@ class Camera:
1725
1725
 
1726
1726
  # current camera frame since the start of recording
1727
1727
  self._player = None # media player instance
1728
- self._status = NOT_STARTED
1728
+ self.status = NOT_STARTED
1729
1729
  self._isRecording = False
1730
1730
  self._bufferSecs = float(bufferSecs)
1731
1731
  self._lastFrame = None # use None to avoid imports for ImageStim
@@ -1943,20 +1943,6 @@ class Camera:
1943
1943
  """
1944
1944
  return getCameraDescriptions(collapse=collapse)
1945
1945
 
1946
- @property
1947
- def status(self):
1948
- """Status flag for the camera (`int`).
1949
-
1950
- Can be either `RECORDING`, `STOPPED`, `STOPPING`, or `NOT_STARTED`. This
1951
- property used in Builder output scripts and does not update on its own.
1952
-
1953
- """
1954
- return self._status
1955
-
1956
- @status.setter
1957
- def status(self, value):
1958
- self._status = value
1959
-
1960
1946
  @property
1961
1947
  def device(self):
1962
1948
  """Camera to use (`str` or `None`).
@@ -19,18 +19,17 @@ instead (bundled with Standalone PsychoPy)::
19
19
  """
20
20
 
21
21
  import psychopy.logging as logging
22
+ from psychopy.tools.pkgtools import PluginStub
23
+
24
+
25
+ class RB730(
26
+ PluginStub,
27
+ plugin="psychopy-cedrus",
28
+ doclink="https://psychopy.github.io/psychopy-cedrus/coder/RB730"
29
+ ):
30
+ pass
31
+
22
32
 
23
- try:
24
- from psychopy_cedrus import RB730
25
- except (ModuleNotFoundError, ImportError):
26
- logging.error(
27
- "Support for Cedrus Corporation hardware is not available this "
28
- "session. Please install `psychopy-cedrus` and restart the session "
29
- "to enable support.")
30
- except Exception as e:
31
- logging.error(
32
- "Error encountered while loading `psychopy-cedrus`. Check logs for "
33
- "more information.")
34
33
 
35
34
  if __name__ == "__main__":
36
35
  pass
@@ -5,26 +5,17 @@
5
5
  # Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2024 Open Science Tools Ltd.
6
6
  # Distributed under the terms of the GNU General Public License (GPL).
7
7
 
8
- import psychopy.logging as logging
9
-
10
- try:
11
- from psychopy_crs.colorcal import ColorCAL
12
- except (ModuleNotFoundError, ImportError, NameError):
13
- logging.error(
14
- "Support for Cambridge Research Systems ColorCAL is not available this "
15
- "session. Please install `psychopy-crs` and restart the session to "
16
- "enable support.")
17
- except Exception as e:
18
- logging.error(
19
- "Error encountered while loading `psychopy-crs`. Check logs for more "
20
- "information.")
21
- else:
22
- # Monkey-patch our metadata into CRS class if missing required attributes
23
- if not hasattr(ColorCAL, "longName"):
24
- setattr(ColorCAL, "longName", "CRS ColorCAL")
25
-
26
- if not hasattr(ColorCAL, "driverFor"):
27
- setattr(ColorCAL, "driverFor", ["colorcal"])
28
-
29
- if __name__ == "__main__":
8
+
9
+ from psychopy.tools.pkgtools import PluginStub
10
+
11
+
12
+ class ColorCAL(PluginStub, plugin="psychopy-crs", doclink="https://psychopy.github.io/psychopy-crs/coder/ColorCAL"):
30
13
  pass
14
+
15
+
16
+ # Monkey-patch our metadata into CRS class if missing required attributes
17
+ if not hasattr(ColorCAL, "longName"):
18
+ setattr(ColorCAL, "longName", "CRS ColorCAL")
19
+
20
+ if not hasattr(ColorCAL, "driverFor"):
21
+ setattr(ColorCAL, "driverFor", ["colorcal"])
@@ -21,26 +21,16 @@
21
21
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
22
  # THE SOFTWARE.
23
23
 
24
- import psychopy.logging as logging
24
+ from psychopy.tools.pkgtools import PluginStub
25
25
 
26
- try:
27
- from psychopy_crs.optical import OptiCAL
28
- except (ModuleNotFoundError, ImportError):
29
- logging.error(
30
- "Support for Cambridge Research Systems OptiCAL is not available this "
31
- "session. Please install `psychopy-crs` and restart the session to "
32
- "enable support.")
33
- except Exception as e:
34
- logging.error(
35
- "Error encountered while loading `psychopy-crs`. Check logs for more "
36
- "information.")
37
- else:
38
- # Monkey-patch our metadata into CRS class if missing required attributes
39
- if not hasattr(OptiCAL, "longName"):
40
- setattr(OptiCAL, "longName", "CRS OptiCal")
41
26
 
42
- if not hasattr(OptiCAL, "driverFor"):
43
- setattr(OptiCAL, "driverFor", ["optical"])
44
-
45
- if __name__ == "__main__":
27
+ class OptiCAL(PluginStub, plugin="psychopy-crs", doclink="https://psychopy.github.io/psychopy-crs/coder/OptiCAL"):
46
28
  pass
29
+
30
+
31
+ # Monkey-patch our metadata into CRS class if missing required attributes
32
+ if not hasattr(OptiCAL, "longName"):
33
+ setattr(OptiCAL, "longName", "CRS OptiCal")
34
+
35
+ if not hasattr(OptiCAL, "driverFor"):
36
+ setattr(OptiCAL, "driverFor", ["optical"])
@@ -18,20 +18,23 @@ These are optional components that can be obtained by installing the
18
18
 
19
19
  """
20
20
 
21
- import psychopy.logging as logging
22
-
23
- try:
24
- from psychopy_mri_emulator import (
25
- SyncGenerator, ResponseEmulator, launchScan)
26
- except (ModuleNotFoundError, ImportError):
27
- logging.error(
28
- "Support for software fMRI emulation is not available this session. "
29
- "Please install `psychopy-mri-emulator` and restart the session to "
30
- "enable support.")
31
- except Exception as e:
32
- logging.error(
33
- "Error encountered while loading `psychopy-mri-emulator`. Check logs "
34
- "for more information.")
21
+
22
+ from psychopy.tools.pkgtools import PluginStub
23
+
24
+
25
+ class SyncGenerator(
26
+ PluginStub,
27
+ plugin="psychopy-mri-emulator"
28
+ ):
29
+ pass
30
+
31
+
32
+ class ResponseEmulator(
33
+ PluginStub,
34
+ plugin="psychopy-mri-emulator"
35
+ ):
36
+ pass
37
+
35
38
 
36
39
  if __name__ == "__main__":
37
40
  pass
@@ -1,48 +1,46 @@
1
1
  from psychopy.constants import STARTED, NOT_STARTED, PAUSED, STOPPED, FINISHED
2
2
  from psychopy.alerts import alert
3
3
  from psychopy import logging
4
+ from psychopy.iohub.devices import importDeviceModule
4
5
  from psychopy.tools.attributetools import AttributeGetSetMixin
5
6
  from copy import copy
7
+ import importlib
6
8
  import sys
7
9
 
8
10
 
9
11
  class EyetrackerControl(AttributeGetSetMixin):
10
- currentlyRecording = False
11
12
 
12
13
  def __init__(self, tracker, actionType="Start and Stop"):
13
14
  self.tracker = tracker
14
15
  self.actionType = actionType
15
- self._status = NOT_STARTED
16
+ self.status = NOT_STARTED
17
+
18
+ def start(self):
19
+ """
20
+ Start recording
21
+ """
22
+ # if previously at a full stop, clear events
23
+ if not self.tracker.isRecordingEnabled():
24
+ logging.exp("eyetracker.clearEvents()")
25
+ self.tracker.clearEvents()
26
+ # start recording
27
+ self.tracker.setRecordingState(True)
28
+ logging.exp("eyetracker.setRecordingState(True)")
29
+
30
+ def stop(self):
31
+ """
32
+ Stop recording
33
+ """
34
+ self.tracker.setRecordingState(False)
35
+ logging.exp("eyetracker.setRecordingState(False)")
16
36
 
17
37
  @property
18
- def status(self):
19
- return self._status
20
-
21
- @status.setter
22
- def status(self, value):
23
- old = self._status
24
- new = self._status = value
25
- # Skip if there's no change
26
- if new == old:
27
- return
28
- # Start recording if set to STARTED
29
- if new in (STARTED,):
30
- if old in (NOT_STARTED, STOPPED, FINISHED):
31
- # If was previously at a full stop, clear events before starting again
32
- if self.actionType.find('Start') >= 0 and EyetrackerControl.currentlyRecording is False:
33
- logging.exp("eyetracker.clearEvents()")
34
- self.tracker.clearEvents()
35
- # Start recording
36
- if self.actionType.find('Start') >= 0 and not EyetrackerControl.currentlyRecording:
37
- self.tracker.setRecordingState(True)
38
- logging.exp("eyetracker.setRecordingState(True)")
39
- EyetrackerControl.currentlyRecording = True
40
- # Stop recording if set to any stop constants
41
- if new in (NOT_STARTED, PAUSED, STOPPED, FINISHED):
42
- if self.actionType.find('Stop') >= 0 and EyetrackerControl.currentlyRecording:
43
- self.tracker.setRecordingState(False)
44
- logging.exp("eyetracker.setRecordingState(False)")
45
- EyetrackerControl.currentlyRecording = False
38
+ def currentlyRecording(self):
39
+ """
40
+ Check if the eyetracker is currently recording
41
+ added for backwards compatibility, should be removed in future
42
+ """
43
+ return self.tracker.isRecordingEnabled()
46
44
 
47
45
  @property
48
46
  def pos(self):
@@ -84,94 +82,20 @@ class EyetrackerCalibration:
84
82
  def __iter__(self):
85
83
  """Overload dict() method to return in ioHub format"""
86
84
  tracker = self.eyetracker.getIOHubDeviceClass(full=True)
87
-
88
- # Make sure that target will use the same color space and units as calibration
89
- if self.target.colorSpace == self.colorSpace and self.target.units == self.units:
90
- target = self.target
91
- else:
92
- target = copy(self.target)
93
- target.colorSpace = self.colorSpace
94
- target.units = self.units
95
- # Get self as dict
96
- asDict = {}
97
-
98
- textColor = self.textColor
99
- if isinstance(textColor, str) and textColor.lower() == 'auto':
100
- textColor = None
101
-
102
- if tracker == 'eyetracker.hw.sr_research.eyelink.EyeTracker':
103
- # As EyeLink
104
- asDict = {
105
- 'target_attributes': dict(target),
106
- 'type': self.targetLayout,
107
- 'auto_pace': self.progressMode == "time",
108
- 'pacing_speed': self.targetDelay,
109
- 'randomize': self.randomisePos,
110
- 'text_color': textColor,
111
- 'screen_background_color': getattr(self.win._color, self.colorSpace)
112
- }
113
- elif tracker == 'eyetracker.hw.tobii.EyeTracker':
114
- # As Tobii
115
- targetAttrs = dict(target)
116
- targetAttrs['animate'] = {
117
- 'enable': self.movementAnimation,
118
- 'expansion_ratio': self.expandScale,
119
- 'contract_only': self.expandScale == 1
120
- }
121
- asDict = {
122
- 'target_attributes': targetAttrs,
123
- 'type': self.targetLayout,
124
- 'randomize': self.randomisePos,
125
- 'auto_pace': self.progressMode == "time",
126
- 'target_delay': self.targetDelay,
127
- 'target_duration': self.targetDur,
128
- 'unit_type': self.units,
129
- 'color_type': self.colorSpace,
130
- 'text_color': textColor,
131
- 'screen_background_color': getattr(self.win._color, self.colorSpace),
132
- }
133
- elif tracker == 'eyetracker.hw.gazepoint.gp3.EyeTracker':
134
- # As GazePoint
135
- targetAttrs = dict(target)
136
- targetAttrs['animate'] = {
137
- 'enable': self.movementAnimation,
138
- 'expansion_ratio': self.expandScale,
139
- 'contract_only': self.expandScale == 1
140
- }
141
- asDict = {
142
- 'use_builtin': False,
143
- 'target_delay': self.targetDelay,
144
- 'target_duration': self.targetDur,
145
- 'target_attributes': targetAttrs,
146
- 'type': self.targetLayout,
147
- 'randomize': self.randomisePos,
148
- 'unit_type': self.units,
149
- 'color_type': self.colorSpace,
150
- 'text_color': textColor,
151
- 'screen_background_color': getattr(self.win._color, self.colorSpace),
152
- }
153
-
154
- elif tracker == 'eyetracker.hw.mouse.EyeTracker':
155
- # As MouseGaze
156
- targetAttrs = dict(target)
157
- targetAttrs['animate'] = {
158
- 'enable': self.movementAnimation,
159
- 'expansion_ratio': self.expandScale,
160
- 'contract_only': self.expandScale == 1
161
- }
162
- # Run as MouseGaze
163
- asDict = {
164
- 'target_attributes': targetAttrs,
165
- 'type': self.targetLayout,
166
- 'randomize': self.randomisePos,
167
- 'auto_pace': self.progressMode == "time",
168
- 'pacing_speed': self.targetDelay,
169
- 'unit_type': self.units,
170
- 'color_type': self.colorSpace,
171
- 'text_color': textColor,
172
- 'screen_background_color': getattr(self.win._color, self.colorSpace),
173
- }
174
- # Return
85
+ # split into package and class name
86
+ pkgName = ".".join(tracker.split(".")[:-1])
87
+ clsName = tracker.split(".")[-1]
88
+ # make sure pkgName is fully qualified
89
+ if not pkgName.startswith("psychopy.iohub.devices."):
90
+ pkgName = "psychopy.iohub.devices." + pkgName
91
+ # import package
92
+ pkg = importDeviceModule(pkgName)
93
+ # get tracker class
94
+ trackerCls = getattr(pkg, clsName)
95
+ # get self as dict
96
+ asDict = trackerCls.getCalibrationDict(self)
97
+
98
+ # return
175
99
  for key, value in asDict.items():
176
100
  yield key, value
177
101
 
@@ -14,19 +14,8 @@ These are optional components that can be obtained by installing the
14
14
 
15
15
  """
16
16
 
17
- import psychopy.logging as logging
18
-
19
- try:
20
- from psychopy_gammasci import S470
21
- except (ModuleNotFoundError, ImportError):
22
- logging.error(
23
- "Support for Gamma-Scientific Inc. hardware is not available this "
24
- "session. Please install `psychopy-gammasci` and restart the session "
25
- "to enable support.")
26
- except Exception as e:
27
- logging.error(
28
- "Error encountered while loading `psychopy-gammasci`. Check logs for "
29
- "more information.")
30
-
31
- if __name__ == "__main__":
17
+ from psychopy.tools.pkgtools import PluginStub
18
+
19
+
20
+ class S470(PluginStub, plugin="psychopy-gammasci", doclink="https://psychopy.github.io/psychopy-gammasci/coder/S470"):
32
21
  pass
@@ -67,6 +67,7 @@ import psychopy.clock
67
67
  from psychopy import logging
68
68
  from psychopy.constants import NOT_STARTED
69
69
  import time
70
+ import numpy as np
70
71
 
71
72
  from psychopy.hardware.base import BaseResponseDevice, BaseResponse
72
73
  from psychopy.hardware import DeviceManager
@@ -231,6 +232,25 @@ class Keyboard(AttributeGetSetMixin):
231
232
  keyList=keyList, ignoreKeys=ignoreKeys, waitRelease=waitRelease, clear=clear
232
233
  )
233
234
 
235
+ def getState(self, keys):
236
+ """
237
+ Get the current state of a key or set of keys
238
+
239
+ Parameters
240
+ ----------
241
+ keys : str or list[str]
242
+ Either the code for a single key, or a list of key codes.
243
+
244
+ Returns
245
+ -------
246
+ keys : bool or list[bool]
247
+ True if pressed, False if not. Will be a single value if given a
248
+ single key, or a list of bools if given a list of keys.
249
+ """
250
+ return self.device.getState(
251
+ keys=keys
252
+ )
253
+
234
254
  def waitKeys(self, maxWait=float('inf'), keyList=None, waitRelease=True,
235
255
  clear=True):
236
256
  return self.device.waitKeys(
@@ -493,7 +513,7 @@ class KeyboardDevice(BaseResponseDevice, aliases=["keyboard"]):
493
513
  if resp.value in ignoreKeys:
494
514
  wanted = False
495
515
  # if we got this far and the key is still wanted and not present, add it to output
496
- if wanted and resp not in keys:
516
+ if wanted and not any(k is resp for k in keys):
497
517
  keys.append(resp)
498
518
  # if clear=True, mark wanted responses as toClear
499
519
  if wanted and clear:
@@ -504,6 +524,70 @@ class KeyboardDevice(BaseResponseDevice, aliases=["keyboard"]):
504
524
 
505
525
  return keys
506
526
 
527
+ def getState(self, keys):
528
+ """
529
+ Get the current state of a key or set of keys
530
+
531
+ Parameters
532
+ ----------
533
+ keys : str or list[str]
534
+ Either the code for a single key, or a list of key codes.
535
+
536
+ Returns
537
+ -------
538
+ keys : bool or list[bool]
539
+ True if pressed, False if not. Will be a single value if given a
540
+ single key, or a list of bools if given a list of keys.
541
+ """
542
+ # if given a string, convert to a list
543
+ if isinstance(keys, str):
544
+ keys = [keys]
545
+ # start off False
546
+ state = [False] * len(keys)
547
+
548
+ if KeyboardDevice._backend == 'ptb':
549
+ # use ptb.Keyboard.check if backend is ptb
550
+ for buffer in self._buffers.values():
551
+ # get output from ptb
552
+ anyPressed, t, mat = buffer.dev.check()
553
+ # if got any key...
554
+ if mat.any():
555
+ # convert each key index to a key name
556
+ for i in np.where(mat.flatten())[0]:
557
+ # account for ptb's 1-based indexing
558
+ i = int(i) + 1
559
+ # get key name from index (or None if not applicable)
560
+ name = keyNames.get(i, None)
561
+ # check if it's on our list
562
+ if name in keys:
563
+ state[keys.index(name)] = True
564
+ elif KeyboardDevice._backend == 'iohub':
565
+ # get current state of ioHub keyboard
566
+ ioHubState = KeyboardDevice._iohubKeyboard.getCurrentDeviceState()
567
+ # iterate through pressed keys
568
+ for i in ioHubState.get("pressed_keys", {}):
569
+ # iohub returns strings - integerise
570
+ i = int(i)
571
+ # get key name from index (or None if not applicable)
572
+ name = keyNames.get(i, None)
573
+ # check if it's on our list
574
+ if name in keys:
575
+ state[keys.index(name)] = True
576
+ else:
577
+ # make a key state handler
578
+ handler = event.pyglet.window.key.KeyStateHandler()
579
+ # iterate through our list of keys
580
+ for i, key in enumerate(keys):
581
+ # if handler has an entry for the given key, it's pressed
582
+ state[i] = handler[getattr(event.pyglet.window.key, key.upper())]
583
+
584
+ # if state is a single value, remove list wrapper
585
+ if len(state) == 1:
586
+ state = state[0]
587
+
588
+ return state
589
+
590
+
507
591
  def dispatchMessages(self):
508
592
  if KeyboardDevice._backend == 'ptb':
509
593
  for buffer in self._buffers.values():
@@ -521,7 +605,6 @@ class KeyboardDevice(BaseResponseDevice, aliases=["keyboard"]):
521
605
  elif KeyboardDevice._backend == 'iohub':
522
606
  # get events from backend (need to reverse order)
523
607
  key_events = KeyboardDevice._iohubKeyboard.getKeys(clear=True)
524
- key_events.reverse()
525
608
  # parse and receive each event
526
609
  for k in key_events:
527
610
  kpress = self.parseMessage(k)
@@ -552,10 +635,13 @@ class KeyboardDevice(BaseResponseDevice, aliases=["keyboard"]):
552
635
  response = None
553
636
 
554
637
  if KeyboardDevice._backend == 'ptb':
555
- message['time'] -= self.clock.getLastResetTime()
556
638
  if message['down']:
557
639
  # if message is from a key down event, make a new response
558
- response = KeyPress(code=message['keycode'], tDown=message['time'])
640
+ response = KeyPress(
641
+ code=message['keycode'],
642
+ tDown=message['time'] - logging.defaultClock.getLastResetTime()
643
+ )
644
+ response.rt = message['time'] - self.clock.getLastResetTime()
559
645
  self._keysStillDown.append(response)
560
646
  else:
561
647
  # if message is from a key up event, alter existing response
@@ -563,7 +649,7 @@ class KeyboardDevice(BaseResponseDevice, aliases=["keyboard"]):
563
649
  if key.code == message['keycode']:
564
650
  response = key
565
651
  # calculate duration
566
- key.duration = message['time'] - key.tDown
652
+ key.duration = message['time'] - key.tDown - logging.defaultClock.getLastResetTime()
567
653
  # remove key from stillDown
568
654
  self._keysStillDown.remove(key)
569
655
  # stop processing keys as we're done
@@ -573,7 +659,8 @@ class KeyboardDevice(BaseResponseDevice, aliases=["keyboard"]):
573
659
  if message.type == "KEYBOARD_PRESS":
574
660
  # if message is from a key down event, make a new response
575
661
  response = KeyPress(code=message.char, tDown=message.time, name=message.key)
576
- response.rt = response.tDown - self.clock.getLastResetTime()
662
+ response.rt = response.tDown - (
663
+ self.clock.getLastResetTime() - self._iohubKeyboard.clock.getLastResetTime())
577
664
  self._keysStillDown.append(response)
578
665
  else:
579
666
  # if message is from a key up event, alter existing response
@@ -600,9 +687,9 @@ class KeyboardDevice(BaseResponseDevice, aliases=["keyboard"]):
600
687
 
601
688
  def waitKeys(self, maxWait=float('inf'), keyList=None, waitRelease=True,
602
689
  clear=True):
603
- """Same as `~psychopy.hardware.keyboard.Keyboard.getKeys`,
690
+ """Same as `~psychopy.hardware.keyboard.Keyboard.getKeys`,
604
691
  but halts everything (including drawing) while awaiting keyboard input.
605
-
692
+
606
693
  :Parameters:
607
694
  maxWait : any numeric value.
608
695
  Maximum number of seconds period and which keys to wait for.
@@ -622,9 +709,9 @@ class KeyboardDevice(BaseResponseDevice, aliases=["keyboard"]):
622
709
  clear : **True** or False
623
710
  Whether to clear the keyboard event buffer (and discard preceding
624
711
  keypresses) before starting to monitor for new keypresses.
625
-
712
+
626
713
  Returns None if times out.
627
-
714
+
628
715
  """
629
716
  timer = psychopy.clock.Clock()
630
717
 
@@ -643,6 +730,7 @@ class KeyboardDevice(BaseResponseDevice, aliases=["keyboard"]):
643
730
 
644
731
  def clearEvents(self, eventType=None):
645
732
  """Clear the events from the Keyboard such as previous key presses"""
733
+ # clear backend buffers
646
734
  if KeyboardDevice._backend == 'ptb':
647
735
  for buffer in self._buffers.values():
648
736
  buffer.flush() # flush the device events to the soft buffer
@@ -654,6 +742,9 @@ class KeyboardDevice(BaseResponseDevice, aliases=["keyboard"]):
654
742
  else:
655
743
  global event
656
744
  event.clearEvents(eventType)
745
+ # clear dispatched responses
746
+ self.responses = []
747
+
657
748
  logging.info("Keyboard events cleared", obj=self)
658
749
 
659
750
 
@@ -745,7 +836,7 @@ class _KeyBuffer(object):
745
836
  if not keyList and not waitRelease:
746
837
  keyPresses = list(self._keysStillDown)
747
838
  for k in list(self._keys):
748
- if not any(x.name == k.name and x.tDown == k.tDown for x in keyPresses):
839
+ if not any(x.name == k.name and x.tDown == k.tDown for x in keyPresses):
749
840
  keyPresses.append(k)
750
841
  if clear:
751
842
  self._keys = deque()
@@ -135,6 +135,9 @@ class ListenerLoop(threading.Thread):
135
135
  # dispatch messages from devices
136
136
  for device in self.devices:
137
137
  device.dispatchMessages()
138
+ # if there are no more devices attached, stop
139
+ if not len(self.devices):
140
+ self._active = False
138
141
  # sleep for 10ms
139
142
  time.sleep(self.refreshRate)
140
143