Skip to content

Commit

Permalink
Merge branch 'dev' of https://github.com/psychopy/psychopy into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
peircej committed Jun 13, 2023
2 parents 4ecc8d7 + 3720c03 commit 1cff6be
Show file tree
Hide file tree
Showing 26 changed files with 930 additions and 173 deletions.
4 changes: 4 additions & 0 deletions docs/source/api/tools/movietools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ Overview
MovieFileWriter.codec
MovieFileWriter.pixelFormat
MovieFileWriter.encoderLib
MovieFileWriter.encoderOpts
MovieFileWriter.frameRate
MovieFileWriter.frameSize
MovieFileWriter.lastVideoFile
MovieFileWriter.isOpen
MovieFileWriter.framesOut
MovieFileWriter.bytesOut
MovieFileWriter.framesWaiting
MovieFileWriter.totalFrames
MovieFileWriter.frameInterval
MovieFileWriter.duration
MovieFileWriter.open
MovieFileWriter.flush
MovieFileWriter.close
Expand Down
12 changes: 6 additions & 6 deletions psychopy/CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,21 @@ PsychoPy 2023.1

# Release series 2023.1. Highlights

- **Pavlovia Surveys:** can now be integrated into PsychoPy online studies. You still create your
survey in the Pavlovia Survey Creator (on you Pavlovia Dashboard) but you can then incorporate
them as either a link or an embedded json file into your PsychoJS study. This only works for
- **Pavlovia Surveys:** can now be integrated into PsychoPy online studies. You still create your
survey in the Pavlovia Survey Creator (on you Pavlovia Dashboard) but you can then incorporate
them as either a link or an embedded json file into your PsychoJS study. This only works for
online studies at present
- **Panorama Stimuli:** now supported in PsychoPy (lab-based) studies. You can add a photo-sphere
as a stimulus (e.g. create them in Street View app on Android phones) and then insert them into
Builder and explore
- **Plugins and Packages:** You can now add packages to your PsychoPy installation with a GUI that
installs new packages to your user folder (without needing admin rights). Some of PsychoPy's
- **Plugins and Packages:** You can now add packages to your PsychoPy installation with a GUI that
installs new packages to your user folder (without needing admin rights). Some of PsychoPy's
existing functionality has also moved out to "Plugins"

- For users, plugins mean more control over what functionality you want PsychoPy to support
- For developers, plugins mean keeping more independence over your code and ownership

- **Speech Point:** on TextBox allows a tail to be added giving the impression of a cartoon speech
- **Speech Point:** on TextBox allows a tail to be added giving the impression of a cartoon speech
bubble (lab-based only, currently)
- **MovieStim:** further improvements to performance and fixes

Expand Down
134 changes: 88 additions & 46 deletions psychopy/app/builder/builder.py

Large diffs are not rendered by default.

27 changes: 22 additions & 5 deletions psychopy/app/builder/dialogs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .. validators import NameValidator, CodeSnippetValidator, WarningManager
from .dlgsConditions import DlgConditions
from .dlgsCode import DlgCodeComponentProperties, CodeBox
from .findDlg import BuilderFindDlg
from . import paramCtrls
from psychopy import data, logging, exceptions
from psychopy.localization import _translate
Expand Down Expand Up @@ -544,12 +545,13 @@ def updateCodeFont(self, evt=None):

class ParamNotebook(wx.Notebook, handlers.ThemeMixin):
class CategoryPage(wx.Panel, handlers.ThemeMixin):
def __init__(self, parent, dlg, params):
def __init__(self, parent, dlg, params, categ=None):
wx.Panel.__init__(self, parent, size=(600, -1))
self.parent = parent
self.parent = parent
self.dlg = dlg
self.app = self.dlg.app
self.categ = categ
# Setup sizer
self.border = wx.BoxSizer()
self.SetSizer(self.border)
Expand Down Expand Up @@ -732,7 +734,7 @@ def __init__(self, parent, element, experiment):
# Setup pages
self.paramCtrls = {}
for categ, params in paramsByCateg.items():
page = self.CategoryPage(self, self.parent, params)
page = self.CategoryPage(self, self.parent, params, categ=categ)
self.paramCtrls.update(page.ctrls)
# Add page to notebook
self.AddPage(page, _translate(categ))
Expand Down Expand Up @@ -796,6 +798,16 @@ def getParams(self):
del self.params[fieldName]
return self.params

def getCategoryIndex(self, categ):
"""
Get page index for a given category
"""
# iterate through pages by index
for i in range(self.GetPageCount()):
# if this page is the correct category, return current index
if self.GetPage(i).categ == categ:
return i

