def add_editor_control(self, var, label, syntax=False): """ Adds a QProgEdit that is linked to a variable. arguments: var: The associated variable. label: The label text. keywords: syntax: A boolean indicating whether Python syntax highlighting should be activated. returns: A QProgEdit widget. """ from QProgEdit import QTabManager if syntax: lang = u'python' else: lang = u'text' qprogedit = QTabManager(cfg=cfg) qprogedit.focusLost.connect(self.apply_edit_changes) qprogedit.handlerButtonClicked.connect(self.apply_edit_changes) qprogedit.addTab(label).setLang(lang) if var is not None: self.auto_editor[var] = qprogedit self.edit_vbox.addWidget(qprogedit) self.set_focus_widget(qprogedit) return qprogedit
def add_editor_control(self, var, label, syntax=False, tooltip=None, \ default=None): """ Adds a QProgEdit that is linked to a variable. Arguments: var -- Name of the associated variable. label -- Label text. Keyword arguments: syntax -- A boolean indicating whether Python syntax highlighting should be activated. (default=False) tooltip -- A tooltip text. (default=None) default -- DEPRECATED Returns: A QProgEdit widget. """ from QProgEdit import QTabManager if syntax: lang = u'python' else: lang = u'text' qprogedit = QTabManager(cfg=cfg) qprogedit.focusLost.connect(self.apply_edit_changes) qprogedit.handlerButtonClicked.connect(self.apply_edit_changes) qprogedit.addTab(_(label, context=self.name)).setLang(lang) if var is not None: self.auto_editor[var] = qprogedit self.edit_vbox.addWidget(qprogedit) self.set_focus_widget(qprogedit) return qprogedit
def init_script_widget(self): """Build the script tab""" from QProgEdit import QTabManager self.script_qprogedit = QTabManager(handler= \ self.apply_script_and_close, defaultLang=u'OpenSesame', \ handlerButtonText=_(u'Apply and close script editor'), \ focusOutHandler=self.apply_script_changes, cfg=cfg) self.script_qprogedit.addTab(u'Script') hbox = QtGui.QHBoxLayout() hbox.addWidget(self.experiment.label_image(self.item_type)) self.script_header = QtGui.QLabel() hbox.addWidget(self.script_header) hbox.addStretch() hbox.setContentsMargins(0, 0, 0, 0) hwidget = QtGui.QWidget() hwidget.setLayout(hbox) vbox = QtGui.QVBoxLayout() vbox.addWidget(hwidget) vbox.addWidget(self.script_qprogedit) self._script_widget = QtGui.QWidget() self._script_widget.setLayout(vbox) self._script_widget.__script_item__ = self.name
def main(): """Runs a simple QProgEdit demonstration.""" print(u'API: %s' % os.environ[u'QT_API']) validate.addPythonBuiltins([u'builtin_var']) validate.setPyFlakesFilter(lambda msg: msg.message == u'undefined name %r') app = QtWidgets.QApplication(sys.argv) treeWidgetItem1 = QtWidgets.QTreeWidgetItem([u'Tab 1']) treeWidgetItem3 = QtWidgets.QTreeWidgetItem([u'Tab 3']) symbolTree = QtWidgets.QTreeWidget() symbolTree.addTopLevelItem(treeWidgetItem1) symbolTree.addTopLevelItem(treeWidgetItem3) symbolTree.itemActivated.connect(activateSymbolTree) tabManager = QTabManager(handlerButtonText=u'apply', runButton=True) tabManager.setWindowIcon(QtGui.QIcon.fromTheme(u'accessories-text-editor')) tabManager.setWindowTitle(u'QProgEdit') tabManager.resize(800, 600) tabManager.cursorRowChanged.connect(cursorRowChanged) tabManager.focusLost.connect(focusLost) tabManager.focusReceived.connect(focusReceived) tabManager.handlerButtonClicked.connect(handlerButtonClicked) tabManager.execute.connect(runSelectedText) tab = tabManager.addTab(u'Tab 1') tab.setLang(u'Python') tab.setSymbolTree(treeWidgetItem1) tab.setText(open(__file__).read()) tab = tabManager.addTab(u'Tab 2') tab.setText(u'Some plain text') tab = tabManager.addTab(u'Tab 3') tab.setLang(u'Python') tab.setSymbolTree(treeWidgetItem3) if os.path.exists(u'content.txt'): tab.setText(open(u'content.txt').read()) layout = QtWidgets.QHBoxLayout() layout.addWidget(symbolTree) layout.addWidget(tabManager) container = QtWidgets.QWidget() container.setLayout(layout) container.show() res = app.exec_() open(u'content.txt', u'w').write(tab.text()) sys.exit(res)
def main(): """Runs a simple QProgEdit demonstration.""" validate.addPythonBuiltins(['builtin_var']) app = QtGui.QApplication(sys.argv) treeWidgetItem1 = QtGui.QTreeWidgetItem([u'Tab 1']) treeWidgetItem3 = QtGui.QTreeWidgetItem([u'Tab 3']) symbolTree = QtGui.QTreeWidget() symbolTree.addTopLevelItem(treeWidgetItem1) symbolTree.addTopLevelItem(treeWidgetItem3) symbolTree.itemActivated.connect(activateSymbolTree) tabManager = QTabManager(handlerButtonText=u'apply') tabManager.setWindowIcon(QtGui.QIcon.fromTheme(u'accessories-text-editor')) tabManager.setWindowTitle(u'QProgEdit') tabManager.resize(800, 600) tabManager.cursorRowChanged.connect(cursorRowChanged) tabManager.focusLost.connect(focusLost) tabManager.focusReceived.connect(focusReceived) tabManager.handlerButtonClicked.connect(handlerButtonClicked) tab = tabManager.addTab(u'Tab 1') tab.setLang(u'Python') tab.setSymbolTree(treeWidgetItem1) tab.setText(open(__file__).read()) print tab.symbols() tab = tabManager.addTab(u'Tab 2') tab.setText(u'Some plain text') tab = tabManager.addTab(u'Tab 3') tab.setLang(u'Python') tab.setSymbolTree(treeWidgetItem3) if os.path.exists(u'content.txt'): tab.setText(open(u'content.txt').read().decode(u'utf-8')) print tab.symbols() layout = QtGui.QHBoxLayout() layout.addWidget(symbolTree) layout.addWidget(tabManager) container = QtGui.QWidget() container.setLayout(layout) container.show() res = app.exec_() open(u'content.txt', u'w').write(tab.text().encode(u'utf-8')) sys.exit(res)
def init_script_widget(self): """Build the script tab""" from QProgEdit import QTabManager self.script_qprogedit = QTabManager(handler=self.apply_script_and_close, \ defaultLang=u'OpenSesame', handlerButtonText= \ _(u'Apply and close script editor'), callHandlerOnFocusOut=False, cfg=cfg) self.script_qprogedit.addTab(u'Script') hbox = QtGui.QHBoxLayout() hbox.addWidget(self.experiment.label_image(u"%s" % self.item_type)) self.script_header = QtGui.QLabel() hbox.addWidget(self.script_header) hbox.addStretch() hbox.setContentsMargins(0,0,0,0) hwidget = QtGui.QWidget() hwidget.setLayout(hbox) vbox = QtGui.QVBoxLayout() vbox.addWidget(hwidget) vbox.addWidget(self.script_qprogedit) self._script_widget = QtGui.QWidget() self._script_widget.setLayout(vbox) self._script_widget.__script_item__ = self.name
def init_edit_widget(self): """Constructs the GUI controls.""" from QProgEdit import QTabManager qtitem.qtitem.init_edit_widget(self, False) self.qprogedit = QTabManager(handler=self.apply_edit_changes, \ defaultLang=u'Python', cfg=cfg, focusOutHandler= \ self.apply_edit_changes) self.qprogedit.addTab(u'Prepare') self.qprogedit.addTab(u'Run') # Switch to the run phase, unless there is only content for the prepare # phase. if self._run == u'' and self._prepare != u'': self.qprogedit.setCurrentIndex(0) else: self.qprogedit.setCurrentIndex(1) self.edit_vbox.addWidget(self.qprogedit)
def init_edit_widget(self): """See qtitem.""" from QProgEdit import QTabManager super(inline_script, self).init_edit_widget(stretch=False) self.qprogedit = QTabManager(cfg=cfg) self.qprogedit.handlerButtonClicked.connect(self.apply_edit_changes) self.qprogedit.focusLost.connect(self.apply_edit_changes) self.qprogedit.cursorRowChanged.connect(self.apply_edit_changes) self.qprogedit.addTab(u'Prepare').setLang(u'Python') self.qprogedit.addTab(u'Run').setLang(u'Python') # Switch to the run phase, unless there is only content for the prepare # phase. if self._run == u'' and self._prepare != u'': self.qprogedit.setCurrentIndex(0) else: self.qprogedit.setCurrentIndex(1) self.edit_vbox.addWidget(self.qprogedit)
def init_edit_widget(self): """See qtitem.""" from QProgEdit import QTabManager qtplugin.init_edit_widget(self, stretch=False) self.qprogedit = QTabManager(cfg=cfg, runButton=True) self.qprogedit.execute.connect(self.main_window.console.execute) self.qprogedit.handlerButtonClicked.connect(self.apply_edit_changes) self.qprogedit.focusLost.connect(self.apply_edit_changes) self.qprogedit.cursorRowChanged.connect(self.apply_edit_changes) self.qprogedit.addTab(_(u'Prepare')).setLang(u'Python') self.qprogedit.addTab(_(u'Run')).setLang(u'Python') # Switch to the run phase, unless there is only content for the prepare # phase. if self.var._run == u'' and self.var._prepare != u'': self.qprogedit.setCurrentIndex(0) else: self.qprogedit.setCurrentIndex(1) self.edit_vbox.addWidget(self.qprogedit)
def settings_widget(self): """ desc: returns the QProgEdit settings widget. returns: type: QWidget """ # Create a temporary QProgEdit instance, add one tab, and wrap the # preferences widget for this tab in a group box. from QProgEdit import QTabManager tm = QTabManager(cfg=cfg) tm.addTab() w = tm.tab(0).prefs w.show() w.refresh() group = QtGui.QGroupBox(_(u'Editor preferences (QProgEdit)'), self.main_window) layout = QtGui.QHBoxLayout(group) layout.addWidget(w) group.setLayout(layout) return group
def init_edit_widget(self): """Constructs the GUI controls.""" from QProgEdit import QTabManager qtitem.qtitem.init_edit_widget(self, False) self.qprogedit = QTabManager(handler=self.apply_edit_changes, \ defaultLang=u'Python', cfg=cfg) self.qprogedit.addTab(u'Prepare') self.qprogedit.addTab(u'Run') # Switch to the run phase, unless there is only content for the prepare # phase. if self._run == u'' and self._prepare != u'': self.qprogedit.setCurrentIndex(0) else: self.qprogedit.setCurrentIndex(1) self.edit_vbox.addWidget(self.qprogedit)
def __init__(self, main_window): """ desc: Constructor. arguments: main_window: A qtopensesame object. """ from QProgEdit import QTabManager super(general_script_editor, self).__init__(main_window, ui=u'widgets.general_script_editor') self.ui.qprogedit = QTabManager(handlerButtonText=u'Apply', cfg=cfg) self.ui.qprogedit.handlerButtonClicked.connect(self._apply) self.ui.qprogedit.addTab(u'General script').setLang(u'OpenSesame') self.ui.layout_vbox.addWidget(self.ui.qprogedit) self.tab_name = u'__general_script__'
def __init__(self, main_window): """ Constructor Arguments: main_window -- the main window """ from QProgEdit import QTabManager self.main_window = main_window QtGui.QWidget.__init__(self, main_window) self.ui = Ui_widget_general_script_editor() self.ui.setupUi(self) self.ui.qprogedit = QTabManager(handler=self._apply, defaultLang= \ u'OpenSesame', handlerButtonText=u'Apply', cfg=cfg) self.ui.qprogedit.addTab(u'General script') self.ui.layout_vbox.addWidget(self.ui.qprogedit) self.main_window.theme.apply_theme(self) self.tab_name = '__general_script__'
class inline_script(libopensesame.inline_script.inline_script, qtitem.qtitem): """The inline_script GUI controls""" def __init__(self, name, experiment, string=None): """ Constructor. Arguments: name -- The item name. experiment -- The experiment object. Keywords arguments: string -- A definition string. (default=None) """ libopensesame.inline_script.inline_script.__init__(self, name, \ experiment, string) qtitem.qtitem.__init__(self) self.lock = False self._var_info = None def apply_edit_changes(self, **args): """ Applies the controls. Keywords arguments: args -- A dictionary to accept unused keyword arguments. """ qtitem.qtitem.apply_edit_changes(self, False) sp = self.qprogedit.text(index=0) sr = self.qprogedit.text(index=1) self.set(u'_prepare', sp) self.set(u'_run', sr) self.lock = True self._var_info = None self.experiment.main_window.refresh(self.name) self.lock = False def init_edit_widget(self): """Constructs the GUI controls.""" from QProgEdit import QTabManager qtitem.qtitem.init_edit_widget(self, False) self.qprogedit = QTabManager(handler=self.apply_edit_changes, \ defaultLang=u'Python', cfg=cfg) self.qprogedit.addTab(u'Prepare') self.qprogedit.addTab(u'Run') # Switch to the run phase, unless there is only content for the prepare # phase. if self._run == u'' and self._prepare != u'': self.qprogedit.setCurrentIndex(0) else: self.qprogedit.setCurrentIndex(1) self.edit_vbox.addWidget(self.qprogedit) def edit_widget(self): """ Updates the GUI controls. Returns: The control QWidget. """ qtitem.qtitem.edit_widget(self, False) if not self.lock: self.qprogedit.setText(self._prepare, index=0) self.qprogedit.setText(self._run, index=1) return self._edit_widget def get_ready(self): """Applies pending script changes.""" if self.qprogedit.isModified(): debug.msg(u'applying pending script changes') self.apply_edit_changes(catch=False) return True return qtitem.qtitem.get_ready(self)
class qtitem(QtCore.QObject): """Base class for the GUI controls of other items""" def __init__(self): """Constructor""" QtCore.QObject.__init__(self) # The auto-widgets are stored in name -> (var, widget) dictionaries self.auto_line_edit = {} self.auto_combobox = {} self.auto_spinbox = {} self.auto_slider = {} self.auto_editor = {} self.auto_checkbox = {} self.sanity_criteria = {} self.init_edit_widget() self.init_script_widget() self.script_tab = None self.lock = False self.edit_mode = u'edit' debug.msg(u'created %s' % self.name) def open_help_tab(self, page=None): """Opens a help tab.""" self.experiment.main_window.ui.tabwidget.open_help(self.item_type) def open_tab(self): """Opens the correct tab based on the current edit mode""" if self.edit_mode == u'edit': self.open_edit_tab() else: self.open_script_tab() def init_edit_widget(self, stretch=True): """Build the GUI controls""" # Header widget self.header = header_widget.header_widget(self) self.user_hint_widget = user_hint_widget.user_hint_widget( \ self.experiment.main_window, self) self.header_hbox = QtGui.QHBoxLayout() self.header_hbox.addWidget(self.experiment.label_image(self.item_type)) self.header_hbox.addWidget(self.header) self.header_hbox.addStretch() self.header_hbox.setContentsMargins(0, 0, 0, 16) # Edit script button button = QtGui.QPushButton(self.experiment.icon(u"script"), u"") button.setToolTip(_(u"Edit script")) button.setIconSize(QtCore.QSize(16, 16)) QtCore.QObject.connect(button, QtCore.SIGNAL(u"clicked()"), \ self.open_script_tab) self.header_hbox.addWidget(button) # Help button button = QtGui.QPushButton(self.experiment.icon(u"help"), u"") button.setToolTip(_(u"Tell me more about the %s item") % self.item_type) button.setIconSize(QtCore.QSize(16, 16)) QtCore.QObject.connect(button, QtCore.SIGNAL(u"clicked()"), \ self.open_help_tab) self.header_hbox.addWidget(button) self.header_widget = QtGui.QWidget() self.header_widget.setLayout(self.header_hbox) # The edit_grid is the layout that contains the actual controls for the # items. self.edit_grid = QtGui.QGridLayout() self.edit_grid.setColumnStretch(2, 2) self.edit_grid_widget = QtGui.QWidget() self.edit_grid.setMargin(0) self.edit_grid_widget.setLayout(self.edit_grid) # The edit_vbox contains the edit_grid and the header widget self.edit_vbox = QtGui.QVBoxLayout() self.edit_vbox.setMargin(16) self.edit_vbox.addWidget(self.header_widget) self.edit_vbox.addWidget(self.user_hint_widget) self.edit_vbox.addWidget(self.edit_grid_widget) if stretch: self.edit_vbox.addStretch() self._edit_widget = QtGui.QWidget() self._edit_widget.setLayout(self.edit_vbox) self._edit_widget.__edit_item__ = self.name return self._edit_widget def edit_widget(self, stretch=True): """ A dummy edit widget, to be overridden. Keywords arguments: stretch -- DEPRECATED (default=True) """ if not stretch: debug.msg(u"passing the stretch argument is deprecated", \ reason=u"deprecation") self.user_hint_widget.clear() self.header.restore_name(False) self.header.refresh() self._edit_widget.__edit_item__ = self.name if not self.sanity_check(): self.open_script_tab() return self.auto_edit_widget() self.user_hint_widget.refresh() return self._edit_widget def apply_name_change(self, rebuild=True): """ Apply an item name change Keywords arguments: rebuild -- a deprecated argument (default=True) """ debug.msg() # Sanitize the name, check if it is new and valid, and if so, rename new_name = self.experiment.sanitize(self.header.edit_name.text(), \ strict=True, allow_vars=False) if new_name.lower() != self.name.lower(): valid = self.experiment.check_name(new_name) if valid != True: self.experiment.notify(valid) self.header.edit_name.setText(self.name) return old_name = self.name self.name = new_name self._edit_widget.__edit_item__ = new_name self.experiment.main_window.dispatch.event_name_change.emit(old_name, \ new_name) def apply_edit_changes(self, rebuild=True): """ Applies the GUI controls. Keywords arguments: rebuild -- Specifies whether the overview area (item list) should be rebuild. (default=True) """ debug.msg(self.name) if self.experiment.main_window.lock_refresh: debug.msg(u"skipping, because refresh in progress") return False self.auto_apply_edit_changes() self.set(u"description", \ self.experiment.sanitize(unicode( \ self.header.edit_desc.text()).strip())) if self.description == u"": self.description = u"No description" self.header.label_desc.setText(self.description) self.experiment.main_window.dispatch.event_simple_change.emit(self.name) return True def close_edit_tab(self, index=None): """ Closes the edit tab (does nothing by default). Keywords arguments: index -- The index of the tab in the tab area. (default=None) """ pass def open_edit_tab(self, index=None, focus=True): """ Opens the GUI control tab, or switches to the tab if it was already open. Keywords arguments: index -- The index of the tab (if open). (default=None) focus -- Indicates whether the tab should receive focus. (default=True) """ debug.msg(u"%s (#%s)" % (self.name, hash(self))) # Switch to edit mode and close the script tab if it was open self.edit_mode = u"edit" for i in range(self.experiment.ui.tabwidget.count()): w = self.experiment.ui.tabwidget.widget(i) if hasattr(w, u"__script_item__") and w.__script_item__ == \ self.name: self.experiment.ui.tabwidget.removeTab(i) if index == None: index = i break # Focus the edit tab, instead of reopening, if it was already open for i in range(self.experiment.ui.tabwidget.count()): w = self.experiment.ui.tabwidget.widget(i) if hasattr(w, u"__edit_item__") and w.__edit_item__ == self.name: index = i # Refresh the controls on the tab. In debug mode don't catch any errors if debug.enabled: widget = self.edit_widget() else: try: widget = self.edit_widget() except Exception as e: self.experiment.notify(_(u"%s (Edit the script to fix this)") \ % e) self.open_script_tab() return # Open the tab or focus the tab if it was already open if index == None: self.edit_tab_index = self.experiment.ui.tabwidget.addTab(widget, \ self.experiment.icon(self.item_type), u"%s" % self.name) else: self.experiment.ui.tabwidget.insertTab(index, widget, \ self.experiment.icon(self.item_type), u"%s" % self.name) self.edit_tab_index = index if focus: self.experiment.ui.tabwidget.setCurrentIndex(self.edit_tab_index) def apply_script_and_close(self): """Applies script changes and opens the edit tab""" self.apply_script_changes() self.experiment.main_window.select_item(self.name) def apply_script_changes(self, rebuild=True, catch=True): """ Apply changes to the script, by regenerating the item from the script Keywords arguments: rebuild -- specifies whether the overview area (item list) should be rebuild (default=True) catch -- indicates if exceptions should be caught and shown in a notification dialog (True) or not be caught (False) (default=True) """ debug.msg(self.name) script = self.script_qprogedit.text() # Create a new item and make it a clone of the current item item = self.experiment.main_window.add_item(self.item_type, False, \ name=self.name, interactive=False) if catch: try: self.experiment.items[item].from_string(script) except Exception as e: self.experiment.notify(unicode(e)) return else: self.experiment.items[item].from_string(script) self.experiment.items[item].name = self.name # Replace the current item self.experiment.items[self.name] = self.experiment.items[item] del self.experiment.items[item] self.experiment.items[self.name].init_script_widget() self.experiment.main_window.dispatch.event_script_change.emit(self.name) def strip_script_line(self, s): """ Strips unwanted characters from a line of script Arguments: s -- a line of script Returns: A stripped line of script """ if len(s) > 0 and s[0] == u"\t": return s[1:] + u"\n" return s + u"\n" def init_script_widget(self): """Build the script tab""" from QProgEdit import QTabManager self.script_qprogedit = QTabManager(handler=self.apply_script_and_close, \ defaultLang=u'OpenSesame', handlerButtonText= \ _(u'Apply and close script editor'), callHandlerOnFocusOut=False, cfg=cfg) self.script_qprogedit.addTab(u'Script') hbox = QtGui.QHBoxLayout() hbox.addWidget(self.experiment.label_image(u"%s" % self.item_type)) self.script_header = QtGui.QLabel() hbox.addWidget(self.script_header) hbox.addStretch() hbox.setContentsMargins(0,0,0,0) hwidget = QtGui.QWidget() hwidget.setLayout(hbox) vbox = QtGui.QVBoxLayout() vbox.addWidget(hwidget) vbox.addWidget(self.script_qprogedit) self._script_widget = QtGui.QWidget() self._script_widget.setLayout(vbox) self._script_widget.__script_item__ = self.name def script_widget(self): """ Update the script tab Returns: The QWidget containing the script tab """ self.script_header.setText( \ _(u"Editing script for <b>%s</b> - %s") % (self.name, \ self.item_type)) script = u"" for s in self.to_string().split(u"\n")[1:]: script += self.strip_script_line(s) self.script_qprogedit.setText(script) self._script_widget.__script_item__ = self.name return self._script_widget def open_script_tab(self, index=None, focus=True): """ Open/ show the script tab Keywords arguments: index -- the index of the tab (if it is already open) (default=None) focus -- indicates whether the tab should receive focus (default=True) """ debug.msg(u"%s (#%s)" % (self.name, hash(self))) self.edit_mode = u"script" # Close the edit tab for i in range(self.experiment.ui.tabwidget.count()): w = self.experiment.ui.tabwidget.widget(i) if hasattr(w, u"__edit_item__") and w.__edit_item__ == self.name: self.experiment.ui.tabwidget.removeTab(i) if index == None: index = i break for i in range(self.experiment.ui.tabwidget.count()): w = self.experiment.ui.tabwidget.widget(i) if hasattr(w, u"__script_item__") and w.__script_item__ == self.name: index = i if index == None: self.script_tab_index = self.experiment.ui.tabwidget.addTab( \ self.script_widget(), self.experiment.icon(u"script"), u"%s" \ % self.name) else: self.script_tab_index = index self.experiment.ui.tabwidget.insertTab(index, \ self.script_widget(), self.experiment.icon(u"script"), u"%s" \ % self.name) if focus: self.experiment.ui.tabwidget.setCurrentIndex(self.script_tab_index) def close_script_tab(self, index=None): """ Close the script tab (does nothing by defaut) Keywords arguments: index -- the index of the tab in the tab area (default=None) """ pass def rename(self, from_name, to_name): """ Handle the renaming of an item (not necesarrily the currnet item) Arguments: from_name -- the old item name to_name -- the new item name """ if self.name == from_name: self.name = to_name def delete(self, item_name, item_parent=None, index=None): """ Delete an item (not necessarily the current one) Arguments: item_name -- the name of the item to be deleted Keywords arguments: item_parent -- the parent item (default=None) index -- the index of the item in the parent (default=None) """ pass def rename_var(self, item, from_name, to_name): """ A notification that a variable has been renamed Arguments: item -- the item doing the renaming from_name -- the old variable name to_name -- the new variable name """ pass def item_tree_widget(self, toplevel, icon=None, name=None, tooltip=None): """ Create a single item tree widget Arguments: toplevel -- the toplevel item Keyword arguments: icon -- an icon name or None for default (default=None) name -- the name of the item or None for default (default=None) tooltip -- the tooltip or None for default (default=None) Returns: A QTreeWidgetItem """ if name == None: name = self.name if icon == None: icon = self.item_type if tooltip == None: tooltip = _(u"Type: %s\nDescription: %s") % (self.item_type, \ self.description) font = QtGui.QFont() font.setPointSize(8) font.setItalic(True) widget = QtGui.QTreeWidgetItem(toplevel) widget.setText(0, name) widget.setIcon(0, self.experiment.icon(icon)) widget.name = name widget.setToolTip(0, tooltip) return widget def build_item_tree(self, toplevel=None, items=[]): """ Construct an item tree Keyword arguments: toplevel -- the toplevel widget (default = None) items -- a list of items that have been added, to prevent recursion (default=[]) """ toplevel.addChild(self.item_tree_widget(toplevel)) def is_offspring(self, item): """ Checks if the item is offspring of the current item, in the sense that the current item is contained by the item Arguments: item -- the potential offspring Returns: True if the current item is offspring of the item, False otherwise """ return False def parents(self): """ Creates a list of all the items that the current sequences is connected to upstream Returns: A list of item names """ l = [self.name] for item in self.experiment.items: if self.experiment.items[item].is_offspring(self.name): l.append(item) return l def variable_vars(self, exclude=[]): """ Determines if one of the variables of the current item is defined in terms of another variable Keywords arguments: exclude -- a list of variables that should not be checked Returns: True if there are variably defined variables, False otherwise """ for var in self.variables: if var not in exclude: val = self.variables[var] if isinstance(val, basestring) and u'[' in val: return True return False def get_ready(self): """ This function should be overridden to do any last-minute stuff that and item should do before an experiment is actually run, such as applying pending script changes. Returns: True if some action has been taken, False if nothing was done """ if self.script_qprogedit.isModified(): debug.msg(u'applying pending script changes') self.apply_script_changes(catch=False) return True return False def auto_edit_widget(self): """Update the GUI controls based on the auto-widgets""" debug.msg() for var, edit in self.auto_line_edit.iteritems(): edit.editingFinished.disconnect() if self.has(var): try: edit.setText(self.unistr(self.get(var, _eval=False))) except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) else: edit.setText(u"") edit.editingFinished.connect(self.apply_edit_changes) for var, combobox in self.auto_combobox.iteritems(): combobox.currentIndexChanged.disconnect() if self.has(var): try: combobox.setCurrentIndex(combobox.findText( \ self.unistr(self.get(var, _eval=False)))) except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) combobox.currentIndexChanged.connect(self.apply_edit_changes) for var, spinbox in self.auto_spinbox.iteritems(): spinbox.editingFinished.disconnect() if self.has(var): val = self.get(var, _eval=False) if type(val) in (float, int): try: spinbox.setValue(val) except Exception as e: self.experiment.notify(_( \ u"Failed to set control '%s': %s") % (var, e)) else: spinbox.setDisabled(True) self.user_hint_widget.add_user_hint(_( \ u'"%s" is defined using variables and can only be edited through the script.' \ % var)) spinbox.editingFinished.connect(self.apply_edit_changes) for var, slider in self.auto_slider.iteritems(): slider.valueChanged.disconnect() if self.has(var): val = self.get(var, _eval=False) if type(val) in (float, int): try: slider.setValue(val) except Exception as e: self.experiment.notify(_( \ u"Failed to set control '%s': %s") % (var, e)) else: slider.setDisabled(True) self.user_hint_widget.add_user_hint(_( \ u'"%s" is defined using variables and can only be edited through the script.' \ % var)) slider.valueChanged.connect(self.apply_edit_changes) for var, checkbox in self.auto_checkbox.iteritems(): checkbox.toggled.disconnect() if self.has(var): try: checkbox.setChecked(self.get(var, _eval=False) == u"yes") except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) checkbox.toggled.connect(self.apply_edit_changes) for var, qprogedit in self.auto_editor.iteritems(): if self.has(var): try: qprogedit.setText(self.unistr(self.get(var, _eval=False))) except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) def sanitize_check(self, s, strict=False, allow_vars=True, notify=True): """ Checks whether a string is sane (i.e. unchanged by sanitize()) and optionally presents a warning. Arguments: s -- The string to check. Keyword arguments: strict -- See sanitize(). allow_vars -- See sanitize(). notify -- Indicates whether a notification should be presented if the string is not sane. Returns: True if s is sane, False otherwise. """ sane = s == self.sanitize(s, strict=strict, allow_vars=allow_vars) if not sane and notify: if strict: self.experiment.notify( _(u'All non-alphanumeric characters except underscores have been stripped')) else: self.experiment.notify( _(u'The following characters are not allowed and have been stripped: double-quote ("), backslash (\), and newline')) return sane def sanity_check(self): """ Checks whether all variables match prespecified criteria and fall back to the script editor otherwise. This is usefull to check that certain variables are numeric, etc. """ debug.msg() errors = [] for var_name, criteria in self.sanity_criteria.items(): msg = _(u"Invalid or missing value for variable '%s' (edit script to fix this)") \ % var_name if u'msg' in criteria: msg += u': ' + criteria[u'msg'] if not self.has(var_name) and u'required' in criteria and \ criteria[u'required']: self.experiment.notify(msg) return False else: var = self.get(var_name, _eval=False) if u'type' in criteria: _type = criteria[u'type'] if type(_type) != list: _type = [_type] if type(var) not in _type: self.experiment.notify(msg) return False if u'func' in criteria: if not criteria[u'func'](var): self.experiment.notify(msg) return False return True def auto_apply_edit_changes(self, rebuild=True): """ Apply the auto-widget controls Keyword arguments: rebuild -- deprecated (does nothing) (default=True) """ debug.msg() for var, edit in self.auto_line_edit.iteritems(): if edit.isEnabled() and isinstance(var, basestring): val = unicode(edit.text()).strip() if val != u"": self.set(var, val) # If the variable has no value, we assign a default value if it # has been specified, and unset it otherwise. elif hasattr(edit, u"default"): self.set(var, edit.default) else: self.unset(var) for var, combobox in self.auto_combobox.iteritems(): if combobox.isEnabled() and isinstance(var, basestring): self.set(var, unicode(combobox.currentText())) for var, spinbox in self.auto_spinbox.iteritems(): if spinbox.isEnabled() and isinstance(var, basestring): self.set(var, spinbox.value()) for var, slider in self.auto_slider.iteritems(): if slider.isEnabled() and isinstance(var, basestring): self.set(var, slider.value()) for var, checkbox in self.auto_checkbox.iteritems(): if checkbox.isEnabled() and isinstance(var, basestring): if checkbox.isChecked(): val = u"yes" else: val = u"no" self.set(var, val) for var, qprogedit in self.auto_editor.iteritems(): if isinstance(var, basestring): self.set(var, qprogedit.text()) return True def auto_add_widget(self, widget, var=None): """ Add a widget to the list of auto-widgets Arguments: widget -- a QWidget Keyword arguments: var -- the variable to be linked to the widget (default=None) """ # Use the object id as a fallback name if var == None: var = id(widget) debug.msg(var) if isinstance(widget, QtGui.QSpinBox) or isinstance(widget, \ QtGui.QDoubleSpinBox): widget.editingFinished.connect(self.apply_edit_changes) self.auto_spinbox[var] = widget elif isinstance(widget, QtGui.QComboBox): widget.currentIndexChanged.connect(self.apply_edit_changes) self.auto_combobox[var] = widget elif isinstance(widget, QtGui.QSlider): widget.editingFinished.connect(self.apply_edit_changes) self.auto_slider[var] = widget elif isinstance(widget, QtGui.QLineEdit): widget.editingFinished.connect(self.apply_edit_changes) self.auto_line_edit[var] = widget elif isinstance(widget, QtGui.QCheckBox): widget.toggled.connect(self.apply_edit_changes) self.auto_checkbox[var] = widget else: raise Exception(u"Cannot auto-add widget of type %s" % widget)
class qtitem(base_qtobject): """Base class for the GUI controls of other items""" initial_view = u'controls' label_align = u'right' help_url = None lazy_init = False def __init__(self): """Constructor""" # The auto-widgets are stored in name -> (var, widget) dictionaries self.auto_line_edit = {} self.auto_combobox = {} self.auto_spinbox = {} self.auto_slider = {} self.auto_editor = {} self.auto_checkbox = {} # Lazy initialization means that the control widgets are initialized # only when they are shown for the first time. This dramatically speeds # up opening of files. However, with some items this is not currently # possible, because their widgets are also referred when they are not # shown. Currently this means the inline_script. if self.lazy_init: self.container_widget = None else: self.init_edit_widget() self.lock = False self.maximized = False self.set_validator() debug.msg(u'created %s' % self.name) @property def main_window(self): return self.experiment.main_window @property def overview_area(self): return self.experiment.main_window.ui.itemtree @property def theme(self): return self.experiment.main_window.theme @property def tabwidget(self): return self.experiment.main_window.tabwidget @property def console(self): return self.experiment.main_window.console @property def extension_manager(self): return self.experiment.main_window.extension_manager @property def default_description(self): return _(u'Default description') def open_tab(self, select_in_tree=True): """ desc: Opens the tab if it wasn't yet open, and switches to it. """ unsaved = self.main_window.unsaved_changes self.tabwidget.add(self.widget(), self.item_icon(), self.name) self.main_window.set_unsaved(unsaved) if select_in_tree: self.experiment.main_window.ui.itemtree.select_item(self.name) def close_tab(self): """ desc: Closes the tab if it was open. """ self.tabwidget.remove(self.widget()) def set_focus(self): """ desc: Gives focus to the most important widget. """ if hasattr(self, u'focus_widget') and self.focus_widget is not None: self.focus_widget.setFocus() def set_focus_widget(self, widget, override=False): """ desc: Sets the widget that receives focus when the tab is opened. arguments: widget: desc: The widget to receive focus or `None` to reset. type: [QWidget, NoneType] keywords: override: desc: Indicates whether the focus widget should be changed if there already is a focus widget. type: bool """ if override or not hasattr(self, u'focus_widget') or self.focus_widget is None: self.focus_widget = widget def update_item_icon(self): """ desc: Updates the item icon. """ self.tabwidget.set_icon(self.widget(), self.item_icon()) self.experiment.items.set_icon(self.name, self.item_icon()) self.header_item_icon.setPixmap( self.theme.qpixmap(self.item_icon(), 32)) def item_icon(self): """ returns: desc: The name of the item icon. type: unicode """ return self.item_type def show_tab(self): """ desc: Is called when the tab becomes visible, and updated the contents. """ self.extension_manager.fire(u'prepare_open_item', name=self.name) self.update_script() self.edit_widget() self.main_window.ui.itemtree.select_item(self.name, open_tab=False) self.extension_manager.fire(u'open_item', name=self.name) @requires_init def widget(self): """ returns: desc: The widget that is added to the tabwidget. type: QWidget """ return self.container_widget def init_edit_widget(self, stretch=True): """ desc: Builds the UI. keywords: stretch: desc: Indicates whether a vertical stretch should be added to the bottom of the controls. This is necessary if the controls don't expand. type: bool """ # Header widget self.header = header_widget.header_widget(self) self.header_hbox = QtWidgets.QHBoxLayout() self.header_item_icon = self.theme.qlabel(self.item_icon()) self.header_hbox.addWidget(self.header_item_icon) self.header_hbox.addWidget(self.header) self.header_hbox.setContentsMargins(0, 0, 0, 0) self.header_hbox.setSpacing(12) # Maximize button self.button_toggle_maximize = QtWidgets.QPushButton( self.theme.qicon(u'view-fullscreen'), u'') self.button_toggle_maximize.setToolTip(_(u'Toggle pop-out')) self.button_toggle_maximize.setIconSize(QtCore.QSize(16, 16)) self.button_toggle_maximize.clicked.connect(self.toggle_maximize) self.header_hbox.addWidget(self.button_toggle_maximize) # View button self.button_view = item_view_button(self) self.header_hbox.addWidget(self.button_view) # Help button self.button_help = QtWidgets.QPushButton(self.theme.qicon(u"help"), u"") self.button_help.setToolTip( _(u"Tell me more about the %s item") % self.item_type) self.button_help.setIconSize(QtCore.QSize(16, 16)) self.button_help.clicked.connect(self.open_help_tab) self.header_hbox.addWidget(self.button_help) self.header_widget = QtWidgets.QWidget() self.header_widget.setLayout(self.header_hbox) # The edit_grid is the layout that contains the actual controls for the # items. self.edit_grid = QtWidgets.QFormLayout() if self.label_align == u'right': self.edit_grid.setLabelAlignment(QtCore.Qt.AlignRight) self.edit_grid.setFieldGrowthPolicy( QtWidgets.QFormLayout.FieldsStayAtSizeHint) self.edit_grid.setContentsMargins(0, 0, 0, 0) self.edit_grid.setVerticalSpacing(6) self.edit_grid.setHorizontalSpacing(12) self.edit_grid_widget = QtWidgets.QWidget() self.edit_grid_widget.setLayout(self.edit_grid) # The edit_vbox contains the edit_grid and the header widget self.edit_vbox = QtWidgets.QVBoxLayout() self.edit_vbox.addWidget(self.edit_grid_widget) self.edit_vbox.setContentsMargins(0, 0, 0, 0) self.edit_vbox.setSpacing(12) if stretch: self.edit_vbox.addStretch() self._edit_widget = QtWidgets.QWidget() self._edit_widget.setWindowIcon(self.theme.qicon(self.item_type)) self._edit_widget.setLayout(self.edit_vbox) # The _script_widget contains the script editor from QProgEdit import QTabManager self._script_widget = QTabManager( handlerButtonText=_(u'Apply and close'), cfg=cfg) self._script_widget.focusLost.connect(self.apply_script_changes) self._script_widget.cursorRowChanged.connect(self.apply_script_changes) self._script_widget.handlerButtonClicked.connect( self.apply_script_changes_and_switch_view) self._script_widget.addTab(u'Script').setLang(u'OpenSesame') # The container_widget is the top-level widget that is actually inserted # into the tab widget. self.splitter = qtitem_splitter(self) if self.initial_view == u'controls': self.set_view_controls() elif self.initial_view == u'script': self.set_view_script() elif self.initial_view == u'split': self.set_view_split() else: debug.msg(u'Invalid initial_view: %s' % self.initial_view, reason=u'warning') self.set_view_controls() self.splitter.splitterMoved.connect(self.splitter_moved) self.container_vbox = QtWidgets.QVBoxLayout() self.container_vbox.setContentsMargins(12, 12, 12, 12) self.container_vbox.setSpacing(18) self.container_vbox.addWidget(self.header_widget) self.container_vbox.addWidget(self.splitter) self.container_widget = QtWidgets.QWidget() self.container_widget.setLayout(self.container_vbox) self.container_widget.on_activate = self.show_tab self.container_widget.__item__ = self.name def splitter_moved(self, pos, index): """ desc: Is called when the splitter handle is manually moved. arguments: pos: desc: The splitter-handle position. type: int index: desc: The index of the splitter handle. Since there is only one handle, this is always 0. type: int """ sizes = self.splitter.sizes() self.edit_size = sizes[0] self.script_size = sizes[1] if self.script_size == 0: self.button_view.set_view_icon(u'controls') elif self.edit_size == 0: self.button_view.set_view_icon(u'script') else: self.button_view.set_view_icon(u'split') def set_view_controls(self): """ desc: Puts the splitter in control view. """ self.splitter.setSizes([self.splitter.width(), 0]) self.button_view.set_view_icon(u'controls') def set_view_script(self): """ desc: Puts the splitter in script view. """ self.splitter.setSizes([0, self.splitter.width()]) self.button_view.set_view_icon(u'script') def set_view_split(self): """ desc: Puts the splitter in split view. """ self.splitter.setSizes([self.splitter.width()/2, self.splitter.width()/2]) self.button_view.set_view_icon(u'split') def update(self): """ desc: Updates both the script and the controls. """ # Items are updated when their tab is shown, so we don't need to update # them if they aren't shown. if self.tabwidget.current_item() != self.name: return False self.update_script() self.edit_widget() return True def update_script(self): """ desc: Regenerates the script and updates the script widget. """ # Normally, the script starts with a 'define' line and is indented by # a tab. We want to undo this, and present only unindented content. import textwrap script = self.to_string() script = script[script.find(u'\t'):] script = textwrap.dedent(script) if self._script_widget.text() != script: self.main_window.set_unsaved() self._script_widget.setText(script) self.extension_manager.fire(u'change_item', name=self.name) def edit_widget(self, *deprecated, **_deprecated): """ desc: This function updates the controls based on the item state. """ self.auto_edit_widget() self.header.refresh() def apply_edit_changes(self, *deprecated, **_deprecated): """ desc: Applies changes to the graphical controls. """ self.auto_apply_edit_changes() self.update_script() return True def apply_script_changes(self, *deprecated, **_deprecated): """ desc: Applies changes to the script. """ if not self.validate_script(): return new_script = self._script_widget.text() old_script = self.to_string() self.from_string(new_script) if old_script != new_script: self.main_window.set_unsaved() self.edit_widget() def apply_script_changes_and_switch_view(self, *deprecated, **_deprecated): """ desc: Applies changes to the script if possible. If so, switches to the controls view. """ if self.validate_script(): self.set_view_controls() def validate_script(self): """ desc: Checks whether the script is syntactically valid. If not, the offending line is highlighted if QProgEdit suppors setInvalid(). returns: type: bool """ script = self._script_widget.text() # First create a dummy item to see if the string can be parsed. try: self.validator(self.name, self.experiment, script) return True except Exception as e: # If an error occurs, we first parse the first line, then the first # and second, and so on, until we find the error. l = script.split(u'\n') for line_nr, line in enumerate(l): test_script = u'\n'.join(l[:line_nr]) try: self.validator(self.name, self.experiment, test_script) except Exception as e_: if not isinstance(e_, osexception): e_ = osexception(e_) if hasattr(self._script_widget, u'setInvalid'): self._script_widget.setInvalid(line_nr, e_.markdown()) break self.console.write(e) return False def set_validator(self): """ desc: Sets the validator class, that is, the class that is used to parse scripts to see if they are syntactically correct. Currently, we use the class that defines from_string(). """ import inspect meth = self.from_string for cls in inspect.getmro(self.__class__): if meth.__name__ in cls.__dict__: break debug.msg(u'validator: %s' % cls) self.validator = cls @requires_init def rename(self, from_name, to_name): """ desc: Handles renaming of an item (not necesarrily the current item). arguments: from_name: desc: The old item name. type: unicode to_name: desc: The new item name type: unicode """ if self.name != from_name: return self.name = to_name self.container_widget.__item__ = self.name self.header.set_name(to_name) index = self.tabwidget.indexOf(self.widget()) if index is not None: self.tabwidget.setTabText(index, to_name) def open_help_tab(self): """ desc: Opens a help tab. """ if self.help_url is None: self.tabwidget.open_help(self.item_type) else: self.tabwidget.open_osdoc(self.help_url) def toggle_maximize(self): """ desc: Toggles edit-widget maximization. """ if not self.maximized: # Always ignore close events. This is necessary, because otherwise # the pop-out widget can be closed without re-enabling the main # window. self.main_window.block_close_event = True self.container_widget.closeEvent = lambda e: e.ignore() self.container_widget.setParent(None) self.container_widget.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint|\ QtCore.Qt.WindowMaximizeButtonHint|\ QtCore.Qt.CustomizeWindowHint) self.container_widget.showMaximized() self.container_widget.show() self.button_toggle_maximize.setIcon( self.theme.qicon(u'view-restore')) else: self.main_window.block_close_event = False self.container_widget.setParent(self.main_window) self.open_tab() self.button_toggle_maximize.setIcon( self.theme.qicon(u'view-fullscreen')) self.maximized = not self.maximized self.button_help.setDisabled(self.maximized) self.main_window.setDisabled(self.maximized) def delete(self, item_name, item_parent=None, index=None): """ desc: Is called when an item is deleted (not necessarily the current one). arguments: item_name: desc: The name of the item to be deleted. type: str keywords: item_parent: desc: The name of the parent item. type: str index: desc: The index of the item in the parent. type: int """ pass def build_item_tree(self, toplevel=None, items=[], max_depth=-1, extra_info=None): """ desc: Constructs an item tree. keywords: toplevel: desc: The toplevel widget. type: tree_base_item items: desc: A list of item names that have already been added, to prevent recursion. tyep: list returns: type: tree_item_item """ widget = tree_item_item(self, extra_info=extra_info) items.append(self.name) if toplevel is not None: toplevel.addChild(widget) return widget def parents(self): """ returns: desc: A list of all parents (names) of the current item. type: list """ l = [self.name] for item in self.experiment.items: if self.experiment.items[item].is_child_item(self.name): l.append(item) return l def get_ready(self): """ desc: This function should be overridden to do any last-minute stuff that and item should do before an experiment is actually run, such as applying pending script changes. returns: desc: True if some action has been taken, False if nothing was done. type: bool """ return False def auto_edit_widget(self): """ desc: Updates the GUI controls based on the auto-widgets. """ for var, edit in self.auto_line_edit.items(): if isinstance(var, int): continue if var in self.var: val = safe_decode(self.var.get(var, _eval=False)) else: val = u'' if val != edit.text(): edit.setText(val) for var, combobox in self.auto_combobox.items(): if isinstance(var, int): continue val = self.var.get(var, _eval=False, default=u'') i = combobox.findText(safe_decode(val)) # Set the combobox to the select item if i >= 0: if i != combobox.currentIndex() or not combobox.isEnabled(): combobox.setDisabled(False) combobox.setCurrentIndex(i) # If no value was specified, set the combobox to a blank item elif val == u'': if combobox.currentIndex() >= 0 or not combobox.isEnabled(): combobox.setDisabled(False) combobox.setCurrentIndex(-1) # If an unknown value has been specified, notify the user else: if combobox.isEnabled(): combobox.setDisabled(True) self.extension_manager.fire(u'notify', message=_(u'"%s" is set to a variable or ' u'unknown value and can only be edited through ' u'the script.') % var, category=u'info') for var, spinbox in list(self.auto_spinbox.items()) \ + list(self.auto_slider.items()): if isinstance(var, int): continue if var in self.var: val = self.var.get(var, _eval=False) if type(val) in (float, int): if spinbox.value() == val and spinbox.isEnabled(): continue spinbox.setDisabled(False) try: spinbox.setValue(val) except Exception as e: self.experiment.notify( _(u"Failed to set control '%s': %s") % (var, e)) else: if not spinbox.isEnabled(): continue spinbox.setDisabled(True) self.extension_manager.fire(u'notify', message=_(u'"%s" is defined using ' 'variables and can only be edited through the ' 'script.') % var, category=u'info') for var, checkbox in self.auto_checkbox.items(): if isinstance(var, int): continue if var in self.var: val = self.var.get(var, _eval=False) if val in [u'yes', u'no']: checkbox.setDisabled(False) checked = val == u'yes' if checked != checkbox.isChecked(): checkbox.setChecked(checked) else: checkbox.setDisabled(True) self.extension_manager.fire(u'notify', message=_(u'"%s" is defined using ' u'variables or has an invalid value, and can only be ' u'edited through the script.') % var, category=u'info') for var, qprogedit in self.auto_editor.items(): if isinstance(var, int): continue if var in self.var: val = safe_decode(self.var.get(var, _eval=False)) if val != qprogedit.text(): qprogedit.setText(val) def auto_apply_edit_changes(self, rebuild=True): """ desc: Applies the auto-widget controls. keywords: rebuild: Deprecated (does nothing). """ for var, edit in self.auto_line_edit.items(): if isinstance(var, int): continue if edit.isEnabled() and isinstance(var, basestring): val = str(edit.text()).strip() if val != u'': self.var.set(var, val) continue # If no text was entered, we use a default if available ... if hasattr(edit, u'default'): self.var.set(var, edit.default) continue # ... or unset the variable if no default is available. self.var.unset(var) for var, combobox in self.auto_combobox.items(): if isinstance(var, int): continue if combobox.isEnabled() and isinstance(var, basestring): self.var.set(var, str(combobox.currentText())) for var, spinbox in self.auto_spinbox.items(): if isinstance(var, int): continue if spinbox.isEnabled() and isinstance(var, basestring): self.var.set(var, spinbox.value()) for var, slider in self.auto_slider.items(): if isinstance(var, int): continue if slider.isEnabled() and isinstance(var, basestring): self.var.set(var, slider.value()) for var, checkbox in self.auto_checkbox.items(): if isinstance(var, int): continue if checkbox.isEnabled() and isinstance(var, basestring): if checkbox.isChecked(): val = u"yes" else: val = u"no" self.var.set(var, val) for var, qprogedit in self.auto_editor.items(): if isinstance(var, int): continue if isinstance(var, basestring): self.var.set(var, qprogedit.text()) return True def auto_add_widget(self, widget, var=None, apply_func=None): """ desc: Adds a widget to the list of auto-widgets. arguments: widget: type: QWidget keywords: var: The name of the experimental variable to be linked to the widget, or None to use an automatically chosen name. type: [str, NoneType] """ # Use the object id as a fallback name if var is None: var = id(widget) if apply_func is None: apply_func = self.apply_edit_changes debug.msg(var) self.set_focus_widget(widget) if isinstance(widget, QtWidgets.QSpinBox) or isinstance(widget, QtWidgets.QDoubleSpinBox): widget.editingFinished.connect(apply_func) self.auto_spinbox[var] = widget elif isinstance(widget, QtWidgets.QComboBox): widget.activated.connect(apply_func) self.auto_combobox[var] = widget elif isinstance(widget, QtWidgets.QSlider) \ or isinstance(widget, QtWidgets.QDial): widget.valueChanged.connect(apply_func) self.auto_slider[var] = widget elif isinstance(widget, QtWidgets.QLineEdit) or isinstance(widget, pool_select): widget.editingFinished.connect(apply_func) self.auto_line_edit[var] = widget elif isinstance(widget, QtWidgets.QCheckBox): widget.clicked.connect(apply_func) self.auto_checkbox[var] = widget else: raise Exception(u"Cannot auto-add widget of type %s" % widget) def children(self): """ returns: desc: A list of children, including grand children, and so on. type: list """ return [] def is_child_item(self, item_name): """ desc: Checks if an item is somewhere downstream from the current item in the experimental hierarchy. arguments: item_name: desc: The name of the child item. type: unicode returns: desc: True if the current item is offspring of the item, False otherwise. type: bool """ return False def insert_child_item(self, item_name, index=0): """ desc: Inserts a child item, if applicable to the item type. arguments: item_name: desc: The name of the child item. type: unicode keywords: index: desc: The index of the child item. type: int """ pass def remove_child_item(self, item_name, index=0): """ desc: Removes a child item, if applicable to the item type. arguments: item_name: desc: The name of the child item. type: unicode keywords: index: desc: The index of the child item, if applicable. A negative value indicates all instances. type: int """ pass
def init_edit_widget(self, stretch=True): """ desc: Builds the UI. keywords: stretch: desc: Indicates whether a vertical stretch should be added to the bottom of the controls. This is necessary if the controls don't expand. type: bool """ # Header widget self.header = header_widget.header_widget(self) self.header_hbox = QtWidgets.QHBoxLayout() self.header_item_icon = self.theme.qlabel(self.item_icon()) self.header_hbox.addWidget(self.header_item_icon) self.header_hbox.addWidget(self.header) self.header_hbox.setContentsMargins(0, 0, 0, 0) self.header_hbox.setSpacing(12) # Maximize button self.button_toggle_maximize = QtWidgets.QPushButton( self.theme.qicon(u'view-fullscreen'), u'') self.button_toggle_maximize.setToolTip(_(u'Toggle pop-out')) self.button_toggle_maximize.setIconSize(QtCore.QSize(16, 16)) self.button_toggle_maximize.clicked.connect(self.toggle_maximize) self.header_hbox.addWidget(self.button_toggle_maximize) # View button self.button_view = item_view_button(self) self.header_hbox.addWidget(self.button_view) # Help button self.button_help = QtWidgets.QPushButton(self.theme.qicon(u"help"), u"") self.button_help.setToolTip( _(u"Tell me more about the %s item") % self.item_type) self.button_help.setIconSize(QtCore.QSize(16, 16)) self.button_help.clicked.connect(self.open_help_tab) self.header_hbox.addWidget(self.button_help) self.header_widget = QtWidgets.QWidget() self.header_widget.setLayout(self.header_hbox) # The edit_grid is the layout that contains the actual controls for the # items. self.edit_grid = QtWidgets.QFormLayout() if self.label_align == u'right': self.edit_grid.setLabelAlignment(QtCore.Qt.AlignRight) self.edit_grid.setFieldGrowthPolicy( QtWidgets.QFormLayout.FieldsStayAtSizeHint) self.edit_grid.setContentsMargins(0, 0, 0, 0) self.edit_grid.setVerticalSpacing(6) self.edit_grid.setHorizontalSpacing(12) self.edit_grid_widget = QtWidgets.QWidget() self.edit_grid_widget.setLayout(self.edit_grid) # The edit_vbox contains the edit_grid and the header widget self.edit_vbox = QtWidgets.QVBoxLayout() self.edit_vbox.addWidget(self.edit_grid_widget) self.edit_vbox.setContentsMargins(0, 0, 0, 0) self.edit_vbox.setSpacing(12) if stretch: self.edit_vbox.addStretch() self._edit_widget = QtWidgets.QWidget() self._edit_widget.setWindowIcon(self.theme.qicon(self.item_type)) self._edit_widget.setLayout(self.edit_vbox) # The _script_widget contains the script editor from QProgEdit import QTabManager self._script_widget = QTabManager( handlerButtonText=_(u'Apply and close'), cfg=cfg) self._script_widget.focusLost.connect(self.apply_script_changes) self._script_widget.cursorRowChanged.connect(self.apply_script_changes) self._script_widget.handlerButtonClicked.connect( self.apply_script_changes_and_switch_view) self._script_widget.addTab(u'Script').setLang(u'OpenSesame') # The container_widget is the top-level widget that is actually inserted # into the tab widget. self.splitter = qtitem_splitter(self) if self.initial_view == u'controls': self.set_view_controls() elif self.initial_view == u'script': self.set_view_script() elif self.initial_view == u'split': self.set_view_split() else: debug.msg(u'Invalid initial_view: %s' % self.initial_view, reason=u'warning') self.set_view_controls() self.splitter.splitterMoved.connect(self.splitter_moved) self.container_vbox = QtWidgets.QVBoxLayout() self.container_vbox.setContentsMargins(12, 12, 12, 12) self.container_vbox.setSpacing(18) self.container_vbox.addWidget(self.header_widget) self.container_vbox.addWidget(self.splitter) self.container_widget = QtWidgets.QWidget() self.container_widget.setLayout(self.container_vbox) self.container_widget.on_activate = self.show_tab self.container_widget.__item__ = self.name
class inline_script(inline_script_runtime, qtplugin): """The inline_script GUI controls""" def __init__(self, name, experiment, string=None): """See item.""" inline_script_runtime.__init__(self, name, experiment, string) qtplugin.__init__(self) def apply_edit_changes(self): """See qtitem.""" super(inline_script, self).apply_edit_changes(self) sp = self.qprogedit.text(index=0) sr = self.qprogedit.text(index=1) self.set(u'_prepare', sp) self.set(u'_run', sr) self.update_item_icon() def set_focus(self): """ desc: Allows the item to focus the most important widget. """ self.qprogedit.setFocus() def item_icon(self): """ desc: Determines the icon, based on whether the scripts are syntactically correct. returns: desc: An icon name. type: unicode """ if self.experiment.python_workspace.check_syntax( self.unistr(self.get(u'_prepare', _eval=False)))\ and self.experiment.python_workspace.check_syntax( self.unistr(self.get(u'_run', _eval=False))): return u'os-inline_script' return u'os-inline_script-syntax-error' def build_item_tree(self, toplevel=None, items=[], max_depth=-1, extra_info=None): """See qtitem.""" widget = tree_inline_script_item(self, extra_info=extra_info, symbols=(max_depth < 0 or max_depth > 1)) items.append(self.name) if toplevel != None: toplevel.addChild(widget) return widget def init_edit_widget(self): """See qtitem.""" from QProgEdit import QTabManager super(inline_script, self).init_edit_widget(stretch=False) self.qprogedit = QTabManager(cfg=cfg) self.qprogedit.handlerButtonClicked.connect(self.apply_edit_changes) self.qprogedit.focusLost.connect(self.apply_edit_changes) self.qprogedit.cursorRowChanged.connect(self.apply_edit_changes) self.qprogedit.addTab(u'Prepare').setLang(u'Python') self.qprogedit.addTab(u'Run').setLang(u'Python') # Switch to the run phase, unless there is only content for the prepare # phase. if self._run == u'' and self._prepare != u'': self.qprogedit.setCurrentIndex(0) else: self.qprogedit.setCurrentIndex(1) self.edit_vbox.addWidget(self.qprogedit) def edit_widget(self): """See qtitem.""" super(inline_script, self).edit_widget() self.qprogedit.tab(0).setText(self.unistr(self._prepare)) self.qprogedit.tab(1).setText(self.unistr(self._run)) def get_ready(self): """See qtitem.""" if self.qprogedit.isAnyModified(): debug.msg(u'applying pending script changes') self.apply_edit_changes() return True return super(inline_script, self).get_ready()
class qtitem(base_qtobject): """Base class for the GUI controls of other items""" initial_view = u"controls" def __init__(self): """Constructor""" # The auto-widgets are stored in name -> (var, widget) dictionaries self.auto_line_edit = {} self.auto_combobox = {} self.auto_spinbox = {} self.auto_slider = {} self.auto_editor = {} self.auto_checkbox = {} self.init_edit_widget() self.lock = False self.maximized = False debug.msg(u"created %s" % self.name) @property def main_window(self): return self.experiment.main_window @property def theme(self): return self.experiment.main_window.theme @property def tabwidget(self): return self.experiment.main_window.tabwidget @property def console(self): return self.experiment.main_window.console @property def extension_manager(self): return self.experiment.main_window.extension_manager def open_tab(self, select_in_tree=True): """ desc: Opens the tab if it wasn't yet open, and switches to it. """ unsaved = self.main_window.unsaved_changes self.tabwidget.add(self.widget(), self.item_icon(), self.name) self.main_window.set_unsaved(unsaved) if select_in_tree: self.experiment.main_window.ui.itemtree.select_item(self.name) def close_tab(self): """ desc: Closes the tab if it was open. """ self.tabwidget.remove(self.widget()) def set_focus(self): """ desc: Gives focus to the most important widget. """ if hasattr(self, u"focus_widget") and self.focus_widget is not None: self.focus_widget.setFocus() def set_focus_widget(self, widget, override=False): """ desc: Sets the widget that receives focus when the tab is opened. arguments: widget: desc: The widget to receive focus or `None` to reset. type: [QWidget, NoneType] keywords: override: desc: Indicates whether the focus widget should be changed if there already is a focus widget. type: bool """ if override or not hasattr(self, u"focus_widget") or self.focus_widget is None: self.focus_widget = widget def update_item_icon(self): """ desc: Updates the item icon. """ self.tabwidget.set_icon(self.widget(), self.item_icon()) self.experiment.items.set_icon(self.name, self.item_icon()) self.header_item_icon.setPixmap(self.theme.qpixmap(self.item_icon(), 32)) def item_icon(self): """ returns: desc: The name of the item icon. type: unicode """ return self.item_type def show_tab(self): """ desc: Is called when the tab becomes visible, and updated the contents. """ self.update_script() self.edit_widget() self.main_window.ui.itemtree.select_item(self.name, open_tab=False) def widget(self): """ returns: desc: The widget that is added to the tabwidget. type: QWidget """ return self.container_widget def init_edit_widget(self, stretch=True): """ desc: Builds the UI. keywords: stretch: desc: Indicates whether a vertical stretch should be added to the bottom of the controls. This is necessary if the controls don't expand. type: bool """ # Header widget self.header = header_widget.header_widget(self) self.user_hint_widget = user_hint_widget.user_hint_widget(self.experiment.main_window, self) self.header_hbox = QtGui.QHBoxLayout() self.header_item_icon = self.theme.qlabel(self.item_icon()) self.header_hbox.addWidget(self.header_item_icon) self.header_hbox.addWidget(self.header) self.header_hbox.setContentsMargins(0, 2, 0, 0) # Maximize button self.button_toggle_maximize = QtGui.QPushButton(self.theme.qicon(u"view-fullscreen"), u"") self.button_toggle_maximize.setToolTip(_(u"Toggle pop-out")) self.button_toggle_maximize.setIconSize(QtCore.QSize(16, 16)) self.button_toggle_maximize.clicked.connect(self.toggle_maximize) self.header_hbox.addWidget(self.button_toggle_maximize) # View button self.button_view = item_view_button(self) self.header_hbox.addWidget(self.button_view) # Help button self.button_help = QtGui.QPushButton(self.theme.qicon(u"help"), u"") self.button_help.setToolTip(_(u"Tell me more about the %s item") % self.item_type) self.button_help.setIconSize(QtCore.QSize(16, 16)) self.button_help.clicked.connect(self.open_help_tab) self.header_hbox.addWidget(self.button_help) self.header_widget = QtGui.QWidget() self.header_widget.setLayout(self.header_hbox) # The edit_grid is the layout that contains the actual controls for the # items. self.edit_grid = QtGui.QGridLayout() self.edit_grid.setColumnStretch(2, 2) self.edit_grid_widget = QtGui.QWidget() self.edit_grid_widget.setLayout(self.edit_grid) # The edit_vbox contains the edit_grid and the header widget self.edit_vbox = QtGui.QVBoxLayout() self.edit_vbox.addWidget(self.user_hint_widget) self.edit_vbox.addWidget(self.edit_grid_widget) self.edit_vbox.setContentsMargins(0, 0, 0, 0) self.edit_vbox.setSpacing(0) if stretch: self.edit_vbox.addStretch() self._edit_widget = QtGui.QWidget() self._edit_widget.setWindowIcon(self.theme.qicon(self.item_type)) self._edit_widget.setLayout(self.edit_vbox) # The _script_widget contains the script editor from QProgEdit import QTabManager self._script_widget = QTabManager(handlerButtonText=_(u"Apply and close"), cfg=cfg) self._script_widget.focusLost.connect(self.apply_script_changes) self._script_widget.cursorRowChanged.connect(self.apply_script_changes) self._script_widget.handlerButtonClicked.connect(self.set_view_controls) self._script_widget.addTab(u"Script").setLang(u"OpenSesame") # The container_widget is the top-level widget that is actually inserted # into the tab widget. self.splitter = qtitem_splitter(self) if self.initial_view == u"controls": self.set_view_controls() elif self.initial_view == u"script": self.set_view_script() elif self.initial_view == u"split": self.set_view_split() else: debug.msg(u"Invalid initial_view: %s" % self.initial_view, reason=u"warning") self.set_view_controls() self.splitter.splitterMoved.connect(self.splitter_moved) self.container_vbox = QtGui.QVBoxLayout() self.container_vbox.setContentsMargins(4, 0, 4, 4) self.container_vbox.addWidget(self.header_widget) self.container_vbox.addWidget(self.splitter) self.container_widget = QtGui.QWidget() self.container_widget.setLayout(self.container_vbox) self.container_widget.on_activate = self.show_tab self.container_widget.__item__ = self.name def splitter_moved(self, pos, index): """ desc: Is called when the splitter handle is manually moved. arguments: pos: desc: The splitter-handle position. type: int index: desc: The index of the splitter handle. Since there is only one handle, this is always 0. type: int """ sizes = self.splitter.sizes() self.edit_size = sizes[0] self.script_size = sizes[1] if self.script_size == 0: self.button_view.set_view_icon(u"controls") elif self.edit_size == 0: self.button_view.set_view_icon(u"script") else: self.button_view.set_view_icon(u"split") def set_view_controls(self): """ desc: Puts the splitter in control view. """ self.splitter.setSizes([self.splitter.width(), 0]) self.button_view.set_view_icon(u"controls") def set_view_script(self): """ desc: Puts the splitter in script view. """ self.splitter.setSizes([0, self.splitter.width()]) self.button_view.set_view_icon(u"script") def set_view_split(self): """ desc: Puts the splitter in split view. """ self.splitter.setSizes([self.splitter.width() / 2, self.splitter.width() / 2]) self.button_view.set_view_icon(u"split") def update(self): """ desc: Updates both the script and the controls. """ self.update_script() self.edit_widget() def update_script(self): """ desc: Regenerates the script and updates the script widget. """ # Normally, the script starts with a 'define' line and is indented by # a tab. We want to undo this, and present only unindented content. import textwrap script = self.to_string() script = script[script.find(u"\t") :] script = textwrap.dedent(script) if self._script_widget.text() != script: self.main_window.set_unsaved() self._script_widget.setText(script) self.extension_manager.fire(u"change_item", name=self.name) def edit_widget(self, *deprecated, **_deprecated): """ desc: This function updates the controls based on the item state. """ self.auto_edit_widget() self.header.refresh() def apply_edit_changes(self, *deprecated, **_deprecated): """ desc: Applies changes to the graphical controls. """ self.auto_apply_edit_changes() self.update_script() return True def apply_script_changes(self, *deprecated, **_deprecated): """ desc: Applies changes to the script, by re-parsing the item from string. """ old_script = self.to_string() new_script = self._script_widget.text() try: self.from_string(new_script) except osexception as e: md = _(u"# Error\n\nFailed to apply script for the " u"following reason:\n\n- ") + e.markdown() self.tabwidget.open_markdown(md) self.console.write(e) self.from_string(old_script) if old_script != new_script: self.main_window.set_unsaved() self.edit_widget() def rename(self, from_name, to_name): """ desc: Handles renaming of an item (not necesarrily the current item). arguments: from_name: desc: The old item name. type: unicode to_name: desc: The new item name type: unicode """ if self.name != from_name: return self.name = to_name self.container_widget.__item__ = self.name self.header.set_name(to_name) index = self.tabwidget.indexOf(self.widget()) if index is not None: self.tabwidget.setTabText(index, to_name) def open_help_tab(self): """ desc: Opens a help tab. """ self.experiment.main_window.ui.tabwidget.open_help(self.item_type) def toggle_maximize(self): """ desc: Toggles edit-widget maximization. """ if not self.maximized: # Always ignore close events. This is necessary, because otherwise # the pop-out widget can be closed without re-enabling the main # window. self.main_window.block_close_event = True self.container_widget.closeEvent = lambda e: e.ignore() self.container_widget.setParent(None) self.container_widget.setWindowFlags( QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.WindowMaximizeButtonHint | QtCore.Qt.CustomizeWindowHint ) self.container_widget.showMaximized() self.container_widget.show() self.button_toggle_maximize.setIcon(self.theme.qicon(u"view-restore")) else: self.main_window.block_close_event = False self.container_widget.setParent(self.main_window) self.open_tab() self.button_toggle_maximize.setIcon(self.theme.qicon(u"view-fullscreen")) self.maximized = not self.maximized self.user_hint_widget.disable(self.maximized) self.button_help.setDisabled(self.maximized) self.main_window.setDisabled(self.maximized) def delete(self, item_name, item_parent=None, index=None): """ desc: Is called when an item is deleted (not necessarily the current one). arguments: item_name: desc: The name of the item to be deleted. type: str keywords: item_parent: desc: The name of the parent item. type: str index: desc: The index of the item in the parent. type: int """ pass def build_item_tree(self, toplevel=None, items=[], max_depth=-1, extra_info=None): """ desc: Constructs an item tree. keywords: toplevel: desc: The toplevel widget. type: tree_base_item items: desc: A list of item names that have already been added, to prevent recursion. tyep: list returns: type: tree_item_item """ widget = tree_item_item(self, extra_info=extra_info) items.append(self.name) if toplevel is not None: toplevel.addChild(widget) return widget def parents(self): """ returns: desc: A list of all parents (names) of the current item. type: list """ l = [self.name] for item in self.experiment.items: if self.experiment.items[item].is_child_item(self.name): l.append(item) return l def get_ready(self): """ desc: This function should be overridden to do any last-minute stuff that and item should do before an experiment is actually run, such as applying pending script changes. returns: desc: True if some action has been taken, False if nothing was done. type: bool """ return False def auto_edit_widget(self): """ desc: Updates the GUI controls based on the auto-widgets. """ for var, edit in self.auto_line_edit.items(): if isinstance(var, int): continue if var in self.var: val = safe_decode(self.var.get(var, _eval=False)) else: val = u"" if val != edit.text(): edit.setText(val) for var, combobox in self.auto_combobox.items(): if isinstance(var, int): continue val = self.var.get(var, _eval=False, default=u"") i = combobox.findText(safe_decode(val)) # Set the combobox to the select item if i >= 0: if i != combobox.currentIndex() or not combobox.isEnabled(): combobox.setDisabled(False) combobox.setCurrentIndex(i) # If no value was specified, set the combobox to a blank item elif val == u"": if combobox.currentIndex() >= 0 or not combobox.isEnabled(): combobox.setDisabled(False) combobox.setCurrentIndex(-1) # If an unknown value has been specified, notify the user else: if combobox.isEnabled(): combobox.setDisabled(True) self.user_hint_widget.add( _( u'"%s" is set to a variable or ' u"unknown value and can only be edited through " u"the script." ) % var ) for var, spinbox in list(self.auto_spinbox.items()) + list(self.auto_slider.items()): if isinstance(var, int): continue if var in self.var: val = self.var.get(var, _eval=False) if type(val) in (float, int): if spinbox.value() == val and spinbox.isEnabled(): continue spinbox.setDisabled(False) try: spinbox.setValue(val) except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") % (var, e)) else: if not spinbox.isEnabled(): continue spinbox.setDisabled(True) self.user_hint_widget.add( _(u'"%s" is defined using ' "variables and can only be edited through the " "script.") % var ) for var, checkbox in self.auto_checkbox.items(): if isinstance(var, int): continue if var in self.var: val = self.var.get(var, _eval=False) if val in [u"yes", u"no"]: checkbox.setDisabled(False) checked = val == u"yes" if checked != checkbox.isChecked(): checkbox.setChecked(checked) else: checkbox.setDisabled(True) self.user_hint_widget.add( _( u'"%s" is defined using ' u"variables or has an invalid value, and can only be " u"edited through the script." ) % var ) for var, qprogedit in self.auto_editor.items(): if isinstance(var, int): continue if var in self.var: val = safe_decode(self.var.get(var, _eval=False)) if val != qprogedit.text(): qprogedit.setText(val) self.user_hint_widget.refresh() def auto_apply_edit_changes(self, rebuild=True): """ desc: Applies the auto-widget controls. keywords: rebuild: Deprecated (does nothing). """ for var, edit in self.auto_line_edit.items(): if isinstance(var, int): continue if edit.isEnabled() and isinstance(var, basestring): val = str(edit.text()).strip() if val != u"": self.var.set(var, val) continue # If no text was entered, we use a default if available ... if hasattr(edit, u"default"): self.var.set(var, edit.default) continue # ... or unset the variable if no default is available. self.var.unset(var) for var, combobox in self.auto_combobox.items(): if isinstance(var, int): continue if combobox.isEnabled() and isinstance(var, basestring): self.var.set(var, str(combobox.currentText())) for var, spinbox in self.auto_spinbox.items(): if isinstance(var, int): continue if spinbox.isEnabled() and isinstance(var, basestring): self.var.set(var, spinbox.value()) for var, slider in self.auto_slider.items(): if isinstance(var, int): continue if slider.isEnabled() and isinstance(var, basestring): self.var.set(var, slider.value()) for var, checkbox in self.auto_checkbox.items(): if isinstance(var, int): continue if checkbox.isEnabled() and isinstance(var, basestring): if checkbox.isChecked(): val = u"yes" else: val = u"no" self.var.set(var, val) for var, qprogedit in self.auto_editor.items(): if isinstance(var, int): continue if isinstance(var, basestring): self.var.set(var, qprogedit.text()) return True def auto_add_widget(self, widget, var=None): """ desc: Adds a widget to the list of auto-widgets. arguments: widget: type: QWidget keywords: var: The name of the experimental variable to be linked to the widget, or None to use an automatically chosen name. type: [str, NoneType] """ # Use the object id as a fallback name if var is None: var = id(widget) debug.msg(var) self.set_focus_widget(widget) if isinstance(widget, QtGui.QSpinBox) or isinstance(widget, QtGui.QDoubleSpinBox): widget.editingFinished.connect(self.apply_edit_changes) self.auto_spinbox[var] = widget elif isinstance(widget, QtGui.QComboBox): widget.activated.connect(self.apply_edit_changes) self.auto_combobox[var] = widget elif isinstance(widget, QtGui.QSlider) or isinstance(widget, QtGui.QDial): widget.valueChanged.connect(self.apply_edit_changes) self.auto_slider[var] = widget elif isinstance(widget, QtGui.QLineEdit): widget.editingFinished.connect(self.apply_edit_changes) self.auto_line_edit[var] = widget elif isinstance(widget, QtGui.QCheckBox): widget.clicked.connect(self.apply_edit_changes) self.auto_checkbox[var] = widget else: raise Exception(u"Cannot auto-add widget of type %s" % widget) def clean_cond(self, cond, default=u"always"): """ desc: Cleans a conditional statement. May raise a dialog box if problems are encountered. arguments: cond: desc: A conditional statement. type: str keywords: default: desc: A default conditional statement, which is used for empty and invalid statements. type: str returns: cond: desc: A clean conditional statement. type: str """ cond = self.syntax.sanitize(cond) if cond.strip() == u"": cond = default try: self.syntax.compile_cond(cond) except osexception as e: self.experiment.notify(u'Failed to compile conditional statement "%s": %s' % (cond, e)) return default return cond def children(self): """ returns: desc: A list of children, including grand children, and so on. type: list """ return [] def is_child_item(self, item_name): """ desc: Checks if an item is somewhere downstream from the current item in the experimental hierarchy. arguments: item_name: desc: The name of the child item. type: unicode returns: desc: True if the current item is offspring of the item, False otherwise. type: bool """ return False def insert_child_item(self, item_name, index=0): """ desc: Inserts a child item, if applicable to the item type. arguments: item_name: desc: The name of the child item. type: unicode keywords: index: desc: The index of the child item. type: int """ pass def remove_child_item(self, item_name, index=0): """ desc: Removes a child item, if applicable to the item type. arguments: item_name: desc: The name of the child item. type: unicode keywords: index: desc: The index of the child item, if applicable. A negative value indicates all instances. type: int """ pass
class qtitem(QtCore.QObject): """Base class for the GUI controls of other items""" initial_view = u'controls' def __init__(self): """Constructor""" QtCore.QObject.__init__(self) # The auto-widgets are stored in name -> (var, widget) dictionaries self.auto_line_edit = {} self.auto_combobox = {} self.auto_spinbox = {} self.auto_slider = {} self.auto_editor = {} self.auto_checkbox = {} self.init_edit_widget() self.lock = False self.maximized = False debug.msg(u'created %s' % self.name) @property def main_window(self): return self.experiment.main_window @property def theme(self): return self.experiment.main_window.theme @property def tabwidget(self): return self.experiment.main_window.tabwidget def open_tab(self, select_in_tree=True): """ desc: Opens the tab if it wasn't yet open, and switches to it. """ self.tabwidget.add(self.widget(), self.item_icon(), self.name) if select_in_tree: self.experiment.main_window.ui.itemtree.select_item(self.name) def close_tab(self): """ desc: Closes the tab if it was open. """ self.tabwidget.remove(self.widget()) def set_focus(self): """ desc: Gives focus to the most important widget. """ if hasattr(self, u'focus_widget') and self.focus_widget != None: self.focus_widget.setFocus() def set_focus_widget(self, widget, override=False): """ desc: Sets the widget that receives focus when the tab is opened. arguments: widget: desc: The widget to receive focus or `None` to reset. type: [QWidget, NoneType] keywords: override: desc: Indicates whether the focus widget should be changed if there already is a focus widget. type: bool """ if override or not hasattr( self, u'focus_widget') or self.focus_widget == None: self.focus_widget = widget def update_item_icon(self): """ desc: Updates the item icon. """ self.tabwidget.set_icon(self.widget(), self.item_icon()) self.experiment.items.set_icon(self.name, self.item_icon()) self.header_item_icon.setPixmap( self.theme.qpixmap(self.item_icon(), 32)) def item_icon(self): """ returns: desc: The name of the item icon. type: unicode """ return self.item_type def show_tab(self): """ desc: Is called when the tab becomes visible, and updated the contents. """ self.update_script() self.edit_widget() self.set_focus() def widget(self): """ returns: desc: The widget that is added to the tabwidget. type: QWidget """ return self.container_widget def init_edit_widget(self, stretch=True): """ desc: Builds the UI. keywords: stretch: desc: Indicates whether a vertical stretch should be added to the bottom of the controls. This is necessary if the controls don't expand. type: bool """ # Header widget self.header = header_widget.header_widget(self) self.user_hint_widget = user_hint_widget.user_hint_widget( self.experiment.main_window, self) self.header_hbox = QtGui.QHBoxLayout() self.header_item_icon = self.experiment.label_image(self.item_icon()) self.header_hbox.addWidget(self.header_item_icon) self.header_hbox.addWidget(self.header) self.header_hbox.setContentsMargins(0, 5, 0, 10) # Maximize button self.button_toggle_maximize = QtGui.QPushButton( self.theme.qicon(u'view-fullscreen'), u'') self.button_toggle_maximize.setToolTip(_(u'Toggle pop-out')) self.button_toggle_maximize.setIconSize(QtCore.QSize(16, 16)) self.button_toggle_maximize.clicked.connect(self.toggle_maximize) self.header_hbox.addWidget(self.button_toggle_maximize) # View button self.button_view = item_view_button(self) self.header_hbox.addWidget(self.button_view) # Help button self.button_help = QtGui.QPushButton(self.experiment.icon(u"help"), u"") self.button_help.setToolTip( _(u"Tell me more about the %s item") % self.item_type) self.button_help.setIconSize(QtCore.QSize(16, 16)) self.button_help.clicked.connect(self.open_help_tab) self.header_hbox.addWidget(self.button_help) self.header_widget = QtGui.QWidget() self.header_widget.setLayout(self.header_hbox) # The edit_grid is the layout that contains the actual controls for the # items. self.edit_grid = QtGui.QGridLayout() self.edit_grid.setColumnStretch(2, 2) self.edit_grid_widget = QtGui.QWidget() self.edit_grid.setMargin(0) self.edit_grid_widget.setLayout(self.edit_grid) # The edit_vbox contains the edit_grid and the header widget self.edit_vbox = QtGui.QVBoxLayout() self.edit_vbox.setMargin(5) self.edit_vbox.addWidget(self.user_hint_widget) self.edit_vbox.addWidget(self.edit_grid_widget) if stretch: self.edit_vbox.addStretch() self._edit_widget = QtGui.QWidget() self._edit_widget.setWindowIcon(self.experiment.icon(self.item_type)) self._edit_widget.setLayout(self.edit_vbox) # The _script_widget contains the script editor from QProgEdit import QTabManager self._script_widget = QTabManager( handlerButtonText=_(u'Apply and close'), cfg=cfg) self._script_widget.focusLost.connect(self.apply_script_changes) self._script_widget.handlerButtonClicked.connect( self.set_view_controls) self._script_widget.addTab(u'Script').setLang(u'OpenSesame') # The container_widget is the top-level widget that is actually inserted # into the tab widget. self.splitter = qtitem_splitter(self) if self.initial_view == u'controls': self.set_view_controls() elif self.initial_view == u'script': self.set_view_script() elif self.initial_view == u'split': self.set_view_split() else: debug.msg(u'Invalid initial_view: %s' % self.initial_view, reason=u'warning') self.set_view_controls() self.splitter.splitterMoved.connect(self.splitter_moved) self.container_vbox = QtGui.QVBoxLayout() self.container_vbox.addWidget(self.header_widget) self.container_vbox.addWidget(self.splitter) self.container_widget = QtGui.QWidget() self.container_widget.setLayout(self.container_vbox) self.container_widget.on_activate = self.show_tab self.container_widget.__item__ = self.name def splitter_moved(self, pos, index): """ desc: Is called when the splitter handle is manually moved. arguments: pos: desc: The splitter-handle position. type: int index: desc: The index of the splitter handle. Since there is only one handle, this is always 0. type: int """ sizes = self.splitter.sizes() self.edit_size = sizes[0] self.script_size = sizes[1] if self.script_size == 0: self.button_view.set_view_icon(u'controls') elif self.edit_size == 0: self.button_view.set_view_icon(u'script') else: self.button_view.set_view_icon(u'split') def set_view_controls(self): """ desc: Puts the splitter in control view. """ self.splitter.setSizes([self.splitter.width(), 0]) self.button_view.set_view_icon(u'controls') def set_view_script(self): """ desc: Puts the splitter in script view. """ self.splitter.setSizes([0, self.splitter.width()]) self.button_view.set_view_icon(u'script') def set_view_split(self): """ desc: Puts the splitter in split view. """ self.splitter.setSizes( [self.splitter.width() / 2, self.splitter.width() / 2]) self.button_view.set_view_icon(u'split') def update(self): """ desc: Updates both the script and the controls. """ self.update_script() self.edit_widget() def update_script(self): """ desc: Regenerates the script and updates the script widget. """ # Normally, the script starts with a 'define' line and is indented by # a tab. We want to undo this, and present only unindented content. import textwrap script = self.to_string() script = script[script.find(u'\t'):] script = textwrap.dedent(script) self._script_widget.setText(script) def edit_widget(self, *deprecated, **_deprecated): """ desc: This function updates the controls based on the item state. """ debug.msg() self.auto_edit_widget() self.header.refresh() def apply_edit_changes(self, *deprecated, **_deprecated): """ desc: Applies changes to the graphical controls. """ debug.msg() self.auto_apply_edit_changes() self.update_script() self.main_window.set_unsaved(True) return True def apply_script_changes(self, *deprecated, **_deprecated): """ desc: Applies changes to the script, by re-parsing the item from string. """ debug.msg() old_script = self.to_string() new_script = self._script_widget.text() try: self.from_string(new_script) except osexception as e: self.experiment.notify(e.html()) self.main_window.print_debug_window(e) self.from_string(old_script) self.edit_widget() self.main_window.set_unsaved(True) def rename(self, from_name, to_name): """ desc: Handles renaming of an item (not necesarrily the current item). arguments: from_name: desc: The old item name. type: unicode to_name: desc: The new item name type: unicode """ if self.name != from_name: return self.name = to_name self.container_widget.__item__ = self.name self.header.set_name(to_name) index = self.tabwidget.indexOf(self.widget()) if index != None: self.tabwidget.setTabText(index, to_name) def open_help_tab(self): """ desc: Opens a help tab. """ self.experiment.main_window.ui.tabwidget.open_help(self.item_type) def toggle_maximize(self): """ desc: Toggles edit-widget maximization. """ if not self.maximized: # Always ignore close events. This is necessary, because otherwise # the pop-out widget can be closed without re-enabling the main # window. self.container_widget.closeEvent = lambda e: e.ignore() self.container_widget.setParent(None) self.container_widget.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint|\ QtCore.Qt.WindowMaximizeButtonHint|\ QtCore.Qt.CustomizeWindowHint) self.container_widget.showMaximized() self.container_widget.show() self.button_toggle_maximize.setIcon( self.theme.qicon(u'view-restore')) else: self.container_widget.setParent(self.main_window) self.open_tab() self.button_toggle_maximize.setIcon( self.theme.qicon(u'view-fullscreen')) self.maximized = not self.maximized self.user_hint_widget.disable(self.maximized) self.button_help.setDisabled(self.maximized) self.main_window.setDisabled(self.maximized) def delete(self, item_name, item_parent=None, index=None): """ Delete an item (not necessarily the current one) Arguments: item_name -- the name of the item to be deleted Keywords arguments: item_parent -- the parent item (default=None) index -- the index of the item in the parent (default=None) """ pass def build_item_tree(self, toplevel=None, items=[], max_depth=-1, extra_info=None): """ Construct an item tree Keyword arguments: toplevel -- the toplevel widget (default = None) items -- a list of items that have been added, to prevent recursion (default=[]) """ widget = tree_item_item(self, extra_info=extra_info) items.append(self.name) if toplevel != None: toplevel.addChild(widget) return widget def parents(self): """ Creates a list of all the items that the current sequences is connected to upstream Returns: A list of item names """ l = [self.name] for item in self.experiment.items: if self.experiment.items[item].is_child_item(self.name): l.append(item) return l def variable_vars(self, exclude=[]): """ Determines if one of the variables of the current item is defined in terms of another variable Keywords arguments: exclude -- a list of variables that should not be checked Returns: True if there are variably defined variables, False otherwise """ for var in self.variables: if var not in exclude: val = self.variables[var] if self.experiment.varref(val): return True return False def get_ready(self): """ This function should be overridden to do any last-minute stuff that and item should do before an experiment is actually run, such as applying pending script changes. Returns: True if some action has been taken, False if nothing was done """ return False def auto_edit_widget(self): """Update the GUI controls based on the auto-widgets""" debug.msg() for var, edit in self.auto_line_edit.iteritems(): if self.has(var): edit.setText(self.unistr(self.get(var, _eval=False))) else: edit.setText(u'') for var, combobox in self.auto_combobox.iteritems(): val = self.get_check(var, _eval=False, default=u'') i = combobox.findText(self.unistr(val)) # Set the combobox to the select item if i >= 0: combobox.setDisabled(False) combobox.setCurrentIndex(i) # If no value was specified, set the combobox to a blank item elif val == u'': combobox.setDisabled(False) combobox.setCurrentIndex(-1) # If an unknown value has been specified, notify the user else: combobox.setDisabled(True) self.user_hint_widget.add( _(u'"%s" is set to a ' u'variable or unknown value and can only be edited through ' u'the script.' % var)) for var, spinbox in self.auto_spinbox.iteritems(): if self.has(var): val = self.get(var, _eval=False) if type(val) in (float, int): spinbox.setDisabled(False) try: spinbox.setValue(val) except Exception as e: self.experiment.notify(_( \ u"Failed to set control '%s': %s") % (var, e)) else: spinbox.setDisabled(True) self.user_hint_widget.add(_( \ u'"%s" is defined using variables and can only be edited through the script.' \ % var)) for var, slider in self.auto_slider.iteritems(): if self.has(var): val = self.get(var, _eval=False) if type(val) in (float, int): slider.setDisabled(False) try: slider.setValue(val) except Exception as e: self.experiment.notify(_( \ u"Failed to set control '%s': %s") % (var, e)) else: slider.setDisabled(True) self.user_hint_widget.add(_( \ u'"%s" is defined using variables and can only be edited through the script.' \ % var)) for var, checkbox in self.auto_checkbox.iteritems(): if self.has(var): try: checkbox.setChecked(self.get(var, _eval=False) == u"yes") except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) for var, qprogedit in self.auto_editor.iteritems(): if self.has(var): try: qprogedit.setText(self.unistr(self.get(var, _eval=False))) except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) def sanitize_check(self, s, strict=False, allow_vars=True, notify=True): """ Checks whether a string is sane (i.e. unchanged by sanitize()) and optionally presents a warning. Arguments: s -- The string to check. Keyword arguments: strict -- See sanitize(). allow_vars -- See sanitize(). notify -- Indicates whether a notification should be presented if the string is not sane. Returns: True if s is sane, False otherwise. """ sane = s == self.sanitize(s, strict=strict, allow_vars=allow_vars) if not sane and notify: if strict: self.experiment.notify( _(u'All non-alphanumeric characters except underscores have been stripped' )) else: self.experiment.notify( _(u'The following characters are not allowed and have been stripped: double-quote ("), backslash (\), and newline' )) return sane def auto_apply_edit_changes(self, rebuild=True): """ Apply the auto-widget controls Keyword arguments: rebuild -- deprecated (does nothing) (default=True) """ debug.msg() for var, edit in self.auto_line_edit.iteritems(): if edit.isEnabled() and isinstance(var, basestring): val = unicode(edit.text()).strip() if val != u"": self.set(var, val) # If the variable has no value, we assign a default value if it # has been specified, and unset it otherwise. elif hasattr(edit, u"default"): self.set(var, edit.default) else: self.unset(var) for var, combobox in self.auto_combobox.iteritems(): if combobox.isEnabled() and isinstance(var, basestring): self.set(var, unicode(combobox.currentText())) for var, spinbox in self.auto_spinbox.iteritems(): if spinbox.isEnabled() and isinstance(var, basestring): self.set(var, spinbox.value()) for var, slider in self.auto_slider.iteritems(): if slider.isEnabled() and isinstance(var, basestring): self.set(var, slider.value()) for var, checkbox in self.auto_checkbox.iteritems(): if checkbox.isEnabled() and isinstance(var, basestring): if checkbox.isChecked(): val = u"yes" else: val = u"no" self.set(var, val) for var, qprogedit in self.auto_editor.iteritems(): if isinstance(var, basestring): self.set(var, qprogedit.text()) return True def auto_add_widget(self, widget, var=None): """ Add a widget to the list of auto-widgets Arguments: widget -- a QWidget Keyword arguments: var -- the variable to be linked to the widget (default=None) """ # Use the object id as a fallback name if var == None: var = id(widget) debug.msg(var) self.set_focus_widget(widget) if isinstance(widget, QtGui.QSpinBox) or isinstance( widget, QtGui.QDoubleSpinBox): widget.editingFinished.connect(self.apply_edit_changes) self.auto_spinbox[var] = widget elif isinstance(widget, QtGui.QComboBox): widget.activated.connect(self.apply_edit_changes) self.auto_combobox[var] = widget elif isinstance(widget, QtGui.QSlider): widget.editingFinished.connect(self.apply_edit_changes) self.auto_slider[var] = widget elif isinstance(widget, QtGui.QLineEdit): widget.editingFinished.connect(self.apply_edit_changes) self.auto_line_edit[var] = widget elif isinstance(widget, QtGui.QCheckBox): widget.clicked.connect(self.apply_edit_changes) self.auto_checkbox[var] = widget else: raise Exception(u"Cannot auto-add widget of type %s" % widget) def clean_cond(self, cond, default=u'always'): """ Cleans a conditional statement. May raise a dialog box if problems are encountered. Arguments: cond -- A (potentially filthy) conditional statement. Keyword arguments: default -- A default value to use for empty Returns: cond -- A clean conditional statement conditional statements. (default=u'always') """ cond = self.unistr(cond) if not self.sanitize_check(cond): cond = self.sanitize(cond) if cond.strip() == u'': cond = default try: self.compile_cond(cond) except osexception as e: self.experiment.notify( \ u'Failed to compile conditional statement "%s": %s' % (cond, e)) return default return cond def children(self): """ returns: desc: A list of children, including grand children, and so on. type: list """ return [] def is_child_item(self, item_name): """ desc: Checks if an item is somewhere downstream from the current item in the experimental hierarchy. arguments: item_name: desc: The name of the child item. type: unicode returns: desc: True if the current item is offspring of the item, False otherwise. type: bool """ return False def insert_child_item(self, item_name, index=0): """ desc: Inserts a child item, if applicable to the item type. arguments: item_name: desc: The name of the child item. type: unicode keywords: index: desc: The index of the child item. type: int """ pass def remove_child_item(self, item_name, index=0): """ desc: Removes a child item, if applicable to the item type. arguments: item_name: desc: The name of the child item. type: unicode keywords: index: desc: The index of the child item, if applicable. A negative value indicates all instances. type: int """ pass
class inline_script(libopensesame.inline_script.inline_script, qtitem.qtitem): """The inline_script GUI controls""" def __init__(self, name, experiment, string=None): """ Constructor. Arguments: name -- The item name. experiment -- The experiment object. Keywords arguments: string -- A definition string. (default=None) """ libopensesame.inline_script.inline_script.__init__(self, name, \ experiment, string) qtitem.qtitem.__init__(self) self.lock = False self._var_info = None def apply_edit_changes(self, **args): """ Applies the controls. Keywords arguments: args -- A dictionary to accept unused keyword arguments. """ qtitem.qtitem.apply_edit_changes(self, False) sp = self.qprogedit.text(index=0) sr = self.qprogedit.text(index=1) self.set(u'_prepare', sp) self.set(u'_run', sr) self.lock = True self._var_info = None self.experiment.main_window.refresh(self.name) self.lock = False def init_edit_widget(self): """Constructs the GUI controls.""" from QProgEdit import QTabManager qtitem.qtitem.init_edit_widget(self, False) self.qprogedit = QTabManager(handler=self.apply_edit_changes, \ defaultLang=u'Python', cfg=cfg, focusOutHandler= \ self.apply_edit_changes) self.qprogedit.addTab(u'Prepare') self.qprogedit.addTab(u'Run') # Switch to the run phase, unless there is only content for the prepare # phase. if self._run == u'' and self._prepare != u'': self.qprogedit.setCurrentIndex(0) else: self.qprogedit.setCurrentIndex(1) self.edit_vbox.addWidget(self.qprogedit) def edit_widget(self): """ Updates the GUI controls. Returns: The control QWidget. """ qtitem.qtitem.edit_widget(self, False) if not self.lock: self.qprogedit.setText(self._prepare, index=0) self.qprogedit.setText(self._run, index=1) return self._edit_widget def get_ready(self): """Applies pending script changes.""" if self.qprogedit.isModified(): debug.msg(u'applying pending script changes') self.apply_edit_changes(catch=False) return True return qtitem.qtitem.get_ready(self)
class inline_script(inline_script_runtime, qtplugin): """The inline_script GUI controls""" description = _(u'Executes Python code') help_url = u'manual/python/about' def __init__(self, name, experiment, string=None): """See item.""" inline_script_runtime.__init__(self, name, experiment, string) qtplugin.__init__(self) def apply_edit_changes(self): """See qtitem.""" qtplugin.apply_edit_changes(self) sp = self.qprogedit.text(index=0) sr = self.qprogedit.text(index=1) self.var._prepare = sp self.var._run = sr self.update_item_icon() def set_focus(self): """ desc: Allows the item to focus the most important widget. """ self.qprogedit.setFocus() def item_icon(self): """ desc: Determines the icon, based on whether the scripts are syntactically correct. returns: desc: An icon name. type: unicode """ status = max( self.experiment.python_workspace.check_syntax( self.var.get(u'_prepare', _eval=False)), self.experiment.python_workspace.check_syntax( self.var.get(u'_run', _eval=False))) if status == 2: return u'os-inline_script-syntax-error' if status == 1: return u'os-inline_script-syntax-warning' return u'os-inline_script' def build_item_tree(self, toplevel=None, items=[], max_depth=-1, extra_info=None): """See qtitem.""" widget = tree_inline_script_item(self, extra_info=extra_info, symbols=(max_depth < 0 or max_depth > 1)) items.append(self.name) if toplevel is not None: toplevel.addChild(widget) return widget def init_edit_widget(self): """See qtitem.""" from QProgEdit import QTabManager qtplugin.init_edit_widget(self, stretch=False) self.qprogedit = QTabManager(cfg=cfg, runButton=True) self.qprogedit.execute.connect(self.main_window.console.execute) self.qprogedit.handlerButtonClicked.connect(self.apply_edit_changes) self.qprogedit.focusLost.connect(self.apply_edit_changes) self.qprogedit.cursorRowChanged.connect(self.apply_edit_changes) self.qprogedit.addTab(_(u'Prepare')).setLang(u'Python') self.qprogedit.addTab(_(u'Run')).setLang(u'Python') # Switch to the run phase, unless there is only content for the prepare # phase. if self.var._run == u'' and self.var._prepare != u'': self.qprogedit.setCurrentIndex(0) else: self.qprogedit.setCurrentIndex(1) self.edit_vbox.addWidget(self.qprogedit) def edit_widget(self): """See qtitem.""" qtplugin.edit_widget(self) _prepare = safe_decode(self.var._prepare) if _prepare != self.qprogedit.tab(0).text(): self.qprogedit.tab(0).setText(_prepare) _run = safe_decode(self.var._run) if _run != self.qprogedit.tab(1).text(): self.qprogedit.tab(1).setText(_run) def get_ready(self): """See qtitem.""" if self.qprogedit.isAnyModified(): debug.msg(u'applying pending script changes') self.apply_edit_changes() return True return qtplugin.get_ready(self)
class qtitem(QtCore.QObject): """Base class for the GUI controls of other items""" def __init__(self): """Constructor""" QtCore.QObject.__init__(self) # The auto-widgets are stored in name -> (var, widget) dictionaries self.auto_line_edit = {} self.auto_combobox = {} self.auto_spinbox = {} self.auto_slider = {} self.auto_editor = {} self.auto_checkbox = {} self.sanity_criteria = {} self.init_edit_widget() self.init_script_widget() self.script_tab = None self.lock = False self.edit_mode = u'edit' debug.msg(u'created %s' % self.name) def open_help_tab(self, page=None): """Opens a help tab.""" self.experiment.main_window.ui.tabwidget.open_help(self.item_type) def open_tab(self): """Opens the correct tab based on the current edit mode""" if self.edit_mode == u'edit': self.open_edit_tab() else: self.open_script_tab() def init_edit_widget(self, stretch=True): """Build the GUI controls""" # Header widget self.header = header_widget.header_widget(self) self.user_hint_widget = user_hint_widget.user_hint_widget( \ self.experiment.main_window, self) self.header_hbox = QtGui.QHBoxLayout() self.header_hbox.addWidget(self.experiment.label_image(self.item_type)) self.header_hbox.addWidget(self.header) self.header_hbox.addStretch() self.header_hbox.setContentsMargins(0, 5, 0, 10) # Edit script button button = QtGui.QPushButton(self.experiment.icon(u"script"), u"") button.setToolTip(_(u"Edit script")) button.setIconSize(QtCore.QSize(16, 16)) QtCore.QObject.connect(button, QtCore.SIGNAL(u"clicked()"), \ self.open_script_tab) self.header_hbox.addWidget(button) # Help button button = QtGui.QPushButton(self.experiment.icon(u"help"), u"") button.setToolTip( _(u"Tell me more about the %s item") % self.item_type) button.setIconSize(QtCore.QSize(16, 16)) QtCore.QObject.connect(button, QtCore.SIGNAL(u"clicked()"), \ self.open_help_tab) self.header_hbox.addWidget(button) self.header_widget = QtGui.QWidget() self.header_widget.setLayout(self.header_hbox) # The edit_grid is the layout that contains the actual controls for the # items. self.edit_grid = QtGui.QGridLayout() self.edit_grid.setColumnStretch(2, 2) self.edit_grid_widget = QtGui.QWidget() self.edit_grid.setMargin(0) self.edit_grid_widget.setLayout(self.edit_grid) # The edit_vbox contains the edit_grid and the header widget self.edit_vbox = QtGui.QVBoxLayout() self.edit_vbox.setMargin(5) self.edit_vbox.addWidget(self.header_widget) self.edit_vbox.addWidget(self.user_hint_widget) self.edit_vbox.addWidget(self.edit_grid_widget) if stretch: self.edit_vbox.addStretch() self._edit_widget = QtGui.QWidget() self._edit_widget.setLayout(self.edit_vbox) self._edit_widget.__edit_item__ = self.name return self._edit_widget def edit_widget(self, stretch=True): """ A dummy edit widget, to be overridden. Keywords arguments: stretch -- DEPRECATED (default=True) """ if not stretch: debug.msg(u"passing the stretch argument is deprecated", \ reason=u"deprecation") self.user_hint_widget.clear() self.header.restore_name(False) self.header.refresh() self._edit_widget.__edit_item__ = self.name if not self.sanity_check(): self.open_script_tab() return self.auto_edit_widget() self.user_hint_widget.refresh() return self._edit_widget def apply_name_change(self, rebuild=True): """ Apply an item name change Keywords arguments: rebuild -- a deprecated argument (default=True) """ debug.msg() # Sanitize the name, check if it is new and valid, and if so, rename new_name = self.experiment.sanitize(self.header.edit_name.text(), \ strict=True, allow_vars=False) if new_name.lower() != self.name.lower(): valid = self.experiment.check_name(new_name) if valid != True: self.experiment.notify(valid) self.header.edit_name.setText(self.name) return old_name = self.name self.name = new_name self._edit_widget.__edit_item__ = new_name self.experiment.main_window.dispatch.event_name_change.emit(old_name, \ new_name) def apply_edit_changes(self, rebuild=True): """ Applies the GUI controls. Keywords arguments: rebuild -- Specifies whether the overview area (item list) should be rebuild. (default=True) """ debug.msg(self.name) if self.experiment.main_window.lock_refresh: debug.msg(u"skipping, because refresh in progress") return False self.auto_apply_edit_changes() self.set(u"description", \ self.experiment.sanitize(unicode( \ self.header.edit_desc.text()).strip())) if self.description == u"": self.description = u"No description" self.header.label_desc.setText(self.description) self.experiment.main_window.dispatch.event_simple_change.emit( self.name) return True def close_edit_tab(self, index=None): """ Closes the edit tab (does nothing by default). Keywords arguments: index -- The index of the tab in the tab area. (default=None) """ pass def open_edit_tab(self, index=None, focus=True): """ Opens the GUI control tab, or switches to the tab if it was already open. Keywords arguments: index -- The index of the tab (if open). (default=None) focus -- Indicates whether the tab should receive focus. (default=True) """ debug.msg(u"%s (#%s)" % (self.name, hash(self))) # Switch to edit mode and close the script tab if it was open self.edit_mode = u"edit" for i in range(self.experiment.ui.tabwidget.count()): w = self.experiment.ui.tabwidget.widget(i) if hasattr(w, u"__script_item__") and w.__script_item__ == \ self.name: self.experiment.ui.tabwidget.removeTab(i) if index == None: index = i break # Focus the edit tab, instead of reopening, if it was already open for i in range(self.experiment.ui.tabwidget.count()): w = self.experiment.ui.tabwidget.widget(i) if hasattr(w, u"__edit_item__") and w.__edit_item__ == self.name: index = i # Refresh the controls on the tab. In debug mode don't catch any errors if debug.enabled: widget = self.edit_widget() else: try: widget = self.edit_widget() except Exception as e: self.experiment.notify(_(u"%s (Edit the script to fix this)") \ % e) self.open_script_tab() return # Open the tab or focus the tab if it was already open if index == None: self.edit_tab_index = self.experiment.ui.tabwidget.addTab(widget, \ self.experiment.icon(self.item_type), u"%s" % self.name) else: self.experiment.ui.tabwidget.insertTab(index, widget, \ self.experiment.icon(self.item_type), u"%s" % self.name) self.edit_tab_index = index if focus: self.experiment.ui.tabwidget.setCurrentIndex(self.edit_tab_index) def apply_script_and_close(self): """Applies script changes and opens the edit tab""" self.apply_script_changes(mode=u'edit') self.experiment.main_window.select_item(self.name) def apply_script_changes(self, rebuild=True, catch=True, mode=u'script'): """ Applies changes to the script, by regenerating the item from the script. Keywords arguments: rebuild -- Specifies whether the overview area (item list) should be rebuild. (default=True) catch -- Indicates whether exceptions should be caught and shown in a notification dialog (True) or not be caught (False). (default=True) mode -- Indicates whether the item should re-open in edit or script mode. (default=u'script') """ debug.msg(self.name) script = self.script_qprogedit.text() # Create a new item and make it a clone of the current item item = self.experiment.main_window.add_item(self.item_type, False, \ name=self.name, interactive=False) if catch: try: self.experiment.items[item].from_string(script) except Exception as e: self.experiment.notify(unicode(e)) return else: self.experiment.items[item].from_string(script) self.experiment.items[item].name = self.name # Replace the current item self.experiment.items[self.name] = self.experiment.items[item] del self.experiment.items[item] self.experiment.items[self.name].init_script_widget() self.experiment.items[self.name].edit_mode = mode self.experiment.main_window.dispatch.event_script_change.emit( self.name) # The logic here is pretty complex, and is more-or-less a hack until the # event handling code has been improved. Basically, if we want to apply # the script and stay in script mode, we have to re-open the script tab, # because the entire item is re-generated. This new tab has to be # inserted in place of (i.e. with the same index as) the old tab, which # has to be removed. We always refocus the tab, but if the tab doesn't # actually have focus, we refocus the original tab. This is necessary # to avoid repainting artifacts. # # See also this issue: # - <https://github.com/smathot/OpenSesame/issues/219> if mode == u'script': currentIndex = self.experiment.ui.tabwidget.currentIndex() for i in range(self.experiment.ui.tabwidget.count()): w = self.experiment.ui.tabwidget.widget(i) if hasattr(w, u'__script_item__') and w.__script_item__ == \ self.name: if i == currentIndex: focus = True else: focus = False self.experiment.items[self.name].open_script_tab(index=i, \ focus=True) self.experiment.ui.tabwidget.removeTab(i + 1) if not focus: self.experiment.ui.tabwidget.setCurrentIndex( \ currentIndex) break def strip_script_line(self, s): """ Strips unwanted characters from a line of script Arguments: s -- a line of script Returns: A stripped line of script """ if len(s) > 0 and s[0] == u"\t": return s[1:] + u"\n" return s + u"\n" def init_script_widget(self): """Build the script tab""" from QProgEdit import QTabManager self.script_qprogedit = QTabManager(handler= \ self.apply_script_and_close, defaultLang=u'OpenSesame', \ handlerButtonText=_(u'Apply and close script editor'), \ focusOutHandler=self.apply_script_changes, cfg=cfg) self.script_qprogedit.addTab(u'Script') hbox = QtGui.QHBoxLayout() hbox.addWidget(self.experiment.label_image(self.item_type)) self.script_header = QtGui.QLabel() hbox.addWidget(self.script_header) hbox.addStretch() hbox.setContentsMargins(0, 0, 0, 0) hwidget = QtGui.QWidget() hwidget.setLayout(hbox) vbox = QtGui.QVBoxLayout() vbox.addWidget(hwidget) vbox.addWidget(self.script_qprogedit) self._script_widget = QtGui.QWidget() self._script_widget.setLayout(vbox) self._script_widget.__script_item__ = self.name def script_widget(self): """ Update the script tab Returns: The QWidget containing the script tab """ self.script_header.setText( \ _(u"Editing script for <b>%s</b> - %s") % (self.name, \ self.item_type)) script = u"" for s in self.to_string().split(u"\n")[1:]: script += self.strip_script_line(s) self.script_qprogedit.setText(script) self._script_widget.__script_item__ = self.name return self._script_widget def open_script_tab(self, index=None, focus=True): """ Open/ show the script tab Keywords arguments: index -- the index of the tab (if it is already open) (default=None) focus -- indicates whether the tab should receive focus (default=True) """ debug.msg(u"%s (#%s)" % (self.name, hash(self))) self.edit_mode = u"script" # Close the edit tab for i in range(self.experiment.ui.tabwidget.count()): w = self.experiment.ui.tabwidget.widget(i) if hasattr(w, u"__edit_item__") and w.__edit_item__ == self.name: self.experiment.ui.tabwidget.removeTab(i) if index == None: index = i break for i in range(self.experiment.ui.tabwidget.count()): w = self.experiment.ui.tabwidget.widget(i) if hasattr(w, u"__script_item__") and w.__script_item__ == self.name: index = i if index == None: self.script_tab_index = self.experiment.ui.tabwidget.addTab( \ self.script_widget(), self.experiment.icon(u"script"), u"%s" \ % self.name) else: self.script_tab_index = index self.experiment.ui.tabwidget.insertTab(index, \ self.script_widget(), self.experiment.icon(u"script"), u"%s" \ % self.name) if focus: self.experiment.ui.tabwidget.setCurrentIndex(self.script_tab_index) def close_script_tab(self, index=None): """ Close the script tab (does nothing by defaut) Keywords arguments: index -- the index of the tab in the tab area (default=None) """ pass def rename(self, from_name, to_name): """ Handle the renaming of an item (not necesarrily the currnet item) Arguments: from_name -- the old item name to_name -- the new item name """ if self.name == from_name: self.name = to_name def delete(self, item_name, item_parent=None, index=None): """ Delete an item (not necessarily the current one) Arguments: item_name -- the name of the item to be deleted Keywords arguments: item_parent -- the parent item (default=None) index -- the index of the item in the parent (default=None) """ pass def rename_var(self, item, from_name, to_name): """ A notification that a variable has been renamed Arguments: item -- the item doing the renaming from_name -- the old variable name to_name -- the new variable name """ pass def item_tree_widget(self, toplevel, icon=None, name=None, tooltip=None): """ Create a single item tree widget Arguments: toplevel -- the toplevel item Keyword arguments: icon -- an icon name or None for default (default=None) name -- the name of the item or None for default (default=None) tooltip -- the tooltip or None for default (default=None) Returns: A QTreeWidgetItem """ if name == None: name = self.name if icon == None: icon = self.item_type if tooltip == None: tooltip = _(u"Type: %s\nDescription: %s") % (self.item_type, \ self.description) font = QtGui.QFont() font.setPointSize(8) font.setItalic(True) widget = QtGui.QTreeWidgetItem(toplevel) widget.setText(0, name) widget.setIcon(0, self.experiment.icon(icon)) widget.name = name widget.setToolTip(0, tooltip) return widget def build_item_tree(self, toplevel=None, items=[]): """ Construct an item tree Keyword arguments: toplevel -- the toplevel widget (default = None) items -- a list of items that have been added, to prevent recursion (default=[]) """ toplevel.addChild(self.item_tree_widget(toplevel)) def is_offspring(self, item): """ Checks if the item is offspring of the current item, in the sense that the current item is contained by the item Arguments: item -- the potential offspring Returns: True if the current item is offspring of the item, False otherwise """ return False def parents(self): """ Creates a list of all the items that the current sequences is connected to upstream Returns: A list of item names """ l = [self.name] for item in self.experiment.items: if self.experiment.items[item].is_offspring(self.name): l.append(item) return l def variable_vars(self, exclude=[]): """ Determines if one of the variables of the current item is defined in terms of another variable Keywords arguments: exclude -- a list of variables that should not be checked Returns: True if there are variably defined variables, False otherwise """ for var in self.variables: if var not in exclude: val = self.variables[var] if isinstance(val, basestring) and u'[' in val: return True return False def get_ready(self): """ This function should be overridden to do any last-minute stuff that and item should do before an experiment is actually run, such as applying pending script changes. Returns: True if some action has been taken, False if nothing was done """ if self.script_qprogedit.isModified(): debug.msg(u'applying pending script changes') self.apply_script_changes(catch=False) return True return False def auto_edit_widget(self): """Update the GUI controls based on the auto-widgets""" debug.msg() for var, edit in self.auto_line_edit.iteritems(): edit.editingFinished.disconnect() if self.has(var): try: edit.setText(self.unistr(self.get(var, _eval=False))) except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) else: edit.setText(u"") edit.editingFinished.connect(self.apply_edit_changes) for var, combobox in self.auto_combobox.iteritems(): combobox.currentIndexChanged.disconnect() if self.has(var): try: combobox.setCurrentIndex(combobox.findText( \ self.unistr(self.get(var, _eval=False)))) except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) combobox.currentIndexChanged.connect(self.apply_edit_changes) for var, spinbox in self.auto_spinbox.iteritems(): spinbox.editingFinished.disconnect() if self.has(var): val = self.get(var, _eval=False) if type(val) in (float, int): try: spinbox.setValue(val) except Exception as e: self.experiment.notify(_( \ u"Failed to set control '%s': %s") % (var, e)) else: spinbox.setDisabled(True) self.user_hint_widget.add_user_hint(_( \ u'"%s" is defined using variables and can only be edited through the script.' \ % var)) spinbox.editingFinished.connect(self.apply_edit_changes) for var, slider in self.auto_slider.iteritems(): slider.valueChanged.disconnect() if self.has(var): val = self.get(var, _eval=False) if type(val) in (float, int): try: slider.setValue(val) except Exception as e: self.experiment.notify(_( \ u"Failed to set control '%s': %s") % (var, e)) else: slider.setDisabled(True) self.user_hint_widget.add_user_hint(_( \ u'"%s" is defined using variables and can only be edited through the script.' \ % var)) slider.valueChanged.connect(self.apply_edit_changes) for var, checkbox in self.auto_checkbox.iteritems(): checkbox.toggled.disconnect() if self.has(var): try: checkbox.setChecked(self.get(var, _eval=False) == u"yes") except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) checkbox.toggled.connect(self.apply_edit_changes) for var, qprogedit in self.auto_editor.iteritems(): if self.has(var): try: qprogedit.setText(self.unistr(self.get(var, _eval=False))) except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) def sanitize_check(self, s, strict=False, allow_vars=True, notify=True): """ Checks whether a string is sane (i.e. unchanged by sanitize()) and optionally presents a warning. Arguments: s -- The string to check. Keyword arguments: strict -- See sanitize(). allow_vars -- See sanitize(). notify -- Indicates whether a notification should be presented if the string is not sane. Returns: True if s is sane, False otherwise. """ sane = s == self.sanitize(s, strict=strict, allow_vars=allow_vars) if not sane and notify: if strict: self.experiment.notify( _(u'All non-alphanumeric characters except underscores have been stripped' )) else: self.experiment.notify( _(u'The following characters are not allowed and have been stripped: double-quote ("), backslash (\), and newline' )) return sane def sanity_check(self): """ Checks whether all variables match prespecified criteria and fall back to the script editor otherwise. This is usefull to check that certain variables are numeric, etc. """ debug.msg() errors = [] for var_name, criteria in self.sanity_criteria.items(): msg = _(u"Invalid or missing value for variable '%s' (edit script to fix this)") \ % var_name if u'msg' in criteria: msg += u': ' + criteria[u'msg'] if not self.has(var_name) and u'required' in criteria and \ criteria[u'required']: self.experiment.notify(msg) return False else: var = self.get(var_name, _eval=False) if u'type' in criteria: _type = criteria[u'type'] if type(_type) != list: _type = [_type] if type(var) not in _type: self.experiment.notify(msg) return False if u'func' in criteria: if not criteria[u'func'](var): self.experiment.notify(msg) return False return True def auto_apply_edit_changes(self, rebuild=True): """ Apply the auto-widget controls Keyword arguments: rebuild -- deprecated (does nothing) (default=True) """ debug.msg() for var, edit in self.auto_line_edit.iteritems(): if edit.isEnabled() and isinstance(var, basestring): val = unicode(edit.text()).strip() if val != u"": self.set(var, val) # If the variable has no value, we assign a default value if it # has been specified, and unset it otherwise. elif hasattr(edit, u"default"): self.set(var, edit.default) else: self.unset(var) for var, combobox in self.auto_combobox.iteritems(): if combobox.isEnabled() and isinstance(var, basestring): self.set(var, unicode(combobox.currentText())) for var, spinbox in self.auto_spinbox.iteritems(): if spinbox.isEnabled() and isinstance(var, basestring): self.set(var, spinbox.value()) for var, slider in self.auto_slider.iteritems(): if slider.isEnabled() and isinstance(var, basestring): self.set(var, slider.value()) for var, checkbox in self.auto_checkbox.iteritems(): if checkbox.isEnabled() and isinstance(var, basestring): if checkbox.isChecked(): val = u"yes" else: val = u"no" self.set(var, val) for var, qprogedit in self.auto_editor.iteritems(): if isinstance(var, basestring): self.set(var, qprogedit.text()) return True def auto_add_widget(self, widget, var=None): """ Add a widget to the list of auto-widgets Arguments: widget -- a QWidget Keyword arguments: var -- the variable to be linked to the widget (default=None) """ # Use the object id as a fallback name if var == None: var = id(widget) debug.msg(var) if isinstance(widget, QtGui.QSpinBox) or isinstance(widget, \ QtGui.QDoubleSpinBox): widget.editingFinished.connect(self.apply_edit_changes) self.auto_spinbox[var] = widget elif isinstance(widget, QtGui.QComboBox): widget.currentIndexChanged.connect(self.apply_edit_changes) self.auto_combobox[var] = widget elif isinstance(widget, QtGui.QSlider): widget.editingFinished.connect(self.apply_edit_changes) self.auto_slider[var] = widget elif isinstance(widget, QtGui.QLineEdit): widget.editingFinished.connect(self.apply_edit_changes) self.auto_line_edit[var] = widget elif isinstance(widget, QtGui.QCheckBox): widget.toggled.connect(self.apply_edit_changes) self.auto_checkbox[var] = widget else: raise Exception(u"Cannot auto-add widget of type %s" % widget) def clean_cond(self, cond, default=u'always'): """ Cleans a conditional statement. May raise a dialog box if problems are encountered. Arguments: cond -- A (potentially filthy) conditional statement. Keyword arguments: default -- A default value to use for empty Returns: cond -- A clean conditional statement conditional statements. (default=u'always') """ cond = self.unistr(cond) if not self.sanitize_check(cond): cond = self.sanitize(cond) if cond.strip() == u'': cond = default try: self.compile_cond(cond) except osexception as e: self.experiment.notify( \ u'Failed to compile conditional statement "%s": %s' % (cond, e)) return cond
def init_edit_widget(self, stretch=True): """ desc: Builds the UI. keywords: stretch: desc: Indicates whether a vertical stretch should be added to the bottom of the controls. This is necessary if the controls don't expand. type: bool """ # Header widget self.header = header_widget.header_widget(self) self.user_hint_widget = user_hint_widget.user_hint_widget(self.experiment.main_window, self) self.header_hbox = QtGui.QHBoxLayout() self.header_item_icon = self.theme.qlabel(self.item_icon()) self.header_hbox.addWidget(self.header_item_icon) self.header_hbox.addWidget(self.header) self.header_hbox.setContentsMargins(0, 2, 0, 0) # Maximize button self.button_toggle_maximize = QtGui.QPushButton(self.theme.qicon(u"view-fullscreen"), u"") self.button_toggle_maximize.setToolTip(_(u"Toggle pop-out")) self.button_toggle_maximize.setIconSize(QtCore.QSize(16, 16)) self.button_toggle_maximize.clicked.connect(self.toggle_maximize) self.header_hbox.addWidget(self.button_toggle_maximize) # View button self.button_view = item_view_button(self) self.header_hbox.addWidget(self.button_view) # Help button self.button_help = QtGui.QPushButton(self.theme.qicon(u"help"), u"") self.button_help.setToolTip(_(u"Tell me more about the %s item") % self.item_type) self.button_help.setIconSize(QtCore.QSize(16, 16)) self.button_help.clicked.connect(self.open_help_tab) self.header_hbox.addWidget(self.button_help) self.header_widget = QtGui.QWidget() self.header_widget.setLayout(self.header_hbox) # The edit_grid is the layout that contains the actual controls for the # items. self.edit_grid = QtGui.QGridLayout() self.edit_grid.setColumnStretch(2, 2) self.edit_grid_widget = QtGui.QWidget() self.edit_grid_widget.setLayout(self.edit_grid) # The edit_vbox contains the edit_grid and the header widget self.edit_vbox = QtGui.QVBoxLayout() self.edit_vbox.addWidget(self.user_hint_widget) self.edit_vbox.addWidget(self.edit_grid_widget) self.edit_vbox.setContentsMargins(0, 0, 0, 0) self.edit_vbox.setSpacing(0) if stretch: self.edit_vbox.addStretch() self._edit_widget = QtGui.QWidget() self._edit_widget.setWindowIcon(self.theme.qicon(self.item_type)) self._edit_widget.setLayout(self.edit_vbox) # The _script_widget contains the script editor from QProgEdit import QTabManager self._script_widget = QTabManager(handlerButtonText=_(u"Apply and close"), cfg=cfg) self._script_widget.focusLost.connect(self.apply_script_changes) self._script_widget.cursorRowChanged.connect(self.apply_script_changes) self._script_widget.handlerButtonClicked.connect(self.set_view_controls) self._script_widget.addTab(u"Script").setLang(u"OpenSesame") # The container_widget is the top-level widget that is actually inserted # into the tab widget. self.splitter = qtitem_splitter(self) if self.initial_view == u"controls": self.set_view_controls() elif self.initial_view == u"script": self.set_view_script() elif self.initial_view == u"split": self.set_view_split() else: debug.msg(u"Invalid initial_view: %s" % self.initial_view, reason=u"warning") self.set_view_controls() self.splitter.splitterMoved.connect(self.splitter_moved) self.container_vbox = QtGui.QVBoxLayout() self.container_vbox.setContentsMargins(4, 0, 4, 4) self.container_vbox.addWidget(self.header_widget) self.container_vbox.addWidget(self.splitter) self.container_widget = QtGui.QWidget() self.container_widget.setLayout(self.container_vbox) self.container_widget.on_activate = self.show_tab self.container_widget.__item__ = self.name
class qtitem(QtCore.QObject): """Base class for the GUI controls of other items""" initial_view = u'controls' def __init__(self): """Constructor""" QtCore.QObject.__init__(self) # The auto-widgets are stored in name -> (var, widget) dictionaries self.auto_line_edit = {} self.auto_combobox = {} self.auto_spinbox = {} self.auto_slider = {} self.auto_editor = {} self.auto_checkbox = {} self.init_edit_widget() self.lock = False self.maximized = False debug.msg(u'created %s' % self.name) @property def main_window(self): return self.experiment.main_window @property def theme(self): return self.experiment.main_window.theme @property def tabwidget(self): return self.experiment.main_window.tabwidget def open_tab(self, select_in_tree=True): """ desc: Opens the tab if it wasn't yet open, and switches to it. """ self.tabwidget.add(self.widget(), self.item_icon(), self.name) if select_in_tree: self.experiment.main_window.ui.itemtree.select_item(self.name) def close_tab(self): """ desc: Closes the tab if it was open. """ self.tabwidget.remove(self.widget()) def set_focus(self): """ desc: Gives focus to the most important widget. """ if hasattr(self, u'focus_widget') and self.focus_widget != None: self.focus_widget.setFocus() def set_focus_widget(self, widget, override=False): """ desc: Sets the widget that receives focus when the tab is opened. arguments: widget: desc: The widget to receive focus or `None` to reset. type: [QWidget, NoneType] keywords: override: desc: Indicates whether the focus widget should be changed if there already is a focus widget. type: bool """ if override or not hasattr(self, u'focus_widget') or self.focus_widget == None: self.focus_widget = widget def update_item_icon(self): """ desc: Updates the item icon. """ self.tabwidget.set_icon(self.widget(), self.item_icon()) self.experiment.items.set_icon(self.name, self.item_icon()) self.header_item_icon.setPixmap( self.theme.qpixmap(self.item_icon(), 32)) def item_icon(self): """ returns: desc: The name of the item icon. type: unicode """ return self.item_type def show_tab(self): """ desc: Is called when the tab becomes visible, and updated the contents. """ self.update_script() self.edit_widget() self.set_focus() def widget(self): """ returns: desc: The widget that is added to the tabwidget. type: QWidget """ return self.container_widget def init_edit_widget(self, stretch=True): """ desc: Builds the UI. keywords: stretch: desc: Indicates whether a vertical stretch should be added to the bottom of the controls. This is necessary if the controls don't expand. type: bool """ # Header widget self.header = header_widget.header_widget(self) self.user_hint_widget = user_hint_widget.user_hint_widget( self.experiment.main_window, self) self.header_hbox = QtGui.QHBoxLayout() self.header_item_icon = self.experiment.label_image(self.item_icon()) self.header_hbox.addWidget(self.header_item_icon) self.header_hbox.addWidget(self.header) self.header_hbox.setContentsMargins(0, 5, 0, 10) # Maximize button self.button_toggle_maximize = QtGui.QPushButton( self.theme.qicon(u'view-fullscreen'), u'') self.button_toggle_maximize.setToolTip(_(u'Toggle pop-out')) self.button_toggle_maximize.setIconSize(QtCore.QSize(16, 16)) self.button_toggle_maximize.clicked.connect(self.toggle_maximize) self.header_hbox.addWidget(self.button_toggle_maximize) # View button self.button_view = item_view_button(self) self.header_hbox.addWidget(self.button_view) # Help button self.button_help = QtGui.QPushButton(self.experiment.icon(u"help"), u"") self.button_help.setToolTip( _(u"Tell me more about the %s item") % self.item_type) self.button_help.setIconSize(QtCore.QSize(16, 16)) self.button_help.clicked.connect(self.open_help_tab) self.header_hbox.addWidget(self.button_help) self.header_widget = QtGui.QWidget() self.header_widget.setLayout(self.header_hbox) # The edit_grid is the layout that contains the actual controls for the # items. self.edit_grid = QtGui.QGridLayout() self.edit_grid.setColumnStretch(2, 2) self.edit_grid_widget = QtGui.QWidget() self.edit_grid.setMargin(0) self.edit_grid_widget.setLayout(self.edit_grid) # The edit_vbox contains the edit_grid and the header widget self.edit_vbox = QtGui.QVBoxLayout() self.edit_vbox.setMargin(5) self.edit_vbox.addWidget(self.user_hint_widget) self.edit_vbox.addWidget(self.edit_grid_widget) if stretch: self.edit_vbox.addStretch() self._edit_widget = QtGui.QWidget() self._edit_widget.setWindowIcon(self.experiment.icon(self.item_type)) self._edit_widget.setLayout(self.edit_vbox) # The _script_widget contains the script editor from QProgEdit import QTabManager self._script_widget = QTabManager( handlerButtonText=_(u'Apply and close'), cfg=cfg) self._script_widget.focusLost.connect(self.apply_script_changes) self._script_widget.handlerButtonClicked.connect(self.set_view_controls) self._script_widget.addTab(u'Script').setLang(u'OpenSesame') # The container_widget is the top-level widget that is actually inserted # into the tab widget. self.splitter = qtitem_splitter(self) if self.initial_view == u'controls': self.set_view_controls() elif self.initial_view == u'script': self.set_view_script() elif self.initial_view == u'split': self.set_view_split() else: debug.msg(u'Invalid initial_view: %s' % self.initial_view, reason=u'warning') self.set_view_controls() self.splitter.splitterMoved.connect(self.splitter_moved) self.container_vbox = QtGui.QVBoxLayout() self.container_vbox.addWidget(self.header_widget) self.container_vbox.addWidget(self.splitter) self.container_widget = QtGui.QWidget() self.container_widget.setLayout(self.container_vbox) self.container_widget.on_activate = self.show_tab self.container_widget.__item__ = self.name def splitter_moved(self, pos, index): """ desc: Is called when the splitter handle is manually moved. arguments: pos: desc: The splitter-handle position. type: int index: desc: The index of the splitter handle. Since there is only one handle, this is always 0. type: int """ sizes = self.splitter.sizes() self.edit_size = sizes[0] self.script_size = sizes[1] if self.script_size == 0: self.button_view.set_view_icon(u'controls') elif self.edit_size == 0: self.button_view.set_view_icon(u'script') else: self.button_view.set_view_icon(u'split') def set_view_controls(self): """ desc: Puts the splitter in control view. """ self.splitter.setSizes([self.splitter.width(), 0]) self.button_view.set_view_icon(u'controls') def set_view_script(self): """ desc: Puts the splitter in script view. """ self.splitter.setSizes([0, self.splitter.width()]) self.button_view.set_view_icon(u'script') def set_view_split(self): """ desc: Puts the splitter in split view. """ self.splitter.setSizes([self.splitter.width()/2, self.splitter.width()/2]) self.button_view.set_view_icon(u'split') def update(self): """ desc: Updates both the script and the controls. """ self.update_script() self.edit_widget() def update_script(self): """ desc: Regenerates the script and updates the script widget. """ # Normally, the script starts with a 'define' line and is indented by # a tab. We want to undo this, and present only unindented content. import textwrap script = self.to_string() script = script[script.find(u'\t'):] script = textwrap.dedent(script) self._script_widget.setText(script) def edit_widget(self, *deprecated, **_deprecated): """ desc: This function updates the controls based on the item state. """ debug.msg() self.auto_edit_widget() self.header.refresh() def apply_edit_changes(self, *deprecated, **_deprecated): """ desc: Applies changes to the graphical controls. """ debug.msg() self.auto_apply_edit_changes() self.update_script() self.main_window.set_unsaved(True) return True def apply_script_changes(self, *deprecated, **_deprecated): """ desc: Applies changes to the script, by re-parsing the item from string. """ debug.msg() old_script = self.to_string() new_script = self._script_widget.text() try: self.from_string(new_script) except osexception as e: self.experiment.notify(e.html()) self.main_window.print_debug_window(e) self.from_string(old_script) self.edit_widget() self.main_window.set_unsaved(True) def rename(self, from_name, to_name): """ desc: Handles renaming of an item (not necesarrily the current item). arguments: from_name: desc: The old item name. type: unicode to_name: desc: The new item name type: unicode """ if self.name != from_name: return self.name = to_name self.container_widget.__item__ = self.name self.header.set_name(to_name) index = self.tabwidget.indexOf(self.widget()) if index != None: self.tabwidget.setTabText(index, to_name) def open_help_tab(self): """ desc: Opens a help tab. """ self.experiment.main_window.ui.tabwidget.open_help(self.item_type) def toggle_maximize(self): """ desc: Toggles edit-widget maximization. """ if not self.maximized: # Always ignore close events. This is necessary, because otherwise # the pop-out widget can be closed without re-enabling the main # window. self.container_widget.closeEvent = lambda e: e.ignore() self.container_widget.setParent(None) self.container_widget.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint|\ QtCore.Qt.WindowMaximizeButtonHint|\ QtCore.Qt.CustomizeWindowHint) self.container_widget.showMaximized() self.container_widget.show() self.button_toggle_maximize.setIcon( self.theme.qicon(u'view-restore')) else: self.container_widget.setParent(self.main_window) self.open_tab() self.button_toggle_maximize.setIcon( self.theme.qicon(u'view-fullscreen')) self.maximized = not self.maximized self.user_hint_widget.disable(self.maximized) self.button_help.setDisabled(self.maximized) self.main_window.setDisabled(self.maximized) def delete(self, item_name, item_parent=None, index=None): """ Delete an item (not necessarily the current one) Arguments: item_name -- the name of the item to be deleted Keywords arguments: item_parent -- the parent item (default=None) index -- the index of the item in the parent (default=None) """ pass def build_item_tree(self, toplevel=None, items=[], max_depth=-1, extra_info=None): """ Construct an item tree Keyword arguments: toplevel -- the toplevel widget (default = None) items -- a list of items that have been added, to prevent recursion (default=[]) """ widget = tree_item_item(self, extra_info=extra_info) items.append(self.name) if toplevel != None: toplevel.addChild(widget) return widget def parents(self): """ Creates a list of all the items that the current sequences is connected to upstream Returns: A list of item names """ l = [self.name] for item in self.experiment.items: if self.experiment.items[item].is_child_item(self.name): l.append(item) return l def variable_vars(self, exclude=[]): """ Determines if one of the variables of the current item is defined in terms of another variable Keywords arguments: exclude -- a list of variables that should not be checked Returns: True if there are variably defined variables, False otherwise """ for var in self.variables: if var not in exclude: val = self.variables[var] if self.experiment.varref(val): return True return False def get_ready(self): """ This function should be overridden to do any last-minute stuff that and item should do before an experiment is actually run, such as applying pending script changes. Returns: True if some action has been taken, False if nothing was done """ return False def auto_edit_widget(self): """Update the GUI controls based on the auto-widgets""" debug.msg() for var, edit in self.auto_line_edit.iteritems(): if self.has(var): edit.setText(self.unistr(self.get(var, _eval=False))) else: edit.setText(u'') for var, combobox in self.auto_combobox.iteritems(): val = self.get_check(var, _eval=False, default=u'') i = combobox.findText(self.unistr(val)) # Set the combobox to the select item if i >= 0: combobox.setDisabled(False) combobox.setCurrentIndex(i) # If no value was specified, set the combobox to a blank item elif val == u'': combobox.setDisabled(False) combobox.setCurrentIndex(-1) # If an unknown value has been specified, notify the user else: combobox.setDisabled(True) self.user_hint_widget.add(_(u'"%s" is set to a ' u'variable or unknown value and can only be edited through ' u'the script.' % var)) for var, spinbox in self.auto_spinbox.iteritems(): if self.has(var): val = self.get(var, _eval=False) if type(val) in (float, int): spinbox.setDisabled(False) try: spinbox.setValue(val) except Exception as e: self.experiment.notify(_( \ u"Failed to set control '%s': %s") % (var, e)) else: spinbox.setDisabled(True) self.user_hint_widget.add(_( \ u'"%s" is defined using variables and can only be edited through the script.' \ % var)) for var, slider in self.auto_slider.iteritems(): if self.has(var): val = self.get(var, _eval=False) if type(val) in (float, int): slider.setDisabled(False) try: slider.setValue(val) except Exception as e: self.experiment.notify(_( \ u"Failed to set control '%s': %s") % (var, e)) else: slider.setDisabled(True) self.user_hint_widget.add(_( \ u'"%s" is defined using variables and can only be edited through the script.' \ % var)) for var, checkbox in self.auto_checkbox.iteritems(): if self.has(var): try: checkbox.setChecked(self.get(var, _eval=False) == u"yes") except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) for var, qprogedit in self.auto_editor.iteritems(): if self.has(var): try: qprogedit.setText(self.unistr(self.get(var, _eval=False))) except Exception as e: self.experiment.notify(_(u"Failed to set control '%s': %s") \ % (var, e)) def sanitize_check(self, s, strict=False, allow_vars=True, notify=True): """ Checks whether a string is sane (i.e. unchanged by sanitize()) and optionally presents a warning. Arguments: s -- The string to check. Keyword arguments: strict -- See sanitize(). allow_vars -- See sanitize(). notify -- Indicates whether a notification should be presented if the string is not sane. Returns: True if s is sane, False otherwise. """ sane = s == self.sanitize(s, strict=strict, allow_vars=allow_vars) if not sane and notify: if strict: self.experiment.notify( _(u'All non-alphanumeric characters except underscores have been stripped')) else: self.experiment.notify( _(u'The following characters are not allowed and have been stripped: double-quote ("), backslash (\), and newline')) return sane def auto_apply_edit_changes(self, rebuild=True): """ Apply the auto-widget controls Keyword arguments: rebuild -- deprecated (does nothing) (default=True) """ debug.msg() for var, edit in self.auto_line_edit.iteritems(): if edit.isEnabled() and isinstance(var, basestring): val = unicode(edit.text()).strip() if val != u"": self.set(var, val) # If the variable has no value, we assign a default value if it # has been specified, and unset it otherwise. elif hasattr(edit, u"default"): self.set(var, edit.default) else: self.unset(var) for var, combobox in self.auto_combobox.iteritems(): if combobox.isEnabled() and isinstance(var, basestring): self.set(var, unicode(combobox.currentText())) for var, spinbox in self.auto_spinbox.iteritems(): if spinbox.isEnabled() and isinstance(var, basestring): self.set(var, spinbox.value()) for var, slider in self.auto_slider.iteritems(): if slider.isEnabled() and isinstance(var, basestring): self.set(var, slider.value()) for var, checkbox in self.auto_checkbox.iteritems(): if checkbox.isEnabled() and isinstance(var, basestring): if checkbox.isChecked(): val = u"yes" else: val = u"no" self.set(var, val) for var, qprogedit in self.auto_editor.iteritems(): if isinstance(var, basestring): self.set(var, qprogedit.text()) return True def auto_add_widget(self, widget, var=None): """ Add a widget to the list of auto-widgets Arguments: widget -- a QWidget Keyword arguments: var -- the variable to be linked to the widget (default=None) """ # Use the object id as a fallback name if var == None: var = id(widget) debug.msg(var) self.set_focus_widget(widget) if isinstance(widget, QtGui.QSpinBox) or isinstance(widget, QtGui.QDoubleSpinBox): widget.editingFinished.connect(self.apply_edit_changes) self.auto_spinbox[var] = widget elif isinstance(widget, QtGui.QComboBox): widget.activated.connect(self.apply_edit_changes) self.auto_combobox[var] = widget elif isinstance(widget, QtGui.QSlider): widget.editingFinished.connect(self.apply_edit_changes) self.auto_slider[var] = widget elif isinstance(widget, QtGui.QLineEdit): widget.editingFinished.connect(self.apply_edit_changes) self.auto_line_edit[var] = widget elif isinstance(widget, QtGui.QCheckBox): widget.clicked.connect(self.apply_edit_changes) self.auto_checkbox[var] = widget else: raise Exception(u"Cannot auto-add widget of type %s" % widget) def clean_cond(self, cond, default=u'always'): """ Cleans a conditional statement. May raise a dialog box if problems are encountered. Arguments: cond -- A (potentially filthy) conditional statement. Keyword arguments: default -- A default value to use for empty Returns: cond -- A clean conditional statement conditional statements. (default=u'always') """ cond = self.unistr(cond) if not self.sanitize_check(cond): cond = self.sanitize(cond) if cond.strip() == u'': cond = default try: self.compile_cond(cond) except osexception as e: self.experiment.notify( \ u'Failed to compile conditional statement "%s": %s' % (cond, e)) return default return cond def children(self): """ returns: desc: A list of children, including grand children, and so on. type: list """ return [] def is_child_item(self, item_name): """ desc: Checks if an item is somewhere downstream from the current item in the experimental hierarchy. arguments: item_name: desc: The name of the child item. type: unicode returns: desc: True if the current item is offspring of the item, False otherwise. type: bool """ return False def insert_child_item(self, item_name, index=0): """ desc: Inserts a child item, if applicable to the item type. arguments: item_name: desc: The name of the child item. type: unicode keywords: index: desc: The index of the child item. type: int """ pass def remove_child_item(self, item_name, index=0): """ desc: Removes a child item, if applicable to the item type. arguments: item_name: desc: The name of the child item. type: unicode keywords: index: desc: The index of the child item, if applicable. A negative value indicates all instances. type: int """ pass
class inline_script(inline_script_runtime, qtplugin): """The inline_script GUI controls""" def __init__(self, name, experiment, string=None): """See item.""" inline_script_runtime.__init__(self, name, experiment, string) qtplugin.__init__(self) def apply_edit_changes(self): """See qtitem.""" super(inline_script, self).apply_edit_changes(self) sp = self.qprogedit.text(index=0) sr = self.qprogedit.text(index=1) self.var._prepare = sp self.var._run = sr self.update_item_icon() def set_focus(self): """ desc: Allows the item to focus the most important widget. """ self.qprogedit.setFocus() def item_icon(self): """ desc: Determines the icon, based on whether the scripts are syntactically correct. returns: desc: An icon name. type: unicode """ status = max( self.experiment.python_workspace.check_syntax( self.var.get(u'_prepare', _eval=False)), self.experiment.python_workspace.check_syntax( self.var.get(u'_run', _eval=False))) if status == 2: return u'os-inline_script-syntax-error' if status == 1: return u'os-inline_script-syntax-warning' return u'os-inline_script' def build_item_tree(self, toplevel=None, items=[], max_depth=-1, extra_info=None): """See qtitem.""" widget = tree_inline_script_item(self, extra_info=extra_info, symbols=(max_depth < 0 or max_depth > 1)) items.append(self.name) if toplevel is not None: toplevel.addChild(widget) return widget def init_edit_widget(self): """See qtitem.""" from QProgEdit import QTabManager super(inline_script, self).init_edit_widget(stretch=False) self.qprogedit = QTabManager(cfg=cfg, runButton=True) self.qprogedit.execute.connect(self.main_window.console.execute) self.qprogedit.handlerButtonClicked.connect(self.apply_edit_changes) self.qprogedit.focusLost.connect(self.apply_edit_changes) self.qprogedit.cursorRowChanged.connect(self.apply_edit_changes) self.qprogedit.addTab(u'Prepare').setLang(u'Python') self.qprogedit.addTab(u'Run').setLang(u'Python') # Switch to the run phase, unless there is only content for the prepare # phase. if self.var._run == u'' and self.var._prepare != u'': self.qprogedit.setCurrentIndex(0) else: self.qprogedit.setCurrentIndex(1) self.edit_vbox.addWidget(self.qprogedit) def edit_widget(self): """See qtitem.""" super(inline_script, self).edit_widget() self.qprogedit.tab(0).setText(safe_decode(self.var._prepare)) self.qprogedit.tab(1).setText(safe_decode(self.var._run)) def get_ready(self): """See qtitem.""" if self.qprogedit.isAnyModified(): debug.msg(u'applying pending script changes') self.apply_edit_changes() return True return super(inline_script, self).get_ready()
class qtitem(base_qtobject): """Base class for the GUI controls of other items""" initial_view = u'controls' label_align = u'right' help_url = None lazy_init = False def __init__(self): """Constructor""" # The auto-widgets are stored in name -> (var, widget) dictionaries self.auto_line_edit = {} self.auto_combobox = {} self.auto_spinbox = {} self.auto_slider = {} self.auto_editor = {} self.auto_checkbox = {} # Lazy initialization means that the control widgets are initialized # only when they are shown for the first time. This dramatically speeds # up opening of files. However, with some items this is not currently # possible, because their widgets are also referred when they are not # shown. Currently this means the inline_script. if self.lazy_init: self.container_widget = None else: self.init_edit_widget() self.lock = False self.maximized = False self.set_validator() debug.msg(u'created %s' % self.name) @property def main_window(self): return self.experiment.main_window @property def overview_area(self): return self.experiment.main_window.ui.itemtree @property def theme(self): return self.experiment.main_window.theme @property def tabwidget(self): return self.experiment.main_window.tabwidget @property def console(self): return self.experiment.main_window.console @property def extension_manager(self): return self.experiment.main_window.extension_manager @property def default_description(self): return _(u'Default description') def open_tab(self, select_in_tree=True): """ desc: Opens the tab if it wasn't yet open, and switches to it. """ unsaved = self.main_window.unsaved_changes self.tabwidget.add(self.widget(), self.item_icon(), self.name) self.main_window.set_unsaved(unsaved) if select_in_tree: self.experiment.main_window.ui.itemtree.select_item(self.name) def close_tab(self): """ desc: Closes the tab if it was open. """ self.tabwidget.remove(self.widget()) def set_focus(self): """ desc: Gives focus to the most important widget. """ if hasattr(self, u'focus_widget') and self.focus_widget is not None: self.focus_widget.setFocus() def set_focus_widget(self, widget, override=False): """ desc: Sets the widget that receives focus when the tab is opened. arguments: widget: desc: The widget to receive focus or `None` to reset. type: [QWidget, NoneType] keywords: override: desc: Indicates whether the focus widget should be changed if there already is a focus widget. type: bool """ if override or not hasattr( self, u'focus_widget') or self.focus_widget is None: self.focus_widget = widget def update_item_icon(self): """ desc: Updates the item icon. """ self.tabwidget.set_icon(self.widget(), self.item_icon()) self.experiment.items.set_icon(self.name, self.item_icon()) self.header_item_icon.setPixmap( self.theme.qpixmap(self.item_icon(), 32)) def item_icon(self): """ returns: desc: The name of the item icon. type: unicode """ return self.item_type def show_tab(self): """ desc: Is called when the tab becomes visible, and updated the contents. """ self.update_script() self.edit_widget() self.main_window.ui.itemtree.select_item(self.name, open_tab=False) @requires_init def widget(self): """ returns: desc: The widget that is added to the tabwidget. type: QWidget """ return self.container_widget def init_edit_widget(self, stretch=True): """ desc: Builds the UI. keywords: stretch: desc: Indicates whether a vertical stretch should be added to the bottom of the controls. This is necessary if the controls don't expand. type: bool """ # Header widget self.header = header_widget.header_widget(self) self.header_hbox = QtWidgets.QHBoxLayout() self.header_item_icon = self.theme.qlabel(self.item_icon()) self.header_hbox.addWidget(self.header_item_icon) self.header_hbox.addWidget(self.header) self.header_hbox.setContentsMargins(0, 0, 0, 0) self.header_hbox.setSpacing(12) # Maximize button self.button_toggle_maximize = QtWidgets.QPushButton( self.theme.qicon(u'view-fullscreen'), u'') self.button_toggle_maximize.setToolTip(_(u'Toggle pop-out')) self.button_toggle_maximize.setIconSize(QtCore.QSize(16, 16)) self.button_toggle_maximize.clicked.connect(self.toggle_maximize) self.header_hbox.addWidget(self.button_toggle_maximize) # View button self.button_view = item_view_button(self) self.header_hbox.addWidget(self.button_view) # Help button self.button_help = QtWidgets.QPushButton(self.theme.qicon(u"help"), u"") self.button_help.setToolTip( _(u"Tell me more about the %s item") % self.item_type) self.button_help.setIconSize(QtCore.QSize(16, 16)) self.button_help.clicked.connect(self.open_help_tab) self.header_hbox.addWidget(self.button_help) self.header_widget = QtWidgets.QWidget() self.header_widget.setLayout(self.header_hbox) # The edit_grid is the layout that contains the actual controls for the # items. self.edit_grid = QtWidgets.QFormLayout() if self.label_align == u'right': self.edit_grid.setLabelAlignment(QtCore.Qt.AlignRight) self.edit_grid.setFieldGrowthPolicy( QtWidgets.QFormLayout.FieldsStayAtSizeHint) self.edit_grid.setContentsMargins(0, 0, 0, 0) self.edit_grid.setVerticalSpacing(6) self.edit_grid.setHorizontalSpacing(12) self.edit_grid_widget = QtWidgets.QWidget() self.edit_grid_widget.setLayout(self.edit_grid) # The edit_vbox contains the edit_grid and the header widget self.edit_vbox = QtWidgets.QVBoxLayout() self.edit_vbox.addWidget(self.edit_grid_widget) self.edit_vbox.setContentsMargins(0, 0, 0, 0) self.edit_vbox.setSpacing(12) if stretch: self.edit_vbox.addStretch() self._edit_widget = QtWidgets.QWidget() self._edit_widget.setWindowIcon(self.theme.qicon(self.item_type)) self._edit_widget.setLayout(self.edit_vbox) # The _script_widget contains the script editor from QProgEdit import QTabManager self._script_widget = QTabManager( handlerButtonText=_(u'Apply and close'), cfg=cfg) self._script_widget.focusLost.connect(self.apply_script_changes) self._script_widget.cursorRowChanged.connect(self.apply_script_changes) self._script_widget.handlerButtonClicked.connect( self.apply_script_changes_and_switch_view) self._script_widget.addTab(u'Script').setLang(u'OpenSesame') # The container_widget is the top-level widget that is actually inserted # into the tab widget. self.splitter = qtitem_splitter(self) if self.initial_view == u'controls': self.set_view_controls() elif self.initial_view == u'script': self.set_view_script() elif self.initial_view == u'split': self.set_view_split() else: debug.msg(u'Invalid initial_view: %s' % self.initial_view, reason=u'warning') self.set_view_controls() self.splitter.splitterMoved.connect(self.splitter_moved) self.container_vbox = QtWidgets.QVBoxLayout() self.container_vbox.setContentsMargins(12, 12, 12, 12) self.container_vbox.setSpacing(18) self.container_vbox.addWidget(self.header_widget) self.container_vbox.addWidget(self.splitter) self.container_widget = QtWidgets.QWidget() self.container_widget.setLayout(self.container_vbox) self.container_widget.on_activate = self.show_tab self.container_widget.__item__ = self.name def splitter_moved(self, pos, index): """ desc: Is called when the splitter handle is manually moved. arguments: pos: desc: The splitter-handle position. type: int index: desc: The index of the splitter handle. Since there is only one handle, this is always 0. type: int """ sizes = self.splitter.sizes() self.edit_size = sizes[0] self.script_size = sizes[1] if self.script_size == 0: self.button_view.set_view_icon(u'controls') elif self.edit_size == 0: self.button_view.set_view_icon(u'script') else: self.button_view.set_view_icon(u'split') def set_view_controls(self): """ desc: Puts the splitter in control view. """ self.splitter.setSizes([self.splitter.width(), 0]) self.button_view.set_view_icon(u'controls') def set_view_script(self): """ desc: Puts the splitter in script view. """ self.splitter.setSizes([0, self.splitter.width()]) self.button_view.set_view_icon(u'script') def set_view_split(self): """ desc: Puts the splitter in split view. """ self.splitter.setSizes( [self.splitter.width() / 2, self.splitter.width() / 2]) self.button_view.set_view_icon(u'split') def update(self): """ desc: Updates both the script and the controls. """ # Items are updated when their tab is shown, so we don't need to update # them if they aren't shown. if self.tabwidget.current_item() != self.name: return self.update_script() self.edit_widget() def update_script(self): """ desc: Regenerates the script and updates the script widget. """ # Normally, the script starts with a 'define' line and is indented by # a tab. We want to undo this, and present only unindented content. import textwrap script = self.to_string() script = script[script.find(u'\t'):] script = textwrap.dedent(script) if self._script_widget.text() != script: self.main_window.set_unsaved() self._script_widget.setText(script) self.extension_manager.fire(u'change_item', name=self.name) def edit_widget(self, *deprecated, **_deprecated): """ desc: This function updates the controls based on the item state. """ self.auto_edit_widget() self.header.refresh() def apply_edit_changes(self, *deprecated, **_deprecated): """ desc: Applies changes to the graphical controls. """ self.auto_apply_edit_changes() self.update_script() return True def apply_script_changes(self, *deprecated, **_deprecated): """ desc: Applies changes to the script. """ if not self.validate_script(): return new_script = self._script_widget.text() old_script = self.to_string() self.from_string(new_script) if old_script != new_script: self.main_window.set_unsaved() self.edit_widget() def apply_script_changes_and_switch_view(self, *deprecated, **_deprecated): """ desc: Applies changes to the script if possible. If so, switches to the controls view. """ if self.validate_script(): self.set_view_controls() def validate_script(self): """ desc: Checks whether the script is syntactically valid. If not, the offending line is highlighted if QProgEdit suppors setInvalid(). returns: type: bool """ script = self._script_widget.text() # First create a dummy item to see if the string can be parsed. try: self.validator(self.name, self.experiment, script) return True except Exception as e: # If an error occurs, we first parse the first line, then the first # and second, and so on, until we find the error. l = script.split(u'\n') for line_nr, line in enumerate(l): test_script = u'\n'.join(l[:line_nr]) try: self.validator(self.name, self.experiment, test_script) except Exception as e_: if not isinstance(e_, osexception): e_ = osexception(e_) if hasattr(self._script_widget, u'setInvalid'): self._script_widget.setInvalid(line_nr, e_.markdown()) break self.console.write(e) return False def set_validator(self): """ desc: Sets the validator class, that is, the class that is used to parse scripts to see if they are syntactically correct. Currently, we use the class that defines from_string(). """ import inspect meth = self.from_string for cls in inspect.getmro(self.__class__): if meth.__name__ in cls.__dict__: break debug.msg(u'validator: %s' % cls) self.validator = cls def rename(self, from_name, to_name): """ desc: Handles renaming of an item (not necesarrily the current item). arguments: from_name: desc: The old item name. type: unicode to_name: desc: The new item name type: unicode """ if self.name != from_name: return self.name = to_name self.container_widget.__item__ = self.name self.header.set_name(to_name) index = self.tabwidget.indexOf(self.widget()) if index is not None: self.tabwidget.setTabText(index, to_name) def open_help_tab(self): """ desc: Opens a help tab. """ if self.help_url is None: self.tabwidget.open_help(self.item_type) else: self.tabwidget.open_osdoc(self.help_url) def toggle_maximize(self): """ desc: Toggles edit-widget maximization. """ if not self.maximized: # Always ignore close events. This is necessary, because otherwise # the pop-out widget can be closed without re-enabling the main # window. self.main_window.block_close_event = True self.container_widget.closeEvent = lambda e: e.ignore() self.container_widget.setParent(None) self.container_widget.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint|\ QtCore.Qt.WindowMaximizeButtonHint|\ QtCore.Qt.CustomizeWindowHint) self.container_widget.showMaximized() self.container_widget.show() self.button_toggle_maximize.setIcon( self.theme.qicon(u'view-restore')) else: self.main_window.block_close_event = False self.container_widget.setParent(self.main_window) self.open_tab() self.button_toggle_maximize.setIcon( self.theme.qicon(u'view-fullscreen')) self.maximized = not self.maximized self.button_help.setDisabled(self.maximized) self.main_window.setDisabled(self.maximized) def delete(self, item_name, item_parent=None, index=None): """ desc: Is called when an item is deleted (not necessarily the current one). arguments: item_name: desc: The name of the item to be deleted. type: str keywords: item_parent: desc: The name of the parent item. type: str index: desc: The index of the item in the parent. type: int """ pass def build_item_tree(self, toplevel=None, items=[], max_depth=-1, extra_info=None): """ desc: Constructs an item tree. keywords: toplevel: desc: The toplevel widget. type: tree_base_item items: desc: A list of item names that have already been added, to prevent recursion. tyep: list returns: type: tree_item_item """ widget = tree_item_item(self, extra_info=extra_info) items.append(self.name) if toplevel is not None: toplevel.addChild(widget) return widget def parents(self): """ returns: desc: A list of all parents (names) of the current item. type: list """ l = [self.name] for item in self.experiment.items: if self.experiment.items[item].is_child_item(self.name): l.append(item) return l def get_ready(self): """ desc: This function should be overridden to do any last-minute stuff that and item should do before an experiment is actually run, such as applying pending script changes. returns: desc: True if some action has been taken, False if nothing was done. type: bool """ return False def auto_edit_widget(self): """ desc: Updates the GUI controls based on the auto-widgets. """ for var, edit in self.auto_line_edit.items(): if isinstance(var, int): continue if var in self.var: val = safe_decode(self.var.get(var, _eval=False)) else: val = u'' if val != edit.text(): edit.setText(val) for var, combobox in self.auto_combobox.items(): if isinstance(var, int): continue val = self.var.get(var, _eval=False, default=u'') i = combobox.findText(safe_decode(val)) # Set the combobox to the select item if i >= 0: if i != combobox.currentIndex() or not combobox.isEnabled(): combobox.setDisabled(False) combobox.setCurrentIndex(i) # If no value was specified, set the combobox to a blank item elif val == u'': if combobox.currentIndex() >= 0 or not combobox.isEnabled(): combobox.setDisabled(False) combobox.setCurrentIndex(-1) # If an unknown value has been specified, notify the user else: if combobox.isEnabled(): combobox.setDisabled(True) self.extension_manager.fire( u'notify', message=_( u'"%s" is set to a variable or ' u'unknown value and can only be edited through ' u'the script.') % var, category=u'info') for var, spinbox in list(self.auto_spinbox.items()) \ + list(self.auto_slider.items()): if isinstance(var, int): continue if var in self.var: val = self.var.get(var, _eval=False) if type(val) in (float, int): if spinbox.value() == val and spinbox.isEnabled(): continue spinbox.setDisabled(False) try: spinbox.setValue(val) except Exception as e: self.experiment.notify( _(u"Failed to set control '%s': %s") % (var, e)) else: if not spinbox.isEnabled(): continue spinbox.setDisabled(True) self.extension_manager.fire( u'notify', message=_( u'"%s" is defined using ' 'variables and can only be edited through the ' 'script.') % var, category=u'info') for var, checkbox in self.auto_checkbox.items(): if isinstance(var, int): continue if var in self.var: val = self.var.get(var, _eval=False) if val in [u'yes', u'no']: checkbox.setDisabled(False) checked = val == u'yes' if checked != checkbox.isChecked(): checkbox.setChecked(checked) else: checkbox.setDisabled(True) self.extension_manager.fire( u'notify', message= _(u'"%s" is defined using ' u'variables or has an invalid value, and can only be ' u'edited through the script.') % var, category=u'info') for var, qprogedit in self.auto_editor.items(): if isinstance(var, int): continue if var in self.var: val = safe_decode(self.var.get(var, _eval=False)) if val != qprogedit.text(): qprogedit.setText(val) def auto_apply_edit_changes(self, rebuild=True): """ desc: Applies the auto-widget controls. keywords: rebuild: Deprecated (does nothing). """ for var, edit in self.auto_line_edit.items(): if isinstance(var, int): continue if edit.isEnabled() and isinstance(var, basestring): val = str(edit.text()).strip() if val != u'': self.var.set(var, val) continue # If no text was entered, we use a default if available ... if hasattr(edit, u'default'): self.var.set(var, edit.default) continue # ... or unset the variable if no default is available. self.var.unset(var) for var, combobox in self.auto_combobox.items(): if isinstance(var, int): continue if combobox.isEnabled() and isinstance(var, basestring): self.var.set(var, str(combobox.currentText())) for var, spinbox in self.auto_spinbox.items(): if isinstance(var, int): continue if spinbox.isEnabled() and isinstance(var, basestring): self.var.set(var, spinbox.value()) for var, slider in self.auto_slider.items(): if isinstance(var, int): continue if slider.isEnabled() and isinstance(var, basestring): self.var.set(var, slider.value()) for var, checkbox in self.auto_checkbox.items(): if isinstance(var, int): continue if checkbox.isEnabled() and isinstance(var, basestring): if checkbox.isChecked(): val = u"yes" else: val = u"no" self.var.set(var, val) for var, qprogedit in self.auto_editor.items(): if isinstance(var, int): continue if isinstance(var, basestring): self.var.set(var, qprogedit.text()) return True def auto_add_widget(self, widget, var=None, apply_func=None): """ desc: Adds a widget to the list of auto-widgets. arguments: widget: type: QWidget keywords: var: The name of the experimental variable to be linked to the widget, or None to use an automatically chosen name. type: [str, NoneType] """ # Use the object id as a fallback name if var is None: var = id(widget) if apply_func is None: apply_func = self.apply_edit_changes debug.msg(var) self.set_focus_widget(widget) if isinstance(widget, QtWidgets.QSpinBox) or isinstance( widget, QtWidgets.QDoubleSpinBox): widget.editingFinished.connect(apply_func) self.auto_spinbox[var] = widget elif isinstance(widget, QtWidgets.QComboBox): widget.activated.connect(apply_func) self.auto_combobox[var] = widget elif isinstance(widget, QtWidgets.QSlider) \ or isinstance(widget, QtWidgets.QDial): widget.valueChanged.connect(apply_func) self.auto_slider[var] = widget elif isinstance(widget, QtWidgets.QLineEdit) or isinstance( widget, pool_select): widget.editingFinished.connect(apply_func) self.auto_line_edit[var] = widget elif isinstance(widget, QtWidgets.QCheckBox): widget.clicked.connect(apply_func) self.auto_checkbox[var] = widget else: raise Exception(u"Cannot auto-add widget of type %s" % widget) def children(self): """ returns: desc: A list of children, including grand children, and so on. type: list """ return [] def is_child_item(self, item_name): """ desc: Checks if an item is somewhere downstream from the current item in the experimental hierarchy. arguments: item_name: desc: The name of the child item. type: unicode returns: desc: True if the current item is offspring of the item, False otherwise. type: bool """ return False def insert_child_item(self, item_name, index=0): """ desc: Inserts a child item, if applicable to the item type. arguments: item_name: desc: The name of the child item. type: unicode keywords: index: desc: The index of the child item. type: int """ pass def remove_child_item(self, item_name, index=0): """ desc: Removes a child item, if applicable to the item type. arguments: item_name: desc: The name of the child item. type: unicode keywords: index: desc: The index of the child item, if applicable. A negative value indicates all instances. type: int """ pass
def init_edit_widget(self, stretch=True): """ desc: Builds the UI. keywords: stretch: desc: Indicates whether a vertical stretch should be added to the bottom of the controls. This is necessary if the controls don't expand. type: bool """ # Header widget self.header = header_widget.header_widget(self) self.user_hint_widget = user_hint_widget.user_hint_widget( self.experiment.main_window, self) self.header_hbox = QtGui.QHBoxLayout() self.header_item_icon = self.experiment.label_image(self.item_icon()) self.header_hbox.addWidget(self.header_item_icon) self.header_hbox.addWidget(self.header) self.header_hbox.setContentsMargins(0, 5, 0, 10) # Maximize button self.button_toggle_maximize = QtGui.QPushButton( self.theme.qicon(u'view-fullscreen'), u'') self.button_toggle_maximize.setToolTip(_(u'Toggle pop-out')) self.button_toggle_maximize.setIconSize(QtCore.QSize(16, 16)) self.button_toggle_maximize.clicked.connect(self.toggle_maximize) self.header_hbox.addWidget(self.button_toggle_maximize) # View button self.button_view = item_view_button(self) self.header_hbox.addWidget(self.button_view) # Help button self.button_help = QtGui.QPushButton(self.experiment.icon(u"help"), u"") self.button_help.setToolTip( _(u"Tell me more about the %s item") % self.item_type) self.button_help.setIconSize(QtCore.QSize(16, 16)) self.button_help.clicked.connect(self.open_help_tab) self.header_hbox.addWidget(self.button_help) self.header_widget = QtGui.QWidget() self.header_widget.setLayout(self.header_hbox) # The edit_grid is the layout that contains the actual controls for the # items. self.edit_grid = QtGui.QGridLayout() self.edit_grid.setColumnStretch(2, 2) self.edit_grid_widget = QtGui.QWidget() self.edit_grid.setMargin(0) self.edit_grid_widget.setLayout(self.edit_grid) # The edit_vbox contains the edit_grid and the header widget self.edit_vbox = QtGui.QVBoxLayout() self.edit_vbox.setMargin(5) self.edit_vbox.addWidget(self.user_hint_widget) self.edit_vbox.addWidget(self.edit_grid_widget) if stretch: self.edit_vbox.addStretch() self._edit_widget = QtGui.QWidget() self._edit_widget.setWindowIcon(self.experiment.icon(self.item_type)) self._edit_widget.setLayout(self.edit_vbox) # The _script_widget contains the script editor from QProgEdit import QTabManager self._script_widget = QTabManager( handlerButtonText=_(u'Apply and close'), cfg=cfg) self._script_widget.focusLost.connect(self.apply_script_changes) self._script_widget.handlerButtonClicked.connect( self.set_view_controls) self._script_widget.addTab(u'Script').setLang(u'OpenSesame') # The container_widget is the top-level widget that is actually inserted # into the tab widget. self.splitter = qtitem_splitter(self) if self.initial_view == u'controls': self.set_view_controls() elif self.initial_view == u'script': self.set_view_script() elif self.initial_view == u'split': self.set_view_split() else: debug.msg(u'Invalid initial_view: %s' % self.initial_view, reason=u'warning') self.set_view_controls() self.splitter.splitterMoved.connect(self.splitter_moved) self.container_vbox = QtGui.QVBoxLayout() self.container_vbox.addWidget(self.header_widget) self.container_vbox.addWidget(self.splitter) self.container_widget = QtGui.QWidget() self.container_widget.setLayout(self.container_vbox) self.container_widget.on_activate = self.show_tab