Beispiel #1
0
class RunnerPanel(wx.Panel, ScriptProcess, ThemeMixin):
    def __init__(self, parent=None, id=wx.ID_ANY, title='', app=None):
        super(RunnerPanel, self).__init__(
            parent=parent,
            id=id,
            pos=wx.DefaultPosition,
            size=[400, 700],
            style=wx.DEFAULT_FRAME_STYLE,
            name=title,
        )
        ScriptProcess.__init__(self, app)
        self.Bind(wx.EVT_END_PROCESS, self.onProcessEnded)
        #self.SetBackgroundColour(ThemeMixin.appColors['frame_bg'])
        #self.SetForegroundColour(ThemeMixin.appColors['txt_default'])

        # double buffered better rendering except if retina
        self.SetDoubleBuffered(parent.IsDoubleBuffered())

        expCtrlSize = [500, 150]
        ctrlSize = [500, 150]

        self.app = app
        self.prefs = self.app.prefs.coder
        self.paths = self.app.prefs.paths
        self.parent = parent
        self.serverProcess = None

        self.currentFile = None
        self.currentProject = None  # access from self.currentProject property
        self.currentSelection = None
        self.currentExperiment = None

        # Set ListCtrl for list of tasks
        self.expCtrl = wx.ListCtrl(self,
                                   id=wx.ID_ANY,
                                   size=expCtrlSize,
                                   style=wx.LC_REPORT | wx.BORDER_NONE
                                   | wx.LC_NO_HEADER | wx.LC_SINGLE_SEL)

        self.expCtrl.Bind(wx.EVT_LIST_ITEM_SELECTED, self.onItemSelected,
                          self.expCtrl)
        self.expCtrl.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.onItemDeselected,
                          self.expCtrl)
        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.onDoubleClick, self.expCtrl)
        self.expCtrl.InsertColumn(0, 'File')
        self.expCtrl.InsertColumn(1, 'Path')

        _style = platebtn.PB_STYLE_DROPARROW | platebtn.PB_STYLE_SQUARE
        # Alerts
        self._selectedHiddenAlerts = False  # has user manually hidden alerts?
        self.alertsToggleBtn = PsychopyPlateBtn(self,
                                                -1,
                                                'Alerts',
                                                style=_style,
                                                name='Alerts')
        # mouse event must be bound like this
        self.alertsToggleBtn.Bind(wx.EVT_LEFT_DOWN, self.setAlertsVisible)
        # mouse event must be bound like this
        self.alertsToggleBtn.Bind(wx.EVT_RIGHT_DOWN, self.setAlertsVisible)
        self.alertsCtrl = StdOutText(parent=self,
                                     size=ctrlSize,
                                     style=wx.TE_READONLY | wx.TE_MULTILINE
                                     | wx.BORDER_NONE)

        self.setAlertsVisible(True)

        # StdOut
        self.stdoutToggleBtn = PsychopyPlateBtn(self,
                                                -1,
                                                'Stdout',
                                                style=_style,
                                                name='Stdout')
        # mouse event must be bound like this
        self.stdoutToggleBtn.Bind(wx.EVT_LEFT_DOWN, self.setStdoutVisible)
        # mouse event must be bound like this
        self.stdoutToggleBtn.Bind(wx.EVT_RIGHT_DOWN, self.setStdoutVisible)
        self.stdoutCtrl = StdOutText(parent=self,
                                     size=ctrlSize,
                                     style=wx.TE_READONLY | wx.TE_MULTILINE
                                     | wx.BORDER_NONE)
        self.setStdoutVisible(True)

        # Box sizers
        self.upperSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.upperSizer.Add(self.expCtrl, 1, wx.ALL | wx.EXPAND, 5)

        # Set main sizer
        self.mainSizer = wx.BoxSizer(wx.VERTICAL)
        self.mainSizer.Add(self.upperSizer, 0, wx.EXPAND | wx.ALL, 10)
        self.mainSizer.Add(self.alertsToggleBtn, 0, wx.TOP | wx.EXPAND, 10)
        self.mainSizer.Add(self.alertsCtrl, 1, wx.EXPAND | wx.ALL, 10)
        self.mainSizer.Add(self.stdoutToggleBtn, 0, wx.TOP | wx.EXPAND, 10)
        self.mainSizer.Add(self.stdoutCtrl, 1, wx.EXPAND | wx.ALL, 10)

        self.buttonSizer = wx.BoxSizer(wx.VERTICAL)
        self.upperSizer.Add(self.buttonSizer, 0, wx.ALL | wx.EXPAND, 5)
        self.makeButtons()
        self._applyAppTheme()

    def _applyAppTheme(self, target=None):
        if target is None:
            target = self
        ThemeMixin._applyAppTheme(self, self)
        self.alertsCtrl._applyAppTheme()
        self.stdoutCtrl._applyAppTheme()
        ThemeMixin._applyAppTheme(self.expCtrl)
        for btn in self.buttonSizer.GetChildren():
            if btn.Window:
                btn = btn.Window
                btn.SetBackgroundColour(ThemeMixin.appColors['panel_bg'])

    def makeButtons(self):
        # Set buttons
        icons = self.app.iconCache  # type: IconCache
        self.plusBtn = icons.makeBitmapButton(
            parent=self,
            filename='addExp.png',
            name='addExp',
            tip=_translate("Add experiment to list"),
            size=32)
        self.Bind(wx.EVT_BUTTON, self.addTask, self.plusBtn)
        self.negBtn = icons.makeBitmapButton(
            parent=self,
            filename='removeExp.png',
            name='removeExp',
            tip=_translate("Remove experiment from list"),
            size=32)
        self.Bind(wx.EVT_BUTTON, self.removeTask, self.negBtn)
        self.saveBtn = icons.makeBitmapButton(
            parent=self,
            filename='filesaveas.png',
            name='saveTasks',
            tip=_translate("Save task list to a file"),
            size=32)
        self.Bind(wx.EVT_BUTTON, self.parent.saveTaskList, self.saveBtn)
        self.loadBtn = icons.makeBitmapButton(
            parent=self,
            filename='fileopen.png',
            name='loadTasks',
            tip=_translate("Load tasks from a file"),
            size=32)
        self.Bind(wx.EVT_BUTTON, self.parent.loadTaskList, self.loadBtn)
        self.runBtn = icons.makeBitmapButton(
            parent=self,
            filename='run.png',
            name='run',
            tip=_translate("Run the current script in Python"),
            size=32)
        self.Bind(wx.EVT_BUTTON, self.runLocal, self.runBtn)
        self.stopBtn = icons.makeBitmapButton(parent=self,
                                              filename='stop.png',
                                              name='stop',
                                              tip=_translate("Stop task"),
                                              size=32)
        self.Bind(wx.EVT_BUTTON, self.stopTask, self.stopBtn)
        self.onlineBtn = icons.makeBitmapButton(
            parent=self,
            filename='globe.png',
            emblem='run',
            tip=_translate("Run PsychoJS task from Pavlovia"),
            size=32)
        self.Bind(wx.EVT_BUTTON, self.runOnline, self.onlineBtn)
        self.onlineDebugBtn = icons.makeBitmapButton(
            parent=self,
            filename='globe.png',
            name='globe',
            emblem='bug',
            tip=_translate("Run PsychoJS task in local debug mode"),
            size=32)
        self.Bind(wx.EVT_BUTTON, self.runOnlineDebug, self.onlineDebugBtn)
        # Add buttons to sizer
        self.buttonSizer.AddMany([
            (self.plusBtn, 0, wx.ALL | wx.ALIGN_TOP, 5),
            (self.negBtn, 0, wx.ALL | wx.ALIGN_TOP, 5),
            (self.saveBtn, 0, wx.ALL, 5),
            (self.loadBtn, 0, wx.ALL, 5),
            (self.runBtn, 0, wx.ALL, 5),
            (self.stopBtn, 0, wx.ALL, 5),
            (self.onlineBtn, 0, wx.ALL, 5),
            (self.onlineDebugBtn, 0, wx.ALL, 5),
        ])
        self.stopBtn.Disable()
        self.SetSizerAndFit(self.mainSizer)
        self.SetMinSize(self.Size)

    def onProcessEnded(self):
        ScriptProcess.onProcessEnded(self)
        self.stopTask()

    def setAlertsVisible(self, new=True):
        if type(new) == bool:
            self.alertsCtrl.Show(new)
        # or could be an event from button click (a toggle)
        else:
            show = (not self.alertsCtrl.IsShown())
            self.alertsCtrl.Show(show)
            self._selectedHiddenAlerts = not show
        self.Layout()

    def setStdoutVisible(self, new=True):
        # could be a boolean from our own code
        if type(new) == bool:
            self.stdoutCtrl.Show(new)
        # or could be an event (so toggle) from button click
        else:
            self.stdoutCtrl.Show(not self.stdoutCtrl.IsShown())
        self.Layout()

    def stopTask(self, event=None):
        """Kill script processes currently running."""
        # Stop subprocess script running local server
        if self.serverProcess is not None:
            self.serverProcess.kill()
            self.serverProcess = None

        # Stop local Runner processes
        if self.scriptProcess is not None:
            self.stopFile(event)

        self.stopBtn.Disable()
        self.runBtn.Enable()

    def runLocal(self, evt):
        """Run experiment from new process using inherited ScriptProcess class methods."""
        if self.currentSelection is None:
            return

        currentFile = str(self.currentFile)
        if self.currentFile.suffix == '.psyexp':
            generateScript(experimentPath=currentFile.replace(
                '.psyexp', '_lastrun.py'),
                           exp=self.loadExperiment())
        self.runFile(fileName=currentFile)

        # Enable/Disable btns
        self.runBtn.Disable()
        self.stopBtn.Enable()

    def runOnline(self, evt):
        """Run PsychoJS task from https://pavlovia.org."""
        if self.currentProject not in [
                None, "None", ''
        ] and self.currentFile.suffix == '.psyexp':
            webbrowser.open("https://pavlovia.org/run/{}/{}".format(
                self.currentProject, self.outputPath))

    def runOnlineDebug(self, evt, port=12002):
        """
        Open PsychoJS task on local server running from localhost.

        Local debugging is useful before pushing up to Pavlovia.

        Parameters
        ----------
        port: int
            The port number used for the localhost server
        """
        if self.currentSelection is None:
            return

        # we only want one server process open
        if self.serverProcess is not None:
            self.serverProcess.kill()
            self.serverProcess = None

        # Get PsychoJS libs
        self.getPsychoJS()

        htmlPath = str(self.currentFile.parent / self.outputPath)
        server = ["SimpleHTTPServer", "http.server"][PY3]
        pythonExec = Path(sys.executable)
        command = [str(pythonExec), "-m", server, str(port)]

        if not os.path.exists(htmlPath):
            print('##### HTML output path: "{}" does not exist. '
                  'Try exporting your HTML, and try again #####\n'.format(
                      self.outputPath))
            return

        self.serverProcess = Popen(
            command,
            bufsize=1,
            cwd=htmlPath,
            stdout=PIPE,
            stderr=PIPE,
            shell=False,
            universal_newlines=True,
        )

        time.sleep(.1)  # Wait for subprocess to start server
        webbrowser.open("http://localhost:{}".format(port))
        print("##### Local server started! #####\n\n"
              "##### Running PsychoJS task from {} #####\n".format(htmlPath))

    def onURL(self, evt):
        self.parent.onURL(evt)

    def getPsychoJS(self):
        """
        Download and save the current version of the PsychoJS library.

        Useful for debugging, amending scripts.
        """
        libPath = str(self.currentFile.parent / self.outputPath / 'lib')
        ver = '.'.join(self.app.version.split('.')[:2])
        psychoJSLibs = ['core', 'data', 'util', 'visual', 'sound']

        os.path.exists(libPath) or os.makedirs(libPath)

        if len(sorted(Path(libPath).glob('*.js'))) >= len(
                psychoJSLibs):  # PsychoJS lib files exist
            print("##### PsychoJS lib already exists in {} #####\n".format(
                libPath))
            return

        for lib in psychoJSLibs:
            url = "https://lib.pavlovia.org/{}-{}.js".format(lib, ver)
            req = requests.get(url)
            with open(libPath + "/{}-{}.js".format(lib, ver), 'wb') as f:
                f.write(req.content)

        print("##### PsychoJS libs downloaded to {} #####\n".format(libPath))

    def addTask(self, evt=None, fileName=None):
        """
        Add task to the expList listctrl.

        Only adds entry if current entry does not exist in list.
        Can be passed a filename to add to the list.

        Parameters
        ----------
        evt: wx.Event
        fileName: str
            Filename of task to add to list
        """
        if fileName:  # Filename passed from outside runner
            if Path(fileName).suffix not in ['.py', '.psyexp']:
                print(
                    "##### You can only add Python files or psyexp files to the Runner. #####\n"
                )
                return
            filePaths = [fileName]
        else:
            with wx.FileDialog(self,
                               "Open task...",
                               wildcard="*.py; *.psyexp | *.py; *.psyexp",
                               style=wx.FD_MULTIPLE
                               | wx.FD_FILE_MUST_EXIST) as fileDialog:

                if fileDialog.ShowModal() == wx.ID_CANCEL:
                    return  # the user changed their mind

                filePaths = fileDialog.GetPaths()

        for file in filePaths:
            temp = Path(file)

            # Check list for items
            start = -1
            fullPaths = []
            while start < self.expCtrl.GetItemCount() - 1:
                index = self.expCtrl.FindItem(start, temp.name)
                if index > -1:
                    fullPaths += [
                        Path(
                            self.expCtrl.GetItem(index, 1).Text,
                            self.expCtrl.GetItem(index, 0).Text)
                    ]
                    start = index + 1
                else:
                    start = self.expCtrl.GetItemCount()
            if temp in fullPaths:
                continue

            # Set new item in listCtrl
            index = self.expCtrl.InsertItem(self.expCtrl.GetItemCount(),
                                            str(temp.name))
            self.expCtrl.SetItem(index, 1,
                                 str(temp.parent))  # add the folder name

        if filePaths:  # set selection to the final item to be added
            # Set item selection
            # de-select previous
            self.expCtrl.SetItemState(self.currentSelection or 0, 0,
                                      wx.LIST_STATE_SELECTED)
            # select new
            self.expCtrl.Select(index)

        # Set column width
        self.expCtrl.SetColumnWidth(0, wx.LIST_AUTOSIZE)
        self.expCtrl.SetColumnWidth(1, wx.LIST_AUTOSIZE)

    def removeTask(self, evt):
        """Remove experiment entry from the expList listctrl."""
        if self.currentSelection is None:
            self.currentProject = None
            return

        self.expCtrl.DeleteItem(self.currentSelection)
        if self.expCtrl.GetItemCount() == 0:
            self.currentSelection = None
            self.currentFile = None
            self.currentExperiment = None
            self.currentProject = None
        self.app.updateWindowMenu()

    def onItemSelected(self, evt):
        """Set currentSelection to index of currently selected list item."""
        self.currentSelection = evt.Index
        filename = self.expCtrl.GetItem(self.currentSelection, 0).Text
        folder = self.expCtrl.GetItem(self.currentSelection, 1).Text
        self.currentFile = Path(folder, filename)
        self.currentExperiment = self.loadExperiment()
        self.currentProject = None  # until it's needed (slow to update)
        self.runBtn.Enable()
        self.stopBtn.Disable()
        if self.currentFile.suffix == '.psyexp':
            self.onlineBtn.Enable()
            self.onlineDebugBtn.Enable()
        else:
            self.onlineBtn.Disable()
            self.onlineDebugBtn.Disable()
        self.updateAlerts()
        self.app.updateWindowMenu()

    def updateAlerts(self):
        prev = sys.stdout
        # check for alerts
        sys.stdout = sys.stderr = self.alertsCtrl
        self.alertsCtrl.Clear()
        if hasattr(self.currentExperiment, 'integrityCheck'):
            self.currentExperiment.integrityCheck()
            nAlerts = len(self.alertsCtrl.alerts)
        else:
            nAlerts = 0
        # update labels and text accordingly
        self.alertsToggleBtn.SetLabelText("Alerts ({})".format(nAlerts))
        sys.stdout.flush()
        sys.stdout = sys.stderr = prev
        if nAlerts == 0:
            self.setAlertsVisible(False)
        # elif selected hidden then don't touch
        elif not self._selectedHiddenAlerts:
            self.setAlertsVisible(True)

    def onItemDeselected(self, evt):
        """Set currentSelection, currentFile, currentExperiment and currentProject to None."""
        self.expCtrl.SetItemState(self.currentSelection, 0,
                                  wx.LIST_STATE_SELECTED)
        self.currentSelection = None
        self.currentFile = None
        self.currentExperiment = None
        self.currentProject = None
        self.runBtn.Disable()
        self.stopBtn.Disable()
        self.onlineBtn.Disable()
        self.onlineDebugBtn.Disable()
        self.app.updateWindowMenu()

    def onDoubleClick(self, evt):
        self.currentSelection = evt.Index
        filename = self.expCtrl.GetItem(self.currentSelection, 0).Text
        folder = self.expCtrl.GetItem(self.currentSelection, 1).Text
        filepath = os.path.join(folder, filename)
        if filename.endswith('psyexp'):
            # do we have that file already in a frame?
            builderFrames = self.app.getAllFrames("builder")
            for frame in builderFrames:
                if filepath == frame.filename:
                    frame.Show(True)
                    frame.Raise()
                    self.app.SetTopWindow(frame)
                    return  # we're done
            # that file isn't open so look for a blank frame to reuse
            for frame in builderFrames:
                if frame.filename == 'untitled.psyexp' and frame.lastSavedCopy is None:
                    frame.fileOpen(filename=filepath)
                    frame.Show(True)
                    frame.Raise()
                    self.app.SetTopWindow(frame)
            # no reusable frame so make one
            self.app.showBuilder(fileList=[filepath])
        else:
            self.app.showCoder()  # ensures that a coder window exists
            self.app.coder.setCurrentDoc(filepath)

    def onHover(self, evt):
        cs = ThemeMixin.appColors
        btn = evt.GetEventObject()
        btn.SetBackgroundColour(cs['bmpbutton_bg_hover'])
        btn.SetForegroundColour(cs['bmpbutton_fg_hover'])

    def offHover(self, evt):
        cs = ThemeMixin.appColors
        btn = evt.GetEventObject()
        btn.SetBackgroundColour(cs['panel_bg'])
        btn.SetForegroundColour(cs['text'])

    @property
    def outputPath(self):
        """
        Access and return html output path saved in Experiment Settings from the current experiment.

        Returns
        -------
        output path: str
            The output path, relative to parent folder.
        """
        return self.currentExperiment.settings.params['HTML path'].val

    def loadExperiment(self):
        """
        Load the experiment object for the current psyexp file.

        Returns
        -------
        PsychoPy Experiment object
        """
        fileName = str(self.currentFile)
        if not os.path.exists(fileName):
            raise FileNotFoundError("File not found: {}".format(fileName))

        # If not a Builder file, return
        if not fileName.endswith('.psyexp'):
            return None

        # Load experiment file
        exp = experiment.Experiment(prefs=self.app.prefs)
        try:
            exp.loadFromXML(fileName)
        except Exception:
            print(u"Failed to load {}. Please send the following to"
                  u" the PsychoPy user list".format(fileName))
            traceback.print_exc()

        return exp

    @property
    def currentProject(self):
        """Returns the current project, updating from the git repo if no
        project currently found"""
        if not self._currentProject:
            # Check for project
            try:
                project = getProject(str(self.currentFile))
                if hasattr(project, 'id'):
                    self._currentProject = project.id
            except NotADirectoryError as err:
                self.stdoutCtrl.write(err)
        return self._currentProject

    @currentProject.setter
    def currentProject(self, project):
        self._currentProject = None
