/
builder.py
2723 lines (2464 loc) · 111 KB
/
builder.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Defines the behavior of Psychopy's Builder view window
Part of the PsychoPy library
Copyright (C) 2002-2018 Jonathan Peirce (C) 2019 Open Science Tools Ltd.
Distributed under the terms of the GNU General Public License (GPL).
"""
from __future__ import absolute_import, division, print_function
from pkg_resources import parse_version
import wx
import wx.stc
from wx.lib import platebtn, scrolledpanel
try:
from wx import aui
except ImportError:
import wx.lib.agw.aui as aui # some versions of phoenix
try:
from wx.adv import PseudoDC
except ImportError:
from wx import PseudoDC
if parse_version(wx.__version__) < parse_version('4.0.3'):
wx.NewIdRef = wx.NewId
import sys
import os
import glob
import copy
import traceback
import codecs
import numpy
import time
import subprocess
import threading
try:
from queue import Queue, Empty
except ImportError:
from Queue import Queue, Empty # python 2.x
from psychopy.localization import _translate
from ... import experiment
from .. import stdOutRich, dialogs
from ..icons import getAllIcons
from psychopy import logging, constants, __version__
from psychopy.tools.filetools import mergeFolder
from .dialogs import (DlgComponentProperties, DlgExperimentProperties,
DlgCodeComponentProperties)
from .flow import FlowPanel
from ..utils import FileDropTarget, WindowFrozen
from psychopy.experiment import components
from psychopy.app import pavlovia_ui
from psychopy.projects import pavlovia
from psychopy.scripts import psyexpCompile
canvasColor = [200, 200, 200] # in prefs? ;-)
routineTimeColor = wx.Colour(50, 100, 200, 200)
staticTimeColor = wx.Colour(200, 50, 50, 100)
disabledTimeColor = wx.Colour(127, 127, 127, 100)
nonSlipFill = wx.Colour(150, 200, 150, 255)
nonSlipEdge = wx.Colour(0, 100, 0, 255)
relTimeFill = wx.Colour(200, 150, 150, 255)
relTimeEdge = wx.Colour(200, 50, 50, 255)
routineFlowColor = wx.Colour(200, 150, 150, 255)
darkgrey = wx.Colour(65, 65, 65, 255)
white = wx.Colour(255, 255, 255, 255)
darkblue = wx.Colour(30, 30, 150, 255)
codeSyntaxOkay = wx.Colour(220, 250, 220, 255) # light green
# _localized separates internal (functional) from displayed strings
# long form here allows poedit string discovery
_localized = {
'Field': _translate('Field'),
'Default': _translate('Default'),
'Favorites': _translate('Favorites'),
'Stimuli': _translate('Stimuli'),
'Responses': _translate('Responses'),
'Custom': _translate('Custom'),
'I/O': _translate('I/O'),
'Add to favorites': _translate('Add to favorites'),
'Remove from favorites': _translate('Remove from favorites'),
# contextMenuLabels
'edit': _translate('edit'),
'remove': _translate('remove'),
'copy': _translate('copy'),
'move to top': _translate('move to top'),
'move up': _translate('move up'),
'move down': _translate('move down'),
'move to bottom': _translate('move to bottom')}
class OutputThread(threading.Thread):
def __init__(self, proc):
self.proc = proc
threading.Thread.__init__(self)
self.queue = Queue()
self.daemon = True
self.exit = False
def run(self):
"""start the thread"""
running = self.doCheck() # block until process ends
def doCheck(self):
# will do the next line repeatedly until finds EOL
# after checking each line check if we should quit
try:
for line in iter(self.proc.stdout.readline, b''):
# this runs repeatedly
self.queue.put(line)
if not line:
break
except ValueError:
return False
# then check if the process ended
# self.exit
for line in self.proc.stderr.readlines():
self.queue.put(line)
if not line:
break
return True
def getBuffer(self):
"""Retrieve all lines currently in buffer"""
lines = ''
while not self.queue.empty():
lines += self.queue.get_nowait()
return lines
class RoutineCanvas(wx.ScrolledWindow):
"""Represents a single routine (used as page in RoutinesNotebook)"""
def __init__(self, notebook, id=wx.ID_ANY, routine=None):
"""This window is based heavily on the PseudoDC demo of wxPython
"""
wx.ScrolledWindow.__init__(
self, notebook, id, (0, 0), style=wx.SUNKEN_BORDER)
self.SetBackgroundColour(canvasColor)
self.frame = notebook.frame
self.app = self.frame.app
self.dpi = self.app.dpi
self.lines = []
self.maxWidth = 15 * self.dpi
self.maxHeight = 15 * self.dpi
self.x = self.y = 0
self.curLine = []
self.drawing = False
self.drawSize = self.app.prefs.appData['routineSize']
# auto-rescale based on number of components and window size is jumpy
# when switch between routines of diff drawing sizes
self.iconSize = (24, 24, 48)[self.drawSize] # only 24, 48 so far
self.fontBaseSize = (800, 900, 1000)[self.drawSize] # depends on OS?
self.SetVirtualSize((self.maxWidth, self.maxHeight))
self.SetScrollRate(self.dpi / 4, self.dpi / 4)
self.routine = routine
self.yPositions = None
self.yPosTop = (25, 40, 60)[self.drawSize]
# the step in Y between each component
self.componentStep = (25, 32, 50)[self.drawSize]
self.timeXposStart = (150, 150, 200)[self.drawSize]
# the left hand edge of the icons:
_scale = (1.3, 1.5, 1.5)[self.drawSize]
self.iconXpos = self.timeXposStart - self.iconSize * _scale
self.timeXposEnd = self.timeXposStart + 400 # onResize() overrides
# create a PseudoDC to record our drawing
self.pdc = PseudoDC()
self.pen_cache = {}
self.brush_cache = {}
# vars for handling mouse clicks
self.dragid = -1
self.lastpos = (0, 0)
# use the ID of the drawn icon to retrieve component name:
self.componentFromID = {}
self.contextMenuItems = ['copy', 'edit', 'remove',
'move to top', 'move up',
'move down', 'move to bottom']
# labels are only for display, and allow localization
self.contextMenuLabels = {k: _localized[k]
for k in self.contextMenuItems}
self.contextItemFromID = {}
self.contextIDFromItem = {}
for item in self.contextMenuItems:
id = wx.NewIdRef()
self.contextItemFromID[id] = item
self.contextIDFromItem[item] = id
self.redrawRoutine()
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
self.Bind(wx.EVT_SIZE, self.onResize)
# crashes if drop on OSX:
# self.SetDropTarget(FileDropTarget(builder = self.frame))
def onResize(self, event):
self.sizePix = event.GetSize()
self.timeXposStart = (150, 150, 200)[self.drawSize]
self.timeXposEnd = self.sizePix[0] - (60, 80, 100)[self.drawSize]
self.redrawRoutine() # then redraw visible
def ConvertEventCoords(self, event):
xView, yView = self.GetViewStart()
xDelta, yDelta = self.GetScrollPixelsPerUnit()
return (event.GetX() + (xView * xDelta),
event.GetY() + (yView * yDelta))
def OffsetRect(self, r):
"""Offset the rectangle, r, to appear in the given pos in the window
"""
xView, yView = self.GetViewStart()
xDelta, yDelta = self.GetScrollPixelsPerUnit()
r.OffsetXY(-(xView * xDelta), -(yView * yDelta))
def OnMouse(self, event):
if event.LeftDown():
x, y = self.ConvertEventCoords(event)
icons = self.pdc.FindObjectsByBBox(x, y)
if len(icons):
self.editComponentProperties(
component=self.componentFromID[icons[0]])
elif event.RightDown():
x, y = self.ConvertEventCoords(event)
icons = self.pdc.FindObjectsByBBox(x, y)
menuPos = event.GetPosition()
if self.app.prefs.builder['topFlow']:
# width of components panel
menuPos[0] += self.frame.componentButtons.GetSize()[0]
# height of flow panel
menuPos[1] += self.frame.flowPanel.GetSize()[1]
if len(icons):
self._menuComponent = self.componentFromID[icons[0]]
self.showContextMenu(self._menuComponent, xy=menuPos)
elif event.Dragging() or event.LeftUp():
if self.dragid != -1:
pass
if event.LeftUp():
pass
def showContextMenu(self, component, xy):
menu = wx.Menu()
for item in self.contextMenuItems:
id = self.contextIDFromItem[item]
menu.Append(id, self.contextMenuLabels[item])
menu.Bind(wx.EVT_MENU, self.onContextSelect, id=id)
self.frame.PopupMenu(menu, xy)
menu.Destroy() # destroy to avoid mem leak
def onContextSelect(self, event):
"""Perform a given action on the component chosen
"""
op = self.contextItemFromID[event.GetId()]
component = self._menuComponent
r = self.routine
if op == 'edit':
self.editComponentProperties(component=component)
elif op == 'copy':
self.copyCompon(component=component)
elif op == 'remove':
r.removeComponent(component)
self.frame.addToUndoStack(
"REMOVE `%s` from Routine" % (component.params['name'].val))
self.frame.exp.namespace.remove(component.params['name'].val)
elif op.startswith('move'):
lastLoc = r.index(component)
r.remove(component)
if op == 'move to top':
r.insert(0, component)
if op == 'move up':
r.insert(lastLoc - 1, component)
if op == 'move down':
r.insert(lastLoc + 1, component)
if op == 'move to bottom':
r.append(component)
self.frame.addToUndoStack("MOVED `%s`" %
component.params['name'].val)
self.redrawRoutine()
self._menuComponent = None
def OnPaint(self, event):
# Create a buffered paint DC. It will create the real
# wx.PaintDC and then blit the bitmap to it when dc is
# deleted.
dc = wx.GCDC(wx.BufferedPaintDC(self))
# we need to clear the dc BEFORE calling PrepareDC
bg = wx.Brush(self.GetBackgroundColour())
dc.SetBackground(bg)
dc.Clear()
# use PrepareDC to set position correctly
self.PrepareDC(dc)
# create a clipping rect from our position and size
# and the Update Region
xv, yv = self.GetViewStart()
dx, dy = self.GetScrollPixelsPerUnit()
x, y = (xv * dx, yv * dy)
rgn = self.GetUpdateRegion()
rgn.Offset(x, y)
r = rgn.GetBox()
# draw to the dc using the calculated clipping rect
self.pdc.DrawToDCClipped(dc, r)
def redrawRoutine(self):
self.pdc.Clear() # clear the screen
self.pdc.RemoveAll() # clear all objects (icon buttons)
# work out where the component names and icons should be from name
# lengths
self.setFontSize(self.fontBaseSize // self.dpi, self.pdc)
longest = 0
w = 50
for comp in self.routine:
name = comp.params['name'].val
if len(name) > longest:
longest = len(name)
w = self.GetFullTextExtent(name)[0]
self.timeXpos = w + (50, 50, 90)[self.drawSize]
# separate components according to whether they are drawn in separate
# row
rowComponents = []
staticCompons = []
for n, component in enumerate(self.routine):
if component.type == 'Static':
staticCompons.append(component)
else:
rowComponents.append(component)
# draw static, time grid, normal (row) comp:
yPos = self.yPosTop
yPosBottom = yPos + len(rowComponents) * self.componentStep
# draw any Static Components first (below the grid)
for component in staticCompons:
bottom = max(yPosBottom, self.GetSize()[1])
self.drawStatic(self.pdc, component, yPos, bottom)
self.drawTimeGrid(self.pdc, yPos, yPosBottom)
# normal components, one per row
for component in rowComponents:
self.drawComponent(self.pdc, component, yPos)
yPos += self.componentStep
# the 50 allows space for labels below the time axis
self.SetVirtualSize((self.maxWidth, yPos + 50))
self.Refresh() # refresh the visible window after drawing (OnPaint)
def getMaxTime(self):
"""Return the max time to be drawn in the window
"""
maxTime, nonSlip = self.routine.getMaxTime()
if self.routine.hasOnlyStaticComp():
maxTime = int(maxTime) + 1.0
return maxTime
def drawTimeGrid(self, dc, yPosTop, yPosBottom, labelAbove=True):
"""Draws the grid of lines and labels the time axes
"""
tMax = self.getMaxTime() * 1.1
xScale = self.getSecsPerPixel()
xSt = self.timeXposStart
xEnd = self.timeXposEnd
# dc.SetId(wx.NewIdRef())
dc.SetPen(wx.Pen(wx.Colour(0, 0, 0, 150)))
# draw horizontal lines on top and bottom
dc.DrawLine(x1=xSt, y1=yPosTop,
x2=xEnd, y2=yPosTop)
dc.DrawLine(x1=xSt, y1=yPosBottom,
x2=xEnd, y2=yPosBottom)
# draw vertical time points
# gives roughly 1/10 the width, but in rounded to base 10 of
# 0.1,1,10...
unitSize = 10 ** numpy.ceil(numpy.log10(tMax * 0.8)) / 10.0
if tMax / unitSize < 3:
# gives units of 2 (0.2,2,20)
unitSize = 10 ** numpy.ceil(numpy.log10(tMax * 0.8)) / 50.0
elif tMax / unitSize < 6:
# gives units of 5 (0.5,5,50)
unitSize = 10 ** numpy.ceil(numpy.log10(tMax * 0.8)) / 20.0
for lineN in range(int(numpy.floor((tMax / unitSize)))):
# vertical line:
dc.DrawLine(xSt + lineN * unitSize / xScale, yPosTop - 4,
xSt + lineN * unitSize / xScale, yPosBottom + 4)
# label above:
dc.DrawText('%.2g' % (lineN * unitSize), xSt + lineN *
unitSize / xScale - 4, yPosTop - 20)
if yPosBottom > 300:
# if bottom of grid is far away then draw labels here too
dc.DrawText('%.2g' % (lineN * unitSize), xSt + lineN *
unitSize / xScale - 4, yPosBottom + 10)
# add a label
self.setFontSize(self.fontBaseSize // self.dpi, dc)
# y is y-half height of text
dc.DrawText('t (sec)', xEnd + 5,
yPosTop - self.GetFullTextExtent('t')[1] / 2.0)
# or draw bottom labels only if scrolling is turned on, virtual size >
# available size?
if yPosBottom > 300:
# if bottom of grid is far away then draw labels there too
# y is y-half height of text
dc.DrawText('t (sec)', xEnd + 5,
yPosBottom - self.GetFullTextExtent('t')[1] / 2.0)
def setFontSize(self, size, dc):
font = self.GetFont()
font.SetPointSize(size)
dc.SetFont(font)
def drawStatic(self, dc, component, yPosTop, yPosBottom):
"""draw a static (ISI) component box"""
# set an id for the region of this component (so it can
# act as a button). see if we created this already.
id = None
for key in self.componentFromID:
if self.componentFromID[key] == component:
id = key
if not id: # then create one and add to the dict
id = wx.NewIdRef()
self.componentFromID[id] = component
dc.SetId(id)
# deduce start and stop times if possible
startTime, duration, nonSlipSafe = component.getStartAndDuration()
# ensure static comps are clickable (even if $code start or duration)
unknownTiming = False
if startTime is None:
startTime = 0
unknownTiming = True
if duration is None:
duration = 0 # minimal extent ensured below
unknownTiming = True
# calculate rectangle for component
xScale = self.getSecsPerPixel()
dc.SetPen(wx.Pen(wx.Colour(200, 100, 100, 0), style=wx.TRANSPARENT))
if component.params['disabled'].val:
dc.SetBrush(wx.Brush(disabledTimeColor))
else:
dc.SetBrush(wx.Brush(staticTimeColor))
xSt = self.timeXposStart + startTime // xScale
w = duration // xScale + 1 # +1 b/c border alpha=0 in dc.SetPen
w = max(min(w, 10000), 2) # ensure 2..10000 pixels
h = yPosBottom - yPosTop
# name label, position:
name = component.params['name'].val # "ISI"
if unknownTiming:
# flag it as not literally represented in time, e.g., $code
# duration
name += ' ???'
nameW, nameH = self.GetFullTextExtent(name)[0:2]
x = xSt + w // 2
staticLabelTop = (0, 50, 60)[self.drawSize]
y = staticLabelTop - nameH * 3
fullRect = wx.Rect(x - 20, y, nameW, nameH)
# draw the rectangle, draw text on top:
dc.DrawRectangle(xSt, yPosTop - nameH * 4, w, h + nameH * 5)
dc.DrawText(name, x - nameW // 2, y)
# update bounds to include time bar
fullRect.Union(wx.Rect(xSt, yPosTop, w, h))
dc.SetIdBounds(id, fullRect)
def drawComponent(self, dc, component, yPos):
"""Draw the timing of one component on the timeline"""
# set an id for the region of this component (so it
# can act as a button). see if we created this already
id = None
for key in self.componentFromID:
if self.componentFromID[key] == component:
id = key
if not id: # then create one and add to the dict
id = wx.NewIdRef()
self.componentFromID[id] = component
dc.SetId(id)
iconYOffset = (6, 6, 0)[self.drawSize]
componIcons = getAllIcons(self.app.prefs.builder['componentsFolders'])
thisIcon = componIcons[component.getType()]["{}".format(
self.iconSize)] # getType index 0 is main icon
dc.DrawBitmap(thisIcon, self.iconXpos, yPos + iconYOffset, True)
fullRect = wx.Rect(self.iconXpos, yPos,
thisIcon.GetWidth(), thisIcon.GetHeight())
self.setFontSize(self.fontBaseSize // self.dpi, dc)
name = component.params['name'].val
# get size based on text
w, h = self.GetFullTextExtent(name)[0:2]
# draw text
_base = (self.iconSize, self.iconSize, 10)[self.drawSize]
x = self.iconXpos - self.dpi // 10 - w + _base
_adjust = (5, 5, -2)[self.drawSize]
y = yPos + thisIcon.GetHeight() // 2 - h // 2 + _adjust
dc.DrawText(name, x - 20, y)
fullRect.Union(wx.Rect(x - 20, y, w, h))
# deduce start and stop times if possible
startTime, duration, nonSlipSafe = component.getStartAndDuration()
# draw entries on timeline (if they have some time definition)
if startTime is not None and duration is not None:
# then we can draw a sensible time bar!
xScale = self.getSecsPerPixel()
dc.SetPen(wx.Pen(wx.Colour(200, 100, 100, 0),
style=wx.TRANSPARENT))
if component.params['disabled'].val:
dc.SetBrush(wx.Brush(disabledTimeColor))
else:
dc.SetBrush(wx.Brush(routineTimeColor))
hSize = (3.5, 2.75, 2)[self.drawSize]
yOffset = (3, 3, 0)[self.drawSize]
h = self.componentStep // hSize
xSt = self.timeXposStart + startTime // xScale
w = duration // xScale + 1
if w > 10000:
w = 10000 # limit width to 10000 pixels!
if w < 2:
w = 2 # make sure at least one pixel shows
dc.DrawRectangle(xSt, y + yOffset, w, h)
# update bounds to include time bar
fullRect.Union(wx.Rect(xSt, y + yOffset, w, h))
dc.SetIdBounds(id, fullRect)
def copyCompon(self, event=None, component=None):
"""This is easy - just take a copy of the component into memory
"""
self.app.copiedCompon = copy.deepcopy(component)
def pasteCompon(self, event=None, component=None):
if not self.app.copiedCompon:
return -1 # not possible to paste if nothing copied
exp = self.frame.exp
origName = self.app.copiedCompon.params['name'].val
defaultName = exp.namespace.makeValid(origName)
msg = _translate('New name for copy of "%(copied)s"? [%(default)s]')
vals = {'copied': origName, 'default': defaultName}
message = msg % vals
dlg = wx.TextEntryDialog(self, message=message,
caption=_translate('Paste Component'))
if dlg.ShowModal() == wx.ID_OK:
newName = dlg.GetValue()
newCompon = copy.deepcopy(self.app.copiedCompon)
if not newName:
newName = defaultName
newName = exp.namespace.makeValid(newName)
newCompon.params['name'].val = newName
if 'name' in dir(newCompon):
newCompon.name = newName
self.routine.addComponent(newCompon)
# could do redrawRoutines but would be slower?
self.redrawRoutine()
self.frame.addToUndoStack("PASTE Component `%s`" % newName)
dlg.Destroy()
def editComponentProperties(self, event=None, component=None):
# we got here from a wx.button press (rather than our own drawn icons)
if event:
componentName = event.EventObject.GetName()
component = self.routine.getComponentFromName(componentName)
# does this component have a help page?
if hasattr(component, 'url'):
helpUrl = component.url
else:
helpUrl = None
old_name = component.params['name'].val
old_disabled = component.params['disabled'].val
# check current timing settings of component (if it changes we
# need to update views)
initialTimings = component.getStartAndDuration()
# create the dialog
if hasattr(component, 'type') and component.type.lower() == 'code':
_Dlg = DlgCodeComponentProperties
else:
_Dlg = DlgComponentProperties
dlg = _Dlg(frame=self.frame,
title=component.params['name'].val + ' Properties',
params=component.params,
order=component.order, helpUrl=helpUrl, editing=True,
depends=component.depends)
if dlg.OK:
if component.getStartAndDuration() != initialTimings:
self.redrawRoutine() # need to refresh timings section
self.Refresh() # then redraw visible
self.frame.flowPanel.draw()
# self.frame.flowPanel.Refresh()
elif component.params['name'].val != old_name:
self.redrawRoutine() # need to refresh name
elif component.params['disabled'].val != old_disabled:
self.redrawRoutine() # need to refresh color
self.frame.exp.namespace.remove(old_name)
self.frame.exp.namespace.add(component.params['name'].val)
self.frame.addToUndoStack("EDIT `%s`" %
component.params['name'].val)
def getSecsPerPixel(self):
pixels = float(self.timeXposEnd - self.timeXposStart)
return self.getMaxTime() / pixels
class RoutinesNotebook(aui.AuiNotebook):
"""A notebook that stores one or more routines
"""
def __init__(self, frame, id=-1):
self.frame = frame
self.app = frame.app
self.routineMaxSize = 2
self.appData = self.app.prefs.appData
aui.AuiNotebook.__init__(self, frame, id)
self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.onClosePane)
if not hasattr(self.frame, 'exp'):
return # we haven't yet added an exp
def getCurrentRoutine(self):
routinePage = self.getCurrentPage()
if routinePage:
return routinePage.routine # no routine page
return None
def setCurrentRoutine(self, routine):
for ii in range(self.GetPageCount()):
if routine is self.GetPage(ii).routine:
self.SetSelection(ii)
def getCurrentPage(self):
if self.GetSelection() >= 0:
return self.GetPage(self.GetSelection())
return None
def addRoutinePage(self, routineName, routine):
# routinePage = RoutinePage(parent=self, routine=routine)
routinePage = RoutineCanvas(notebook=self, routine=routine)
self.AddPage(routinePage, routineName)
def renameRoutinePage(self, index, newName, ):
self.SetPageText(index, newName)
def removePages(self):
for ii in range(self.GetPageCount()):
currId = self.GetSelection()
self.DeletePage(currId)
def createNewRoutine(self, returnName=False):
msg = _translate("What is the name for the new Routine? "
"(e.g. instr, trial, feedback)")
dlg = wx.TextEntryDialog(self, message=msg,
caption=_translate('New Routine'))
exp = self.frame.exp
routineName = None
if dlg.ShowModal() == wx.ID_OK:
routineName = dlg.GetValue()
# silently auto-adjust the name to be valid, and register in the
# namespace:
routineName = exp.namespace.makeValid(
routineName, prefix='routine')
exp.namespace.add(routineName) # add to the namespace
exp.addRoutine(routineName) # add to the experiment
# then to the notebook:
self.addRoutinePage(routineName, exp.routines[routineName])
self.frame.addToUndoStack("NEW Routine `%s`" % routineName)
dlg.Destroy()
if returnName:
return routineName
def onClosePane(self, event=None):
"""Close the pane and remove the routine from the exp
"""
routine = self.GetPage(event.GetSelection()).routine
name = routine.name
# update experiment object, namespace, and flow window (if this is
# being used)
if name in self.frame.exp.routines:
# remove names of the routine and its components from namespace
_nsp = self.frame.exp.namespace
for c in self.frame.exp.routines[name]:
_nsp.remove(c.params['name'].val)
_nsp.remove(self.frame.exp.routines[name].name)
del self.frame.exp.routines[name]
if routine in self.frame.exp.flow:
self.frame.exp.flow.removeComponent(routine)
self.frame.flowPanel.draw()
self.frame.addToUndoStack("REMOVE Routine `%s`" % (name))
def increaseSize(self, event=None):
self.appData['routineSize'] = min(
self.routineMaxSize, self.appData['routineSize'] + 1)
with WindowFrozen(self):
self.redrawRoutines()
def decreaseSize(self, event=None):
self.appData['routineSize'] = max(0, self.appData['routineSize'] - 1)
with WindowFrozen(self):
self.redrawRoutines()
def redrawRoutines(self):
"""Removes all the routines, adds them back (alphabetical order),
sets current back to orig
"""
currPage = self.GetSelection()
self.removePages()
displayOrder = sorted(self.frame.exp.routines.keys()) # alphabetical
for routineName in displayOrder:
self.addRoutinePage(
routineName, self.frame.exp.routines[routineName])
if currPage > -1:
self.SetSelection(currPage)
class ComponentsPanel(scrolledpanel.ScrolledPanel):
def __init__(self, frame, id=-1):
"""A panel that displays available components.
"""
self.frame = frame
self.app = frame.app
self.dpi = self.app.dpi
if self.app.prefs.app['largeIcons']:
panelWidth = 3 * 48 + 50
else:
panelWidth = 3 * 24 + 50
scrolledpanel.ScrolledPanel.__init__(
self, frame, id, size=(panelWidth, 10 * self.dpi))
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.components = experiment.getAllComponents(
self.app.prefs.builder['componentsFolders'])
categories = ['Favorites']
categories.extend(components.getAllCategories(
self.app.prefs.builder['componentsFolders']))
# get rid of hidden components
for hiddenComp in self.frame.prefs['hiddenComponents']:
if hiddenComp in self.components:
del self.components[hiddenComp]
# also remove settings - that's in toolbar not components panel
del self.components['SettingsComponent']
# get favorites
self.favorites = FavoriteComponents(componentsPanel=self)
# create labels and sizers for each category
self.componentFromID = {}
self.panels = {}
# to keep track of the objects (sections and section labels)
# within the main sizer
self.sizerList = []
for categ in categories:
if categ in _localized:
label = _localized[categ]
else:
label = categ
_style = platebtn.PB_STYLE_DROPARROW
sectionBtn = platebtn.PlateButton(self, -1, label,
style=_style, name=categ)
# mouse event must be bound like this
sectionBtn.Bind(wx.EVT_LEFT_DOWN, self.onSectionBtn)
# mouse event must be bound like this
sectionBtn.Bind(wx.EVT_RIGHT_DOWN, self.onSectionBtn)
if self.app.prefs.app['largeIcons']:
self.panels[categ] = wx.FlexGridSizer(cols=1)
else:
self.panels[categ] = wx.FlexGridSizer(cols=2)
self.sizer.Add(sectionBtn, flag=wx.EXPAND)
self.sizerList.append(sectionBtn)
self.sizer.Add(self.panels[categ], flag=wx.ALIGN_CENTER)
self.sizerList.append(self.panels[categ])
self.makeComponentButtons()
self._rightClicked = None
# start all except for Favorites collapsed
for section in categories[1:]:
self.toggleSection(self.panels[section])
self.Bind(wx.EVT_SIZE, self.on_resize)
self.SetSizer(self.sizer)
self.SetAutoLayout(True)
self.SetupScrolling()
def on_resize(self, event):
if self.app.prefs.app['largeIcons']:
cols = self.GetClientSize()[0] // 58
else:
cols = self.GetClientSize()[0] // 34
for category in list(self.panels.values()):
category.SetCols(max(1, cols))
def makeFavoriteButtons(self):
# add a copy of each favorite to that panel first
for thisName in self.favorites.getFavorites():
self.addComponentButton(thisName, self.panels['Favorites'])
def makeComponentButtons(self):
"""Make all the components buttons, including favorites
"""
self.makeFavoriteButtons()
# then add another copy for each category that the component itself
# lists
componentNames = list(self.components.keys())
componentNames.sort()
for thisName in componentNames:
thisComp = self.components[thisName]
# NB thisComp is a class - we can't use its methods/attribs until
# it is an instance
for category in thisComp.categories:
panel = self.panels[category]
self.addComponentButton(thisName, panel)
def addComponentButton(self, name, panel):
"""Create a component button and add it to a specific panel's sizer
"""
componIcons = getAllIcons(self.app.prefs.builder['componentsFolders'])
thisComp = self.components[name]
shortName = name
for redundant in ['component', 'Component']:
if redundant in name:
shortName = name.replace(redundant, "")
if self.app.prefs.app['largeIcons']:
thisIcon = componIcons[name][
'48add'] # index 1 is the 'add' icon
else:
thisIcon = componIcons[name][
'24add'] # index 1 is the 'add' icon
btn = wx.BitmapButton(self, -1, thisIcon,
size=(thisIcon.GetWidth() + 10,
thisIcon.GetHeight() + 10),
name=thisComp.__name__)
if name in components.tooltips:
thisTip = components.tooltips[name]
else:
thisTip = shortName
btn.SetToolTip(wx.ToolTip(thisTip))
self.componentFromID[btn.GetId()] = name
# use btn.bind instead of self.Bind in oder to trap event here
btn.Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)
self.Bind(wx.EVT_BUTTON, self.onClick, btn)
# ,wx.EXPAND|wx.ALIGN_CENTER )
panel.Add(btn, proportion=0, flag=wx.ALIGN_RIGHT)
def onSectionBtn(self, evt):
if hasattr(evt, 'GetString'):
buttons = self.panels[evt.GetString()]
else:
btn = evt.GetEventObject()
buttons = self.panels[btn.GetName()]
self.toggleSection(buttons)
def toggleSection(self, section):
ii = self.sizerList.index(section)
self.sizer.Show(ii, not self.sizer.IsShown(ii)) # ie toggle this item
self.sizer.Layout()
self.SetupScrolling()
def getIndexInSizer(self, obj, sizer):
"""Find index of an item within a sizer (to see if it's there
or to toggle visibility)
WX sizers don't (as of v2.8.11) have a way to find the index of
their contents. This method helps get around that.
"""
# if the obj is itself a sizer (e.g. within the main sizer then
# we can't even use sizer.Children (as far as I can work out)
# so we keep a list to track the contents.
# for the main sizer we kept track of everything with a list:
if sizer == self.sizer:
return self.sizerList.index(obj)
index = None
for ii, child in enumerate(sizer.Children):
if child.GetWindow() == obj:
index = ii
break
return index
def onRightClick(self, evt):
"""
Defines rightclick behavior within builder view's
components panel
"""
btn = evt.GetEventObject()
self._rightClicked = btn
index = self.getIndexInSizer(btn, self.panels['Favorites'])
if index is None:
# not currently in favs
msg = "Add to favorites"
clickFunc = self.onAddToFavorites
else:
# is currently in favs
msg = "Remove from favorites"
clickFunc = self.onRemFromFavorites
menu = wx.Menu()
id = wx.NewIdRef()
menu.Append(id, _localized[msg])
menu.Bind(wx.EVT_MENU, clickFunc, id=id)
# where to put the context menu
x, y = evt.GetPosition() # this is position relative to object
xBtn, yBtn = evt.GetEventObject().GetPosition()
self.PopupMenu(menu, (x + xBtn, y + yBtn))
menu.Destroy() # destroy to avoid mem leak
def onClick(self, evt, timeout=None):
"""
Defines left-click behavior for builder views components panel
:param: evt can be a wx.Event OR a component class name (MouseComponent)
"""
# get name of current routine
currRoutinePage = self.frame.routinePanel.getCurrentPage()
if not currRoutinePage:
msg = _translate("Create a routine (Experiment menu) "
"before adding components")
dialogs.MessageDialog(self, msg, type='Info',
title=_translate('Error')).ShowModal()
return False
currRoutine = self.frame.routinePanel.getCurrentRoutine()
# get component name
if hasattr(evt, "GetId"):
newClassStr = self.componentFromID[evt.GetId()]
else:
newClassStr = evt
newCompClass = self.components[newClassStr]
newComp = newCompClass(parentName=currRoutine.name,
exp=self.frame.exp)
# does this component have a help page?
if hasattr(newComp, 'url'):
helpUrl = newComp.url
else:
helpUrl = None
# create component template
if newClassStr == 'CodeComponent':
_Dlg = DlgCodeComponentProperties
else:
_Dlg = DlgComponentProperties
dlg = _Dlg(frame=self.frame,
title='{} Properties'.format(newComp.params['name']),
params=newComp.params, order=newComp.order,
helpUrl=helpUrl,
depends=newComp.depends,
timeout=timeout)
compName = newComp.params['name']
if dlg.OK:
currRoutine.addComponent(newComp) # add to the actual routing
namespace = self.frame.exp.namespace
newComp.params['name'].val = namespace.makeValid(
newComp.params['name'].val)
namespace.add(newComp.params['name'].val)
# update the routine's view with the new component too
currRoutinePage.redrawRoutine()
self.frame.addToUndoStack(
"ADD `%s` to `%s`" % (compName, currRoutine.name))
wasNotInFavs = (newClassStr not in self.favorites.getFavorites())
self.favorites.promoteComponent(newClassStr, 1)
# was that promotion enough to be a favorite?
if wasNotInFavs and newClassStr in self.favorites.getFavorites():
self.addComponentButton(newClassStr, self.panels['Favorites'])
self.sizer.Layout()
return True
def onAddToFavorites(self, evt=None, btn=None):
"""Defines Add To Favorites Menu Behavior"""
if btn is None:
btn = self._rightClicked
if btn.Name not in self.favorites.getFavorites():
# check we aren't duplicating
self.favorites.makeFavorite(btn.Name)
self.addComponentButton(btn.Name, self.panels['Favorites'])
self.sizer.Layout()
self._rightClicked = None
def onRemFromFavorites(self, evt=None, btn=None):
"""Defines Remove from Favorites Menu Behavior"""
if btn is None:
btn = self._rightClicked
index = self.getIndexInSizer(btn, self.panels['Favorites'])
if index is None:
pass
else:
self.favorites.setLevel(btn.Name, -100)
btn.Destroy()
self.sizer.Layout()
self._rightClicked = None
class FavoriteComponents(object):
"""Defines the Favorite Components Object class, meant for dealing with
the user's frequently accessed components"""
def __init__(self, componentsPanel, threshold=20, neutral=0):
super(FavoriteComponents, self).__init__()
self.threshold = 20
self.neutral = 0
self.panel = componentsPanel
self.frame = componentsPanel.frame
self.app = self.frame.app