class ProfilerWidget(PluginMainWidget): """ Profiler widget. """ ENABLE_SPINNER = True DATAPATH = get_conf_path('profiler.results') # --- Signals # ------------------------------------------------------------------------ sig_edit_goto_requested = Signal(str, int, str) """ This signal will request to open a file in a given row and column using a code editor. Parameters ---------- path: str Path to file. row: int Cursor starting row position. word: str Word to select on given row. """ sig_redirect_stdio_requested = Signal(bool) """ This signal is emitted to request the main application to redirect standard output/error when using Open/Save/Browse dialogs within widgets. Parameters ---------- redirect: bool Start redirect (True) or stop redirect (False). """ sig_started = Signal() """This signal is emitted to inform the profiling process has started.""" sig_finished = Signal() """This signal is emitted to inform the profile profiling has finished.""" def __init__(self, name=None, plugin=None, parent=None): super().__init__(name, plugin, parent) self.set_conf('text_color', MAIN_TEXT_COLOR) # Attributes self._last_wdir = None self._last_args = None self._last_pythonpath = None self.error_output = None self.output = None self.running = False self.text_color = self.get_conf('text_color') # Widgets self.process = None self.filecombo = PythonModulesComboBox( self, id_=ProfilerWidgetMainToolbarItems.FileCombo) self.datatree = ProfilerDataTree(self) self.datelabel = QLabel() self.datelabel.ID = ProfilerWidgetInformationToolbarItems.DateLabel # Layout layout = QVBoxLayout() layout.addWidget(self.datatree) self.setLayout(layout) # Signals self.datatree.sig_edit_goto_requested.connect( self.sig_edit_goto_requested) # --- PluginMainWidget API # ------------------------------------------------------------------------ def get_title(self): return _('Profiler') def get_focus_widget(self): return self.datatree def setup(self): self.start_action = self.create_action( ProfilerWidgetActions.Run, text=_("Run profiler"), tip=_("Run profiler"), icon=self.create_icon('run'), triggered=self.run, ) browse_action = self.create_action( ProfilerWidgetActions.Browse, text='', tip=_('Select Python script'), icon=self.create_icon('fileopen'), triggered=lambda x: self.select_file(), ) self.log_action = self.create_action( ProfilerWidgetActions.ShowOutput, text=_("Output"), tip=_("Show program's output"), icon=self.create_icon('log'), triggered=self.show_log, ) self.collapse_action = self.create_action( ProfilerWidgetActions.Collapse, text=_('Collapse'), tip=_('Collapse one level up'), icon=self.create_icon('collapse'), triggered=lambda x=None: self.datatree.change_view(-1), ) self.expand_action = self.create_action( ProfilerWidgetActions.Expand, text=_('Expand'), tip=_('Expand one level down'), icon=self.create_icon('expand'), triggered=lambda x=None: self.datatree.change_view(1), ) self.save_action = self.create_action( ProfilerWidgetActions.SaveData, text=_("Save data"), tip=_('Save profiling data'), icon=self.create_icon('filesave'), triggered=self.save_data, ) self.load_action = self.create_action( ProfilerWidgetActions.LoadData, text=_("Load data"), tip=_('Load profiling data for comparison'), icon=self.create_icon('fileimport'), triggered=self.compare, ) self.clear_action = self.create_action( ProfilerWidgetActions.Clear, text=_("Clear comparison"), tip=_("Clear comparison"), icon=self.create_icon('editdelete'), triggered=self.clear, ) self.clear_action.setEnabled(False) # Main Toolbar toolbar = self.get_main_toolbar() for item in [self.filecombo, browse_action, self.start_action]: self.add_item_to_toolbar( item, toolbar=toolbar, section=ProfilerWidgetMainToolbarSections.Main, ) # Secondary Toolbar secondary_toolbar = self.create_toolbar( ProfilerWidgetToolbars.Information) for item in [ self.collapse_action, self.expand_action, self.create_stretcher( id_=ProfilerWidgetInformationToolbarItems.Stretcher1), self.datelabel, self.create_stretcher( id_=ProfilerWidgetInformationToolbarItems.Stretcher2), self.log_action, self.save_action, self.load_action, self.clear_action ]: self.add_item_to_toolbar( item, toolbar=secondary_toolbar, section=ProfilerWidgetInformationToolbarSections.Main, ) # Setup if not is_profiler_installed(): # This should happen only on certain GNU/Linux distributions # or when this a home-made Python build because the Python # profilers are included in the Python standard library for widget in (self.datatree, self.filecombo, self.start_action): widget.setDisabled(True) url = 'https://docs.python.org/3/library/profile.html' text = '%s <a href=%s>%s</a>' % (_('Please install'), url, _("the Python profiler modules")) self.datelabel.setText(text) def update_actions(self): if self.running: icon = self.create_icon('stop') else: icon = self.create_icon('run') self.start_action.setIcon(icon) self.start_action.setEnabled(bool(self.filecombo.currentText())) # --- Private API # ------------------------------------------------------------------------ def _kill_if_running(self): """Kill the profiling process if it is running.""" if self.process is not None: if self.process.state() == QProcess.Running: self.process.close() self.process.waitForFinished(1000) self.update_actions() def _finished(self, exit_code, exit_status): """ Parse results once the profiling process has ended. Parameters ---------- exit_code: int QProcess exit code. exit_status: str QProcess exit status. """ self.running = False self.show_errorlog() # If errors occurred, show them. self.output = self.error_output + self.output self.datelabel.setText('') self.show_data(justanalyzed=True) self.update_actions() def _read_output(self, error=False): """ Read otuput from QProcess. Parameters ---------- error: bool, optional Process QProcess output or error channels. Default is False. """ if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string(qba.data(), encoding='utf-8') if error: self.error_output += text else: self.output += text # --- Public API # ------------------------------------------------------------------------ def save_data(self): """Save data.""" title = _("Save profiler result") filename, _selfilter = getsavefilename( self, title, getcwd_or_home(), _("Profiler result") + " (*.Result)", ) if filename: self.datatree.save_data(filename) def compare(self): """Compare previous saved run with last run.""" filename, _selfilter = getopenfilename( self, _("Select script to compare"), getcwd_or_home(), _("Profiler result") + " (*.Result)", ) if filename: self.datatree.compare(filename) self.show_data() self.clear_action.setEnabled(True) def clear(self): """Clear data in tree.""" self.datatree.compare(None) self.datatree.hide_diff_cols(True) self.show_data() self.clear_action.setEnabled(False) def analyze(self, filename, wdir=None, args=None, pythonpath=None): """ Start the profiling process. Parameters ---------- wdir: str Working directory path string. Default is None. args: list Arguments to pass to the profiling process. Default is None. pythonpath: str Python path string. Default is None. """ if not is_profiler_installed(): return self._kill_if_running() # TODO: storing data is not implemented yet # index, _data = self.get_data(filename) combo = self.filecombo items = [combo.itemText(idx) for idx in range(combo.count())] index = None if index is None and filename not in items: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count() - 1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): if wdir is None: wdir = osp.dirname(filename) self.start(wdir, args, pythonpath) def select_file(self, filename=None): """ Select filename to profile. Parameters ---------- filename: str, optional Path to filename to profile. default is None. Notes ----- If no `filename` is provided an open filename dialog will be used. """ if filename is None: self.sig_redirect_stdio_requested.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python script"), getcwd_or_home(), _("Python scripts") + " (*.py ; *.pyw)") self.sig_redirect_stdio_requested.emit(True) if filename: self.analyze(filename) def show_log(self): """Show process output log.""" if self.output: output_dialog = TextEditor( self.output, title=_("Profiler output"), readonly=True, parent=self, ) output_dialog.resize(700, 500) output_dialog.exec_() def show_errorlog(self): """Show process error log.""" if self.error_output: output_dialog = TextEditor( self.error_output, title=_("Profiler output"), readonly=True, parent=self, ) output_dialog.resize(700, 500) output_dialog.exec_() def start(self, wdir=None, args=None, pythonpath=None): """ Start the profiling process. Parameters ---------- wdir: str Working directory path string. Default is None. args: list Arguments to pass to the profiling process. Default is None. pythonpath: str Python path string. Default is None. """ filename = to_text_string(self.filecombo.currentText()) if wdir is None: wdir = self._last_wdir if wdir is None: wdir = osp.basename(filename) if args is None: args = self._last_args if args is None: args = [] if pythonpath is None: pythonpath = self._last_pythonpath self._last_wdir = wdir self._last_args = args self._last_pythonpath = pythonpath self.datelabel.setText(_('Profiling, please wait...')) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(wdir) self.process.readyReadStandardOutput.connect(self._read_output) self.process.readyReadStandardError.connect( lambda: self._read_output(error=True)) self.process.finished.connect( lambda ec, es=QProcess.ExitStatus: self._finished(ec, es)) self.process.finished.connect(self.stop_spinner) if pythonpath is not None: env = [ to_text_string(_pth) for _pth in self.process.systemEnvironment() ] add_pathlist_to_PYTHONPATH(env, pythonpath) processEnvironment = QProcessEnvironment() for envItem in env: envName, __, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) processEnvironment.insert("PYTHONIOENCODING", "utf8") self.process.setProcessEnvironment(processEnvironment) self.output = '' self.error_output = '' self.running = True self.start_spinner() p_args = ['-m', 'cProfile', '-o', self.DATAPATH] if os.name == 'nt': # On Windows, one has to replace backslashes by slashes to avoid # confusion with escape characters (otherwise, for example, '\t' # will be interpreted as a tabulation): p_args.append(osp.normpath(filename).replace(os.sep, '/')) else: p_args.append(filename) if args: p_args.extend(shell_split(args)) executable = self.get_conf('executable', section='main_interpreter') self.process.start(executable, p_args) running = self.process.waitForStarted() if not running: QMessageBox.critical( self, _("Error"), _("Process failed to start"), ) self.update_actions() def stop(self): """Stop the running process.""" self.running = False self.process.close() self.process.waitForFinished(1000) self.stop_spinner() self.update_actions() def run(self): """Toggle starting or running the profiling process.""" if self.running: self.stop() else: self.start() def show_data(self, justanalyzed=False): """ Show analyzed data on results tree. Parameters ---------- justanalyzed: bool, optional Default is False. """ if not justanalyzed: self.output = None self.log_action.setEnabled(self.output is not None and len(self.output) > 0) self._kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return self.datelabel.setText(_('Sorting data, please wait...')) QApplication.processEvents() self.datatree.load_data(self.DATAPATH) self.datatree.show_tree() text_style = "<span style=\'color: %s\'><b>%s </b></span>" date_text = text_style % (self.text_color, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) self.datelabel.setText(date_text)
class LSPClient(QObject, LSPMethodProviderMixIn, SpyderConfigurationAccessor): """Language Server Protocol v3.0 client implementation.""" #: Signal to inform the editor plugin that the client has # started properly and it's ready to be used. sig_initialize = Signal(dict, str) #: Signal to report internal server errors through Spyder's # facilities. sig_server_error = Signal(str) #: Signal to warn the user when either the transport layer or the # server went down sig_went_down = Signal(str) def __init__(self, parent, server_settings={}, folder=getcwd_or_home(), language='python'): QObject.__init__(self) self.manager = parent self.zmq_in_socket = None self.zmq_out_socket = None self.zmq_in_port = None self.zmq_out_port = None self.transport = None self.server = None self.stdio_pid = None self.notifier = None self.language = language self.initialized = False self.ready_to_close = False self.request_seq = 1 self.req_status = {} self.watched_files = {} self.watched_folders = {} self.req_reply = {} self.server_unresponsive = False self.transport_unresponsive = False # Select a free port to start the server. # NOTE: Don't use the new value to set server_setttings['port']!! # That's not required because this doesn't really correspond to a # change in the config settings of the server. Else a server # restart would be generated when doing a # workspace/didChangeConfiguration request. if not server_settings['external']: self.server_port = select_port( default_port=server_settings['port']) else: self.server_port = server_settings['port'] self.server_host = server_settings['host'] self.external_server = server_settings.get('external', False) self.stdio = server_settings.get('stdio', False) # Setting stdio on implies that external_server is off if self.stdio and self.external_server: error = ('If server is set to use stdio communication, ' 'then it cannot be an external server') logger.error(error) raise AssertionError(error) self.folder = folder self.configurations = server_settings.get('configurations', {}) self.client_capabilites = CLIENT_CAPABILITES self.server_capabilites = SERVER_CAPABILITES self.context = zmq.Context() # To set server args self._server_args = server_settings.get('args', '') self._server_cmd = server_settings['cmd'] # Save requests name and id. This is only necessary for testing. self._requests = [] def _get_log_filename(self, kind): """ Get filename to redirect server or transport logs to in debugging mode. Parameters ---------- kind: str It can be "server" or "transport". """ if get_debug_level() == 0: return None fname = '{0}_{1}_{2}.log'.format(kind, self.language, os.getpid()) location = get_conf_path(osp.join('lsp_logs', fname)) # Create directory that contains the file, in case it doesn't # exist if not osp.exists(osp.dirname(location)): os.makedirs(osp.dirname(location)) return location def _clean_sys_path(self): """ Remove from sys.path entries that come from our config system. They will be passed to the server in the extra_paths option and are not needed for the transport layer. """ spyder_pythonpath = self.get_conf('spyder_pythonpath', section='main', default=[]) sys_path = sys.path[:] for path in spyder_pythonpath: if path in sys_path: sys_path.remove(path) return sys_path @property def server_log_file(self): """ Filename to redirect the server process stdout/stderr output. """ return self._get_log_filename('server') @property def transport_log_file(self): """ Filename to redirect the transport process stdout/stderr output. """ return self._get_log_filename('transport') @property def server_args(self): """Arguments for the server process.""" args = [] if self.language == 'python': args += [sys.executable, '-m'] args += [self._server_cmd] # Replace host and port placeholders host_and_port = self._server_args.format(host=self.server_host, port=self.server_port) if len(host_and_port) > 0: args += host_and_port.split(' ') if self.language == 'python' and get_debug_level() > 0: args += ['--log-file', self.server_log_file] if get_debug_level() == 2: args.append('-v') elif get_debug_level() == 3: args.append('-vv') return args @property def transport_args(self): """Arguments for the transport process.""" args = [ sys.executable, '-u', osp.join(LOCATION, 'transport', 'main.py'), '--folder', self.folder, '--transport-debug', str(get_debug_level()) ] # Replace host and port placeholders host_and_port = '--server-host {host} --server-port {port} '.format( host=self.server_host, port=self.server_port) args += host_and_port.split(' ') # Add socket ports args += [ '--zmq-in-port', str(self.zmq_out_port), '--zmq-out-port', str(self.zmq_in_port) ] # Adjustments for stdio/tcp if self.stdio: args += ['--stdio-server'] if get_debug_level() > 0: args += ['--server-log-file', self.server_log_file] args += self.server_args else: args += ['--external-server'] return args def create_transport_sockets(self): """Create PyZMQ sockets for transport.""" self.zmq_out_socket = self.context.socket(zmq.PAIR) self.zmq_out_port = self.zmq_out_socket.bind_to_random_port( 'tcp://{}'.format(LOCALHOST)) self.zmq_in_socket = self.context.socket(zmq.PAIR) self.zmq_in_socket.set_hwm(0) self.zmq_in_port = self.zmq_in_socket.bind_to_random_port( 'tcp://{}'.format(LOCALHOST)) @Slot(QProcess.ProcessError) def handle_process_errors(self, error): """Handle errors with the transport layer or server processes.""" self.sig_went_down.emit(self.language) def start_server(self): """Start server.""" # This is not necessary if we're trying to connect to an # external server if self.external_server or self.stdio: return logger.info('Starting server: {0}'.format(' '.join(self.server_args))) # Create server process self.server = QProcess(self) env = self.server.processEnvironment() # Adjustments for the Python language server. if self.language == 'python': # Set the PyLS current working to an empty dir inside # our config one. This avoids the server to pick up user # files such as random.py or string.py instead of the # standard library modules named the same. cwd = osp.join(get_conf_path(), 'lsp_paths', 'cwd') if not osp.exists(cwd): os.makedirs(cwd) if os.name == "nt": # On Windows, some modules (notably Matplotlib) # cause exceptions if they cannot get the user home. # So, we need to pass the USERPROFILE env variable to # the PyLSP. if "USERPROFILE" in os.environ: env.insert("USERPROFILE", os.environ["USERPROFILE"]) # The PyLSP can't start on pip installations if APPDATA # is missing and the user has installed their packages on # that directory. # Fixes spyder-ide/spyder#17661 if (not (is_anaconda() or is_pynsist()) and "APPDATA" in os.environ): env.insert("APPDATA", os.environ["APPDATA"]) else: # There's no need to define a cwd for other servers. cwd = None # Most LSP servers spawn other processes, which may require # some environment variables. for var in os.environ: env.insert(var, os.environ[var]) logger.info('Server process env variables: {0}'.format(env.keys())) # Setup server self.server.setProcessEnvironment(env) self.server.errorOccurred.connect(self.handle_process_errors) self.server.setWorkingDirectory(cwd) self.server.setProcessChannelMode(QProcess.MergedChannels) if self.server_log_file is not None: self.server.setStandardOutputFile(self.server_log_file) # Start server self.server.start(self.server_args[0], self.server_args[1:]) def start_transport(self): """Start transport layer.""" logger.info('Starting transport for {1}: {0}'.format( ' '.join(self.transport_args), self.language)) # Create transport process self.transport = QProcess(self) env = self.transport.processEnvironment() # Most LSP servers spawn other processes other than Python, which may # require some environment variables if self.language != 'python' and self.stdio: for var in os.environ: env.insert(var, os.environ[var]) logger.info('Transport process env variables: {0}'.format( env.keys())) self.transport.setProcessEnvironment(env) # Set up transport self.transport.errorOccurred.connect(self.handle_process_errors) if self.stdio: self.transport.setProcessChannelMode(QProcess.SeparateChannels) if self.transport_log_file is not None: self.transport.setStandardErrorFile(self.transport_log_file) else: self.transport.setProcessChannelMode(QProcess.MergedChannels) if self.transport_log_file is not None: self.transport.setStandardOutputFile(self.transport_log_file) # Start transport self.transport.start(self.transport_args[0], self.transport_args[1:]) def start(self): """Start client.""" # NOTE: DO NOT change the order in which these methods are called. self.create_transport_sockets() self.start_server() self.start_transport() # Create notifier fid = self.zmq_in_socket.getsockopt(zmq.FD) self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self) self.notifier.activated.connect(self.on_msg_received) # This is necessary for tests to pass locally! logger.debug('LSP {} client started!'.format(self.language)) def stop(self): """Stop transport and server.""" logger.info('Stopping {} client...'.format(self.language)) if self.notifier is not None: self.notifier.activated.disconnect(self.on_msg_received) self.notifier.setEnabled(False) self.notifier = None # waitForFinished(): Wait some time for process to exit. This fixes an # error message by Qt (“QProcess: Destroyed while process (…) is still # running.”). No further error handling because we are out of luck # anyway if the process doesn’t finish. if self.transport is not None: self.transport.close() self.transport.waitForFinished(1000) self.context.destroy() if self.server is not None: self.server.close() self.server.waitForFinished(1000) def is_transport_alive(self): """Detect if transport layer is alive.""" state = self.transport.state() return state != QProcess.NotRunning def is_stdio_alive(self): """Check if an stdio server is alive.""" alive = True if not psutil.pid_exists(self.stdio_pid): alive = False else: try: pid_status = psutil.Process(self.stdio_pid).status() except psutil.NoSuchProcess: pid_status = '' if pid_status == psutil.STATUS_ZOMBIE: alive = False return alive def is_server_alive(self): """Detect if a tcp server is alive.""" state = self.server.state() return state != QProcess.NotRunning def is_down(self): """ Detect if the transport layer or server are down to inform our users about it. """ is_down = False if self.transport and not self.is_transport_alive(): logger.debug("Transport layer for {} is down!!".format( self.language)) if not self.transport_unresponsive: self.transport_unresponsive = True self.sig_went_down.emit(self.language) is_down = True if self.server and not self.is_server_alive(): logger.debug("LSP server for {} is down!!".format(self.language)) if not self.server_unresponsive: self.server_unresponsive = True self.sig_went_down.emit(self.language) is_down = True if self.stdio_pid and not self.is_stdio_alive(): logger.debug("LSP server for {} is down!!".format(self.language)) if not self.server_unresponsive: self.server_unresponsive = True self.sig_went_down.emit(self.language) is_down = True return is_down def send(self, method, params, kind): """Send message to transport.""" if self.is_down(): return # Don't send requests to the server before it's been initialized. if not self.initialized and method != 'initialize': return if ClientConstants.CANCEL in params: return _id = self.request_seq if kind == MessageKind.REQUEST: msg = {'id': self.request_seq, 'method': method, 'params': params} self.req_status[self.request_seq] = method elif kind == MessageKind.RESPONSE: msg = {'id': self.request_seq, 'result': params} elif kind == MessageKind.NOTIFICATION: msg = {'method': method, 'params': params} logger.debug('Perform request {0} with id {1}'.format(method, _id)) # Save requests to check their ordering. if running_under_pytest(): self._requests.append((_id, method)) # Try sending a message. If the send queue is full, keep trying for a # a second before giving up. timeout = 1 start_time = time.time() timeout_time = start_time + timeout while True: try: self.zmq_out_socket.send_pyobj(msg, flags=zmq.NOBLOCK) self.request_seq += 1 return int(_id) except zmq.error.Again: if time.time() > timeout_time: self.sig_went_down.emit(self.language) return # The send queue is full! wait 0.1 seconds before retrying. if self.initialized: logger.warning("The send queue is full! Retrying...") time.sleep(.1) @Slot() def on_msg_received(self): """Process received messages.""" self.notifier.setEnabled(False) while True: try: # events = self.zmq_in_socket.poll(1500) resp = self.zmq_in_socket.recv_pyobj(flags=zmq.NOBLOCK) try: method = resp['method'] logger.debug('{} response: {}'.format( self.language, method)) except KeyError: pass if 'error' in resp: logger.debug('{} Response error: {}'.format( self.language, repr(resp['error']))) if self.language == 'python': # Show PyLS errors in our error report dialog only in # debug or development modes if get_debug_level() > 0 or DEV: message = resp['error'].get('message', '') traceback = (resp['error'].get( 'data', {}).get('traceback')) if traceback is not None: traceback = ''.join(traceback) traceback = traceback + '\n' + message self.sig_server_error.emit(traceback) req_id = resp['id'] if req_id in self.req_reply: self.req_reply[req_id](None, {'params': []}) elif 'method' in resp: if resp['method'][0] != '$': if 'id' in resp: self.request_seq = int(resp['id']) if resp['method'] in self.handler_registry: handler_name = ( self.handler_registry[resp['method']]) handler = getattr(self, handler_name) handler(resp['params']) elif 'result' in resp: if resp['result'] is not None: req_id = resp['id'] if req_id in self.req_status: req_type = self.req_status[req_id] if req_type in self.handler_registry: handler_name = self.handler_registry[req_type] handler = getattr(self, handler_name) handler(resp['result'], req_id) self.req_status.pop(req_id) if req_id in self.req_reply: self.req_reply.pop(req_id) except RuntimeError: # This is triggered when a codeeditor instance has been # removed before the response can be processed. pass except zmq.ZMQError: self.notifier.setEnabled(True) return def perform_request(self, method, params): if method in self.sender_registry: handler_name = self.sender_registry[method] handler = getattr(self, handler_name) _id = handler(params) if 'response_callback' in params: if params['requires_response']: self.req_reply[_id] = params['response_callback'] return _id # ------ LSP initialization methods -------------------------------- @handles(SERVER_READY) @send_request(method=CompletionRequestTypes.INITIALIZE) def initialize(self, params, *args, **kwargs): self.stdio_pid = params['pid'] pid = self.transport.processId() if not self.external_server else None params = { 'processId': pid, 'rootUri': pathlib.Path(osp.abspath(self.folder)).as_uri(), 'capabilities': self.client_capabilites, 'trace': TRACE } return params @send_request(method=CompletionRequestTypes.SHUTDOWN) def shutdown(self): params = {} return params @handles(CompletionRequestTypes.SHUTDOWN) def handle_shutdown(self, response, *args): self.ready_to_close = True @send_notification(method=CompletionRequestTypes.EXIT) def exit(self): params = {} return params @handles(CompletionRequestTypes.INITIALIZE) def process_server_capabilities(self, server_capabilites, *args): """ Register server capabilities and inform other plugins that it's available. """ # Update server capabilities with the info sent by the server. server_capabilites = server_capabilites['capabilities'] if isinstance(server_capabilites['textDocumentSync'], int): kind = server_capabilites['textDocumentSync'] server_capabilites['textDocumentSync'] = TEXT_DOCUMENT_SYNC_OPTIONS server_capabilites['textDocumentSync']['change'] = kind if server_capabilites['textDocumentSync'] is None: server_capabilites.pop('textDocumentSync') self.server_capabilites.update(server_capabilites) # The initialized notification needs to be the first request sent by # the client according to the protocol. self.initialized = True self.initialized_call() # This sends a DidChangeConfiguration request to pass to the server # the configurations set by the user in our config system. self.send_configurations(self.configurations) # Inform other plugins that the server is up. self.sig_initialize.emit(self.server_capabilites, self.language) @send_notification(method=CompletionRequestTypes.INITIALIZED) def initialized_call(self): params = {} return params # ------ Settings queries -------------------------------- @property def support_multiple_workspaces(self): workspace_settings = self.server_capabilites['workspace'] return workspace_settings['workspaceFolders']['supported'] @property def support_workspace_update(self): workspace_settings = self.server_capabilites['workspace'] return workspace_settings['workspaceFolders']['changeNotifications']
class AsyncClient(QObject): """ A class which handles a connection to a client through a QProcess. """ # Emitted when the client has initialized. initialized = Signal() # Emitted when the client errors. errored = Signal() # Emitted when a request response is received. received = Signal(object) def __init__(self, target, executable=None, name=None, extra_args=None, libs=None, cwd=None, env=None): super(AsyncClient, self).__init__() self.executable = executable or sys.executable self.extra_args = extra_args self.target = target self.name = name or self self.libs = libs self.cwd = cwd self.env = env self.is_initialized = False self.closing = False self.context = zmq.Context() QApplication.instance().aboutToQuit.connect(self.close) # Set up the heartbeat timer. self.timer = QTimer(self) self.timer.timeout.connect(self._heartbeat) def run(self): """Handle the connection with the server. """ # Set up the zmq port. self.socket = self.context.socket(zmq.PAIR) self.port = self.socket.bind_to_random_port('tcp://*') # Set up the process. self.process = QProcess(self) if self.cwd: self.process.setWorkingDirectory(self.cwd) p_args = ['-u', self.target, str(self.port)] if self.extra_args is not None: p_args += self.extra_args # Set up environment variables. processEnvironment = QProcessEnvironment() env = self.process.systemEnvironment() if (self.env and 'PYTHONPATH' not in self.env) or DEV: python_path = osp.dirname(get_module_path('spyderlib')) # Add the libs to the python path. for lib in self.libs: try: path = osp.dirname(imp.find_module(lib)[1]) python_path = osp.pathsep.join([python_path, path]) except ImportError: pass env.append("PYTHONPATH=%s" % python_path) if self.env: env.update(self.env) for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) # Start the process and wait for started. self.process.start(self.executable, p_args) self.process.finished.connect(self._on_finished) running = self.process.waitForStarted() if not running: raise IOError('Could not start %s' % self) # Set up the socket notifer. fid = self.socket.getsockopt(zmq.FD) self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self) self.notifier.activated.connect(self._on_msg_received) def request(self, func_name, *args, **kwargs): """Send a request to the server. The response will be a dictionary the 'request_id' and the 'func_name' as well as a 'result' field with the object returned by the function call or or an 'error' field with a traceback. """ if not self.is_initialized: return request_id = uuid.uuid4().hex request = dict(func_name=func_name, args=args, kwargs=kwargs, request_id=request_id) self._send(request) return request_id def close(self): """Cleanly close the connection to the server. """ self.closing = True self.is_initialized = False self.timer.stop() self.notifier.activated.disconnect(self._on_msg_received) self.notifier.setEnabled(False) del self.notifier self.request('server_quit') self.process.waitForFinished(1000) self.process.close() self.context.destroy() def _on_finished(self): """Handle a finished signal from the process. """ if self.closing: return if self.is_initialized: debug_print('Restarting %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.is_initialized = False self.notifier.setEnabled(False) self.run() else: debug_print('Errored %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.errored.emit() def _on_msg_received(self): """Handle a message trigger from the socket. """ self.notifier.setEnabled(False) while 1: try: resp = self.socket.recv_pyobj(flags=zmq.NOBLOCK) except zmq.ZMQError: self.notifier.setEnabled(True) return if not self.is_initialized: self.is_initialized = True debug_print('Initialized %s' % self.name) self.initialized.emit() self.timer.start(HEARTBEAT) continue resp['name'] = self.name self.received.emit(resp) def _heartbeat(self): """Send a heartbeat to keep the server alive. """ self._send(dict(func_name='server_heartbeat')) def _send(self, obj): """Send an object to the server. """ try: self.socket.send_pyobj(obj) except Exception as e: debug_print(e) self.is_initialized = False self._on_finished()
class ProcessWorker(QObject): """ """ sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs={}): super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs self._timer = QTimer() self._process = QProcess() self._timer.setInterval(50) self._timer.timeout.connect(self._communicate) self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) json_stdout = stdout.replace('\n\x00', '') try: json_stdout = json.loads(json_stdout) except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """ """ if not self._communicate_first: if self._process.state() == QProcess.NotRunning: self.communicate() elif self._fired: self._timer.stop() def communicate(self): """ """ self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda cloud api site' not in stderr.lower(): if stderr.strip() and self._conda: raise Exception('{0}:\n' 'STDERR:\n{1}\nEND' ''.format(' '.join(self._cmd_list), stderr)) # elif stderr.strip() and self._pip: # raise PipError(self._cmd_list) else: result[-1] = '' if self._parse and stdout: try: result = json.loads(stdout), result[-1] except ValueError as error: result = stdout, error if 'error' in result[0]: error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['error']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: logger.error(str(('error', result[-1]))) self._fired = True return result def close(self): """ """ self._process.close() def is_finished(self): """ """ return self._process.state() == QProcess.NotRunning and self._fired def start(self): """ """ logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class ProcessWorker(QObject): """Process worker based on a QProcess for non blocking UI.""" sig_started = Signal(object) sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, environ=None): """ Process worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. environ : dict Process environment, """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._fired = False self._communicate_first = False self._partial_stdout = None self._started = False self._timer = QTimer() self._process = QProcess() self._set_environment(environ) self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _get_encoding(self): """Return the encoding/codepage to use.""" enco = 'utf-8' # Currently only cp1252 is allowed? if WIN: import ctypes codepage = to_text_string(ctypes.cdll.kernel32.GetACP()) # import locale # locale.getpreferredencoding() # Differences? enco = 'cp' + codepage return enco def _set_environment(self, environ): """Set the environment on the QProcess.""" if environ: q_environ = self._process.processEnvironment() for k, v in environ.items(): q_environ.insert(k, v) self._process.setProcessEnvironment(q_environ) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, self._get_encoding()) if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() enco = self._get_encoding() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, enco) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, enco) result = [stdout.encode(enco), stderr.encode(enco)] if PY2: stderr = stderr.decode() result[-1] = '' self._result = result if not self._fired: self.sig_finished.emit(self, result[0], result[-1]) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def _start(self): """Start process.""" if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() def terminate(self): """Terminate running processes.""" if self._process.state() == QProcess.Running: try: self._process.terminate() except Exception: pass self._fired = True def start(self): """Start worker.""" if not self._started: self.sig_started.emit(self) self._started = True
class ProcessWorker(QObject): """Conda worker based on a QProcess for non blocking UI.""" sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs=None): """Conda worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. parse : bool (optional) Parse json from output. pip : bool (optional) Define as a pip command. callback : func (optional) If the process has a callback to process output from comd_list. extra_kwargs : dict Arguments for the callback. """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs if extra_kwargs else {} self._timer = QTimer() self._process = QProcess() self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) # self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) json_stdout = stdout.replace('\n\x00', '') try: json_stdout = json.loads(json_stdout) except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda' not in stderr.lower(): if stderr.strip() and self._conda: logger.error('{0}:\nSTDERR:\n{1}\nEND'.format( ' '.join(self._cmd_list), stderr)) elif stderr.strip() and self._pip: logger.error("pip error: {}".format(self._cmd_list)) result[-1] = '' if self._parse and stdout: try: result = json.loads(stdout), result[-1] except Exception as error: result = stdout, str(error) if 'error' in result[0]: if not isinstance(result[0], dict): result = {'error': str(result[0])}, None error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['error']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: logger.error(str(('error', result[-1]))) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def start(self): """Start process.""" logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class ProcessWorker(QObject): """Conda worker based on a QProcess for non blocking UI.""" sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs=None): """Conda worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. parse : bool (optional) Parse json from output. pip : bool (optional) Define as a pip command. callback : func (optional) If the process has a callback to process output from comd_list. extra_kwargs : dict Arguments for the callback. """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs if extra_kwargs else {} self._timer = QTimer() self._process = QProcess() self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) # self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) try: json_stdout = [json.loads(s) for s in stdout.split('\x00') if s] json_stdout = json_stdout[-1] except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda' not in stderr.lower(): if stderr.strip() and self._conda: d = {'command': ' '.join(self._cmd_list), 'stderr': stderr} # print(d) # Should we just ignore any message here??? logger.warning('Conda command output on stderr', extra=d) elif stderr.strip() and self._pip: d = {'command': ' '.join(self._cmd_list)} # print(d) # Should we just ignore any message here??? logger.warning('Pip command output on stderr', extra=d) result[-1] = '' if self._parse and stdout: json_stdout = [] json_lines_output = stdout.split('\x00') for i, l in enumerate(json_lines_output): if l: try: json_stdout.append(json.loads(l)) except Exception as error: # An exception here could be product of: # - conda env installing pip stuff that is thrown to # stdout in non json form # - a post link script might be printing stuff to # stdout in non json format logger.warning( ('Problem parsing conda json output. ' 'Line {0}. Data - {1}. Error - {2}'.format( i, l, str(error))), ) if json_stdout: json_stdout = json_stdout[-1] result = json_stdout, result[-1] if 'exception_name' in result[0] or 'exception_type' in result[0]: if not isinstance(result[0], dict): result = {'error': str(result[0])}, None error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['message']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: d = {'stderr': result[-1]} logger.error('error', extra=d) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def start(self): """Start process.""" logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class PyChopGui(QMainWindow): """ GUI Class using PyQT for PyChop to help users plan inelastic neutron experiments at spallation sources by calculating the resolution and flux at a given neutron energies. """ instruments = {} choppers = {} minE = {} maxE = {} hyspecS2 = 35. def __init__(self, parent=None, window_flags=None): super(PyChopGui, self).__init__(parent) if window_flags: self.setWindowFlags(window_flags) self.folder = os.path.dirname(sys.modules[self.__module__].__file__) for fname in os.listdir(self.folder): if fname.endswith('.yaml'): instobj = Instrument(os.path.join(self.folder, fname)) self.instruments[instobj.name] = instobj self.choppers[instobj.name] = instobj.getChopperNames() self.minE[instobj.name] = max([instobj.emin, 0.01]) self.maxE[instobj.name] = instobj.emax self.drawLayout() self.setInstrument(list(self.instruments.keys())[0]) self.resaxes_xlim = 0 self.qeaxes_xlim = 0 self.isFramePlotted = 0 # help self.assistant_process = QProcess(self) # pylint: disable=protected-access self.mantidplot_name = 'PyChop' def closeEvent(self, event): self.assistant_process.close() self.assistant_process.waitForFinished() event.accept() def setInstrument(self, instname): """ Defines the instrument parameters by the name of the instrument. """ self.engine = self.instruments[str(instname)] self.tabs.setTabEnabled(self.tdtabID, False) self.widgets['ChopperCombo']['Combo'].clear() self.widgets['FrequencyCombo']['Combo'].clear() self.widgets['FrequencyCombo']['Label'].setText('Frequency') self.widgets['PulseRemoverCombo']['Combo'].clear() for item in self.choppers[str(instname)]: self.widgets['ChopperCombo']['Combo'].addItem(item) rep = self.engine.moderator.source_rep maxfreq = self.engine.chopper_system.max_frequencies # At the moment, the GUI only supports up to two independent frequencies if not hasattr(maxfreq, '__len__') or len(maxfreq) == 1: self.widgets['PulseRemoverCombo']['Combo'].hide() self.widgets['PulseRemoverCombo']['Label'].hide() for fq in range( rep, (maxfreq[0] if hasattr(maxfreq, '__len__') else maxfreq) + 1, rep): self.widgets['FrequencyCombo']['Combo'].addItem(str(fq)) if hasattr(self.engine.chopper_system, 'frequency_names'): self.widgets['FrequencyCombo']['Label'].setText( self.engine.chopper_system.frequency_names[0]) else: self.widgets['PulseRemoverCombo']['Combo'].show() self.widgets['PulseRemoverCombo']['Label'].show() if hasattr(self.engine.chopper_system, 'frequency_names'): for idx, chp in enumerate([ self.widgets['FrequencyCombo']['Label'], self.widgets['PulseRemoverCombo']['Label'] ]): chp.setText( self.engine.chopper_system.frequency_names[idx]) for fq in range(rep, maxfreq[0] + 1, rep): self.widgets['FrequencyCombo']['Combo'].addItem(str(fq)) for fq in range(rep, maxfreq[1] + 1, rep): self.widgets['PulseRemoverCombo']['Combo'].addItem(str(fq)) if len(self.engine.chopper_system.choppers) > 1: self.widgets['MultiRepCheck'].setEnabled(True) self.tabs.setTabEnabled(self.tdtabID, True) else: self.widgets['MultiRepCheck'].setEnabled(False) self.widgets['MultiRepCheck'].setChecked(False) self.widgets['Chopper2Phase']['Edit'].hide() self.widgets['Chopper2Phase']['Label'].hide() if self.engine.chopper_system.isPhaseIndependent: self.widgets['Chopper2Phase']['Edit'].show() self.widgets['Chopper2Phase']['Label'].show() self.widgets['Chopper2Phase']['Edit'].setText( str(self.engine.chopper_system.defaultPhase[0])) self.widgets['Chopper2Phase']['Label'].setText( self.engine.chopper_system.phaseNames[0]) # Special case for MERLIN - hide phase control from normal users if 'MERLIN' in str(instname) and not self.instSciAct.isChecked(): self.widgets['Chopper2Phase']['Edit'].hide() self.widgets['Chopper2Phase']['Label'].hide() self.engine.setChopper( str(self.widgets['ChopperCombo']['Combo'].currentText())) self.engine.setFrequency( float(self.widgets['FrequencyCombo']['Combo'].currentText())) val = self.flxslder.val * self.maxE[self.engine.instname] / 100 self.flxedt.setText('%3.2f' % (val)) nframe = self.engine.moderator.n_frame if hasattr( self.engine.moderator, 'n_frame') else 1 self.repfig_nframe_edit.setText(str(nframe)) self.repfig_nframe_rep1only.setChecked(False) if hasattr(self.engine.chopper_system, 'default_frequencies'): cb = [ self.widgets['FrequencyCombo']['Combo'], self.widgets['PulseRemoverCombo']['Combo'] ] for idx, freq in enumerate( self.engine.chopper_system.default_frequencies): cb[idx].setCurrentIndex([ i for i in range(cb[idx].count()) if str(freq) in cb[idx].itemText(i) ][0]) if idx > 1: break self.tabs.setTabEnabled(self.qetabID, False) if self.engine.has_detector and hasattr(self.engine.detector, 'tthlims'): self.tabs.setTabEnabled(self.qetabID, True) # show s2 for HYSPEC only if 'HYSPEC' in str(instname): self.widgets[f"S2Edit"]['Edit'].show() self.widgets[f"S2Edit"]['Edit'].setText(str(self.hyspecS2)) self.widgets[f"S2Edit"]['Label'].show() else: self.widgets[f"S2Edit"]['Edit'].hide() self.widgets[f"S2Edit"]['Label'].hide() def setChopper(self, choppername): """ Defines the Fermi chopper slit package type by name, or the disk chopper arrangement variant. """ self.engine.setChopper(str(choppername)) self.engine.setFrequency( float(self.widgets['FrequencyCombo']['Combo'].currentText())) # Special case for MERLIN - only enable multirep for 'G' chopper if 'MERLIN' in self.engine.instname: if 'G' in str(choppername): self.widgets['MultiRepCheck'].setEnabled(True) self.tabs.setTabEnabled(self.tdtabID, True) self.widgets['Chopper2Phase']['Edit'].setText('1500') self.widgets['Chopper2Phase']['Label'].setText( 'Disk chopper phase delay time') if self.instSciAct.isChecked(): self.widgets['Chopper2Phase']['Edit'].show() self.widgets['Chopper2Phase']['Label'].show() else: self.widgets['MultiRepCheck'].setEnabled(False) self.widgets['MultiRepCheck'].setChecked(False) self.tabs.setTabEnabled(self.tdtabID, False) self.widgets['Chopper2Phase']['Edit'].hide() self.widgets['Chopper2Phase']['Label'].hide() def setFreq(self, freqtext=None, **kwargs): """ Sets the chopper frequency(ies), in Hz. """ freq_gui = float(self.widgets['FrequencyCombo']['Combo'].currentText()) freq_in = kwargs['manual_freq'] if ('manual_freq' in kwargs.keys()) else freq_gui if len(self.engine.getFrequency()) > 1 and ( not hasattr(freq_in, '__len__') or len(freq_in) == 1): freqpr = float( self.widgets['PulseRemoverCombo']['Combo'].currentText()) freq_in = [freq_in, freqpr] if not self.widgets['Chopper2Phase']['Label'].isHidden(): chop2phase = self.widgets['Chopper2Phase']['Edit'].text() if isinstance(self.engine.chopper_system.defaultPhase[0], str): chop2phase = str(chop2phase) else: chop2phase = float(chop2phase) % ( 1e6 / self.engine.moderator.source_rep) self.engine.setFrequency(freq_in, phase=chop2phase) else: self.engine.setFrequency(freq_in) def setEi(self): """ Sets the incident energy (or focused incident energy for multi-rep case). """ try: eitxt = float(self.widgets['EiEdit']['Edit'].text()) except ValueError: raise ValueError('No Ei specified, or Ei string not understood') self.engine.setEi(eitxt) if self.eiPlots.isChecked(): self.calc_callback() def setS2(self): """ Sets the S2 tank rotation for HYSPEC instrument """ try: S2txt = float(self.widgets['S2Edit']['Edit'].text()) except: raise ValueError('No S2 specified, or S2 string not understood') if np.abs(S2txt) > 150: raise ValueError('S2 must be between -150 and 150 degrees') self.hyspecS2 = S2txt def calc_callback(self): """ Calls routines to calculate the resolution / flux and to update the Matplotlib graphs. """ try: if self.engine.getChopper() is None: self.setChopper( self.widgets['ChopperCombo']['Combo'].currentText()) self.setEi() if self.engine.name == 'HYSPEC': self.setS2() self.setFreq() self.calculate() if self.errormess: idx = [ i for i, ei in enumerate(self.eis) if np.abs(ei - self.engine.getEi()) < 1.e-4 ] if idx and self.flux[idx[0]] == 0: raise ValueError(self.errormess) self.errormessage(self.errormess) self.plot_res() self.plot_frame() if self.instSciAct.isChecked(): self.update_script() except ValueError as err: self.errormessage(err) else: self.plot_flux_ei() self.plot_flux_hz() def calculate(self): """ Performs the resolution and flux calculations. """ self.errormess = None if self.engine.getEi() is None: self.setEi() if self.widgets['MultiRepCheck'].isChecked(): en = np.linspace(0, 0.95, 200) self.eis = self.engine.getAllowedEi() with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always', UserWarning) self.res = self.engine.getMultiRepResolution(en) self.flux = self.engine.getMultiRepFlux() if len(w) > 0: mess = [str(w[i].message) for i in range(len(w))] self.errormess = '\n'.join( [m for m in mess if 'tchop' in m]) else: en = np.linspace(0, 0.95 * self.engine.getEi(), 200) with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always', UserWarning) self.res = self.engine.getResolution(en) self.flux = self.engine.getFlux() if len(w) > 0: raise ValueError(w[0].message) def _set_overplot(self, overplot, axisname): axis = getattr(self, axisname) if overplot: if matplotlib.compare_versions('2.1.0', matplotlib.__version__): axis.hold(True) else: setattr(self, axisname + '_xlim', 0) axis.clear() axis.axhline(color='k') def plot_res(self): """ Plots the resolution in the resolution tab """ overplot = self.widgets['HoldCheck'].isChecked() multiplot = self.widgets['MultiRepCheck'].isChecked() self._set_overplot(overplot, 'resaxes') self._set_overplot(overplot, 'qeaxes') inst = self.engine.instname freq = self.engine.getFrequency() if hasattr(freq, '__len__'): freq = freq[0] if multiplot: if matplotlib.compare_versions('2.1.0', matplotlib.__version__): self.resaxes.hold(True) for ie, Ei in enumerate(self.eis): en = np.linspace(0, 0.95 * Ei, 200) if any(self.res[ie]): if not self.flux[ie]: continue line, = self.resaxes.plot(en, self.res[ie]) label_text = '%s_%3.2fmeV_%dHz_Flux=%fn/cm2/s' % ( inst, Ei, freq, self.flux[ie]) line.set_label(label_text) if self.tabs.isTabEnabled(self.qetabID): self.plot_qe(Ei, label_text, hold=True) self.resaxes_xlim = max(Ei, self.resaxes_xlim) if matplotlib.compare_versions('2.1.0', matplotlib.__version__): self.resaxes.hold(False) else: ei = self.engine.getEi() en = np.linspace(0, 0.95 * ei, 200) line, = self.resaxes.plot(en, self.res) chopper = self.engine.getChopper() label_text = '%s_%s_%3.2fmeV_%dHz_Flux=%fn/cm2/s' % ( inst, chopper, ei, freq, self.flux) line.set_label(label_text) if self.tabs.isTabEnabled(self.qetabID): self.plot_qe(ei, label_text, overplot) self.resaxes_xlim = max(ei, self.resaxes_xlim) self.resaxes.set_xlim([0, self.resaxes_xlim]) legend_set_draggable(self.resaxes.legend(), True) self.resaxes.set_xlabel('Energy Transfer (meV)') self.resaxes.set_ylabel(r'$\Delta$E (meV FWHM)') self.rescanvas.draw() def plot_qe(self, Ei, label_text, hold=False): """ Plots the Q-E diagram """ from scipy import constants E2q, meV2J = (2. * constants.m_n / (constants.hbar**2), constants.e / 1000.) en = np.linspace(-Ei / 5., Ei, 100) q2 = [] if self.engine.name == 'HYSPEC': if abs(self.hyspecS2) <= 30: self.engine.detector.tthlims = [0, abs(self.hyspecS2) + 30] else: self.engine.detector.tthlims = [ abs(self.hyspecS2) - 30, abs(self.hyspecS2) + 30 ] label_text += '_S2={}'.format(self.hyspecS2) for tth in self.engine.detector.tthlims: q = np.sqrt(E2q * (2 * Ei - en - 2 * np.sqrt(Ei * (Ei - en)) * np.cos(np.deg2rad(tth))) * meV2J) / 1e10 q2.append(np.concatenate((np.flipud(q), q))) self._set_overplot(hold, 'qeaxes') self.qeaxes_xlim = max(np.max(q2), self.qeaxes_xlim) line, = self.qeaxes.plot( np.hstack(q2), np.concatenate((np.flipud(en), en)).tolist() * len(self.engine.detector.tthlims)) line.set_label(label_text) self.qeaxes.set_xlim([0, self.qeaxes_xlim]) legend_set_draggable(self.qeaxes.legend(), True) self.qeaxes.set_xlabel(r'$|Q| (\mathrm{\AA}^{-1})$') self.qeaxes.set_ylabel('Energy Transfer (meV)') self.qecanvas.draw() def plot_flux_ei(self, **kwargs): """ Plots the flux vs Ei in the middle tab """ inst = self.engine.instname chop = self.engine.getChopper() freq = self.engine.getFrequency() overplot = self.widgets['HoldCheck'].isChecked() if hasattr(freq, '__len__'): freq = freq[0] update = kwargs['update'] if 'update' in kwargs.keys() else False # Do not recalculate if all relevant parameters still the same. _, labels = self.flxaxes2.get_legend_handles_labels() searchStr = '([A-Z]+) "(.+)" ([0-9]+) Hz' tmpinst = [] if (labels and (overplot or len(labels) == 1)) or update: for prevtitle in labels: prevInst, prevChop, prevFreq = re.search(searchStr, prevtitle).groups() if update: tmpinst.append( copy.deepcopy( Instrument(self.instruments[prevInst], prevChop, float(prevFreq)))) else: if inst == prevInst and chop == prevChop and freq == float( prevFreq): return ne = 25 mn = self.minE[inst] mx = (self.flxslder.val / 100) * self.maxE[inst] eis = np.linspace(mn, mx, ne) flux = eis * 0 elres = eis * 0 if update: self.flxaxes1.clear() self.flxaxes2.clear() if matplotlib.compare_versions('2.1.0', matplotlib.__version__): self.flxaxes1.hold(True) self.flxaxes2.hold(True) for ii, instrument in enumerate(tmpinst): for ie, ei in enumerate(eis): with warnings.catch_warnings(record=True): warnings.simplefilter('always', UserWarning) flux[ie] = instrument.getFlux(ei) elres[ie] = instrument.getResolution(0., ei)[0] self.flxaxes1.plot(eis, flux) line, = self.flxaxes2.plot(eis, elres) line.set_label(labels[ii]) else: for ie, ei in enumerate(eis): with warnings.catch_warnings(record=True): warnings.simplefilter('always', UserWarning) flux[ie] = self.engine.getFlux(ei) elres[ie] = self.engine.getResolution(0., ei)[0] if overplot: if matplotlib.compare_versions('2.1.0', matplotlib.__version__): self.flxaxes1.hold(True) self.flxaxes2.hold(True) else: self.flxaxes1.clear() self.flxaxes2.clear() self.flxaxes1.plot(eis, flux) line, = self.flxaxes2.plot(eis, elres) line.set_label('%s "%s" %d Hz' % (inst, chop, freq)) self.flxaxes1.set_xlim([mn, mx]) self.flxaxes2.set_xlim([mn, mx]) self.flxaxes1.set_xlabel('Incident Energy (meV)') self.flxaxes1.set_ylabel('Flux (n/cm$^2$/s)') self.flxaxes1.set_xlabel('Incident Energy (meV)') self.flxaxes2.set_ylabel('Elastic Resolution FWHM (meV)') lg = self.flxaxes2.legend() legend_set_draggable(lg, True) self.flxcanvas.draw() def update_slider(self, val=None): """ Callback function for the x-axis slider of the flux tab """ if val is None: val = float( self.flxedt.text()) / self.maxE[self.engine.instname] * 100 if val < self.minE[self.engine.instname]: self.errormessage("Max Ei must be greater than %2.1f" % (self.minE[self.engine.instname])) val = (self.minE[self.engine.instname] + 0.1) / self.maxE[self.engine.instname] * 100 self.flxslder.set_val(val) else: val = self.flxslder.val * self.maxE[self.engine.instname] / 100 self.flxedt.setText('%3.2f' % (val)) self.plot_flux_ei(update=True) self.flxcanvas.draw() def plot_flux_hz(self): """ Plots the flux vs freq in the middle tab """ inst = self.engine.instname chop = self.engine.getChopper() ei = float(self.widgets['EiEdit']['Edit'].text()) overplot = self.widgets['HoldCheck'].isChecked() # Do not recalculate if one of the plots has the same parametersc _, labels = self.frqaxes2.get_legend_handles_labels() searchStr = '([A-Z]+) "(.+)" Ei = ([0-9.-]+) meV' if labels and (overplot or len(labels) == 1): for prevtitle in labels: prevInst, prevChop, prevEi = re.search(searchStr, prevtitle).groups() if inst == prevInst and chop == prevChop and abs( ei - float(prevEi)) < 0.01: return freq0 = self.engine.getFrequency() rep = self.engine.moderator.source_rep maxfreq = self.engine.chopper_system.max_frequencies freqs = range( rep, (maxfreq[0] if hasattr(maxfreq, '__len__') else maxfreq) + 1, rep) flux = np.zeros(len(freqs)) elres = np.zeros(len(freqs)) for ie, freq in enumerate(freqs): if hasattr(freq0, '__len__'): self.setFreq(manual_freq=[freq] + freq0[1:]) else: self.setFreq(manual_freq=freq) with warnings.catch_warnings(record=True): warnings.simplefilter('always', UserWarning) flux[ie] = self.engine.getFlux(ei) elres[ie] = self.engine.getResolution(0., ei)[0] if overplot: if matplotlib.compare_versions('2.1.0', matplotlib.__version__): self.frqaxes1.hold(True) self.frqaxes2.hold(True) else: self.frqaxes1.clear() self.frqaxes2.clear() self.setFreq(manual_freq=freq0) self.frqaxes1.set_xlabel('Chopper Frequency (Hz)') self.frqaxes1.set_ylabel('Flux (n/cm$^2$/s)') line, = self.frqaxes1.plot(freqs, flux, 'o-') self.frqaxes1.set_xlim([0, np.max(freqs)]) self.frqaxes2.set_xlabel('Chopper Frequency (Hz)') self.frqaxes2.set_ylabel('Elastic Resolution FWHM (meV)') line, = self.frqaxes2.plot(freqs, elres, 'o-') line.set_label('%s "%s" Ei = %5.3f meV' % (inst, chop, ei)) lg = self.frqaxes2.legend() legend_set_draggable(lg, True) self.frqaxes2.set_xlim([0, np.max(freqs)]) self.frqcanvas.draw() def instSciCB(self): """ Callback function for the "Instrument Scientist Mode" menu option """ # MERLIN is a special case - want to hide ability to change phase from users if 'MERLIN' in self.engine.instname and 'G' in self.engine.getChopper( ): if self.instSciAct.isChecked(): self.widgets['Chopper2Phase']['Edit'].show() self.widgets['Chopper2Phase']['Label'].show() self.widgets['Chopper2Phase']['Edit'].setText('1500') self.widgets['Chopper2Phase']['Label'].setText( 'Disk chopper phase delay time') else: self.widgets['Chopper2Phase']['Edit'].hide() self.widgets['Chopper2Phase']['Label'].hide() if self.instSciAct.isChecked(): self.tabs.insertTab(self.scrtabID, self.scrtab, 'ScriptOutput') self.scrtab.show() else: self.tabs.removeTab(self.scrtabID) self.scrtab.hide() def errormessage(self, message): msg = QMessageBox() msg.setText(str(message)) msg.setStandardButtons(QMessageBox.Ok) msg.exec_() def loadYaml(self): yaml_file = QFileDialog().getOpenFileName(self.mainWidget, 'Open Instrument YAML File', self.folder, 'Files (*.yaml)') if isinstance(yaml_file, tuple): yaml_file = yaml_file[0] yaml_file = str(yaml_file) new_folder = os.path.dirname(yaml_file) if new_folder != self.folder: self.folder = new_folder try: new_inst = Instrument(yaml_file) except (RuntimeError, AttributeError, ValueError) as err: self.errormessage(err) newname = new_inst.name if newname in self.instruments.keys( ) and not self.overwriteload.isChecked(): overwrite, newname = self._ask_overwrite() if overwrite == 1: return elif overwrite == 0: newname = new_inst.name self.instruments[newname] = new_inst self.choppers[newname] = new_inst.getChopperNames() self.minE[newname] = max([new_inst.emin, 0.01]) self.maxE[newname] = new_inst.emax self.updateInstrumentList() combo = self.widgets['InstrumentCombo']['Combo'] idx = [ i for i in range(combo.count()) if str(combo.itemText(i)) == newname ] combo.setCurrentIndex(idx[0]) self.setInstrument(newname) def _ask_overwrite(self): msg = QDialog() msg.setWindowTitle('Load overwrite') layout = QGridLayout() layout.addWidget( QLabel('Instrument %s already exists in memory. Overwrite this?'), 0, 0, 1, -1) buttons = [ QPushButton(label) for label in ['Load and overwrite', 'Cancel Load', 'Load and rename to'] ] locations = [[1, 0], [1, 1], [2, 0]] self.overwrite_flag = 1 def overwriteCB(idx): self.overwrite_flag = idx msg.accept() for idx, button in enumerate(buttons): button.clicked.connect(lambda _, idx=idx: overwriteCB(idx)) layout.addWidget(button, locations[idx][0], locations[idx][1]) newname = QLineEdit() newname.editingFinished.connect(lambda: overwriteCB(2)) layout.addWidget(newname, 2, 1) msg.setLayout(layout) msg.exec_() newname = str(newname.text()) if not newname or newname in self.instruments: self.errormessage('Invalid instrument name. Cancelling load.') self.overwrite_flag = 1 return self.overwrite_flag, newname def updateInstrumentList(self): combo = self.widgets['InstrumentCombo']['Combo'] old_instruments = [ str(combo.itemText(i)) for i in range(combo.count()) ] new_instruments = [ inst for inst in self.instruments if inst not in old_instruments ] for inst in new_instruments: combo.addItem(inst) def plot_frame(self): """ Plots the distance-time diagram in the right tab """ if len(self.engine.chopper_system.choppers) > 1: self.engine.n_frame = int(self.repfig_nframe_edit.text()) self.repaxes.clear() self.engine.plotMultiRepFrame( self.repaxes, first_rep=self.repfig_nframe_rep1only.isChecked()) self.repcanvas.draw() def _gen_text_ei(self, ei, obj_in): obj = Instrument(obj_in) obj.setEi(ei) en = np.linspace(0, 0.95 * ei, 10) try: flux = self.engine.getFlux() res = self.engine.getResolution(en) except ValueError as err: self.errormessage(err) raise ValueError(err) tsqvan, tsqdic, tsqmodchop = obj.getVanVar() v_mod, v_chop = tuple(np.sqrt(tsqmodchop[:2]) * 1e6) x0, _, x1, x2, _ = obj.chopper_system.getDistances() first_component = 'moderator' if x0 != tsqmodchop[2]: x0 = tsqmodchop[2] first_component = 'chopper 1' txt = '# ------------------------------------------------------------- #\n' txt += '# Ei = %8.2f meV\n' % (ei) txt += '# Flux = %8.2f n/cm2/s\n' % (flux) txt += '# Elastic resolution = %6.2f meV\n' % (res[0]) txt += '# Time width at sample = %6.2f us, of which:\n' % ( 1e6 * np.sqrt(tsqvan)) for ky, val in list(tsqdic.items()): txt += '# %20s : %6.2f us\n' % (ky, 1e6 * np.sqrt(val)) txt += '# %s distances:\n' % (obj.instname) txt += '# x0 = %6.2f m (%s to Fermi)\n' % (x0, first_component) txt += '# x1 = %6.2f m (Fermi to sample)\n' % (x1) txt += '# x2 = %6.2f m (sample to detector)\n' % (x2) txt += '# Approximate inelastic resolution is given by:\n' txt += '# dE = 2 * E2V * sqrt(ef**3 * t_van**2) / x2\n' txt += '# where: E2V = 4.373e-4 meV/(m/us) conversion from energy to speed\n' txt += '# t_van**2 = (geom*t_mod)**2 + ((1+geom)*t_chop)**2\n' txt += '# geom = (x1 + x2*(ei/ef)**1.5) / x0\n' txt += '# and t_mod and t_chop are the moderator and chopper time widths at the\n' txt += '# moderator and chopper positions (not at the sample as listed above).\n' txt += '# Which in this case is:\n' txt += '# %.4e*sqrt(ef**3 * ( (%6.5f*(%.3f+%.3f*(ei/ef)**1.5))**2 \n' % ( 874.78672e-6 / x2, v_mod, x1 / x0, x2 / x0) txt += '# + (%6.5f*(%.3f+%.3f*(ei/ef)**1.5))**2) )\n' % ( v_chop, 1 + x1 / x0, x2 / x0) txt += '# EN (meV) Full dE (meV) Approx dE (meV)\n' for ii in range(len(res)): ef = ei - en[ii] approx = (874.78672e-6 / x2) * np.sqrt(ef**3 * ( (v_mod * ((x1 / x0) + (x2 / x0) * (ei / ef)**1.5))**2 + (v_chop * (1 + (x1 / x0) + (x2 / x0) * (ei / ef)**1.5))**2)) txt += '%12.5f %12.5f %12.5f\n' % (en[ii], res[ii], approx) return txt def genText(self): """ Generates text output of the resolution function versus energy transfer and other information. """ multiplot = self.widgets['MultiRepCheck'].isChecked() obj = self.engine if obj.getChopper() is None: self.setChopper( self.widgets['ChopperCombo']['Combo'].currentText()) if obj.getEi() is None: self.setEi() instname, chtyp, freqs, ei_in = tuple( [obj.instname, obj.getChopper(), obj.getFrequency(), obj.getEi()]) txt = '# ------------------------------------------------------------- #\n' txt += '# Chop calculation for instrument %s\n' % (instname) if obj.isFermi: txt += '# with chopper %s at %3i Hz\n' % (chtyp, freqs[0]) else: txt += '# in %s mode with:\n' % (chtyp) freq_names = obj.chopper_system.frequency_names for idx in range(len(freq_names)): txt += '# %s at %3i Hz\n' % (freq_names[idx], freqs[idx]) txt += self._gen_text_ei(ei_in, obj) if multiplot: for ei in sorted(self.engine.getAllowedEi()): if np.abs(ei - ei_in) > 0.001: txt += self._gen_text_ei(ei, obj) return txt def showText(self): """ Creates a dialog to show the generated text output. """ try: generatedText = self.genText() except ValueError: return self.txtwin = QDialog() self.txtedt = QTextEdit() self.txtbtn = QPushButton('OK') self.txtwin.layout = QVBoxLayout(self.txtwin) self.txtwin.layout.addWidget(self.txtedt) self.txtwin.layout.addWidget(self.txtbtn) self.txtbtn.clicked.connect(self.txtwin.deleteLater) self.txtedt.setText(generatedText) self.txtedt.setReadOnly(True) self.txtwin.setWindowTitle('Resolution information') self.txtwin.setWindowModality(Qt.ApplicationModal) self.txtwin.setAttribute(Qt.WA_DeleteOnClose) self.txtwin.setMinimumSize(400, 600) self.txtwin.resize(400, 600) self.txtwin.show() self.txtloop = QEventLoop() self.txtloop.exec_() def saveText(self): """ Saves the generated text to a file (opens file dialog). """ fname = QFileDialog.getSaveFileName(self, 'Open file', '') if isinstance(fname, tuple): fname = fname[0] fid = open(fname, 'w') fid.write(self.genText()) fid.close() def update_script(self): """ Updates the text window with information about the previous calculation. """ if self.widgets['MultiRepCheck'].isChecked(): out = self.engine.getMultiWidths() new_str = '\n' for ie, ee in enumerate(out['Eis']): res = out['Energy'][ie] percent = res / ee * 100 chop_width = out['chopper'][ie] mod_width = out['moderator'][ie] new_str += 'Ei is %6.2f meV, resolution is %6.2f ueV, percentage resolution is %6.3f\n' % ( ee, res * 1000, percent) new_str += 'FWHM at sample from chopper and moderator are %6.2f us, %6.2f us\n' % ( chop_width, mod_width) else: ei = self.engine.getEi() out = self.engine.getWidths() res = out['Energy'] percent = res / ei * 100 chop_width = out['chopper'] mod_width = out['moderator'] new_str = '\nEi is %6.2f meV, resolution is %6.2f ueV, percentage resolution is %6.3f\n' % ( ei, res * 1000, percent) new_str += 'FWHM at sample from chopper and moderator are %6.2f us, %6.2f us\n' % ( chop_width, mod_width) self.scredt.append(new_str) def onHelp(self): """ Shows the help page """ try: from mantidqt.gui_helper import show_interface_help show_interface_help(self.mantidplot_name, self.assistant_process, area='direct') except ImportError: helpTxt = "PyChop is a tool to allow direct inelastic neutron\nscattering users to estimate the inelastic resolution\n" helpTxt += "and incident flux for a given spectrometer setting.\n\nFirst select the instrument, chopper settings and\n" helpTxt += "Ei, and then click 'Calculate and Plot'. Data for all\nthe graphs will be generated (may take 1-2s) and\n" helpTxt += "all graphs will be updated. If the 'Hold current plot'\ncheck box is ticked, additional settings will be\n" helpTxt += "overplotted on the existing graphs if they are\ndifferent from previous settings.\n\nMore in-depth help " helpTxt += "can be obtained from the\nMantid help pages." self.hlpwin = QDialog() self.hlpedt = QLabel(helpTxt) self.hlpbtn = QPushButton('OK') self.hlpwin.layout = QVBoxLayout(self.hlpwin) self.hlpwin.layout.addWidget(self.hlpedt) self.hlpwin.layout.addWidget(self.hlpbtn) self.hlpbtn.clicked.connect(self.hlpwin.deleteLater) self.hlpwin.setWindowTitle('Help') self.hlpwin.setWindowModality(Qt.ApplicationModal) self.hlpwin.setAttribute(Qt.WA_DeleteOnClose) self.hlpwin.setMinimumSize(370, 300) self.hlpwin.resize(370, 300) self.hlpwin.show() self.hlploop = QEventLoop() self.hlploop.exec_() def drawLayout(self): """ Draws the GUI layout. """ self.widgetslist = [ [ 'pair', 'show', 'Instrument', 'combo', self.instruments, self.setInstrument, 'InstrumentCombo' ], [ 'pair', 'show', 'Chopper', 'combo', '', self.setChopper, 'ChopperCombo' ], [ 'pair', 'show', 'Frequency', 'combo', '', self.setFreq, 'FrequencyCombo' ], [ 'pair', 'hide', 'Pulse remover chopper freq', 'combo', '', self.setFreq, 'PulseRemoverCombo' ], ['pair', 'show', 'Ei', 'edit', '', self.setEi, 'EiEdit'], [ 'pair', 'hide', 'Chopper 2 phase delay time', 'edit', '5', self.setFreq, 'Chopper2Phase' ], ['pair', 'hide', 'S2', 'edit', '', self.setS2, 'S2Edit'], ['spacer'], [ 'single', 'show', 'Calculate and Plot', 'button', self.calc_callback, 'CalculateButton' ], [ 'single', 'show', 'Hold current plot', 'check', lambda: None, 'HoldCheck' ], [ 'single', 'show', 'Show multi-reps', 'check', lambda: None, 'MultiRepCheck' ], ['spacer'], [ 'single', 'show', 'Show data ascii window', 'button', self.showText, 'ShowAsciiButton' ], [ 'single', 'show', 'Save data as ascii', 'button', self.saveText, 'SaveAsciiButton' ] ] self.droplabels = [] self.dropboxes = [] self.singles = [] self.widgets = {} self.leftPanel = QVBoxLayout() self.rightPanel = QVBoxLayout() self.tabs = QTabWidget(self) self.fullWindow = QGridLayout() for widget in self.widgetslist: if 'pair' in widget[0]: self.droplabels.append(QLabel(widget[2])) if 'combo' in widget[3]: self.dropboxes.append(QComboBox(self)) self.dropboxes[-1].activated['QString'].connect(widget[5]) for item in widget[4]: self.dropboxes[-1].addItem(item) self.widgets[widget[-1]] = { 'Combo': self.dropboxes[-1], 'Label': self.droplabels[-1] } elif 'edit' in widget[3]: self.dropboxes.append(QLineEdit(self)) self.dropboxes[-1].returnPressed.connect(widget[5]) self.widgets[widget[-1]] = { 'Edit': self.dropboxes[-1], 'Label': self.droplabels[-1] } else: raise RuntimeError( 'Bug in code - widget %s is not recognised.' % (widget[3])) self.leftPanel.addWidget(self.droplabels[-1]) self.leftPanel.addWidget(self.dropboxes[-1]) if 'hide' in widget[1]: self.droplabels[-1].hide() self.dropboxes[-1].hide() elif 'single' in widget[0]: if 'check' in widget[3]: self.singles.append(QCheckBox(widget[2], self)) self.singles[-1].stateChanged.connect(widget[4]) elif 'button' in widget[3]: self.singles.append(QPushButton(widget[2])) self.singles[-1].clicked.connect(widget[4]) else: raise RuntimeError( 'Bug in code - widget %s is not recognised.' % (widget[3])) self.leftPanel.addWidget(self.singles[-1]) if 'hide' in widget[1]: self.singles[-1].hide() self.widgets[widget[-1]] = self.singles[-1] elif 'spacer' in widget[0]: self.leftPanel.addItem(QSpacerItem(0, 35)) else: raise RuntimeError( 'Bug in code - widget class %s is not recognised.' % (widget[0])) # Right panel, matplotlib figures self.resfig = Figure() self.resfig.patch.set_facecolor('white') self.rescanvas = FigureCanvas(self.resfig) self.resaxes = self.resfig.add_subplot(111) self.resaxes.axhline(color='k') self.resaxes.set_xlabel('Energy Transfer (meV)') self.resaxes.set_ylabel(r'$\Delta$E (meV FWHM)') self.resfig_controls = NavigationToolbar(self.rescanvas, self) self.restab = QWidget(self.tabs) self.restabbox = QVBoxLayout() self.restabbox.addWidget(self.rescanvas) self.restabbox.addWidget(self.resfig_controls) self.restab.setLayout(self.restabbox) self.flxfig = Figure() self.flxfig.patch.set_facecolor('white') self.flxcanvas = FigureCanvas(self.flxfig) self.flxaxes1 = self.flxfig.add_subplot(121) self.flxaxes1.set_xlabel('Incident Energy (meV)') self.flxaxes1.set_ylabel('Flux (n/cm$^2$/s)') self.flxaxes2 = self.flxfig.add_subplot(122) self.flxaxes2.set_xlabel('Incident Energy (meV)') self.flxaxes2.set_ylabel('Elastic Resolution FWHM (meV)') self.flxfig_controls = NavigationToolbar(self.flxcanvas, self) self.flxsldfg = Figure() self.flxsldfg.patch.set_facecolor('white') self.flxsldcv = FigureCanvas(self.flxsldfg) self.flxsldax = self.flxsldfg.add_subplot(111) self.flxslder = Slider(self.flxsldax, 'Ei (meV)', 0, 100, valinit=100) self.flxslder.valtext.set_visible(False) self.flxslder.on_changed(self.update_slider) self.flxedt = QLineEdit() self.flxedt.setText('1000') self.flxedt.returnPressed.connect(self.update_slider) self.flxtab = QWidget(self.tabs) self.flxsldbox = QHBoxLayout() self.flxsldbox.addWidget(self.flxsldcv) self.flxsldbox.addWidget(self.flxedt) self.flxsldwdg = QWidget() self.flxsldwdg.setLayout(self.flxsldbox) sz = self.flxsldwdg.maximumSize() sz.setHeight(50) self.flxsldwdg.setMaximumSize(sz) self.flxtabbox = QVBoxLayout() self.flxtabbox.addWidget(self.flxcanvas) self.flxtabbox.addWidget(self.flxsldwdg) self.flxtabbox.addWidget(self.flxfig_controls) self.flxtab.setLayout(self.flxtabbox) self.frqfig = Figure() self.frqfig.patch.set_facecolor('white') self.frqcanvas = FigureCanvas(self.frqfig) self.frqaxes1 = self.frqfig.add_subplot(121) self.frqaxes1.set_xlabel('Chopper Frequency (Hz)') self.frqaxes1.set_ylabel('Flux (n/cm$^2$/s)') self.frqaxes2 = self.frqfig.add_subplot(122) self.frqaxes1.set_xlabel('Chopper Frequency (Hz)') self.frqaxes2.set_ylabel('Elastic Resolution FWHM (meV)') self.frqfig_controls = NavigationToolbar(self.frqcanvas, self) self.frqtab = QWidget(self.tabs) self.frqtabbox = QVBoxLayout() self.frqtabbox.addWidget(self.frqcanvas) self.frqtabbox.addWidget(self.frqfig_controls) self.frqtab.setLayout(self.frqtabbox) self.repfig = Figure() self.repfig.patch.set_facecolor('white') self.repcanvas = FigureCanvas(self.repfig) self.repaxes = self.repfig.add_subplot(111) self.repaxes.axhline(color='k') self.repaxes.set_xlabel(r'TOF ($\mu$sec)') self.repaxes.set_ylabel('Distance (m)') self.repfig_controls = NavigationToolbar(self.repcanvas, self) self.repfig_nframe_label = QLabel('Number of frames to plot') self.repfig_nframe_edit = QLineEdit('1') self.repfig_nframe_button = QPushButton('Replot') self.repfig_nframe_button.clicked.connect(lambda: self.plot_frame()) self.repfig_nframe_rep1only = QCheckBox('First Rep Only') self.repfig_nframe_box = QHBoxLayout() self.repfig_nframe_box.addWidget(self.repfig_nframe_label) self.repfig_nframe_box.addWidget(self.repfig_nframe_edit) self.repfig_nframe_box.addWidget(self.repfig_nframe_button) self.repfig_nframe_box.addWidget(self.repfig_nframe_rep1only) self.reptab = QWidget(self.tabs) self.repfig_nframe = QWidget(self.reptab) self.repfig_nframe.setLayout(self.repfig_nframe_box) self.repfig_nframe.setSizePolicy( QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)) self.reptabbox = QVBoxLayout() self.reptabbox.addWidget(self.repcanvas) self.reptabbox.addWidget(self.repfig_nframe) self.reptabbox.addWidget(self.repfig_controls) self.reptab.setLayout(self.reptabbox) self.qefig = Figure() self.qefig.patch.set_facecolor('white') self.qecanvas = FigureCanvas(self.qefig) self.qeaxes = self.qefig.add_subplot(111) self.qeaxes.axhline(color='k') self.qeaxes.set_xlabel(r'$|Q| (\mathrm{\AA}^{-1})$') self.qeaxes.set_ylabel('Energy Transfer (meV)') self.qefig_controls = NavigationToolbar(self.qecanvas, self) self.qetabbox = QVBoxLayout() self.qetabbox.addWidget(self.qecanvas) self.qetabbox.addWidget(self.qefig_controls) self.qetab = QWidget(self.tabs) self.qetab.setLayout(self.qetabbox) self.scrtab = QWidget(self.tabs) self.scredt = QTextEdit() self.scrcls = QPushButton("Clear") self.scrcls.clicked.connect(lambda: self.scredt.clear()) self.scrbox = QVBoxLayout() self.scrbox.addWidget(self.scredt) self.scrbox.addWidget(self.scrcls) self.scrtab.setLayout(self.scrbox) self.scrtab.hide() self.tabs.addTab(self.restab, 'Resolution') self.tabs.addTab(self.flxtab, 'Flux-Ei') self.tabs.addTab(self.frqtab, 'Flux-Freq') self.tabs.addTab(self.reptab, 'Time-Distance') self.tdtabID = 3 self.tabs.setTabEnabled(self.tdtabID, False) self.tabs.addTab(self.qetab, 'Q-E') self.qetabID = 4 self.tabs.setTabEnabled(self.qetabID, False) self.scrtabID = 5 self.rightPanel.addWidget(self.tabs) self.menuLoad = QMenu('Load') self.loadAct = QAction('Load YAML', self.menuLoad) self.loadAct.triggered.connect(self.loadYaml) self.menuLoad.addAction(self.loadAct) self.menuOptions = QMenu('Options') self.instSciAct = QAction('Instrument Scientist Mode', self.menuOptions, checkable=True) self.instSciAct.triggered.connect(self.instSciCB) self.menuOptions.addAction(self.instSciAct) self.eiPlots = QAction('Press Enter in Ei box updates plots', self.menuOptions, checkable=True) self.menuOptions.addAction(self.eiPlots) self.overwriteload = QAction('Always overwrite instruments in memory', self.menuOptions, checkable=True) self.menuOptions.addAction(self.overwriteload) self.menuBar().addMenu(self.menuLoad) self.menuBar().addMenu(self.menuOptions) self.leftPanelWidget = QWidget() self.leftPanelWidget.setLayout(self.leftPanel) self.leftPanelWidget.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)) self.fullWindow.addWidget(self.leftPanelWidget, 0, 0) self.fullWindow.addLayout(self.rightPanel, 0, 1) self.helpbtn = QPushButton("?", self) self.helpbtn.setMaximumWidth(30) self.helpbtn.clicked.connect(self.onHelp) self.fullWindow.addWidget(self.helpbtn, 1, 0, 1, -1) self.mainWidget = QWidget() self.mainWidget.setLayout(self.fullWindow) self.setCentralWidget(self.mainWidget) self.setWindowTitle('PyChopGUI') self.show()
class QJackCaptureMainWindow(QDialog): sample_formats = { "32-bit float": "FLOAT", "8-bit integer": "8", "16-bit integer": "16", "24-bit integer": "24", "32-bit integer": "32", } def __init__(self, parent, jack_client, jack_name=PROGRAM): QDialog.__init__(self, parent) self.ui = Ui_MainWindow() self.ui.setupUi(self) self.fFreewheel = False self.fLastTime = -1 self.fMaxTime = 180 self.fTimer = QTimer(self) self.fProcess = QProcess(self) self.fJackClient = jack_client self.fJackName = jack_name self.fBufferSize = self.fJackClient.get_buffer_size() self.fSampleRate = self.fJackClient.get_sample_rate() # Selected ports used as recording sources self.rec_sources = set() self.createUi() self.loadSettings() self.populatePortLists(init=True) # listen to changes to JACK ports self._refresh_timer = None self.fJackClient.ports_changed.connect(self.slot_refreshPortsLists) @Slot() def slot_refreshPortsLists(self, delay=200): if not self._refresh_timer or not self._refresh_timer.isActive(): log.debug("Scheduling port lists refresh in %i ms...", delay) self._refresh_timer = QTimer() self._refresh_timer.setSingleShot(True) self._refresh_timer.timeout.connect(self.populatePortLists) self._refresh_timer.start(delay) def populateFileFormats(self): # Get list of supported file formats self.fProcess.start(gJackCapturePath, ["-pf"]) self.fProcess.waitForFinished() formats = [] for fmt in str(self.fProcess.readAllStandardOutput(), encoding="utf-8").split(): fmt = fmt.strip() if fmt: formats.append(fmt) # Put all file formats in combo-box, select 'wav' option self.ui.cb_format.clear() for i, fmt in enumerate(sorted(formats)): self.ui.cb_format.addItem(fmt) if fmt == "wav": self.ui.cb_format.setCurrentIndex(i) def populateSampleFormats(self): # Put all sample formats in combo-box, select 'FLOAT' option self.ui.cb_depth.clear() for i, (label, fmt) in enumerate(self.sample_formats.items()): self.ui.cb_depth.addItem(label, fmt) if fmt == "FLOAT": self.ui.cb_depth.setCurrentIndex(i) def populatePortLists(self, init=False): log.debug("Populating port lists (init=%s)...", init) if init: self.outputs_model = QStandardItemModel(0, 1, self) self.inputs_model = QStandardItemModel(0, 1, self) else: self.outputs_model.clear() self.inputs_model.clear() output_ports = list(self.fJackClient.get_output_ports()) self.populatePortList(self.outputs_model, self.ui.tree_outputs, output_ports) input_ports = list(self.fJackClient.get_input_ports()) self.populatePortList(self.inputs_model, self.ui.tree_inputs, input_ports) # Remove ports, which are no longer present, from recording sources all_ports = set((p.client, p.name) for p in output_ports) all_ports |= set((p.client, p.name) for p in input_ports) self.rec_sources.intersection_update(all_ports) self.slot_toggleRecordingSource() def makePortTooltip(self, port): s = [] if port.pretty_name: s.append(f"<b>Pretty name:</b> <em>{port.pretty_name}</em><br>") s.append(f"<b>Port:</b> <tt>{port.client}:{port.name}</tt><br>") for i, alias in enumerate(port.aliases, 1): s.append(f"<b>Alias {i}:</b> <tt>{alias}</tt><br>") s.append(f"<b>UUID:</b> <tt>{port.uuid}</tt>") return "<small>{}</small>".format("\n".join(s)) def populatePortList(self, model, tv, ports): tv.setModel(model) root = model.invisibleRootItem() portsdict = {} for port in ports: if port.client not in portsdict: portsdict[port.client] = [] portsdict[port.client].append(port) for client in humansorted(portsdict): clientitem = QStandardItem(client) for port in humansorted(portsdict[client], key=attrgetter("group", "order", "name")): portspec = (port.client, port.name) if port.pretty_name: label = "%s (%s)" % (port.pretty_name, port.name) else: label = port.name portitem = QStandardItem(label) portitem.setData(portspec) portitem.setCheckable(True) portitem.setUserTristate(False) # Check box toggling is done in the treeview clicked handler "on_port_clicked" portitem.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) portitem.setToolTip(self.makePortTooltip(port)) if portspec in self.rec_sources: portitem.setCheckState(2) clientitem.appendRow(portitem) root.appendRow(clientitem) tv.expandAll() def createUi(self): # ------------------------------------------------------------- # Set-up GUI stuff for i in range(self.ui.cb_buffer_size.count()): if int(self.ui.cb_buffer_size.itemText(i)) == self.fBufferSize: self.ui.cb_buffer_size.setCurrentIndex(i) break else: self.ui.cb_buffer_size.addItem(str(self.fBufferSize)) self.ui.cb_buffer_size.setCurrentIndex( self.ui.cb_buffer_size.count() - 1) self.populateFileFormats() self.populateSampleFormats() self.ui.rb_stereo.setChecked(True) self.ui.te_end.setTime(QTime(0, 3, 0)) self.ui.progressBar.setFormat("") self.ui.progressBar.setMinimum(0) self.ui.progressBar.setMaximum(1) self.ui.progressBar.setValue(0) self.ui.b_render.setIcon(get_icon("media-record")) self.ui.b_stop.setIcon(get_icon("media-playback-stop")) self.ui.b_close.setIcon(get_icon("window-close")) self.ui.b_open.setIcon(get_icon("document-open")) self.ui.b_stop.setVisible(False) self.ui.le_folder.setText(expanduser("~")) # ------------------------------------------------------------- # Set-up connections self.ui.b_render.clicked.connect(self.slot_renderStart) self.ui.b_stop.clicked.connect(self.slot_renderStop) self.ui.b_open.clicked.connect(self.slot_getAndSetPath) self.ui.b_now_start.clicked.connect(self.slot_setStartNow) self.ui.b_now_end.clicked.connect(self.slot_setEndNow) self.ui.te_start.timeChanged.connect(self.slot_updateStartTime) self.ui.te_end.timeChanged.connect(self.slot_updateEndTime) self.ui.group_time.clicked.connect(self.slot_transportChecked) self.ui.rb_source_default.toggled.connect( self.slot_toggleRecordingSource) self.ui.rb_source_manual.toggled.connect( self.slot_toggleRecordingSource) self.ui.rb_source_selected.toggled.connect( self.slot_toggleRecordingSource) self.fTimer.timeout.connect(self.slot_updateProgressbar) for tv in (self.ui.tree_outputs, self.ui.tree_inputs): menu = QMenu() menu.addAction(get_icon("expand-all"), self.tr("E&xpand all"), tv.expandAll) menu.addAction(get_icon("collapse-all"), self.tr("&Collapse all"), tv.collapseAll) menu.addSeparator() menu.addAction( get_icon("list-select-all"), self.tr("&Select all in group"), partial(self.on_select_port_group, tv), ) menu.addAction( get_icon("list-select-none"), self.tr("&Unselect all in group"), partial(self.on_select_port_group, tv, enable=False), ) menu.addSeparator() if tv is self.ui.tree_outputs: menu.addAction( get_icon("select-none"), self.tr("Unselect all &outputs"), partial(self.on_clear_all_ports, tv), ) else: menu.addAction( get_icon("select-none"), self.tr("Unselect all &inputs"), partial(self.on_clear_all_ports, tv), ) tv.setContextMenuPolicy(Qt.CustomContextMenu) tv.customContextMenuRequested.connect( partial(self.on_port_menu, treeview=tv, menu=menu)) tv.clicked.connect(self.on_port_clicked) def enable_port(self, item, enable=True): item.setCheckState(2 if enable else 0) port = item.data() if enable: self.rec_sources.add(port) else: self.rec_sources.discard(port) def on_port_menu(self, pos, treeview=None, menu=None): if treeview and menu: menu.popup(treeview.viewport().mapToGlobal(pos)) def foreach_item(self, model, parent, func, leaves_only=True): for row in range(model.rowCount(parent)): index = model.index(row, 0, parent) is_leaf = not model.hasChildren(index) if is_leaf or not leaves_only: func(model.itemFromIndex(index)) if not is_leaf: self.foreach_item(model, index, func) def on_clear_all_ports(self, treeview): self.foreach_item(treeview.model(), QModelIndex(), partial(self.enable_port, enable=False)) self.checkRecordEnable() def on_port_clicked(self, index): model = index.model() item = model.itemFromIndex(index) if not model.hasChildren(index): self.enable_port(item, not item.checkState()) self.checkRecordEnable() def on_select_port_group(self, treeview, enable=True): index = treeview.currentIndex() model = index.model() if not model.hasChildren(index): index = index.parent() self.foreach_item(model, index, partial(self.enable_port, enable=enable)) self.checkRecordEnable() @Slot() def slot_renderStart(self): if not exists(self.ui.le_folder.text()): QMessageBox.warning( self, self.tr("Warning"), self. tr("The selected directory does not exist. Please choose a valid one." ), ) return timeStart = self.ui.te_start.time() timeEnd = self.ui.te_end.time() minTime = (timeStart.hour() * 3600) + (timeStart.minute() * 60) + (timeStart.second()) maxTime = (timeEnd.hour() * 3600) + (timeEnd.minute() * 60) + (timeEnd.second()) newBufferSize = int(self.ui.cb_buffer_size.currentText()) useTransport = self.ui.group_time.isChecked() self.fFreewheel = self.ui.rb_freewheel.isChecked() self.fLastTime = -1 self.fMaxTime = maxTime if self.fFreewheel: self.fTimer.setInterval(100) else: self.fTimer.setInterval(500) self.ui.group_render.setEnabled(False) self.ui.group_time.setEnabled(False) self.ui.group_encoding.setEnabled(False) self.ui.b_render.setVisible(False) self.ui.b_stop.setVisible(True) self.ui.b_close.setEnabled(False) if useTransport: self.ui.progressBar.setFormat("%p%") self.ui.progressBar.setMinimum(minTime) self.ui.progressBar.setMaximum(maxTime) self.ui.progressBar.setValue(minTime) else: self.ui.progressBar.setFormat("") self.ui.progressBar.setMinimum(0) self.ui.progressBar.setMaximum(0) self.ui.progressBar.setValue(0) self.ui.progressBar.update() arguments = [] # JACK client name arguments.append("-jn") arguments.append(self.fJackName) # Filename prefix arguments.append("-fp") arguments.append(self.ui.le_prefix.text()) # File format arguments.append("-f") arguments.append(self.ui.cb_format.currentText()) # Sanple format (bit depth, int/float) arguments.append("-b") arguments.append(self.ui.cb_depth.currentData()) # Channels arguments.append("-c") if self.ui.rb_mono.isChecked(): arguments.append("1") elif self.ui.rb_stereo.isChecked(): arguments.append("2") else: arguments.append(str(self.ui.sb_channels.value())) # Recording sources if self.ui.rb_source_manual.isChecked(): arguments.append("-mc") elif self.ui.rb_source_selected.isChecked(): for client, port in self.rec_sources: arguments.append("-p") arguments.append("{}:{}".format(client, port)) # Controlled only by freewheel if self.fFreewheel: arguments.append("-jf") # Controlled by transport elif useTransport: arguments.append("-jt") # Silent mode arguments.append("--daemon") # Extra arguments extra_args = self.ui.le_extra_args.text().strip() if extra_args: arg_list = shlex.split(extra_args) arguments.extend(arg_list) # Change current directory os.chdir(self.ui.le_folder.text()) if newBufferSize != self.fJackClient.get_buffer_size(): log.info("Buffer size changed before render.") self.fJackClient.set_buffer_size(newBufferSize) if useTransport: if self.fJackClient.transport_running(): # rolling or starting self.fJackClient.transport_stop() self.fJackClient.transport_locate(minTime * self.fSampleRate) log.debug("jack_capture command line args: %r", arguments) self.fProcess.start(gJackCapturePath, arguments) status = self.fProcess.waitForStarted() if not status: self.fProcess.close() log.error("Could not start jack_capture.") return if self.fFreewheel: log.info("Rendering in freewheel mode.") sleep(1) self.fJackClient.set_freewheel(True) if useTransport: log.info("Rendering using JACK transport.") self.fTimer.start() self.fJackClient.transport_start() @Slot() def slot_renderStop(self): useTransport = self.ui.group_time.isChecked() if useTransport: self.fJackClient.transport_stop() if self.fFreewheel: self.fJackClient.set_freewheel(False) sleep(1) self.fProcess.terminate() # self.fProcess.waitForFinished(5000) if useTransport: self.fTimer.stop() self.ui.group_render.setEnabled(True) self.ui.group_time.setEnabled(True) self.ui.group_encoding.setEnabled(True) self.ui.b_render.setVisible(True) self.ui.b_stop.setVisible(False) self.ui.b_close.setEnabled(True) self.ui.progressBar.setFormat("") self.ui.progressBar.setMinimum(0) self.ui.progressBar.setMaximum(1) self.ui.progressBar.setValue(0) self.ui.progressBar.update() # Restore buffer size newBufferSize = self.fJackClient.get_buffer_size() if newBufferSize != self.fBufferSize: self.fJackClient.set_buffer_size(newBufferSize) @Slot() def slot_getAndSetPath(self): new_path = QFileDialog.getExistingDirectory(self, self.tr("Set Path"), self.ui.le_folder.text(), QFileDialog.ShowDirsOnly) if new_path: self.ui.le_folder.setText(new_path) @Slot() def slot_setStartNow(self): time = self.fJackClient.transport_frame() // self.fSampleRate secs = time % 60 mins = int(time / 60) % 60 hrs = int(time / 3600) % 60 self.ui.te_start.setTime(QTime(hrs, mins, secs)) @Slot() def slot_setEndNow(self): time = self.fJackClient.transport_frame() // self.fSampleRate secs = time % 60 mins = int(time / 60) % 60 hrs = int(time / 3600) % 60 self.ui.te_end.setTime(QTime(hrs, mins, secs)) @Slot(QTime) def slot_updateStartTime(self, time): if time >= self.ui.te_end.time(): self.ui.te_end.setTime(time) renderEnabled = False else: renderEnabled = True if self.ui.group_time.isChecked(): self.ui.b_render.setEnabled(renderEnabled) @Slot(QTime) def slot_updateEndTime(self, time): if time <= self.ui.te_start.time(): self.ui.te_start.setTime(time) renderEnabled = False else: renderEnabled = True if self.ui.group_time.isChecked(): self.ui.b_render.setEnabled(renderEnabled) @Slot(bool) def slot_toggleRecordingSource(self, dummy=None): enabled = self.ui.rb_source_selected.isChecked() self.ui.tree_outputs.setEnabled(enabled) self.ui.tree_inputs.setEnabled(enabled) self.checkRecordEnable() @Slot(bool) def slot_transportChecked(self, dummy=None): self.checkRecordEnable() @Slot() def slot_updateProgressbar(self): time = self.fJackClient.transport_frame() / self.fSampleRate self.ui.progressBar.setValue(time) if time > self.fMaxTime or (self.fLastTime > time and not self.fFreewheel): self.slot_renderStop() self.fLastTime = time def checkRecordEnable(self): enable = True if self.ui.rb_source_selected.isChecked() and not self.rec_sources: enable = False if self.ui.group_time.isChecked( ) and self.ui.te_end.time() <= self.ui.te_start.time(): enable = False self.ui.b_render.setEnabled(enable) log.debug("Recording sources: %s", ", ".join( ("%s:%s" % (c, p) for c, p in self.rec_sources))) def saveSettings(self): settings = QSettings(ORGANIZATION, PROGRAM) if self.ui.rb_mono.isChecked(): channels = 1 elif self.ui.rb_stereo.isChecked(): channels = 2 else: channels = self.ui.sb_channels.value() settings.setValue("Geometry", self.saveGeometry()) settings.setValue("OutputFolder", self.ui.le_folder.text()) settings.setValue("FilenamePrefix", self.ui.le_prefix.text()) settings.setValue("EncodingFormat", self.ui.cb_format.currentText()) settings.setValue("EncodingDepth", self.ui.cb_depth.currentData()) settings.setValue("EncodingChannels", channels) settings.setValue("UseTransport", self.ui.group_time.isChecked()) settings.setValue("StartTime", self.ui.te_start.time()) settings.setValue("EndTime", self.ui.te_end.time()) settings.setValue("ExtraArgs", self.ui.le_extra_args.text().strip()) if self.ui.rb_source_default.isChecked(): settings.setValue("RecordingSource", 0) elif self.ui.rb_source_manual.isChecked(): settings.setValue("RecordingSource", 1) elif self.ui.rb_source_selected.isChecked(): settings.setValue("RecordingSource", 2) settings.beginWriteArray("Sources") for i, (client, port) in enumerate(self.rec_sources): settings.setArrayIndex(i) settings.setValue("Client", client) settings.setValue("Port", port) settings.endArray() def loadSettings(self): settings = QSettings(ORGANIZATION, PROGRAM) self.restoreGeometry(settings.value("Geometry", b"")) outputFolder = settings.value("OutputFolder", get_user_dir("MUSIC")) if isdir(outputFolder): self.ui.le_folder.setText(outputFolder) self.ui.le_prefix.setText( settings.value("FilenamePrefix", "jack_capture_")) encFormat = settings.value("EncodingFormat", "wav", type=str) for i in range(self.ui.cb_format.count()): if self.ui.cb_format.itemText(i) == encFormat: self.ui.cb_format.setCurrentIndex(i) break encDepth = settings.value("EncodingDepth", "FLOAT", type=str) for i in range(self.ui.cb_depth.count()): if self.ui.cb_depth.itemData(i) == encDepth: self.ui.cb_depth.setCurrentIndex(i) break encChannels = settings.value("EncodingChannels", 2, type=int) if encChannels == 1: self.ui.rb_mono.setChecked(True) elif encChannels == 2: self.ui.rb_stereo.setChecked(True) else: self.ui.rb_outro.setChecked(True) self.ui.sb_channels.setValue(encChannels) recSource = settings.value("RecordingSource", 0, type=int) if recSource == 1: self.ui.rb_source_manual.setChecked(True) elif recSource == 2: self.ui.rb_source_selected.setChecked(True) else: self.ui.rb_source_default.setChecked(True) self.ui.group_time.setChecked( settings.value("UseTransport", False, type=bool)) self.ui.te_start.setTime( settings.value("StartTime", self.ui.te_start.time(), type=QTime)) self.ui.te_end.setTime( settings.value("EndTime", self.ui.te_end.time(), type=QTime)) self.ui.le_extra_args.setText(settings.value("ExtraArgs", "", type=str)) size = settings.beginReadArray("Sources") for i in range(size): settings.setArrayIndex(i) client = settings.value("Client", type=str) port = settings.value("Port", type=str) if client and port: self.rec_sources.add((client, port)) settings.endArray() def closeEvent(self, event): self.saveSettings() self.fJackClient.close() QDialog.closeEvent(self, event) def done(self, r): QDialog.done(self, r) self.close()