Beispiel #2
0
    def __init__(self, parent=None, id=wx.ID_ANY, title='', app=None):
        super(RunnerPanel, self).__init__(
            parent=parent,
            id=id,
            pos=wx.DefaultPosition,
            size=[400, 700],
            style=wx.DEFAULT_FRAME_STYLE,
            name=title,
        )
        ScriptProcess.__init__(self, app)
        self.Bind(wx.EVT_END_PROCESS, self.onProcessEnded)
        #self.SetBackgroundColour(ThemeMixin.appColors['frame_bg'])
        #self.SetForegroundColour(ThemeMixin.appColors['txt_default'])

        # double buffered better rendering except if retina
        self.SetDoubleBuffered(parent.IsDoubleBuffered())

        expCtrlSize = [500, 150]
        ctrlSize = [500, 150]

        self.app = app
        self.prefs = self.app.prefs.coder
        self.paths = self.app.prefs.paths
        self.parent = parent
        self.serverProcess = None

        self.currentFile = None
        self.currentProject = None  # access from self.currentProject property
        self.currentSelection = None
        self.currentExperiment = None

        # Set ListCtrl for list of tasks
        self.expCtrl = wx.ListCtrl(self,
                                   id=wx.ID_ANY,
                                   size=expCtrlSize,
                                   style=wx.LC_REPORT | wx.BORDER_NONE
                                   | wx.LC_NO_HEADER | wx.LC_SINGLE_SEL)

        self.expCtrl.Bind(wx.EVT_LIST_ITEM_SELECTED, self.onItemSelected,
                          self.expCtrl)
        self.expCtrl.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.onItemDeselected,
                          self.expCtrl)
        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.onDoubleClick, self.expCtrl)
        self.expCtrl.InsertColumn(0, 'File')
        self.expCtrl.InsertColumn(1, 'Path')

        _style = platebtn.PB_STYLE_DROPARROW | platebtn.PB_STYLE_SQUARE
        # Alerts
        self._selectedHiddenAlerts = False  # has user manually hidden alerts?
        self.alertsToggleBtn = PsychopyPlateBtn(self,
                                                -1,
                                                'Alerts',
                                                style=_style,
                                                name='Alerts')
        # mouse event must be bound like this
        self.alertsToggleBtn.Bind(wx.EVT_LEFT_DOWN, self.setAlertsVisible)
        # mouse event must be bound like this
        self.alertsToggleBtn.Bind(wx.EVT_RIGHT_DOWN, self.setAlertsVisible)
        self.alertsCtrl = StdOutText(parent=self,
                                     size=ctrlSize,
                                     style=wx.TE_READONLY | wx.TE_MULTILINE
                                     | wx.BORDER_NONE)

        self.setAlertsVisible(True)

        # StdOut
        self.stdoutToggleBtn = PsychopyPlateBtn(self,
                                                -1,
                                                'Stdout',
                                                style=_style,
                                                name='Stdout')
        # mouse event must be bound like this
        self.stdoutToggleBtn.Bind(wx.EVT_LEFT_DOWN, self.setStdoutVisible)
        # mouse event must be bound like this
        self.stdoutToggleBtn.Bind(wx.EVT_RIGHT_DOWN, self.setStdoutVisible)
        self.stdoutCtrl = StdOutText(parent=self,
                                     size=ctrlSize,
                                     style=wx.TE_READONLY | wx.TE_MULTILINE
                                     | wx.BORDER_NONE)
        self.setStdoutVisible(True)

        # Box sizers
        self.upperSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.upperSizer.Add(self.expCtrl, 1, wx.ALL | wx.EXPAND, 5)

        # Set main sizer
        self.mainSizer = wx.BoxSizer(wx.VERTICAL)
        self.mainSizer.Add(self.upperSizer, 0, wx.EXPAND | wx.ALL, 10)
        self.mainSizer.Add(self.alertsToggleBtn, 0, wx.TOP | wx.EXPAND, 10)
        self.mainSizer.Add(self.alertsCtrl, 1, wx.EXPAND | wx.ALL, 10)
        self.mainSizer.Add(self.stdoutToggleBtn, 0, wx.TOP | wx.EXPAND, 10)
        self.mainSizer.Add(self.stdoutCtrl, 1, wx.EXPAND | wx.ALL, 10)

        self.buttonSizer = wx.BoxSizer(wx.VERTICAL)
        self.upperSizer.Add(self.buttonSizer, 0, wx.ALL | wx.EXPAND, 5)
        self.makeButtons()
        self._applyAppTheme()
