Source code for schrodinger.ui.qt.input_selector

"""
This module provides the InputSelector class, which allows the user to
specify the source of structures (Workspace, Project Table, File).
"""

import os
import shutil
import sys

import schrodinger
from schrodinger import structure
from schrodinger.application.desmond import cms
from schrodinger.project import project
from schrodinger.project import utils
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import swidgets
from schrodinger.utils import fileutils
from schrodinger.utils import imputils

# Avoids circular import
desmond_gui = imputils.lazy_import('schrodinger.application.desmond.gui')

maestro = schrodinger.get_maestro()


# TODO: Move to maestro.py
[docs]def get_workspace_structure(): """ Returns the Workspace structure. If only one entry is in workspace, then the properties are included as well. """ # Ev:71000 try: st = maestro.get_included_entry() except RuntimeError: # 0 or >1 entries included in in Workspace: st = maestro.workspace_get() return st
[docs]def format_list_to_filter_string(filetypes): """ Converts Tkinter-style file types list to Qt-style filter string. Deprecated. Use Qt style filter strings directly instead. """ # These together represent a dict with sorted keys: format_names = [] format_dict = {} for name, format in filetypes: if name not in format_names: format_names.append(name) format_dict[name] = [] format_dict[name].append(format) # Create a filter string of the following format: # "Images (*.png *.xpm *.jpg);;Text files (*.txt);;XML files (*.xml)" filter_items = [] for name in format_names: formats = format_dict[name] t = name + " (%s)" % " ".join(formats) filter_items.append(t) filter_string = ';;'.join(filter_items) # Select the first string in the list: return filter_string
class _InputParameters: """ Class for holding information about the user's input selection. """ def __init__(self): self.reset() def reset(self): self.inputstrucfile = None self.inputstrucfiles = None self.inputstrucorigfile = None self.inputstrucsource = None
[docs]class InputSelector(QtWidgets.QFrame): """ An application input source selection widget. Allows the user to choose the source of the job input: Project Table, files, or Workspace (which is not included entries, just the Workspace ct). Configuration options set via constructor keywords. :param filetypes: If file input allowed, the file filters used in the browse dialog. Either a QFileDialog filter string, or a Tk-type list of tuples, in format: ``[('Maestro Files', '*.mae'), ...]`` :param initialdir: If file input allowed, the default initial directory in the file dialog. Default is '.'. :param file: Allow an external file as an input source. Default is True. :param selected_entries: Allow the selected Project Table entries to be used as an input source. Maestro only. Default is True. :param included_entries: Allow the included Project Table entries to be used as an input source. Maestro only. Default is False. :param included_entry: Allow the single included Project Table entry to be used as an input source. An error is presented if more than one entry is included. Properties are preserved. Maestro only. Default is False. :param workspace: Allow the Workspace structure as an input source. If more than one entry is included, they are merged and CT-level properties are dropped. Maestro only. Default is True. Consider using the 'included_entries' or 'included_entry' instead. :param default_source: What the default source should be. Must be one of: "file", "selected_entries", "included_entries", "included_entry", or "workspace". Default is "selected_entries", if enabled; if not, then "file" (if enabled) :param writefile: Automatically generate the '<jobname>.<ext>' file from WS/PT/FILE source. Default is True. If True: The written file name is stored as 'inputstrucfile' in the _InputParameters object. If False: For FILE source, store file path to "inputstrucfile". For other sources, nothing is stored. It is user's job then use this information as desired. :param label_text: Default is: 'Use structures from:' :param file_text: Default is: "File name:" / "File names:" :param tracking: Track whether structures selected for input have changed. Type of changes tracked: ( User changes input type (File/Workspace/Etc.), User selects a new file (via Browse or typing), PT inclusion/selection changes when those are the selected input, Workspace changes when Workspace is the selected input). When one of these changes occur, the InputSelector object emits an input_changed signal. Workspace changes are not tracked for PT selection source, unless extra_ws_tracking is set to True. Tracking will only occur if the panel is currently shown. The default is tracking=False because this adds overhead to every workspace change and project update. :param extra_ws_tracking: Typically input_changed signal is not emitted on Workspace changes if source is PT Selection. If this option is set, and one of selected entries if included in the Workspace, emit input_changed if the Workspce changes with one of these events: WORKSPACE_CHANGED_EVERYTHING, WORKSPACE_CHANGED_APPEND, or WORKSPACE_CHANGED_CONNECTIVITY. This will result in multiple input_changed signal emissions for a single change in inclusion - one for the project update and multiple emits because Maestro calls the callback multiple times for each (un)inclusion. No signal will be emitted for workspace changes if none of the selected entries are currently included in the Workspace. The default is extra_ws_tracking=False due to the extra overhead of this signal. These are not _InputParameters attributes, and the input structure file is not written by this class. InputSelector does that work. :ivar source_changed: signal emitted when the input source has changed; emitted with the new input source string :vartype source_changed: QtCore.pyqtSignal """ # Add signals to emit when tracking input changes input_changed = QtCore.pyqtSignal() source_changed = QtCore.pyqtSignal(str) # Input sources used in the option menu FILE = "file" SELECTED_ENTRIES = "selected_entries" PROJECT_TABLE = SELECTED_ENTRIES # For backwards compatability only INCLUDED_ENTRIES = "included_entries" INCLUDED_ENTRY = "included_entry" # Ev:112467 WORKSPACE = "workspace"
[docs] def __init__(self, parent, **kwargs): """ See class docstring. Raises an Exception if no input source is allowed. """ # Reference to AppFramework instance: self.parent = parent # Initialize default options self.options = { 'filetypes': None, # Custom file formats # Whether strict Maestro format is supported 'support_mae': True, # Whether SD format is supported 'support_sd': False, # Whether PDB format is supported 'support_pdb': False, # Whether CMS format is supported 'support_cms': False, # For file dialog 'initialdir': None, # Show "File" (or "Files") source? 'file': True, # Can select more than one file? 'multiplefiles': False, # Show "Project Table (selected entries)" source: 'selected_entries': True, # Show "Workspace (included entries)" source: 'included_entries': False, # Show "Workspace (included entry)" source (only one entry can be included) 'included_entry': False, # Show "Workspace" source (multiple included entries are merged) 'workspace': False, # Write the user-specified structures to <jobname>.<ext>? 'writefile': True, # label to display 'label_text': 'Use structures from:', # File label to display 'file_text': 'File name:', # Default source 'default_source': None, # Emit signals for input changes 'tracking': False, # Emit input_changed signal on Workspace modification if source # is PT selection, and at least one selected entry is also included. 'extra_ws_tracking': False, # Place label above combo box; remove file label 'narrow_layout': False, # Show from & to spinboxes for selecting structure range. 'file_range': False, # File history and previous file dialog location will be shared # between all file dialogs with identical ids 'file_dialog_id': str(self.parent), } # Overwrite defaults with userspecified options self.options.update(kwargs) if 'projecttable' in self.options: self.options['selected_entries'] = self.options['projecttable'] if self.options['projecttable']: # If True (no point to compalain if we are not using this # source) import warnings warnings.warn( "AppFramework: 'projecttable' option is obsolete. Please use the new name 'selected_entries'. See EV 84755.", PendingDeprecationWarning) # Do not allow more than one "Workspace" source: wslist = [ self.options["workspace"], self.options["included_entry"], self.options["included_entries"] ] num_ws_options = sum(map(int, wslist)) if num_ws_options > 1: raise ValueError( '"workspace", "included_entry", and "included_entries" options are mutually exclusive' ) # Properties for input change tracking - these properties are only # populated if tracking=True if self.options['extra_ws_tracking'] and not self.options['tracking']: raise ValueError('tracking=True is required if ' 'extra_ws_tracking=True') self._registered_callbacks = {} self.current_file = None """ The currently chosen file path if tracking is on""" self._previous_selection = None if maestro: self.ws_changes_requiring_update = \ [maestro.WORKSPACE_CHANGED_EVERYTHING, maestro.WORKSPACE_CHANGED_APPEND, maestro.WORKSPACE_CHANGED_CONNECTIVITY] # Maps the function that adds a callback to the function that # removes it self.callback_removers = { maestro.workspace_changed_function_add: maestro.workspace_changed_function_remove, maestro.project_update_callback_add: maestro.project_update_callback_remove } QtWidgets.QFrame.__init__(self, parent) self.input_layout = QtWidgets.QVBoxLayout() self.input_layout.setContentsMargins(3, 3, 3, 3) self.setLayout(self.input_layout) # Determine Input Source options for inputMnu # These are only initial values; some item texts will be dynamically # updated as the user changes PT selection/inclusion: menu_items = [] if maestro: if self.options['workspace']: menu_items.append((self.WORKSPACE, "Workspace")) if self.options['included_entries']: menu_items.append( (self.INCLUDED_ENTRIES, "Workspace (included entries)")) if self.options['included_entry']: menu_items.append( (self.INCLUDED_ENTRY, "Workspace (included entry)")) if self.options['selected_entries']: menu_items.append( (self.SELECTED_ENTRIES, "Project Table (selected entries)")) else: # PANEL-8108 Since 'file' is the only option that works outside # of Maestro, always add it: self.options['file'] = True if self.options['file']: if self.options['multiplefiles']: menu_items.append((self.FILE, "Files")) else: menu_items.append((self.FILE, "File")) if self.options['file_range']: if not self.options['file']: raise ValueError("file_range option requires file option.") if self.options['multiplefiles']: raise ValueError( "file_range option not supported with multiplefiles option." ) if not menu_items: raise ValueError( "At least one input source is required to render a Job Input Frame." ) if maestro: default_source = self.options['default_source'] if default_source: if default_source == 'workspace' and self.options['workspace']: self._default_source = self.WORKSPACE elif default_source == 'included_entry' and self.options[ 'included_entry']: self._default_source = self.INCLUDED_ENTRY elif default_source == 'selected_entries' and self.options[ 'selected_entries']: self._default_source = self.SELECTED_ENTRIES elif default_source == 'included_entries' and self.options[ 'included_entries']: self._default_source = self.INCLUDED_ENTRIES elif default_source == 'file' and self.options['file']: self._default_source = self.FILE else: raise Exception("Invalid default_source: %s" % default_source) else: # No default_source specified if self.options['selected_entries']: self._default_source = self.SELECTED_ENTRIES elif self.options['file']: self._default_source = self.FILE elif self.options['included_entries']: self._default_source = self.INCLUDED_ENTRIES elif self.options['included_entry']: self._default_source = self.INCLUDED_ENTRY else: # only workspace allowed self._default_source = self.WORKSPACE else: # Outside of Maestro only allow "file" source self._default_source = self.FILE # source of job input menu label_text = self.options['label_text'] self.input_menu_fr = swidgets.SFrame(layout=self.input_layout, layout_type=swidgets.HORIZONTAL) if label_text is not None: self.input_menu_label = QtWidgets.QLabel(self) self.input_menu_label.setText(label_text) self.input_menu_fr.addWidget(self.input_menu_label) self.input_menu = swidgets.SComboBox(self) self.input_menu.setSizeAdjustPolicy( QtWidgets.QComboBox.AdjustToContents) self.input_menu_fr.addWidget(self.input_menu) self.pt_button = swidgets.ProjectTableButton(self) self.input_menu_fr.addWidget(self.pt_button) self.input_menu_fr.addStretch() # Add a stretch to the right self.enabled_sources = [] for source, text in menu_items: self.input_menu.addItem(text, source) self.enabled_sources.append(source) self.input_menu.currentIndexChanged.connect(self._inputSourceChanged) self.input_menu.show() if self.options['file']: self.addFileWidgets() if self.options['narrow_layout']: self.input_layout.insertWidget(0, self.input_menu_label) if self.options.get('file'): self.file_label.setVisible(False) # Enable/disable widgets as needed, and set input source to default # source: self.reset() self.params = _InputParameters() self._inputSourceChanged() # Used by AF2: self.original_cwd = None
[docs] def setInputSourceComboVisible(self, set_visible): """ Show or hide input source combo. :type set_visible: bool :param set_visible: True - show input combo, False - hide it. """ self.input_menu_fr.setVisible(set_visible)
[docs] def addFileWidgets(self): """ Add widgets associated with a file input source to the input frame. """ # From file... structure file # container frame for file input self.file_layout = QtWidgets.QHBoxLayout() self.input_layout.addLayout(self.file_layout) self.file_layout.setContentsMargins(0, 0, 0, 0) # File name: self.file_label = QtWidgets.QLabel(self) if self.options["file_text"] == "File name:" and self.options[ "multiplefiles"]: self.options["file_text"] = "File names:" self.file_label.setText(self.options['file_text']) self.file_layout.addWidget(self.file_label) self.file_text = QtWidgets.QLineEdit(self) self.file_layout.addWidget(self.file_text, 10) # browse input file selector self.browse_button = QtWidgets.QPushButton("Browse...", self) self.file_layout.addWidget(self.browse_button) self.browse_button.clicked.connect(self.browseFiles) if self.options['tracking']: self.file_text.editingFinished.connect(self.getFileInputInfo) if self.options['file_range']: self.addRangeWidgets()
[docs] def addRangeWidgets(self): """ Add from/to range widgets under the file entry field. """ self.range_layout = QtWidgets.QHBoxLayout() self.input_layout.addLayout(self.range_layout) self.range_layout.setContentsMargins(0, 0, 0, 0) self.range_label = QtWidgets.QLabel("Range:", self) self.range_layout.addWidget(self.range_label) self.from_sb = QtWidgets.QSpinBox(self) self.from_sb.setRange(1, 99999999) self.range_layout.addWidget(self.from_sb) self.to_label = QtWidgets.QLabel("to:", self) self.range_layout.addWidget(self.to_label) self.to_sb = QtWidgets.QSpinBox(self) self.to_sb.setRange(1, 99999999) self.to_sb.setValue( 5) # Default maximum in Binding Mode Metadynamics GUI. self.range_layout.addWidget(self.to_sb) self.end_cb = QtWidgets.QCheckBox("End", self) self.end_cb.toggled.connect(self.to_sb.setDisabled) self.end_cb.setChecked(True) self.range_layout.addWidget(self.end_cb) self.range_layout.addStretch() if self.options['tracking']: self.from_sb.valueChanged.connect(self.getFileInputInfo) self.to_sb.valueChanged.connect(self.getFileInputInfo) self.end_cb.toggled.connect(self.getFileInputInfo)
def _inputSourceChanged(self, ignored=None): """ Callback method of the input option menu. Disables the file selector and entry field if that is not the source chosen. If tracking is on, this also checks to see if the set of structures selected for input has changed, emits input_changed signal, and saves the current selection for tracking changes later. """ source = self.inputState() self.source_changed.emit(source) if self.FILE in self.enabled_sources: enable = source == self.FILE self._fileSelectSetEnabled(enable) self.pt_button.setVisible(source != self.FILE) if self.options['tracking']: # Always emit input_changed signal when changing source if source in (self.SELECTED_ENTRIES, self.INCLUDED_ENTRY, self.INCLUDED_ENTRIES): # Trigger input_changed only if actual entry selection changed. # e.g. if same entry is selected and included, changing the # source would not have affect. self.getSelectedEntryInfo() else: # Changing to a project-independent input source (e.g. file or # workspace) will always cause `input_changed` to be emitted. # The `_previous_selection` value should be `None` to # differentiate from `set()`, which would be the previous value # if a project-dependent input source were selected but no entry # was specified. self._previous_selection = None self.input_changed.emit() def _fileSelectSetEnabled(self, enable): """ Turn the file selector browse button and entry field or or off. """ widgets = (self.browse_button, self.file_label, self.file_text) if self.options['file_range']: self.to_sb.setVisible(enable and not self.end_cb.isChecked()) widgets += (self.range_label, self.from_sb, self.to_label, self.end_cb) for widget in widgets: widget.setVisible(enable)
[docs] def inputState(self): """ Return the input type as class constant FILE, SELECTED_ENTRIES, INCLUDED_ENTRIES, INCLUDED_ENTRY, or WORKSPACE. Returns the current state of the menu. For the state at the time of call to setup(), use InputSelector.sourceType() method. """ # TODO consider renaming to getInputState() return self.input_menu.itemData(self.input_menu.currentIndex())
[docs] def setInputState(self, input_state): """ Set the input type. :param input_state: The input state to change to. It must one of FILE, SELECTED_ENTRIES, INCLUDED_ENTRIES, INCLUDED_ENTRY, or WORKSPACE. :type input_state: str :raise ValueError: If the specified input type is not present """ try: index = self.enabled_sources.index(input_state) except ValueError: raise ValueError("Specified option not present") self.input_menu.setCurrentIndex(index)
[docs] def getFile(self): """ Private method; use structFile() method instead. Returns the contents of the filename entry field. If self.original_dir is defined, relative paths will be converted to absolute paths based on that (This is expected by AF2). Returns '' if this is called while file input is not allowed. """ if not hasattr(self, "file_text"): return "" filename = self.file_text.text() if filename == '': return filename if not self.original_cwd: return filename if not os.path.isabs(filename): filename = os.path.join(self.original_cwd, filename) return filename
[docs] def getFiles(self): """ Private method; use structFiles() method instead. Return the list of file names from the file entry field. This is a true list, not a comma-separated list. """ # FIXME: The getFiles/browseFiles hack used here will not work with # fileis that have ", " embedded in their names. if self.inputState() == self.FILE: # NOTE: This honors the self.original_cwd option (used by AF2) return self.getFile().split(',') else: return []
[docs] def setFile(self, filename): """ Private method; do not use directly. Select the specified filename in the input selector. To specify multiple files, separate them using commas. "File" or "Files" will be selected as the input source as appropriate. :param filename: The name of the file or files to select :type filename: str :raise RuntimeError: If the file entry input state is not avaiable. :raise ValueError: If multiple files were provided, but the multiple files input state is not available. """ try: self.setInputState(self.FILE) except ValueError: raise RuntimeError("File entry not available") if "," in filename and not self.options['multiplefiles']: raise ValueError("The filename contains a comma, but multiple " "input files are not allowed.") self.file_text.setText(filename) self.file_text.editingFinished.emit()
[docs] def validate(self): """ Validate that the InputSelector is in a consistent and complete state. Returns an error string if the input is empty or invalid, or None if input is valid. :rtype: None or str :return: None on success, or an error string if no structures are specified (i.e. the source is set to FILE but there is no specified file name, source is selected entries and no entries are selected, etc.) or otherwise invalid. """ inputstate = self.inputState() if inputstate == self.FILE: if not self.getFile(): return "No file specified as a structure source" for fname in self.getFiles(): if not os.path.exists(fname): return 'The specified file does not exist: %s' % fname elif inputstate == self.SELECTED_ENTRIES: pt = maestro.project_table_get() if len(pt.selected_rows) < 1: return "No Project Table entries are selected" error = self.validateEntries(pt.selected_rows) if error: return error elif inputstate == self.INCLUDED_ENTRIES: pt = maestro.project_table_get() if len(pt.included_rows) < 1: return "No Project Table entries are included in the Workspace" error = self.validateEntries(pt.included_rows) if error: return error elif inputstate == self.INCLUDED_ENTRY: # Unlike WORKSPACE, do not allow more than one entry to be included try: st = maestro.get_included_entry() except RuntimeError: # More than one structure is in Workspace return "Only one entry must be included in the Workspace" else: if st.atom_total == 0: return "The workspace is empty" elif inputstate == self.WORKSPACE: # Allows more than one entry to be included st = get_workspace_structure() if st.atom_total == 0: return "The workspace is empty" else: # Should never happen; added this check just in case raise ValueError("Invalid input state: %s" % inputstate) return None
[docs] def validateEntries(self, rows): """ Ensure that the passed entry rows are valid :param iterable rows: Project rows :rtype: str or None :return: Error message """ maestro.project_table_synchronize() for row in rows: struct = row.getStructure(props=False, copy=False, workspace_sync=False) if struct.atom_total == 0: return f'Row {row.row_number} entry is empty.' return None
[docs] def writePTEntries(self, filename, source): """ Write selected Project Table entries to 'filename'. Raises a RuntimeError on error. Returns False if the user cancelled. Returns True on success. """ pt = maestro.project_table_get() if source == self.WORKSPACE or source == self.INCLUDED_ENTRIES: if len(pt.included_rows) == 0: raise RuntimeError("No entries are included") st_iterator = utils.get_included_structures(copy=False) elif source == self.SELECTED_ENTRIES: if len(pt.selected_rows) == 0: raise RuntimeError("No entries are selected") st_iterator = utils.get_selected_structures(copy=False) else: raise ValueError("Invalid source.") structure.write_cts(st_iterator, filename) return True
def _reset(self): # For backwards compatability self.reset()
[docs] def reset(self): """ Clear the file path (if any) and range (if enabled), and set the source to the default source. """ # Set source in Input Menu to default: self.setInputState(self._default_source) self.current_file = None if self.FILE in self.enabled_sources: self.file_text.setText("") enable = self.inputState() == self.FILE self._fileSelectSetEnabled(enable) if enable: # setText() won't emit an editingFinished signal like user # interaction would, so emit it manually so that input_changed # can get emitted if necessary. self.file_text.editingFinished.emit() if self.options['file_range']: self.from_sb.setValue(1) self.to_sb.setValue(1) self.end_cb.setChecked(True)
[docs] def setup(self, jobname): """ This method serializes the input structure(s) to disk and stores the name of the file in the self.params instance. Returns False (i.e., aborts the setup cascade) if no input structure file is written. Otherwise, returns True. :param jobname: Determines what base name to use for the structure file that gets written. :type jobname: str Job parameters set by this method are... inputstrucsource - The input source type that was used (FILE, SELECTED_ENTRIES, INCLUDED_ENTIRES, INCLUDED_ENTRY, or WORKSPACE). inputstrucfile - If "writefile", the name of the written structure file (<jobname>.<ext>); otherwise the path to the user-specified structure file. (not set when "multiplefiles" is True) inputstrucfiles - If "writefile" is True, this is a list with one file (same as inputstrucfile); if False a list of selected file paths. (only set if "multiplefiles" is True) inputstrucorigfile - Original path to the user-specified input file, (only set if source is FILE, "writefile" is True, and "multiplefiles" is False). Unused parameters are set to None. """ if not jobname: jobname = 'temp' self.params.reset() jobparam = self.params # It is expected that InputSelector was already validated; but check # again to make sure, and raise an exception if it wasn't: if self.validate(): raise RuntimeError( "validation failed in setup(); please validate first") state = self.inputState() jobparam.inputstrucsource = state # Validate input: # TODO: Some of this overlaps with checks that validate() method does. if state == self.FILE: fileselection = self.getFiles() if not fileselection: self.parent.warning("No file specified as a structure source") return False # Validate file range fields: if self.options['file_range'] and self.to_sb.isEnabled(): if self.from_sb.value() > self.to_sb.value(): self.parent.warning("Invalid range specified") return False else: # Validate sources other than FILE if state == self.SELECTED_ENTRIES: pt = maestro.project_table_get() if len(pt.selected_rows) < 1: self.parent.warning("No Project Table entries are selected") return False elif state == self.INCLUDED_ENTRIES: pt = maestro.project_table_get() if len(pt.included_rows) < 1: self.parent.warning( "No Project Table entries are included in the Workspace" ) return False elif state in [self.WORKSPACE, self.INCLUDED_ENTRY]: # NOTE: If INCLUDED_ENTRY, the validate() has already made # sure that only one entry is included. st = get_workspace_structure() if st.atom_total == 0: self.parent.warning("The workspace is empty") return False if self.options['writefile'] and not \ (state == self.FILE and self.options['multiplefiles']): # With FILE and multiplefiles - whether or not writefile is set, # the <jobname>-ext file is not written (input files may have # different extensions). # Write the input structure(s) to <jobname>.<ext>: written_file = self._writeFilename(jobname) if written_file is None: return False jobparam.inputstrucfile = written_file if self.options['multiplefiles']: jobparam.inputstrucfiles = [written_file] if state == self.FILE: jobparam.inputstrucorigfile = fileselection[0] else: # Not writing a <jobname>.<ext> file if state == self.FILE: if self.options['multiplefiles']: jobparam.inputstrucfiles = fileselection else: jobparam.inputstrucfile = fileselection[0] return True
[docs] def browseFiles(self): """ Callback method of the file browse button. Pops up a file selector and places the selected file name into the file text field """ # Generate string key to remember last browsed dir for this panel idname = self.options['file_dialog_id'] new_files = None if self.options['filetypes']: # Custom input file requested: filetypes = self.options['filetypes'] if isinstance(filetypes, str): # Was given a QFileDialog filter string: filter_string = filetypes else: filter_string = format_list_to_filter_string(filetypes) else: # No "filetypes" option specified. from schrodinger.ui.qt.appframework import \ filter_string_from_supported filter_string = filter_string_from_supported( support_mae=self.options['support_mae'], support_sd=self.options['support_sd'], support_pdb=self.options['support_pdb'], support_cms=self.options['support_cms']) if self.options['multiplefiles']: new_files = filedialog.get_open_file_names( self, "Select Input File(s)", self.options['initialdir'], filter_string, id=idname, ) # selected_string argument may be added after filter_string else: # Supporting only single input file new_file = filedialog.get_open_file_name( self, "Select Input File", self.options['initialdir'], filter_string, id=idname, ) # selected_string argument may be added after filter_string if new_file: new_files = [new_file] if new_files: self.file_text.setText(",".join(new_files)) if self.options['tracking']: self.getFileInputInfo()
[docs] def addCallback(self, callback_adder, method): """ Add a callback for a method - quietly do nothing if such callback already exists. Example: self.addCallback(maestro.workspace_changed_function_add, self.wsChanged) This method tracks callbacks that are added for easy removal later. This tracking is done because Maestro prints an uncatchable WARNING to the terminal if we try to remove a callback that has not been added. :type callback_adder: callable :param callback_adder: A callable function that adds a callback, such as maestro.workspace_changed_function_add :type method: callable :param method: The method to use as the callback function. """ if callback_adder not in self.callback_removers: raise KeyError('The callback removal function for %s must be ' 'added to the callback_removers dictionary.' % callback_adder) self._registered_callbacks[callback_adder] = method try: callback_adder(method) except ValueError: # Already registered pass
[docs] def removeCallback(self, callback_adder, method): """ Remove a callback that may have been registered previously - silently do nothing if the callback isn't registered. We need to track callbacks in this manner because Maestro prints a WARNING message to the terminal if a non-registered callback is attempted to be removed. :type callback_adder: callable :param callback_adder: A callable function that adds a callback, such as maestro.workspace_changed_function_add. This adder and its remover must be entered into the callback_removers dictionary. :type method: callable :param method: The method to use as the callback function. """ if self._registered_callbacks.get(callback_adder) == method: try: self.callback_removers[callback_adder](method) except KeyError: raise KeyError('The callback removal function for %s must be ' 'added to the callback_removers dictionary.' % callback_adder) self._registered_callbacks.pop(callback_adder)
[docs] def addAllCallbacks(self): """ Add callbacks. """ self.addCallback(maestro.project_update_callback_add, self.projectUpdated) # Update the source menu items (counts of included/selected entries): self.projectUpdated() self.addCallback(maestro.workspace_changed_function_add, self.workspaceChanged)
[docs] def removeAllCallbacks(self): """ Remove any previously added callbacks. We need to track callbacks in this manner because Maestro prints a WARNING message to the terminal if a non-registered callback is attempted to be removed. """ for adder, method in self._registered_callbacks.items(): try: self.callback_removers[adder](method) except KeyError: raise KeyError('The callback removal function for %s must be ' 'added to the callback_removers dictionary.' % adder) self._registered_callbacks = {}
[docs] def getFileInputInfo(self): """ Emit input_changed if the new file picked by the user is different from the old file """ if not self.isVisible(): return new_file = self.getFile() # If new file name does not match old file name, unless there is no file # name and there was no previous input if new_file != self.current_file and new_file: self.current_file = new_file self.input_changed.emit()
[docs] def getSelectedEntryInfo(self): """ Set self._previous_selection to the entries that are selected by the user, depending on the source. Trigger input_changed if the selection has changed. For INCLUDED_ENTRY and INCLUDED_ENTRIES, this is the IDs of the entries included in the Workspace. For SELECTED_ENTRIES, it's the IDs of the entries selected in the PT. """ if not self.isVisible(): return try: pt = maestro.project_table_get() except project.ProjectException: # Project closing self._previous_selection = None self.input_changed.emit() return source = self.inputState() if source == self.SELECTED_ENTRIES: new_selected_ids = {e.entry_id for e in pt.selected_rows} elif source in (self.INCLUDED_ENTRY, self.INCLUDED_ENTRIES, self.WORKSPACE): new_selected_ids = {e.entry_id for e in pt.included_rows} if new_selected_ids != self._previous_selection: # Emit `input_changed` if the set of specified structures has # changed, or if the user has just switched to a project-dependent # input source (e.g. selected or included entries) from a project- # independent source (e.g. a file). If the user has switched between # two project-dependent input sources and the specified # entries have not changed, do not emit `input_changed`. # # Note that if the slot connected to input_changed does something # that results in the Project Table update() method being called, # this function (getSelectedEntryInfo) will be called again # even though the actual entry selection may not have changed. This # could result in an infinite loop if we don't set # _previous_selection to the new selection so that the next time # this function is called (with the same selection) we don't end up # in this if block again. self._previous_selection = new_selected_ids self.input_changed.emit() else: self._previous_selection = new_selected_ids
def _setSourceItemText(self, source, text): """ Set the menu item text for the given source to specified string. """ try: index = self.enabled_sources.index(source) except ValueError: raise ValueError("Source not found: %s" % source) self.input_menu.setItemText(index, text)
[docs] def workspaceChanged(self, what_changed): """ If tracking is enabled, emit input_changed if the Workspace change merits it. :type what_changed: maestro module constant (str) :param what_changed: Description of what changed """ if not self.isVisible(): return if what_changed not in self.ws_changes_requiring_update: return if self.options['tracking'] and self.options['extra_ws_tracking']: source = self.inputState() if source in (self.INCLUDED_ENTRIES, self.INCLUDED_ENTRY, self.WORKSPACE): # User's selection is certainly in the Workspace self.input_changed.emit() elif source == self.SELECTED_ENTRIES: # Verify that users selection (PT selection) is actually # affected (is in the Workspace) try: pt = maestro.project_table_get() except project.ProjectException: # This can happen during project close and the project table # has an unknown state. return for eid in maestro.get_included_entry_ids(): if pt.getRow(eid).is_selected: self.input_changed.emit() break
[docs] def projectUpdated(self): """ If tracking is enabled, emit input_changed when PT source is included or selected entries, and user's entry selection changes. """ if self.options['tracking']: source = self.inputState() if source in (self.SELECTED_ENTRIES, self.INCLUDED_ENTRIES, self.INCLUDED_ENTRY, self.WORKSPACE): self.getSelectedEntryInfo() self._updateEntryCounts()
def _updateEntryCounts(self): """ Update the menu items of source input menu based on the numbers of entries that are currently included/selected. """ try: pt = maestro.project_table_get() except project.ProjectException: # It is possible for this to get called during project close when # the project table has an unknown state (MATSCI-7317). It's OK to # do nothing here as another update should occur when the new # project opens and projectUpdated gets called again return num_included = len(pt.included_rows) if self.INCLUDED_ENTRIES in self.enabled_sources: if num_included == 1: text = "Workspace (1 included entry)" else: text = "Workspace (%i included entries)" % num_included self._setSourceItemText(self.INCLUDED_ENTRIES, text) if self.SELECTED_ENTRIES in self.enabled_sources: num_selected = len(pt.selected_rows) if num_selected == 1: text = "Project Table (1 selected entry)" else: text = "Project Table (%i selected entries)" % num_selected self._setSourceItemText(self.SELECTED_ENTRIES, text) # NOTE: Text for "Workspace (included entry)" option is not changed.
[docs] def showEvent(self, event): """ Make sure the proper signals are emitted and proper callbacks are registered when the panel shows itself :type event: QShowEvent :param event: The QEvent object generated by this event :return: The return value of the QFrame showEvent method """ value = QtWidgets.QFrame.showEvent(self, event) if maestro: self.addAllCallbacks() return value
[docs] def hideEvent(self, event): """ Deregister callbacks when the panel hides itself so that we are not monitoring changes when the panel is inactive. :type event: QHideEvent :param event: The QEvent object generated by this event :return: The return value of the QFrame hideEvent method """ value = QtWidgets.QFrame.hideEvent(self, event) self.removeAllCallbacks() return value
[docs] def structures(self, validate=True): """ A generator that returns the user-specified input structures one at a time. API Example:: # appframework1 for struct in self._if.structures(): do something # appframework2 for struct in self.input_selector.structures(): do something :type validate: bool :param validate: Validate that the InputSelector is in a consistent and complete state (rows are actually selected or file is specified, etc.) Return an empty iterator if input is empty or invalid. Default is True. This option is obsolete. """ iterator = iter([]) if validate: try: if self.validate() is not None: return iterator except project.ProjectException: # No project currently open - this can occur if structures() is # being called as a result of an input_changed signal that is # fired because of a project close event. return iterator source = self.inputState() if source == self.FILE: # inputState does not distinguish "File" or "Files" if not self.options['multiplefiles']: return self._getFileStsIterator() else: filenames = self.getFiles() if filenames: # pass_errors=False makes the StructureReader & MultiReader # cases act the same if a file error occurs - the calling # code has to handle it. iterator = structure.MultiFileStructureReader( filenames, pass_errors=False) elif source in (self.INCLUDED_ENTRIES, self.INCLUDED_ENTRY, self.SELECTED_ENTRIES): try: ptable = maestro.project_table_get() except project.ProjectException: # No currently open project return iterator # ( x.method() for x in alist ) is a generator that evaluates the # next x.method() when its .next() method is called - this way not # all the structures are in memory at once if source == self.SELECTED_ENTRIES: iterator = utils.get_selected_structures(pt=ptable) else: iterator = utils.get_included_structures(pt=ptable) elif source == self.WORKSPACE: struct = maestro.workspace_get() if struct.atom_total != 0: iterator = iter([struct]) else: raise RuntimeError("Unkown source: %s" % source) return iterator
[docs] def cmsModels(self): """ A generator that returns the user-specified Cms model structures one at a time. :rtype: generator of cms.Cms :return: Each item yielded is a Cms model :raise TypeError: If any of the selected input is not a valid Cms model """ input_state = self.inputState() if input_state == self.FILE: try: model = cms.Cms(self.getFile()) except (TypeError, ValueError, OSError) as err: message = 'Invalid model: %s.' % str(err) raise TypeError(message) else: desmond_gui.process_loaded_model(model) yield model else: eid_msg = ("Entry ID {eid} does not appear to be a valid Desmond " "system. Please ensure all specified input entries " "are Desmond systems.") pt = maestro.project_table_get() if input_state == self.SELECTED_ENTRIES: rows = pt.selected_rows else: rows = pt.included_rows for row in rows: model = desmond_gui.get_model_from_pt_row(row) if model is None: message = eid_msg.format(eid=row.entry_id) raise TypeError(message) else: desmond_gui.process_loaded_model(model) yield model
[docs] def countStructures(self): """ Return the number of structures specified. If input is invalid, zero will be returned. NOTE: If input source is file, this call can take some time for larger structures files. :return: Number of input structrues :rtype: int """ if self.validate() is not None: return 0 # No need to call getStructure() for PT sources: source = self.inputState() if source == self.SELECTED_ENTRIES: return len(maestro.project_table_get().selected_rows) elif source in (self.INCLUDED_ENTRIES, self.INCLUDED_ENTRY): return len(maestro.project_table_get().included_rows) # For FILE and WORKSPACE sources, use code from structures(): # No need to re-validate, as we already did that above return sum(1 for st in self.structures(validate=False))
def _getFileStsIterator(self): """ Return an iterator of structures in the user-selected file, honoring the range, is specified. :return: Iterator of structures in the specified file. :rtype: Iterator of `structure.Structure` """ filename = self.getFile() if not filename: return try: st_reader = structure.StructureReader(filename) except IOError: # Failed to open the file for reading, which can happen if this # function is called prior to validating the input (such as if # structures() is called with validate=False. We return an empty # iterator similar to what is done for other invalid input. return if self.options['file_range']: from_num = self.from_sb.value() to_num = self.to_sb.value( ) if not self.end_cb.isChecked() else sys.maxsize for i, st in enumerate(st_reader, start=1): if i < from_num: continue if i > to_num: break yield st else: for st in st_reader: yield st def _validate(self, stop_on_fail=True): # This method is required for AF2 msg = self.validate() if msg: self.params.reset() return False, msg else: return True def _getValue(self): if self.params: return self.params.__dict__ def _writeFilename(self, jobname): """ Write the input structure file and return the file name. The file name will be derived from the jobname. Return None if not successful. This method won't do anything but return the filename if it has been called previously. """ state = self.inputState() if state == self.FILE: # Will return None upon failure (after displaying error): filename = self._setupFile(jobname) elif state == self.SELECTED_ENTRIES: # Will return None upon failure (after displaying error): filename = self._setupProjectTable(jobname, source=self.SELECTED_ENTRIES) elif state == self.INCLUDED_ENTRIES: filename = self._setupProjectTable(jobname, source=self.INCLUDED_ENTRIES) elif state == self.INCLUDED_ENTRY: # Will return None upon failure (after displaying error): filename = self._setupWorkspace(jobname, allow_multiple=False) # NOTE: Will display an error if more than one entry is # included in the Workspace elif state == self.WORKSPACE: # Will return None upon failure (after displaying error): filename = self._setupWorkspace(jobname, allow_multiple=True) return filename def _setupFile(self, jobname): """ Copies the user-specified file to <jobname>.<ext>, where <ext> is the extension of the selected file. Returns None if user cancelled or on error (after displaying error). """ fileselection = self.getFile() if not fileselection: # This should never happen because validate() checks this. raise RuntimeError( "No file path specified, and validate() did not catch this condition" ) else: # Copy the selected file to "jobname.<ext>": # NOTE: *.mae.gz is treated specially by OUR splitext(): # This copy operation will preserve the file format, that's why it's # important to keep the same extension. ext = fileutils.splitext(fileselection)[1] filename = jobname + ext if not os.path.isfile(fileselection): msg = 'File does not exist: %s' % fileselection self.parent.warning(msg) return None try: shutil.copy(fileselection, filename) # May be that fileselection == filename except Exception as err: if "are the same file" in str(err): # Use selected <jobname>.<ext> file as source. # It's okay that copy command failed in this case. pass else: msg = 'Failed to copy file "%s" to "%s"!' % (fileselection, filename) self.parent.warning(msg) return None return filename def _setupProjectTable(self, jobname, source): """ Do setup for project table structure source. Write the selected Project Table entries to the file <jobname>.maegz. Pops up an "OK"/"Cancel" dialog if the file already exists, and overwrites the file if "OK" is chosen. Returns None if there is a problem exporting the structure(s) from Maestro, or if the writing is cancelled. """ filename = jobname + ".maegz" # Returns False if user cancelled, raises exception on error: if self.writePTEntries(filename, source): return filename else: return None def _setupWorkspace(self, jobname, allow_multiple=False): """ Do setup for workspace structure source. Write Workspace structure to file <jobname>.maegz. Pops up an "OK"/"Cancel" dialog if the file already exists, and overwrites the file if "OK" is chosen. Returns None if there is no WS structure, or if the writing is cancelled. """ # Check to see if workspace is empty, and return None if so: try: st = maestro.get_included_entry() except RuntimeError: if not allow_multiple: self.parent.warning("More than one entry is included") return None else: # More than one structure is in Workspace st = maestro.workspace_get() if st.atom_total == 0: self.parent.warning("The workspace is empty") return None filename = jobname + ".maegz" st.write(filename) return filename # Methods for reading the state of the input frame, after setup() was # called:
[docs] def structFile(self): if self.params: return self.params.inputstrucfile
[docs] def structFiles(self): if self.params: return self.params.inputstrucfiles
[docs] def originalStructFile(self): if self.params: return self.params.inputstrucorigfile
[docs] def sourceType(self): if self.params: return self.params.inputstrucsource
[docs] def af2SettingsGetValue(self): """ This function adds support for the settings mixin. :return: (Input source string, file path) :rtype: (str, str) """ return (self.inputState(), self.getFile())
[docs] def af2SettingsSetValue(self, value): """ This function adds support for the settings mixin. :type value: (str, str) :param value: (Input source string, file path). """ ''' PANEL-10177 Loading older version of settings file in 17-2, affects KNIME nodes. So this is a temporary fix until a permennant solution is identified. ''' if isinstance(value, dict): # Restore Source Combobox from json file from prior release. # (works only outside of Maestro) assert value['input_menu'] == "File" assert self.input_menu.currentText() == "File" self.file_text.setText(value['file_text']) return source, filename = value self.setInputState(source) # Even if file is not the current source, we still want to restore # the file path, in case the user wants to re-select "File" later: if hasattr(self, "file_text"): self.file_text.setText(filename)