def _updateStaticUpdates(self, fieldName, updates, newUpdates):
"""If the old/new updates ctrl is using a Static component then we
need to remove/add the component name to the appropriate static
Expand Down Expand Up @@ -825,7 +837,7 @@ def __init__(self, frame, element, experiment,
showAdvanced=False,
size=wx.DefaultSize,
style=_style, editing=False,
timeout=None):
timeout=None, openToPage=None):

# translate title
if "name" in element.params:
Expand Down Expand Up @@ -883,6 +895,10 @@ def __init__(self, frame, element, experiment,

self.ctrls = ParamNotebook(self, element, experiment)
self.paramCtrls = self.ctrls.paramCtrls
# open to page
if openToPage is not None:
i = self.ctrls.getCategoryIndex(openToPage)
self.ctrls.ChangeSelection(i)

self.mainSizer.Add(self.ctrls, # ctrls is the notebook of params
proportion=1, flag=wx.EXPAND | wx.ALL, border=5)
Expand Down Expand Up @@ -1839,13 +1855,14 @@ def __init__(self, frame, element, experiment,
suppressTitles=True, size=wx.DefaultSize,
style=wx.DEFAULT_DIALOG_STYLE | wx.DIALOG_NO_PARENT,
editing=False,
timeout=None, testing=False, type=None):
timeout=None, testing=False, type=None,
openToPage=None):
style = style | wx.RESIZE_BORDER
self.type = type or element.type
_BaseParamsDlg.__init__(self, frame=frame, element=element, experiment=experiment,
size=size,
style=style, editing=editing,
timeout=timeout)
timeout=timeout, openToPage=openToPage)
self.frame = frame
self.app = frame.app
self.dpi = self.app.dpi
Expand Down
7 changes: 5 additions & 2 deletions psychopy/app/builder/dialogs/dlgsCode.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ class DlgCodeComponentProperties(wx.Dialog):
def __init__(self, frame, element, experiment,
helpUrl=None, suppressTitles=True, size=(1000,600),
style=_style, editing=False, depends=[],
timeout=None, type="Code"):
timeout=None, type="Code",
openToPage=None):

# translate title
if "name" in element.params:
Expand Down Expand Up @@ -75,7 +76,6 @@ def __init__(self, frame, element, experiment,
# in FlatNoteBook the tab controls (left,right,close) are ugly on mac
# and also can't be killed

openToPage = None
for paramN, paramName in enumerate(self.order):
param = self.params.get(paramName)
if paramName == 'name':
Expand Down Expand Up @@ -119,6 +119,9 @@ def __init__(self, frame, element, experiment,
_panel.tabN = len(self.tabs)
_panel.app = self.app
self.tabs[tabName] = _panel
# if openToPage refers to this page by name, convert to index
if openToPage == paramName:
openToPage = _panel.tabN

self.codeBoxes[paramName] = CodeBox(_panel, wx.ID_ANY,
pos=wx.DefaultPosition,
Expand Down
189 changes: 189 additions & 0 deletions psychopy/app/builder/dialogs/findDlg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import wx
from psychopy import experiment
from psychopy.app import utils
from psychopy.app.themes import icons
from psychopy.localization import _translate


class BuilderFindDlg(wx.Dialog):
def __init__(self, frame, exp):
self.frame = frame
self.exp = exp

self.results = []

wx.Dialog.__init__(
self,
parent=frame,
title=_translate("Find in experiment..."),
size=(512, 512),
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
)
# setup sizer
self.border = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(self.border)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.border.Add(self.sizer, border=12, proportion=1, flag=wx.EXPAND | wx.ALL)

# create search box
self.termCtrl = wx.SearchCtrl(self)
self.termCtrl.Bind(wx.EVT_SEARCH, self.onSearch)
self.sizer.Add(self.termCtrl, border=6, flag=wx.EXPAND | wx.ALL)

# create results box
self.resultsCtrl = utils.ListCtrl(self, style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
self.resetListCtrl()
self.resultsCtrl.Bind(wx.EVT_LIST_ITEM_SELECTED, self.onSelectResult)
self.resultsCtrl.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.onSelectResult)
self.resultsCtrl.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.onViewResult)
self.sizer.Add(self.resultsCtrl, border=6, proportion=1, flag=wx.EXPAND | wx.ALL)

# setup component icons
self.imageList = wx.ImageList(16, 16)
self.imageMap = {}
for cls in experiment.getAllElements().values():
i = self.imageList.Add(
icons.ComponentIcon(cls, theme="light", size=16).bitmap
)
self.imageMap[cls] = i
self.resultsCtrl.SetImageList(self.imageList, wx.IMAGE_LIST_SMALL)

# add buttons
btnSzr = self.CreateButtonSizer(wx.OK | wx.CANCEL)
self.border.Add(btnSzr, border=12, flag=wx.EXPAND | wx.ALL)
# relabel OK to Go
for child in btnSzr.GetChildren():
if child.Window and child.Window.GetId() == wx.ID_OK:
self.okBtn = child.Window
self.okBtn.SetLabel(_translate("Go"))
self.okBtn.Disable()
# rebind OK to view method
self.okBtn.Bind(wx.EVT_BUTTON, self.onViewResult)

self.Layout()
self.termCtrl.SetFocus()

def resetListCtrl(self):
self.resultsCtrl.ClearAll()
self.resultsCtrl.AppendColumn(_translate("Component"), width=120)
self.resultsCtrl.AppendColumn(_translate("Parameter"), width=120)
self.resultsCtrl.AppendColumn(_translate("Value"), width=-1)
self.resultsCtrl.resizeLastColumn(minWidth=120)
self.selectedResult = None

def onSearch(self, evt):
# get term to search
term = evt.GetString()
if term:
# get locations of term in experiment
self.results = getParamLocations(self.exp, term=term)
else:
# return nothing for blank string
self.results = []
# clear old output
self.resetListCtrl()
# show new output
for result in self.results:
# unpack result
rt, comp, paramName, param = result
# sanitize val for display
val = str(param.val)
if "\n" in val:
# if multiline, show first line with match
for line in val.split("\n"):
if self.termCtrl.GetValue() in line:
val = line
break
# construct entry
entry = [comp.name, param.label, val]
# add entry
self.resultsCtrl.Append(entry)
# set image for comp
self.resultsCtrl.SetItemImage(
item=self.resultsCtrl.GetItemCount()-1,
image=self.imageMap[type(comp)]
)
# size
self.resultsCtrl.Layout()
# disable Go button until item selected
self.okBtn.Disable()

def onSelectResult(self, evt):
if evt.GetEventType() == wx.EVT_LIST_ITEM_SELECTED.typeId:
# if item is selected, store its info
self.selectedResult = self.results[evt.GetIndex()]
# enable Go button
self.okBtn.Enable()
else:
# if no item selected, clear its info
self.selectedResult = None
# disable Go button
self.okBtn.Disable()

evt.Skip()

def onViewResult(self, evt):
# there should be a selected result if this button was enabled, but just in case...
if self.selectedResult is None:
return
# do usual OK button stuff
self.Close()
# unpack
rt, comp, paramName, param = self.selectedResult
# navigate to routine
self.frame.routinePanel.setCurrentRoutine(rt)
# navigate to component & category
page = self.frame.routinePanel.getCurrentPage()
if isinstance(comp, experiment.components.BaseComponent):
# if we have a component, open its dialog and navigate to categ page
if hasattr(comp, 'type') and comp.type.lower() == 'code':
openToPage = paramName
else:
openToPage = param.categ
page.editComponentProperties(component=comp, openToPage=openToPage)
else:
# if we're in a standalone routine, just navigate to categ page
i = page.ctrls.getCategoryIndex(param.categ)
page.ctrls.ChangeSelection(i)


def getParamLocations(exp, term):
"""
Get locations of params containing the given term.
Parameters
----------
term : str
Term to search for
Returns
-------
list
List of tuples, with each tuple functioning as a path to the found
param
"""
# array to store results in
found = []

# go through all routines
for rt in exp.routines.values():
if isinstance(rt, experiment.routines.BaseStandaloneRoutine):
# find in standalone routine
for paramName, param in rt.params.items():
if term in str(param.val):
# append path (routine -> param)
found.append(
(rt, rt, paramName, param)
)
if isinstance(rt, experiment.routines.Routine):
# find in regular routine
for comp in rt:
for paramName, param in comp.params.items():
if term in str(param.val):
# append path (routine -> component -> param)
found.append(
(rt, comp, paramName, param)
)

return found

16 changes: 16 additions & 0 deletions psychopy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import sys, os, copy
from os.path import abspath, join
from types import SimpleNamespace

NOT_STARTED = 0
PLAYING = 1
Expand All @@ -19,6 +20,21 @@
STOPPING = -3
INVALID = -9999


salience = SimpleNamespace()
salience.__doc__ = (
"- CRITICAL (30): Always at the start of the data file, generally reserved for Routine start times\n "
"- HIGH (20): Important columns which are near the front of the data file\n"
"- MEDIUM (10): Possibly important columns which are around the middle of the data file\n"
"- LOW (0): Columns unlikely to be important which are at the end of the data file\n"
"- EXCLUDE (-10): Always at the end of the data file, actively marked as unimportant\n"
)
salience.CRITICAL = SALIENCE_CRITICAL = 30
salience.HIGH = SALIENCE_HIGH = 20
salience.MEDIUM = SALIENCE_MEDIUM = 10
salience.LOW = SALIENCE_LOW = 0
salience.EXCLUDE = SALIENCE_EXCLUDE = -10

# for button box:
PRESSED = 1
RELEASED = -1
Expand Down
9 changes: 0 additions & 9 deletions psychopy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,3 @@ def shellCall(shellCmd, stdin='', stderr=False, env=None, encoding=None):
return stdoutData.strip(), stderrData.strip()
else:
return stdoutData.strip()


class ComponentPlaceholder:
"""
When a component is not implemented, we need an object to represent it when running, which will accept any
attribute given without causing an error and breaking the experiment. This object extends the base Python
`object` class, with no additions.
"""
pass
Loading

0 comments on commit 1cff6be

Please sign in to comment.