Beispiel #3
0
    def __init__(self, parent=None, id=wx.ID_ANY, title='', app=None):
        super(RunnerPanel, self).__init__(
            parent=parent,
            id=id,
            pos=wx.DefaultPosition,
            size=[400, 700],
            style=wx.DEFAULT_FRAME_STYLE,
            name=title,
        )
        ScriptProcess.__init__(self, app)
        self.Bind(wx.EVT_END_PROCESS, self.onProcessEnded)
        self.SetBackgroundColour(cs['frame_bg'])

        expCtrlSize = [500, 150]
        ctrlSize = [500, 150]

        self.app = app
        self.prefs = self.app.prefs.coder
        self.paths = self.app.prefs.paths
        self.parent = parent
        self.serverProcess = None

        self.currentFile = None
        self.currentProject = None  # access from self.currentProject property
        self.currentSelection = None
        self.currentExperiment = None

        # Set ListCtrl for list of tasks
        self.expCtrl = wx.ListCtrl(self,
                                   id=wx.ID_ANY,
                                   size=expCtrlSize,
                                   style=wx.LC_REPORT | wx.BORDER_NONE
                                   | wx.LC_NO_HEADER)
        self.expCtrl.SetBackgroundColour(cs['tab_active'])
        self.expCtrl.SetForegroundColour(cs['tab_txt'])

        self.expCtrl.Bind(wx.EVT_LIST_ITEM_SELECTED, self.onItemSelected,
                          self.expCtrl)
        self.expCtrl.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.onItemDeselected,
                          self.expCtrl)
        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.onDoubleClick, self.expCtrl)
        self.expCtrl.InsertColumn(0, 'File')
        self.expCtrl.InsertColumn(1, 'Path')

        _style = platebtn.PB_STYLE_DROPARROW | platebtn.PB_STYLE_SQUARE
        # Alerts
        self._selectedHiddenAlerts = False  # has user manually hidden alerts?
        self.alertsToggleBtn = PsychopyPlateBtn(self,
                                                -1,
                                                'Alerts',
                                                style=_style,
                                                name='Alerts')
        # mouse event must be bound like this
        self.alertsToggleBtn.Bind(wx.EVT_LEFT_DOWN, self.setAlertsVisible)
        # mouse event must be bound like this
        self.alertsToggleBtn.Bind(wx.EVT_RIGHT_DOWN, self.setAlertsVisible)
        self.alertsCtrl = StdOutText(parent=self,
                                     size=ctrlSize,
                                     style=wx.TE_READONLY | wx.TE_MULTILINE
                                     | wx.BORDER_NONE)

        self.setAlertsVisible(True)

        # StdOut
        self.stdoutToggleBtn = PsychopyPlateBtn(self,
                                                -1,
                                                'Stdout',
                                                style=_style,
                                                name='Stdout')
        # mouse event must be bound like this
        self.stdoutToggleBtn.Bind(wx.EVT_LEFT_DOWN, self.setStdoutVisible)
        # mouse event must be bound like this
        self.stdoutToggleBtn.Bind(wx.EVT_RIGHT_DOWN, self.setStdoutVisible)
        self.stdoutCtrl = StdOutText(parent=self,
                                     size=ctrlSize,
                                     style=wx.TE_READONLY | wx.TE_MULTILINE
                                     | wx.BORDER_NONE)
        self.setStdoutVisible(True)

        # Set buttons
        plusBtn = self.makeBmpButton(main='addExp32.png')
        negBtn = self.makeBmpButton(main='removeExp32.png')
        self.runBtn = runLocalBtn = self.makeBmpButton(main='run32.png')
        self.stopBtn = stopTaskBtn = self.makeBmpButton(main='stop32.png')
        self.onlineBtn = self.makeBmpButton(main='globe32.png',
                                            emblem='run16.png')
        self.onlineDebugBtn = self.makeBmpButton(main='globe32.png',
                                                 emblem='bug16.png')

        plusBtn.SetToolTip(wx.ToolTip(_translate("Add experiment to list")))
        negBtn.SetToolTip(wx.ToolTip(
            _translate("Remove experiment from list")))
        runLocalBtn.SetToolTip(
            wx.ToolTip(_translate("Run the current script in Python")))
        stopTaskBtn.SetToolTip(wx.ToolTip(_translate("Stop Task")))
        self.onlineBtn.SetToolTip(
            wx.ToolTip(_translate("Run PsychoJS task from Pavlovia")))
        self.onlineDebugBtn.SetToolTip(
            wx.ToolTip(_translate("Run PsychoJS task in local debug mode")))

        # Bind events to buttons
        self.Bind(wx.EVT_BUTTON, self.addTask, plusBtn)
        self.Bind(wx.EVT_BUTTON, self.removeTask, negBtn)
        self.Bind(wx.EVT_BUTTON, self.runLocal, runLocalBtn)
        self.Bind(wx.EVT_BUTTON, self.stopTask, stopTaskBtn)
        self.Bind(wx.EVT_BUTTON, self.runOnline, self.onlineBtn)
        self.Bind(wx.EVT_BUTTON, self.runOnlineDebug, self.onlineDebugBtn)

        # Box sizers
        self.upperSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.buttonSizer = wx.BoxSizer(wx.VERTICAL)

        self.upperSizer.Add(self.expCtrl, 1, wx.ALL | wx.EXPAND, 5)
        self.upperSizer.Add(self.buttonSizer, 0, wx.ALL | wx.EXPAND, 5)
        self.buttonSizer.Add(plusBtn, 0, wx.ALL | wx.ALIGN_TOP, 5)
        self.buttonSizer.Add(negBtn, 0, wx.ALL | wx.ALIGN_TOP, 5)
        self.buttonSizer.AddStretchSpacer()
        self.buttonSizer.AddMany([
            (runLocalBtn, 0, wx.ALL, 5),
            (stopTaskBtn, 0, wx.ALL, 5),
            (self.onlineBtn, 0, wx.ALL, 5),
            (self.onlineDebugBtn, 0, wx.ALL, 5),
        ])

        # Set main sizer
        self.mainSizer = wx.BoxSizer(wx.VERTICAL)
        self.mainSizer.Add(self.upperSizer, 0, wx.EXPAND | wx.ALL, 10)

        self.mainSizer.Add(self.alertsToggleBtn, 0, wx.TOP | wx.EXPAND, 10)
        self.mainSizer.Add(self.alertsCtrl, 1, wx.EXPAND | wx.ALL, 10)
        self.mainSizer.Add(self.stdoutToggleBtn, 0, wx.TOP | wx.EXPAND, 10)
        self.mainSizer.Add(self.stdoutCtrl, 1, wx.EXPAND | wx.ALL, 10)

        self.stopBtn.Disable()

        self.SetSizerAndFit(self.mainSizer)
        self.SetMinSize(self.Size)