class LayerCommunicator(QtCore.QObject): layer_check_changed = QtCore.Signal(object, bool)
class SliceWidget(QtWidgets.QWidget): label = TextProperty('_ui_label') slider_label = TextProperty('_ui_slider.label') slider_unit = TextProperty('_ui_slider.text_unit') slice_center = ValueProperty('_ui_slider.slider') mode = CurrentComboProperty('_ui_mode') use_world = ButtonProperty('_ui_slider.checkbox_world') slice_changed = QtCore.Signal(int) mode_changed = QtCore.Signal(str) def __init__(self, label='', world=None, lo=0, hi=10, parent=None, aggregation=None, world_unit=None, world_warning=False): super(SliceWidget, self).__init__(parent) if aggregation is not None: raise NotImplemented("Aggregation option not implemented") self._world = np.asarray(world) self._world_warning = world_warning self._world_unit = world_unit layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(3, 1, 3, 1) layout.setSpacing(0) top = QtWidgets.QHBoxLayout() top.setContentsMargins(3, 3, 3, 3) label = QtWidgets.QLabel(label) top.addWidget(label) mode = QtWidgets.QComboBox() mode.addItem('x', 'x') mode.addItem('y', 'y') mode.addItem('slice', 'slice') mode.currentIndexChanged.connect( lambda x: self.mode_changed.emit(self.mode)) mode.currentIndexChanged.connect(self._update_mode) top.addWidget(mode) layout.addLayout(top) slider = load_ui('data_slice_widget.ui', None, directory=os.path.dirname(__file__)) self._ui_slider = slider font = slider.label_warning.font() font.setPointSize(font.pointSize() * 0.75) slider.label_warning.setFont(font) slider.button_first.setStyleSheet('border: 0px') slider.button_first.setIcon(get_icon('playback_first')) slider.button_prev.setStyleSheet('border: 0px') slider.button_prev.setIcon(get_icon('playback_prev')) slider.button_back.setStyleSheet('border: 0px') slider.button_back.setIcon(get_icon('playback_back')) slider.button_stop.setStyleSheet('border: 0px') slider.button_stop.setIcon(get_icon('playback_stop')) slider.button_forw.setStyleSheet('border: 0px') slider.button_forw.setIcon(get_icon('playback_forw')) slider.button_next.setStyleSheet('border: 0px') slider.button_next.setIcon(get_icon('playback_next')) slider.button_last.setStyleSheet('border: 0px') slider.button_last.setIcon(get_icon('playback_last')) slider.slider.setMinimum(lo) slider.slider.setMaximum(hi) slider.slider.setValue((lo + hi) / 2) slider.slider.valueChanged.connect( lambda x: self.slice_changed.emit(self.mode)) slider.slider.valueChanged.connect( nonpartial(self.set_label_from_slider)) slider.label.setMinimumWidth(80) slider.label.setText(str(slider.slider.value())) slider.label.editingFinished.connect( nonpartial(self.set_slider_from_label)) self._play_timer = QtCore.QTimer() self._play_timer.setInterval(500) self._play_timer.timeout.connect(nonpartial(self._play_slice)) slider.button_first.clicked.connect( nonpartial(self._browse_slice, 'first')) slider.button_prev.clicked.connect( nonpartial(self._browse_slice, 'prev')) slider.button_back.clicked.connect( nonpartial(self._adjust_play, 'back')) slider.button_stop.clicked.connect( nonpartial(self._adjust_play, 'stop')) slider.button_forw.clicked.connect( nonpartial(self._adjust_play, 'forw')) slider.button_next.clicked.connect( nonpartial(self._browse_slice, 'next')) slider.button_last.clicked.connect( nonpartial(self._browse_slice, 'last')) slider.checkbox_world.toggled.connect( nonpartial(self.set_label_from_slider)) if world is None: self.use_world = False slider.checkbox_world.hide() else: self.use_world = not world_warning if world_unit: self.slider_unit = world_unit else: self.slider_unit = '' layout.addWidget(slider) self.setLayout(layout) self._ui_label = label self._ui_mode = mode self._update_mode() self._frozen = False self._play_speed = 0 self.set_label_from_slider() def set_label_from_slider(self): value = self._ui_slider.slider.value() if self.use_world: text = str(self._world[value]) if self._world_warning: self._ui_slider.label_warning.show() else: self._ui_slider.label_warning.hide() self.slider_unit = self._world_unit else: text = str(value) self._ui_slider.label_warning.hide() self.slider_unit = '' self._ui_slider.label.setText(text) def set_slider_from_label(self): text = self._ui_slider.label.text() if self.use_world: # Don't want to assume world is sorted, pick closest value value = np.argmin(np.abs(self._world - float(text))) self._ui_slider.label.setText(str(self._world[value])) else: value = int(text) self._ui_slider.slider.setValue(value) def _adjust_play(self, action): if action == 'stop': self._play_speed = 0 elif action == 'back': if self._play_speed > 0: self._play_speed = -1 else: self._play_speed -= 1 elif action == 'forw': if self._play_speed < 0: self._play_speed = +1 else: self._play_speed += 1 if self._play_speed == 0: self._play_timer.stop() else: self._play_timer.start() self._play_timer.setInterval(500 / abs(self._play_speed)) def _play_slice(self): if self._play_speed > 0: self._browse_slice('next', play=True) elif self._play_speed < 0: self._browse_slice('prev', play=True) def _browse_slice(self, action, play=False): imin = self._ui_slider.slider.minimum() imax = self._ui_slider.slider.maximum() value = self._ui_slider.slider.value() # If this was not called from _play_slice, we should stop the # animation. if not play: self._adjust_play('stop') if action == 'first': value = imin elif action == 'last': value = imax elif action == 'prev': value = value - 1 if value < imin: value = imax elif action == 'next': value = value + 1 if value > imax: value = imin else: raise ValueError("Action should be one of first/prev/next/last") self._ui_slider.slider.setValue(value) def _update_mode(self, *args): if self.mode != 'slice': self._ui_slider.hide() self._adjust_play('stop') else: self._ui_slider.show() def freeze(self): self.mode = 'slice' self._ui_mode.setEnabled(False) self._ui_slider.hide() self._frozen = True @property def frozen(self): return self._frozen
class Executor(QtCore.QObject, threading.Thread): """Executor represents a thread of control that runs a python function with a single input. Once created with the proper inputs, threading.Thread has the following attributes: self.module - the loaded module object provided to __init__() self.args - the argument to the target function. Usually a dict. self.func_name - the function name that will be called. self.log_manager - the LogManager instance managing logs for this script self.failed - defaults to False. Indicates whether the thread raised an exception while running. self.execption - defaults to None. If not None, points to the exception raised while running the thread. The Executor.run() function is an overridden function from threading.Thread and is started in the same manner by calling Executor.start(). The run() function is extremely simple by design: Print the arguments to the logfile and run the specified function. If an execption is raised, it is printed and saved locally for retrieval later on. In keeping with convention, a single Executor thread instance is only designed to be run once. To run the same function again, it is best to create a new Executor instance and run that.""" finished = QtCore.Signal() def __init__(self, target, args, kwargs, logfile, tempdir=None): QtCore.QObject.__init__(self) threading.Thread.__init__(self) self.target = target self.tempdir = tempdir if not args: args = () self.args = args if not kwargs: kwargs = {} self.kwargs = kwargs if logfile is None: logfile = os.path.join(tempfile.mkdtemp(), 'logfile.txt') self.logfile = logfile self.failed = False self.exception = None self.traceback = None def run(self): """Run the python script provided by the user with the arguments specified. This function also prints the arguments to the logfile handler. If an exception is raised in either the loading or execution of the module or function, a traceback is printed and the exception is saved.""" try: self.target(*self.args, **self.kwargs) except Exception as error: # We deliberately want to catch all possible exceptions. LOGGER.exception(error) self.failed = True self.exception = error self.traceback = traceback.format_exc() finally: LOGGER.info('Execution finished') self.finished.emit()
class FilterComboBox(QtWidgets.QToolButton): checkedItemsChanged = QtCore.Signal(list) def __init__(self, parent=None): super(FilterComboBox, self).__init__(parent) self.setText("(no filter)") # QtGui.QToolButton.InstantPopup would be slightly less work (the # whole button works by default, instead of only the arrow) but it is # uglier self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) menu = FilterMenu(self) self.setMenu(menu) self._menu = menu menu.checkedItemsChanged.connect(self.on_checked_items_changed) self.installEventFilter(self) def on_checked_items_changed(self, indices_checked): num_checked = len(indices_checked) model = self._menu._model if num_checked == 0 or num_checked == len(model) - 1: self.setText("(no filter)") elif num_checked == 1: self.setText(model[indices_checked[0] + 1].text()) else: self.setText("multi") self.checkedItemsChanged.emit(indices_checked) def addItem(self, text): self._menu.addItem(text) def addItems(self, items): self._menu.addItems(items) def eventFilter(self, obj, event): event_type = event.type() # this is not enabled because it causes all kind of troubles # if event_type == QtCore.QEvent.KeyPress: # key = event.key() # # # allow opening the popup via enter/return # if (obj == self and # key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter)): # self.showMenu() # return True if event_type == QtCore.QEvent.KeyRelease: key = event.key() # allow opening the popup with up/down if (obj == self and key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_Space)): self.showMenu() return True # return key activates *one* item and closes the popup # first time the key is sent to self, afterwards to list_view elif (obj == self and key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return)): self._menu.activate.emit(self._list_view.currentIndex().row()) self._menu.hide() return True if event_type == QtCore.QEvent.MouseButtonRelease: # clicking anywhere (not just arrow) on the button shows the popup if obj == self: self.showMenu() return False
class JupyterWidget(IPythonWidget): """A FrontendWidget for a Jupyter kernel.""" # If set, the 'custom_edit_requested(str, int)' signal will be emitted when # an editor is needed for a file. This overrides 'editor' and 'editor_line' # settings. custom_edit = Bool(False) custom_edit_requested = QtCore.Signal(object, object) editor = Unicode(default_editor, config=True, help=""" A command for invoking a GUI text editor. If the string contains a {filename} format specifier, it will be used. Otherwise, the filename will be appended to the end the command. To use a terminal text editor, the command should launch a new terminal, e.g. ``"gnome-terminal -- vim"``. """) editor_line = Unicode(config=True, help=""" The editor command to use when a specific line number is requested. The string should contain two format specifiers: {line} and {filename}. If this parameter is not specified, the line number option to the %edit magic will be ignored. """) style_sheet = Unicode(config=True, help=""" A CSS stylesheet. The stylesheet can contain classes for: 1. Qt: QPlainTextEdit, QFrame, QWidget, etc 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter) 3. QtConsole: .error, .in-prompt, .out-prompt, etc """) syntax_style = Unicode(config=True, help=""" If not empty, use this Pygments style for syntax highlighting. Otherwise, the style sheet is queried for Pygments style information. """) # Prompts. in_prompt = Unicode(default_in_prompt, config=True) out_prompt = Unicode(default_out_prompt, config=True) input_sep = Unicode(default_input_sep, config=True) output_sep = Unicode(default_output_sep, config=True) output_sep2 = Unicode(default_output_sep2, config=True) # JupyterWidget protected class variables. _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number']) _payload_source_edit = 'edit_magic' _payload_source_exit = 'ask_exit' _payload_source_next_input = 'set_next_input' _payload_source_page = 'page' _retrying_history_request = False _starting = False #--------------------------------------------------------------------------- # 'object' interface #--------------------------------------------------------------------------- def __init__(self, *args, **kw): super().__init__(*args, **kw) # JupyterWidget protected variables. self._payload_handlers = { self._payload_source_edit: self._handle_payload_edit, self._payload_source_exit: self._handle_payload_exit, self._payload_source_page: self._handle_payload_page, self._payload_source_next_input: self._handle_payload_next_input } self._previous_prompt_obj = None self._keep_kernel_on_exit = None # Initialize widget styling. if self.style_sheet: self._style_sheet_changed() self._syntax_style_changed() else: self.set_default_style() # Initialize language name. self.language_name = None #--------------------------------------------------------------------------- # 'BaseFrontendMixin' abstract interface # # For JupyterWidget, override FrontendWidget methods which implement the # BaseFrontend Mixin abstract interface #--------------------------------------------------------------------------- def _handle_complete_reply(self, rep): """Support Jupyter's improved completion machinery. """ self.log.debug("complete: %s", rep.get('content', '')) cursor = self._get_cursor() info = self._request_info.get('complete') if (info and info.id == rep['parent_header']['msg_id'] and info.pos == self._get_input_buffer_cursor_pos() and info.code == self.input_buffer): content = rep['content'] matches = content['matches'] start = content['cursor_start'] end = content['cursor_end'] start = max(start, 0) end = max(end, start) # Move the control's cursor to the desired end point cursor_pos = self._get_input_buffer_cursor_pos() if end < cursor_pos: cursor.movePosition(QtGui.QTextCursor.Left, n=(cursor_pos - end)) elif end > cursor_pos: cursor.movePosition(QtGui.QTextCursor.Right, n=(end - cursor_pos)) # This line actually applies the move to control's cursor self._control.setTextCursor(cursor) offset = end - start # Move the local cursor object to the start of the match and # complete. cursor.movePosition(QtGui.QTextCursor.Left, n=offset) self._complete_with_items(cursor, matches) def _handle_execute_reply(self, msg): """Support prompt requests. """ msg_id = msg['parent_header'].get('msg_id') info = self._request_info['execute'].get(msg_id) if info and info.kind == 'prompt': content = msg['content'] if content['status'] == 'aborted': self._show_interpreter_prompt() else: number = content['execution_count'] + 1 self._show_interpreter_prompt(number) self._request_info['execute'].pop(msg_id) else: super()._handle_execute_reply(msg) def _handle_history_reply(self, msg): """ Handle history tail replies, which are only supported by Jupyter kernels. """ content = msg['content'] if 'history' not in content: self.log.error("History request failed: %r" % content) if content.get('status', '') == 'aborted' and \ not self._retrying_history_request: # a *different* action caused this request to be aborted, so # we should try again. self.log.error("Retrying aborted history request") # prevent multiple retries of aborted requests: self._retrying_history_request = True # wait out the kernel's queue flush, which is currently timed at 0.1s time.sleep(0.25) self.kernel_client.history(hist_access_type='tail', n=1000) else: self._retrying_history_request = False return # reset retry flag self._retrying_history_request = False history_items = content['history'] self.log.debug("Received history reply with %i entries", len(history_items)) items = [] last_cell = "" for _, _, cell in history_items: cell = cell.rstrip() if cell != last_cell: items.append(cell) last_cell = cell self._set_history(items) def _insert_other_input(self, cursor, content, remote=True): """Insert function for input from other frontends""" n = content.get('execution_count', 0) prompt = self._make_in_prompt(n, remote=remote) cont_prompt = self._make_continuation_prompt(self._prompt, remote=remote) cursor.insertText('\n') for i, line in enumerate(content['code'].strip().split('\n')): if i == 0: self._insert_html(cursor, prompt) else: self._insert_html(cursor, cont_prompt) self._insert_plain_text(cursor, line + '\n') # Update current prompt number self._update_prompt(n + 1) def _handle_execute_input(self, msg): """Handle an execute_input message""" self.log.debug("execute_input: %s", msg.get('content', '')) if self.include_output(msg): self._append_custom(self._insert_other_input, msg['content'], before_prompt=True) elif not self._prompt: self._append_custom(self._insert_other_input, msg['content'], before_prompt=True, remote=False) def _handle_execute_result(self, msg): """Handle an execute_result message""" self.log.debug("execute_result: %s", msg.get('content', '')) if self.include_output(msg): self.flush_clearoutput() content = msg['content'] prompt_number = content.get('execution_count', 0) data = content['data'] if 'text/plain' in data: self._append_plain_text(self.output_sep, before_prompt=True) self._append_html(self._make_out_prompt( prompt_number, remote=not self.from_here(msg)), before_prompt=True) text = data['text/plain'] # If the repr is multiline, make sure we start on a new line, # so that its lines are aligned. if "\n" in text and not self.output_sep.endswith("\n"): self._append_plain_text('\n', before_prompt=True) self._append_plain_text(text + self.output_sep2, before_prompt=True) if not self.from_here(msg): self._append_plain_text('\n', before_prompt=True) def _handle_display_data(self, msg): """The base handler for the ``display_data`` message.""" # For now, we don't display data from other frontends, but we # eventually will as this allows all frontends to monitor the display # data. But we need to figure out how to handle this in the GUI. if self.include_output(msg): self.flush_clearoutput() data = msg['content']['data'] metadata = msg['content']['metadata'] # In the regular JupyterWidget, we simply print the plain text # representation. if 'text/plain' in data: text = data['text/plain'] self._append_plain_text(text, True) # This newline seems to be needed for text and html output. self._append_plain_text('\n', True) def _handle_kernel_info_reply(self, rep): """Handle kernel info replies.""" content = rep['content'] self.language_name = content['language_info']['name'] pygments_lexer = content['language_info'].get('pygments_lexer', '') try: # Other kernels with pygments_lexer info will have to be # added here by hand. if pygments_lexer == 'ipython3': lexer = IPython3Lexer() elif pygments_lexer == 'ipython2': lexer = IPythonLexer() else: lexer = get_lexer_by_name(self.language_name) self._highlighter._lexer = lexer except ClassNotFound: pass self.kernel_banner = content.get('banner', '') if self._starting: # finish handling started channels self._starting = False super()._started_channels() def _started_channels(self): """Make a history request""" self._starting = True self.kernel_client.kernel_info() self.kernel_client.history(hist_access_type='tail', n=1000) #--------------------------------------------------------------------------- # 'FrontendWidget' protected interface #--------------------------------------------------------------------------- def _process_execute_error(self, msg): """Handle an execute_error message""" self.log.debug("execute_error: %s", msg.get('content', '')) content = msg['content'] traceback = '\n'.join(content['traceback']) + '\n' if False: # FIXME: For now, tracebacks come as plain text, so we can't # use the html renderer yet. Once we refactor ultratb to # produce properly styled tracebacks, this branch should be the # default traceback = traceback.replace(' ', ' ') traceback = traceback.replace('\n', '<br/>') ename = content['ename'] ename_styled = '<span class="error">%s</span>' % ename traceback = traceback.replace(ename, ename_styled) self._append_html(traceback) else: # This is the fallback for now, using plain text with ansi # escapes self._append_plain_text(traceback, before_prompt=not self.from_here(msg)) def _process_execute_payload(self, item): """ Reimplemented to dispatch payloads to handler methods. """ handler = self._payload_handlers.get(item['source']) if handler is None: # We have no handler for this type of payload, simply ignore it return False else: handler(item) return True def _show_interpreter_prompt(self, number=None): """ Reimplemented for IPython-style prompts. """ # If a number was not specified, make a prompt number request. if number is None: msg_id = self.kernel_client.execute('', silent=True) info = self._ExecutionRequest(msg_id, 'prompt') self._request_info['execute'][msg_id] = info return # Show a new prompt and save information about it so that it can be # updated later if the prompt number turns out to be wrong. self._prompt_sep = self.input_sep self._show_prompt(self._make_in_prompt(number), html=True) block = self._control.document().lastBlock() length = len(self._prompt) self._previous_prompt_obj = self._PromptBlock(block, length, number) # Update continuation prompt to reflect (possibly) new prompt length. self._set_continuation_prompt(self._make_continuation_prompt( self._prompt), html=True) def _update_prompt(self, new_prompt_number): """Replace the last displayed prompt with a new one.""" if self._previous_prompt_obj is None: return block = self._previous_prompt_obj.block # Make sure the prompt block has not been erased. if block.isValid() and block.text(): # Remove the old prompt and insert a new prompt. cursor = QtGui.QTextCursor(block) cursor.movePosition(QtGui.QTextCursor.Right, QtGui.QTextCursor.KeepAnchor, self._previous_prompt_obj.length) prompt = self._make_in_prompt(new_prompt_number) self._prompt = self._insert_html_fetching_plain_text( cursor, prompt) # When the HTML is inserted, Qt blows away the syntax # highlighting for the line, so we need to rehighlight it. self._highlighter.rehighlightBlock(cursor.block()) # Update the prompt cursor self._prompt_cursor.setPosition(cursor.position() - 1) # Store the updated prompt. block = self._control.document().lastBlock() length = len(self._prompt) self._previous_prompt_obj = self._PromptBlock( block, length, new_prompt_number) def _show_interpreter_prompt_for_reply(self, msg): """ Reimplemented for IPython-style prompts. """ # Update the old prompt number if necessary. content = msg['content'] # abort replies do not have any keys: if content['status'] == 'aborted': if self._previous_prompt_obj: previous_prompt_number = self._previous_prompt_obj.number else: previous_prompt_number = 0 else: previous_prompt_number = content['execution_count'] if self._previous_prompt_obj and \ self._previous_prompt_obj.number != previous_prompt_number: self._update_prompt(previous_prompt_number) self._previous_prompt_obj = None # Show a new prompt with the kernel's estimated prompt number. self._show_interpreter_prompt(previous_prompt_number + 1) #--------------------------------------------------------------------------- # 'JupyterWidget' interface #--------------------------------------------------------------------------- def set_default_style(self, colors='lightbg'): """ Sets the widget style to the class defaults. Parameters ---------- colors : str, optional (default lightbg) Whether to use the default light background or dark background or B&W style. """ colors = colors.lower() if colors == 'lightbg': self.style_sheet = styles.default_light_style_sheet self.syntax_style = styles.default_light_syntax_style elif colors == 'linux': self.style_sheet = styles.default_dark_style_sheet self.syntax_style = styles.default_dark_syntax_style elif colors == 'nocolor': self.style_sheet = styles.default_bw_style_sheet self.syntax_style = styles.default_bw_syntax_style else: raise KeyError("No such color scheme: %s" % colors) #--------------------------------------------------------------------------- # 'JupyterWidget' protected interface #--------------------------------------------------------------------------- def _edit(self, filename, line=None): """ Opens a Python script for editing. Parameters ---------- filename : str A path to a local system file. line : int, optional A line of interest in the file. """ if self.custom_edit: self.custom_edit_requested.emit(filename, line) elif not self.editor: self._append_plain_text( 'No default editor available.\n' 'Specify a GUI text editor in the `JupyterWidget.editor` ' 'configurable to enable the %edit magic') else: try: filename = '"%s"' % filename if line and self.editor_line: command = self.editor_line.format(filename=filename, line=line) else: try: command = self.editor.format() except KeyError: command = self.editor.format(filename=filename) else: command += ' ' + filename except KeyError: self._append_plain_text('Invalid editor command.\n') else: try: Popen(command, shell=True) except OSError: msg = 'Opening editor with command "%s" failed.\n' self._append_plain_text(msg % command) def _make_in_prompt(self, number, remote=False): """ Given a prompt number, returns an HTML In prompt. """ try: body = self.in_prompt % number except TypeError: # allow in_prompt to leave out number, e.g. '>>> ' from xml.sax.saxutils import escape body = escape(self.in_prompt) if remote: body = self.other_output_prefix + body return '<span class="in-prompt">%s</span>' % body def _make_continuation_prompt(self, prompt, remote=False): """ Given a plain text version of an In prompt, returns an HTML continuation prompt. """ end_chars = '...: ' space_count = len(prompt.lstrip('\n')) - len(end_chars) if remote: space_count += len(self.other_output_prefix.rsplit('\n')[-1]) body = ' ' * space_count + end_chars return '<span class="in-prompt">%s</span>' % body def _make_out_prompt(self, number, remote=False): """ Given a prompt number, returns an HTML Out prompt. """ try: body = self.out_prompt % number except TypeError: # allow out_prompt to leave out number, e.g. '<<< ' from xml.sax.saxutils import escape body = escape(self.out_prompt) if remote: body = self.other_output_prefix + body return '<span class="out-prompt">%s</span>' % body #------ Payload handlers -------------------------------------------------- # Payload handlers with a generic interface: each takes the opaque payload # dict, unpacks it and calls the underlying functions with the necessary # arguments. def _handle_payload_edit(self, item): self._edit(item['filename'], item['line_number']) def _handle_payload_exit(self, item): self._keep_kernel_on_exit = item['keepkernel'] self.exit_requested.emit(self) def _handle_payload_next_input(self, item): self.input_buffer = item['text'] def _handle_payload_page(self, item): # Since the plain text widget supports only a very small subset of HTML # and we have no control over the HTML source, we only page HTML # payloads in the rich text widget. data = item['data'] if 'text/html' in data and self.kind == 'rich': self._page(data['text/html'], html=True) else: self._page(data['text/plain'], html=False) #------ Trait change handlers -------------------------------------------- @observe('style_sheet') def _style_sheet_changed(self, changed=None): """ Set the style sheets of the underlying widgets. """ self.setStyleSheet(self.style_sheet) if self._control is not None: self._control.document().setDefaultStyleSheet(self.style_sheet) if self._page_control is not None: self._page_control.document().setDefaultStyleSheet( self.style_sheet) @observe('syntax_style') def _syntax_style_changed(self, changed=None): """ Set the style for the syntax highlighter. """ if self._highlighter is None: # ignore premature calls return if self.syntax_style: self._highlighter.set_style(self.syntax_style) self._ansi_processor.set_background_color(self.syntax_style) else: self._highlighter.set_style_sheet(self.style_sheet) #------ Trait default initializers ----------------------------------------- @default('banner') def _banner_default(self): return "Jupyter QtConsole {version}\n".format(version=__version__)
class LaserGUI(GUIBase): """ FIXME: Please document """ _modclass = 'lasergui' _modtype = 'gui' ## declare connectors _in = {'laserlogic': 'LaserLogic'} sigLaser = QtCore.Signal(bool) sigShutter = QtCore.Signal(bool) sigPower = QtCore.Signal(float) sigCurrent = QtCore.Signal(float) sigCtrlMode = QtCore.Signal(ControlMode) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) def on_activate(self, e=None): """ Definition and initialisation of the GUI plus staring the measurement. @param object e: Fysom.event object from Fysom class. An object created by the state machine module Fysom, which is connected to a specific event (have a look in the Base Class). This object contains the passed event, the state before the event happened and the destination of the state which should be reached after the event had happened. """ self._laser_logic = self.get_in_connector('laserlogic') ##################### # Configuring the dock widgets # Use the inherited class 'CounterMainWindow' to create the GUI window self._mw = LaserWindow() # Setup dock widgets self._mw.setDockNestingEnabled(True) self._mw.actionReset_View.triggered.connect(self.restoreDefaultView) # set up plot pw = self._mw.graphicsView plot1 = pw.plotItem plot1.setLabel('left', 'Some Value', units='some unit', color='#00ff00') plot1.setLabel('bottom', 'Number of values', units='some unit') self.plots = {} colorlist = (palette.c1, palette.c2, palette.c3, palette.c4, palette.c5, palette.c6) i = 0 for k in self._laser_logic.data: if k != 'time': self.plots[k] = plot1.plot() self.plots[k].setPen(colorlist[(2 * i) % len(colorlist)]) i += 1 self.updateButtonsEnabled() self._mw.laserButton.clicked.connect(self.changeLaserState) self._mw.shutterButton.clicked.connect(self.changeShutterState) self.sigLaser.connect(self._laser_logic.set_laser_state) self.sigShutter.connect(self._laser_logic.set_shutter_state) self.sigCurrent.connect(self._laser_logic.set_current) self.sigPower.connect(self._laser_logic.set_power) self._mw.controlModeButtonGroup.buttonClicked.connect( self.changeControlMode) self.sliderProxy = pg.SignalProxy( self._mw.setValueVerticalSlider.valueChanged, 0.1, 5, self.updateFromSlider) self._mw.setValueDoubleSpinBox.editingFinished.connect( self.updateFromSpinBox) self._laser_logic.sigUpdate.connect(self.updateGui) def on_deactivate(self, e): """ Deactivate the module properly. @param object e: Fysom.event object from Fysom class. A more detailed explanation can be found in the method initUI. """ self._mw.close() def show(self): """Make window visible and put it above all other windows. """ QtWidgets.QMainWindow.show(self._mw) self._mw.activateWindow() self._mw.raise_() def restoreDefaultView(self): """ Restore the arrangement of DockWidgets to the default """ # Show any hidden dock widgets self._mw.adjustDockWidget.show() self._mw.plotDockWidget.show() # re-dock any floating dock widgets self._mw.adjustDockWidget.setFloating(False) self._mw.plotDockWidget.setFloating(False) # Arrange docks widgets self._mw.addDockWidget(QtCore.Qt.DockWidgetArea(1), self._mw.adjustDockWidget) self._mw.addDockWidget(QtCore.Qt.DockWidgetArea(2), self._mw.plotDockWidget) def changeLaserState(self, on): """ """ self._mw.laserButton.setEnabled(False) self.sigLaser.emit(on) def changeShutterState(self, on): """ """ self._mw.shutterButton.setEnabled(False) self.sigShutter.emit(on) @QtCore.Slot(int) def changeControlMode(self, buttonId): """ """ cur = self._mw.currentRadioButton.isChecked( ) and self._mw.currentRadioButton.isEnabled() pwr = self._mw.powerRadioButton.isChecked( ) and self._mw.powerRadioButton.isEnabled() if pwr and not cur: lpr = self._laser_logic.laser_power_range self._mw.setValueDoubleSpinBox.setRange(lpr[0], lpr[1]) self._mw.setValueDoubleSpinBox.setValue( self._laser_logic._laser.get_power_setpoint()) self._mw.setValueVerticalSlider.setValue( self._laser_logic._laser.get_power_setpoint() / (lpr[1] - lpr[0]) * 100 - lpr[0]) self.sigCtrlMode.emit(ControlMode.POWER) elif cur and not pwr: lcr = self._laser_logic.laser_current_range self._mw.setValueDoubleSpinBox.setRange(lcr[0], lcr[1]) self._mw.setValueDoubleSpinBox.setValue( self._laser_logic._laser.get_current_setpoint()) self._mw.setValueVerticalSlider.setValue( self._laser_logic._laser.get_current_setpoint() / (lcr[1] - lcr[0]) * 100 - lcr[0]) self.sigCtrlMode.emit(ControlMode.CURRENT) else: self.log.error('Nope.') @QtCore.Slot() def updateButtonsEnabled(self): """ """ self._mw.laserButton.setEnabled(self._laser_logic.laser_can_turn_on) if self._laser_logic.laser_state == LaserState.ON: self._mw.laserButton.setText('Laser: ON') self._mw.laserButton.setStyleSheet('') elif self._laser_logic.laser_state == LaserState.OFF: self._mw.laserButton.setText('Laser: OFF') elif self._laser_logic.laser_state == LaserState.LOCKED: self._mw.laserButton.setText('INTERLOCK') else: self._mw.laserButton.setText('Laser: ?') self._mw.shutterButton.setEnabled(self._laser_logic.has_shutter) if self._laser_logic.laser_shutter == ShutterState.OPEN: self._mw.shutterButton.setText('Shutter: OPEN') elif self._laser_logic.laser_shutter == ShutterState.CLOSED: self._mw.shutterButton.setText('Shutter: CLOSED') elif self._laser_logic.laser_shutter == ShutterState.NOSHUTTER: self._mw.shutterButton.setText('No shutter.') else: self._mw.laserButton.setText('Shutter: ?') self._mw.currentRadioButton.setEnabled( self._laser_logic.laser_can_current) self._mw.powerRadioButton.setEnabled(self._laser_logic.laser_can_power) @QtCore.Slot() def updateGui(self): """ """ self._mw.currentLabel.setText('{0:6.3f} {1}'.format( self._laser_logic.laser_current, self._laser_logic.laser_current_unit)) self._mw.powerLabel.setText('{0:6.3f} W'.format( self._laser_logic.laser_power)) self._mw.extraLabel.setText(self._laser_logic.laser_extra) self.updateButtonsEnabled() for k in self.plots: self.plots[k].setData(x=self._laser_logic.data['time'], y=self._laser_logic.data[k]) @QtCore.Slot() def updateFromSpinBox(self): """ """ self._mw.setValueVerticalSlider.setValue( self._mw.setValueDoubleSpinBox.value()) cur = self._mw.currentRadioButton.isChecked( ) and self._mw.currentRadioButton.isEnabled() pwr = self._mw.powerRadioButton.isChecked( ) and self._mw.powerRadioButton.isEnabled() if pwr and not cur: self.sigPower.emit(self._mw.setValueDoubleSpinBox.value()) elif cur and not pwr: self.sigCurrent.emit(self._mw.setValueDoubleSpinBox.value()) @QtCore.Slot() def updateFromSlider(self): """ """ cur = self._mw.currentRadioButton.isChecked( ) and self._mw.currentRadioButton.isEnabled() pwr = self._mw.powerRadioButton.isChecked( ) and self._mw.powerRadioButton.isEnabled() if pwr and not cur: lpr = self._laser_logic.laser_power_range self._mw.setValueDoubleSpinBox.setValue( lpr[0] + self._mw.setValueVerticalSlider.value() / 100 * (lpr[1] - lpr[0])) self.sigPower.emit(lpr[0] + self._mw.setValueVerticalSlider.value() / 100 * (lpr[1] - lpr[0])) elif cur and not pwr: self._mw.setValueDoubleSpinBox.setValue( self._mw.setValueVerticalSlider.value()) self.sigCurrent.emit(self._mw.setValueDoubleSpinBox.value())
class PIDLogic(GenericLogic): """ Control a process via software PID. """ _modclass = 'pidlogic' _modtype = 'logic' ## declare connectors _connectors = { 'controller': 'PIDControllerInterface', 'savelogic': 'SaveLogic' } sigUpdateDisplay = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.info('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.info('{0}: {1}'.format(key, config[key])) #number of lines in the matrix plot self.NumberOfSecondsLog = 100 self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ self._controller = self.get_connector('controller') self._save_logic = self.get_connector('savelogic') config = self.getConfiguration() # load parameters stored in app state store if 'bufferlength' in self._statusVariables: self.bufferLength = self._statusVariables['bufferlength'] else: self.bufferLength = 1000 if 'timestep' in self._statusVariables: self.timestep = self._statusVariables['timestep'] else: self.timestep = 100 self.history = np.zeros([3, self.bufferLength]) self.savingState = False self.enabled = False self.timer = QtCore.QTimer() self.timer.setSingleShot(True) self.timer.setInterval(self.timestep) self.timer.timeout.connect(self.loop) def on_deactivate(self): """ Perform required deactivation. """ # save parameters stored in ap state store self._statusVariables['bufferlength'] = self.bufferLength self._statusVariables['timestep'] = self.timestep def getBufferLength(self): """ Get the current data buffer length. """ return self.bufferLength def startLoop(self): """ Start the data recording loop. """ self.enabled = True self.timer.start(self.timestep) def stopLoop(self): """ Stop the data recording loop. """ self.enabled = False def loop(self): """ Execute step in the data recording loop: save one of each control and process values """ self.history = np.roll(self.history, -1, axis=1) self.history[0, -1] = self._controller.get_process_value() self.history[1, -1] = self._controller.get_control_value() self.history[2, -1] = self._controller.get_setpoint() self.sigUpdateDisplay.emit() if self.enabled: self.timer.start(self.timestep) def getSavingState(self): """ Return whether we are saving data @return bool: whether we are saving data right now """ return self.savingState def startSaving(self): """ Start saving data. Function does nothing right now. """ pass def saveData(self): """ Stop saving data and write data to file. Function does nothing right now. """ pass def setBufferLength(self, newBufferLength): """ Change buffer length to new value. @param int newBufferLength: new buffer length """ self.bufferLength = newBufferLength self.history = np.zeros([3, self.bufferLength]) def get_kp(self): """ Return the proportional constant. @return float: proportional constant of PID controller """ return self._controller.get_kp() def set_kp(self, kp): """ Set the proportional constant of the PID controller. @prarm float kp: proportional constant of PID controller """ return self._controller.set_kp(kp) def get_ki(self): """ Get the integration constant of the PID controller @return float: integration constant of the PID controller """ return self._controller.get_ki() def set_ki(self, ki): """ Set the integration constant of the PID controller. @param float ki: integration constant of the PID controller """ return self._controller.set_ki(ki) def get_kd(self): """ Get the derivative constant of the PID controller @return float: the derivative constant of the PID controller """ return self._controller.get_kd() def set_kd(self, kd): """ Set the derivative constant of the PID controller @param float kd: the derivative constant of the PID controller """ return self._controller.set_kd(kd) def get_setpoint(self): """ Get the current setpoint of the PID controller. @return float: current set point of the PID controller """ return self.history[2, -1] def set_setpoint(self, setpoint): """ Set the current setpoint of the PID controller. @param float setpoint: new set point of the PID controller """ self._controller.set_setpoint(setpoint) def get_manual_value(self): """ Return the control value for manual mode. @return float: control value for manual mode """ return self._controller.get_manual_value() def set_manual_value(self, manualvalue): """ Set the control value for manual mode. @param float manualvalue: control value for manual mode of controller """ return self._controller.set_manual_value(manualvalue) def get_enabled(self): """ See if the PID controller is controlling a process. @return bool: whether the PID controller is preparing to or conreolling a process """ return self.enabled def set_enabled(self, enabled): """ Set the state of the PID controller. @param bool enabled: desired state of PID controller """ if enabled and not self.enabled: self.startLoop() if not enabled and self.enabled: self.stopLoop() def get_control_limits(self): """ Get the minimum and maximum value of the control actuator. @return list(float): (minimum, maximum) values of the control actuator """ return self._controller.get_control_limits() def set_control_limits(self, limits): """ Set the minimum and maximum value of the control actuator. @param list(float) limits: (minimum, maximum) values of the control actuator This function does nothing, control limits are handled by the control module """ return self._controller.set_control_limits(limits) def get_pv(self): """ Get current process input value. @return float: current process input value """ return self.history[0, -1] def get_cv(self): """ Get current control output value. @return float: control output value """ return self.history[1, -1]
class PropertyEditor( QtWidgets.QFrame ): propertyChanged = QtCore.Signal( object, str, object ) objectChanged = QtCore.Signal( object ) contextMenuRequested = QtCore.Signal( object, str ) _fieldEditorCacheWidget = None _fieldEditorCache = {} def __init__( self, parent ): super( PropertyEditor, self ).__init__( parent ) if not PropertyEditor._fieldEditorCacheWidget: PropertyEditor._fieldEditorCacheWidget = QtWidgets.QWidget() layout = QtWidgets.QFormLayout( ) self.setLayout( layout ) self._layout = layout self._layout.setHorizontalSpacing( 4 ) self._layout.setVerticalSpacing( 2 ) self._layout.setContentsMargins( 4 , 4 , 4 , 4 ) self._layout.setLabelAlignment( Qt.AlignLeft ) self._layout.setFieldGrowthPolicy( QtWidgets.QFormLayout.ExpandingFieldsGrow ) self.setSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding ) self.editors = {} self.target = None self.refreshing = False self.context = None self.model = False self.readonly = False self.clear() def addFieldEditor( self, field ): editor = buildFieldEditor( self, field ) if not editor: logging.info( 'no field editor for:' + str(field.label) ) return None label = field.label labelWidget = editor.initLabel( label, self ) editorWidget = editor.initEditor( self ) if labelWidget in (None, False): self._layout.addRow ( editorWidget ) else: labelWidget.setObjectName( 'FieldLabel' ) self._layout.addRow ( labelWidget, editorWidget ) if editorWidget: editorWidget.setObjectName( 'FieldEditor' ) self.editors[ field ] = editor editor.postInit( self ) return editor def getFieldEditor( self, field ): return self.editors.get( field, None ) def addSeparator( self ): line = QtWidgets.QFrame( self ) line.setSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed ) # line.setStyleSheet('background:none; border:none; ') line.setStyleSheet('background:none; border-top:1px solid #353535; margin: 2px 0 2px 0;') line.setMinimumSize( 30, 7 ) self._layout.addRow( line ) def clear( self ): for editor in self.editors.values(): editor.clear() layout = self._layout while layout.count() > 0: child = layout.takeAt( 0 ) if child : w = child.widget() if w: w.setParent( None ) else: break self.editors.clear() self.target = None def setContext( self, context ): self.context = context def onObjectChanged( self ): if self.refreshing: return self.objectChanged.emit( self.target ) return self.refreshAll() def onPropertyChanged( self, field, value ): if self.refreshing : return self.model.setFieldValue( self.target, field.id, value ) self.propertyChanged.emit( self.target, field.id, value ) self.objectChanged.emit( self.target ) def onContextMenuRequested( self, field ): self.contextMenuRequested.emit( self.target, field.id ) def setReadonly( self, readonly = True ): self.readonly = readonly self.refreshAll() def isReadonly( self ): return self.readonly def getTarget( self ): return self.target def setTarget( self, target, **kwargs ): oldtarget = self.target self.hide() model = kwargs.get( 'model', None ) if not model: model = ModelManager.get().getModel(target) if not model: self.model = None self.clear() return rebuildFields = model != self.model assert(model) wasSeparator = False if rebuildFields: self.clear() self.refreshing = True #install field info currentId = None for field in model.fieldList: currentId = field.id if field.getOption('no_edit'): if field.id == '----' and not wasSeparator: wasSeparator = True self.addSeparator() continue lastId = currentId self.addFieldEditor( field ) wasSeparator = False assert self.refreshing self.refreshing = False self.model = model self.target = target self.refreshAll() self.show() def refreshFor( self, target ): if target == self.target: return self.refreshAll() def refreshAll( self ): target=self.target if not target: return for field in self.model.fieldList: #todo: just use propMap to iter? self._refreshField( field ) def refreshField( self, fieldId ): for field in self.model.fieldList: #todo: just use propMap to iter? if field.id == fieldId: self._refreshField( field ) return True return False def _refreshField( self, field ): target = self.target if not target: return editor = self.editors.get( field, None ) if editor: v = self.model.getFieldValue( target, field.id ) self.refreshing = True #avoid duplicated update editor.refreshing = True editor.refreshState() editor.set( v ) editor.refreshing = False self.refreshing = False editor.setOverrided( self.model.isFieldOverrided( target, field.id ) ) def refershFieldState( self, fieldId ): target = self.target if not target: return for field in self.model.fieldList: #todo: just use propMap to iter? if field.id == fieldId: editor = self.editors.get( field, None ) if not editor: return editor.setOverrided( self.model.isFieldOverrided( target, field.id ) )
class PulsedMasterLogic(GenericLogic): """ This logic module combines the functionality of two modules. It can be used to generate pulse sequences/waveforms and to control the settings for the pulse generator via SequenceGeneratorLogic. Essentially this part controls what is played on the pulse generator. Furthermore it can be used to set up a pulsed measurement with an already set-up pulse generator together with a fast counting device via PulsedMeasurementLogic. The main purpose for this module is to provide a single interface while maintaining a modular structure for complex pulsed measurements. Each of the sub-modules can be used without this module but more care has to be taken in that case. Automatic transfer of information from one sub-module to the other for convenience is also handled here. Another important aspect is the use of this module in scripts (e.g. jupyter notebooks). All calls to sub-module setter functions (PulsedMeasurementLogic and SequenceGeneratorLogic) are decoupled from the calling thread via Qt queued connections. This ensures a more intuitive and less error prone use of scripting. """ _modclass = 'pulsedmasterlogic' _modtype = 'logic' # declare connectors pulsedmeasurementlogic = Connector(interface='PulsedMeasurementLogic') sequencegeneratorlogic = Connector(interface='SequenceGeneratorLogic') # PulsedMeasurementLogic control signals sigDoFit = QtCore.Signal(str) sigToggleMeasurement = QtCore.Signal(bool, str) sigToggleMeasurementPause = QtCore.Signal(bool) sigTogglePulser = QtCore.Signal(bool) sigToggleExtMicrowave = QtCore.Signal(bool) sigFastCounterSettingsChanged = QtCore.Signal(dict) sigMeasurementSettingsChanged = QtCore.Signal(dict) sigExtMicrowaveSettingsChanged = QtCore.Signal(dict) sigAnalysisSettingsChanged = QtCore.Signal(dict) sigExtractionSettingsChanged = QtCore.Signal(dict) sigTimerIntervalChanged = QtCore.Signal(float) sigAlternativeDataTypeChanged = QtCore.Signal(str) sigManuallyPullData = QtCore.Signal() # signals for master module (i.e. GUI) coming from PulsedMeasurementLogic sigMeasurementDataUpdated = QtCore.Signal() sigTimerUpdated = QtCore.Signal(float, int, float) sigFitUpdated = QtCore.Signal(str, np.ndarray, object) sigMeasurementStatusUpdated = QtCore.Signal(bool, bool) sigPulserRunningUpdated = QtCore.Signal(bool) sigExtMicrowaveRunningUpdated = QtCore.Signal(bool) sigExtMicrowaveSettingsUpdated = QtCore.Signal(dict) sigFastCounterSettingsUpdated = QtCore.Signal(dict) sigMeasurementSettingsUpdated = QtCore.Signal(dict) sigAnalysisSettingsUpdated = QtCore.Signal(dict) sigExtractionSettingsUpdated = QtCore.Signal(dict) # SequenceGeneratorLogic control signals sigSavePulseBlock = QtCore.Signal(object) sigSaveBlockEnsemble = QtCore.Signal(object) sigSaveSequence = QtCore.Signal(object) sigDeletePulseBlock = QtCore.Signal(str) sigDeleteBlockEnsemble = QtCore.Signal(str) sigDeleteSequence = QtCore.Signal(str) sigLoadBlockEnsemble = QtCore.Signal(str) sigLoadSequence = QtCore.Signal(str) sigSampleBlockEnsemble = QtCore.Signal(str) sigSampleSequence = QtCore.Signal(str) sigClearPulseGenerator = QtCore.Signal() sigGeneratorSettingsChanged = QtCore.Signal(dict) sigSamplingSettingsChanged = QtCore.Signal(dict) sigGeneratePredefinedSequence = QtCore.Signal(str, dict) # signals for master module (i.e. GUI) coming from SequenceGeneratorLogic sigBlockDictUpdated = QtCore.Signal(dict) sigEnsembleDictUpdated = QtCore.Signal(dict) sigSequenceDictUpdated = QtCore.Signal(dict) sigAvailableWaveformsUpdated = QtCore.Signal(list) sigAvailableSequencesUpdated = QtCore.Signal(list) sigSampleEnsembleComplete = QtCore.Signal(object) sigSampleSequenceComplete = QtCore.Signal(object) sigLoadedAssetUpdated = QtCore.Signal(str, str) sigGeneratorSettingsUpdated = QtCore.Signal(dict) sigSamplingSettingsUpdated = QtCore.Signal(dict) sigPredefinedSequenceGenerated = QtCore.Signal(object, bool) def __init__(self, config, **kwargs): """ Create PulsedMasterLogic object with connectors. @param dict kwargs: optional parameters """ super().__init__(config=config, **kwargs) # Dictionary servings as status register self.status_dict = dict() return def on_activate(self): """ Initialisation performed during activation of the module. """ # Initialize status register self.status_dict = { 'sampling_ensemble_busy': False, 'sampling_sequence_busy': False, 'sampload_busy': False, 'loading_busy': False, 'pulser_running': False, 'measurement_running': False, 'microwave_running': False, 'predefined_generation_busy': False, 'fitting_busy': False } # Connect signals controlling PulsedMeasurementLogic self.sigDoFit.connect(self.pulsedmeasurementlogic().do_fit, QtCore.Qt.QueuedConnection) self.sigToggleMeasurement.connect( self.pulsedmeasurementlogic().toggle_pulsed_measurement, QtCore.Qt.QueuedConnection) self.sigToggleMeasurementPause.connect( self.pulsedmeasurementlogic().toggle_measurement_pause, QtCore.Qt.QueuedConnection) self.sigTogglePulser.connect( self.pulsedmeasurementlogic().toggle_pulse_generator, QtCore.Qt.QueuedConnection) self.sigToggleExtMicrowave.connect( self.pulsedmeasurementlogic().toggle_microwave, QtCore.Qt.QueuedConnection) self.sigFastCounterSettingsChanged.connect( self.pulsedmeasurementlogic().set_fast_counter_settings, QtCore.Qt.QueuedConnection) self.sigMeasurementSettingsChanged.connect( self.pulsedmeasurementlogic().set_measurement_settings, QtCore.Qt.QueuedConnection) self.sigExtMicrowaveSettingsChanged.connect( self.pulsedmeasurementlogic().set_microwave_settings, QtCore.Qt.QueuedConnection) self.sigAnalysisSettingsChanged.connect( self.pulsedmeasurementlogic().set_analysis_settings, QtCore.Qt.QueuedConnection) self.sigExtractionSettingsChanged.connect( self.pulsedmeasurementlogic().set_extraction_settings, QtCore.Qt.QueuedConnection) self.sigTimerIntervalChanged.connect( self.pulsedmeasurementlogic().set_timer_interval, QtCore.Qt.QueuedConnection) self.sigAlternativeDataTypeChanged.connect( self.pulsedmeasurementlogic().set_alternative_data_type, QtCore.Qt.QueuedConnection) self.sigManuallyPullData.connect( self.pulsedmeasurementlogic().manually_pull_data, QtCore.Qt.QueuedConnection) # Connect signals coming from PulsedMeasurementLogic self.pulsedmeasurementlogic().sigMeasurementDataUpdated.connect( self.sigMeasurementDataUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigTimerUpdated.connect( self.sigTimerUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigFitUpdated.connect( self.fit_updated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigMeasurementStatusUpdated.connect( self.measurement_status_updated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigPulserRunningUpdated.connect( self.pulser_running_updated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigExtMicrowaveRunningUpdated.connect( self.ext_microwave_running_updated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigExtMicrowaveSettingsUpdated.connect( self.sigExtMicrowaveSettingsUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigFastCounterSettingsUpdated.connect( self.sigFastCounterSettingsUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigMeasurementSettingsUpdated.connect( self.sigMeasurementSettingsUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigAnalysisSettingsUpdated.connect( self.sigAnalysisSettingsUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigExtractionSettingsUpdated.connect( self.sigExtractionSettingsUpdated, QtCore.Qt.QueuedConnection) # Connect signals controlling SequenceGeneratorLogic self.sigSavePulseBlock.connect( self.sequencegeneratorlogic().save_block, QtCore.Qt.QueuedConnection) self.sigSaveBlockEnsemble.connect( self.sequencegeneratorlogic().save_ensemble, QtCore.Qt.QueuedConnection) self.sigSaveSequence.connect( self.sequencegeneratorlogic().save_sequence, QtCore.Qt.QueuedConnection) self.sigDeletePulseBlock.connect( self.sequencegeneratorlogic().delete_block, QtCore.Qt.QueuedConnection) self.sigDeleteBlockEnsemble.connect( self.sequencegeneratorlogic().delete_ensemble, QtCore.Qt.QueuedConnection) self.sigDeleteSequence.connect( self.sequencegeneratorlogic().delete_sequence, QtCore.Qt.QueuedConnection) self.sigLoadBlockEnsemble.connect( self.sequencegeneratorlogic().load_ensemble, QtCore.Qt.QueuedConnection) self.sigLoadSequence.connect( self.sequencegeneratorlogic().load_sequence, QtCore.Qt.QueuedConnection) self.sigSampleBlockEnsemble.connect( self.sequencegeneratorlogic().sample_pulse_block_ensemble, QtCore.Qt.QueuedConnection) self.sigSampleSequence.connect( self.sequencegeneratorlogic().sample_pulse_sequence, QtCore.Qt.QueuedConnection) self.sigClearPulseGenerator.connect( self.sequencegeneratorlogic().clear_pulser, QtCore.Qt.QueuedConnection) self.sigGeneratorSettingsChanged.connect( self.sequencegeneratorlogic().set_pulse_generator_settings, QtCore.Qt.QueuedConnection) self.sigSamplingSettingsChanged.connect( self.sequencegeneratorlogic().set_generation_parameters, QtCore.Qt.QueuedConnection) self.sigGeneratePredefinedSequence.connect( self.sequencegeneratorlogic().generate_predefined_sequence, QtCore.Qt.QueuedConnection) # Connect signals coming from SequenceGeneratorLogic self.sequencegeneratorlogic().sigBlockDictUpdated.connect( self.sigBlockDictUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigEnsembleDictUpdated.connect( self.sigEnsembleDictUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigSequenceDictUpdated.connect( self.sigSequenceDictUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigAvailableWaveformsUpdated.connect( self.sigAvailableWaveformsUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigAvailableSequencesUpdated.connect( self.sigAvailableSequencesUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigGeneratorSettingsUpdated.connect( self.sigGeneratorSettingsUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigSamplingSettingsUpdated.connect( self.sigSamplingSettingsUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigPredefinedSequenceGenerated.connect( self.predefined_sequence_generated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigSampleEnsembleComplete.connect( self.sample_ensemble_finished, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigSampleSequenceComplete.connect( self.sample_sequence_finished, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigLoadedAssetUpdated.connect( self.loaded_asset_updated, QtCore.Qt.QueuedConnection) return def on_deactivate(self): """ @return: """ # Disconnect all signals # Disconnect signals controlling PulsedMeasurementLogic self.sigDoFit.disconnect() self.sigToggleMeasurement.disconnect() self.sigToggleMeasurementPause.disconnect() self.sigTogglePulser.disconnect() self.sigToggleExtMicrowave.disconnect() self.sigFastCounterSettingsChanged.disconnect() self.sigMeasurementSettingsChanged.disconnect() self.sigExtMicrowaveSettingsChanged.disconnect() self.sigAnalysisSettingsChanged.disconnect() self.sigExtractionSettingsChanged.disconnect() self.sigTimerIntervalChanged.disconnect() self.sigAlternativeDataTypeChanged.disconnect() self.sigManuallyPullData.disconnect() # Disconnect signals coming from PulsedMeasurementLogic self.pulsedmeasurementlogic().sigMeasurementDataUpdated.disconnect() self.pulsedmeasurementlogic().sigTimerUpdated.disconnect() self.pulsedmeasurementlogic().sigFitUpdated.disconnect() self.pulsedmeasurementlogic().sigMeasurementStatusUpdated.disconnect() self.pulsedmeasurementlogic().sigPulserRunningUpdated.disconnect() self.pulsedmeasurementlogic().sigExtMicrowaveRunningUpdated.disconnect( ) self.pulsedmeasurementlogic( ).sigExtMicrowaveSettingsUpdated.disconnect() self.pulsedmeasurementlogic().sigFastCounterSettingsUpdated.disconnect( ) self.pulsedmeasurementlogic().sigMeasurementSettingsUpdated.disconnect( ) self.pulsedmeasurementlogic().sigAnalysisSettingsUpdated.disconnect() self.pulsedmeasurementlogic().sigExtractionSettingsUpdated.disconnect() # Disconnect signals controlling SequenceGeneratorLogic self.sigSavePulseBlock.disconnect() self.sigSaveBlockEnsemble.disconnect() self.sigSaveSequence.disconnect() self.sigDeletePulseBlock.disconnect() self.sigDeleteBlockEnsemble.disconnect() self.sigDeleteSequence.disconnect() self.sigLoadBlockEnsemble.disconnect() self.sigLoadSequence.disconnect() self.sigSampleBlockEnsemble.disconnect() self.sigSampleSequence.disconnect() self.sigClearPulseGenerator.disconnect() self.sigGeneratorSettingsChanged.disconnect() self.sigSamplingSettingsChanged.disconnect() self.sigGeneratePredefinedSequence.disconnect() # Disconnect signals coming from SequenceGeneratorLogic self.sequencegeneratorlogic().sigBlockDictUpdated.disconnect() self.sequencegeneratorlogic().sigEnsembleDictUpdated.disconnect() self.sequencegeneratorlogic().sigSequenceDictUpdated.disconnect() self.sequencegeneratorlogic().sigAvailableWaveformsUpdated.disconnect() self.sequencegeneratorlogic().sigAvailableSequencesUpdated.disconnect() self.sequencegeneratorlogic().sigGeneratorSettingsUpdated.disconnect() self.sequencegeneratorlogic().sigSamplingSettingsUpdated.disconnect() self.sequencegeneratorlogic( ).sigPredefinedSequenceGenerated.disconnect() self.sequencegeneratorlogic().sigSampleEnsembleComplete.disconnect() self.sequencegeneratorlogic().sigSampleSequenceComplete.disconnect() self.sequencegeneratorlogic().sigLoadedAssetUpdated.disconnect() return ####################################################################### ### Pulsed measurement properties ### ####################################################################### @property def fast_counter_constraints(self): return self.pulsedmeasurementlogic().fast_counter_constraints @property def fast_counter_settings(self): return self.pulsedmeasurementlogic().fast_counter_settings @property def ext_microwave_constraints(self): return self.pulsedmeasurementlogic().ext_microwave_constraints @property def ext_microwave_settings(self): return self.pulsedmeasurementlogic().ext_microwave_settings @property def measurement_settings(self): return self.pulsedmeasurementlogic().measurement_settings @property def timer_interval(self): return self.pulsedmeasurementlogic().timer_interval @property def analysis_methods(self): return self.pulsedmeasurementlogic().analysis_methods @property def extraction_methods(self): return self.pulsedmeasurementlogic().extraction_methods @property def analysis_settings(self): return self.pulsedmeasurementlogic().analysis_settings @property def extraction_settings(self): return self.pulsedmeasurementlogic().extraction_settings @property def signal_data(self): return self.pulsedmeasurementlogic().signal_data @property def signal_alt_data(self): return self.pulsedmeasurementlogic().signal_alt_data @property def measurement_error(self): return self.pulsedmeasurementlogic().measurement_error @property def raw_data(self): return self.pulsedmeasurementlogic().raw_data @property def laser_data(self): return self.pulsedmeasurementlogic().laser_data @property def alternative_data_type(self): return self.pulsedmeasurementlogic().alternative_data_type @property def fit_container(self): return self.pulsedmeasurementlogic().fc ####################################################################### ### Pulsed measurement methods ### ####################################################################### @QtCore.Slot(dict) def set_measurement_settings(self, settings_dict=None, **kwargs): """ @param settings_dict: @param kwargs: """ if isinstance(settings_dict, dict): self.sigMeasurementSettingsChanged.emit(settings_dict) else: self.sigMeasurementSettingsChanged.emit(kwargs) return @QtCore.Slot(dict) def set_fast_counter_settings(self, settings_dict=None, **kwargs): """ @param settings_dict: @param kwargs: """ if isinstance(settings_dict, dict): self.sigFastCounterSettingsChanged.emit(settings_dict) else: self.sigFastCounterSettingsChanged.emit(kwargs) return @QtCore.Slot(dict) def set_ext_microwave_settings(self, settings_dict=None, **kwargs): """ @param settings_dict: @param kwargs: """ if isinstance(settings_dict, dict): self.sigExtMicrowaveSettingsChanged.emit(settings_dict) else: self.sigExtMicrowaveSettingsChanged.emit(kwargs) return @QtCore.Slot(dict) def set_analysis_settings(self, settings_dict=None, **kwargs): """ @param settings_dict: @param kwargs: """ if isinstance(settings_dict, dict): self.sigAnalysisSettingsChanged.emit(settings_dict) else: self.sigAnalysisSettingsChanged.emit(kwargs) return @QtCore.Slot(dict) def set_extraction_settings(self, settings_dict=None, **kwargs): """ @param settings_dict: @param kwargs: """ if isinstance(settings_dict, dict): self.sigExtractionSettingsChanged.emit(settings_dict) else: self.sigExtractionSettingsChanged.emit(kwargs) return @QtCore.Slot(int) @QtCore.Slot(float) def set_timer_interval(self, interval): """ @param int|float interval: The timer interval to set in seconds. """ if isinstance(interval, (int, float)): self.sigTimerIntervalChanged.emit(interval) return @QtCore.Slot(str) def set_alternative_data_type(self, alt_data_type): """ @param alt_data_type: @return: """ if isinstance(alt_data_type, str): self.sigAlternativeDataTypeChanged.emit(alt_data_type) return @QtCore.Slot() def manually_pull_data(self): """ """ self.sigManuallyPullData.emit() return @QtCore.Slot(bool) def toggle_ext_microwave(self, switch_on): """ @param switch_on: """ if isinstance(switch_on, bool): self.sigToggleExtMicrowave.emit(switch_on) return @QtCore.Slot(bool) def ext_microwave_running_updated(self, is_running): """ @param is_running: """ if isinstance(is_running, bool): self.status_dict['microwave_running'] = is_running self.sigExtMicrowaveRunningUpdated.emit(is_running) return @QtCore.Slot(bool) def toggle_pulse_generator(self, switch_on): """ @param switch_on: """ if isinstance(switch_on, bool): self.sigTogglePulser.emit(switch_on) return @QtCore.Slot(bool) def pulser_running_updated(self, is_running): """ @param is_running: """ if isinstance(is_running, bool): self.status_dict['pulser_running'] = is_running self.sigPulserRunningUpdated.emit(is_running) return @QtCore.Slot(bool) @QtCore.Slot(bool, str) def toggle_pulsed_measurement(self, start, stash_raw_data_tag=''): """ @param bool start: @param str stash_raw_data_tag: """ if isinstance(start, bool) and isinstance(stash_raw_data_tag, str): self.sigToggleMeasurement.emit(start, stash_raw_data_tag) return @QtCore.Slot(bool) def toggle_pulsed_measurement_pause(self, pause): """ @param pause: """ if isinstance(pause, bool): self.sigToggleMeasurementPause.emit(pause) return @QtCore.Slot(bool, bool) def measurement_status_updated(self, is_running, is_paused): """ @param is_running: @param is_paused: """ if isinstance(is_running, bool) and isinstance(is_paused, bool): self.status_dict['measurement_running'] = is_running self.sigMeasurementStatusUpdated.emit(is_running, is_paused) return @QtCore.Slot(str) def do_fit(self, fit_function): """ @param fit_function: """ if isinstance(fit_function, str): self.status_dict['fitting_busy'] = True self.sigDoFit.emit(fit_function) return @QtCore.Slot(str, np.ndarray, object) def fit_updated(self, fit_name, fit_data, fit_result): """ @return: """ self.status_dict['fitting_busy'] = False self.sigFitUpdated.emit(fit_name, fit_data, fit_result) return def save_measurement_data(self, tag, with_error): """ Prepare data to be saved and create a proper plot of the data. This is just handed over to the measurement logic. @param str tag: a filetag which will be included in the filename @param bool with_error: select whether errors should be saved/plotted """ self.pulsedmeasurementlogic().save_measurement_data(tag, with_error) return ####################################################################### ### Sequence generator properties ### ####################################################################### @property def pulse_generator_constraints(self): return self.sequencegeneratorlogic().pulse_generator_constraints @property def pulse_generator_settings(self): return self.sequencegeneratorlogic().pulse_generator_settings @property def generation_parameters(self): return self.sequencegeneratorlogic().generation_parameters @property def analog_channels(self): return self.sequencegeneratorlogic().analog_channels @property def digital_channels(self): return self.sequencegeneratorlogic().digital_channels @property def saved_pulse_blocks(self): return self.sequencegeneratorlogic().saved_pulse_blocks @property def saved_pulse_block_ensembles(self): return self.sequencegeneratorlogic().saved_pulse_block_ensembles @property def saved_pulse_sequences(self): return self.sequencegeneratorlogic().saved_pulse_sequences @property def sampled_waveforms(self): return self.sequencegeneratorlogic().sampled_waveforms @property def sampled_sequences(self): return self.sequencegeneratorlogic().sampled_sequences @property def loaded_asset(self): return self.sequencegeneratorlogic().loaded_asset @property def generate_methods(self): return self.sequencegeneratorlogic().generate_methods @property def generate_method_params(self): return self.sequencegeneratorlogic().generate_method_params ####################################################################### ### Sequence generator methods ### ####################################################################### @QtCore.Slot() def clear_pulse_generator(self): still_busy = self.status_dict[ 'sampling_ensemble_busy'] or self.status_dict[ 'sampling_sequence_busy'] or self.status_dict[ 'loading_busy'] or self.status_dict['sampload_busy'] if still_busy: self.log.error( 'Can not clear pulse generator. Sampling/Loading still in progress.' ) else: self.sigClearPulseGenerator.emit() return @QtCore.Slot(str) @QtCore.Slot(str, bool) def sample_ensemble(self, ensemble_name, with_load=False): already_busy = self.status_dict[ 'sampling_ensemble_busy'] or self.status_dict[ 'sampling_sequence_busy'] or self.sequencegeneratorlogic( ).module_state() == 'locked' if already_busy: self.log.error( 'Sampling of a different asset already in progress.\n' 'PulseBlockEnsemble "{0}" not sampled!'.format(ensemble_name)) else: if with_load: self.status_dict['sampload_busy'] = True self.status_dict['sampling_ensemble_busy'] = True self.sigSampleBlockEnsemble.emit(ensemble_name) return @QtCore.Slot(object) def sample_ensemble_finished(self, ensemble): self.status_dict['sampling_ensemble_busy'] = False self.sigSampleEnsembleComplete.emit(ensemble) if self.status_dict['sampload_busy'] and not self.status_dict[ 'sampling_sequence_busy']: if ensemble is None: self.status_dict['sampload_busy'] = False self.sigLoadedAssetUpdated.emit(*self.loaded_asset) else: self.load_ensemble(ensemble.name) return @QtCore.Slot(str) @QtCore.Slot(str, bool) def sample_sequence(self, sequence_name, with_load=False): already_busy = self.status_dict[ 'sampling_ensemble_busy'] or self.status_dict[ 'sampling_sequence_busy'] or self.sequencegeneratorlogic( ).module_state() == 'locked' if already_busy: self.log.error( 'Sampling of a different asset already in progress.\n' 'PulseSequence "{0}" not sampled!'.format(sequence_name)) else: if with_load: self.status_dict['sampload_busy'] = True self.status_dict['sampling_sequence_busy'] = True self.sigSampleSequence.emit(sequence_name) return @QtCore.Slot(object) def sample_sequence_finished(self, sequence): self.status_dict['sampling_sequence_busy'] = False self.sigSampleSequenceComplete.emit(sequence) if self.status_dict['sampload_busy']: if sequence is None: self.status_dict['sampload_busy'] = False self.sigLoadedAssetUpdated.emit(*self.loaded_asset) else: self.load_sequence(sequence.name) return @QtCore.Slot(str) def load_ensemble(self, ensemble_name): if self.status_dict['loading_busy']: self.log.error( 'Loading of a different asset already in progress.\n' 'PulseBlockEnsemble "{0}" not loaded!'.format(ensemble_name)) else: self.status_dict['loading_busy'] = True self.sigLoadBlockEnsemble.emit(ensemble_name) return @QtCore.Slot(str) def load_sequence(self, sequence_name): if self.status_dict['loading_busy']: self.log.error( 'Loading of a different asset already in progress.\n' 'PulseSequence "{0}" not loaded!'.format(sequence_name)) else: self.status_dict['loading_busy'] = True self.sigLoadSequence.emit(sequence_name) return @QtCore.Slot(str, str) def loaded_asset_updated(self, asset_name, asset_type): """ @param asset_name: @param asset_type: @return: """ self.status_dict['sampload_busy'] = False self.status_dict['loading_busy'] = False self.sigLoadedAssetUpdated.emit(asset_name, asset_type) # Transfer sequence information from PulseBlockEnsemble or PulseSequence to # PulsedMeasurementLogic to be able to invoke measurement settings from them if not asset_type: # If no asset loaded or asset type unknown, clear sequence_information dict object_instance = None elif asset_type == 'PulseBlockEnsemble': object_instance = self.saved_pulse_block_ensembles.get(asset_name) elif asset_type == 'PulseSequence': object_instance = self.saved_pulse_sequences.get(asset_name) else: object_instance = None if object_instance is None: self.pulsedmeasurementlogic().sampling_information = dict() self.pulsedmeasurementlogic().measurement_information = dict() else: self.pulsedmeasurementlogic( ).sampling_information = object_instance.sampling_information self.pulsedmeasurementlogic( ).measurement_information = object_instance.measurement_information return @QtCore.Slot(object) def save_pulse_block(self, block_instance): """ @param block_instance: @return: """ self.sigSavePulseBlock.emit(block_instance) return @QtCore.Slot(object) def save_block_ensemble(self, ensemble_instance): """ @param ensemble_instance: @return: """ self.sigSaveBlockEnsemble.emit(ensemble_instance) return @QtCore.Slot(object) def save_sequence(self, sequence_instance): """ @param sequence_instance: @return: """ self.sigSaveSequence.emit(sequence_instance) return @QtCore.Slot(str) def delete_pulse_block(self, block_name): """ @param block_name: @return: """ self.sigDeletePulseBlock.emit(block_name) return @QtCore.Slot(str) def delete_block_ensemble(self, ensemble_name): """ @param ensemble_name: @return: """ self.sigDeleteBlockEnsemble.emit(ensemble_name) return @QtCore.Slot(str) def delete_sequence(self, sequence_name): """ @param sequence_name: @return: """ self.sigDeleteSequence.emit(sequence_name) return @QtCore.Slot(dict) def set_pulse_generator_settings(self, settings_dict=None, **kwargs): """ Either accept a settings dictionary as positional argument or keyword arguments. If both are present both are being used by updating the settings_dict with kwargs. The keyword arguments take precedence over the items in settings_dict if there are conflicting names. @param settings_dict: @param kwargs: @return: """ if not isinstance(settings_dict, dict): settings_dict = kwargs else: settings_dict.update(kwargs) self.sigGeneratorSettingsChanged.emit(settings_dict) return @QtCore.Slot(dict) def set_generation_parameters(self, settings_dict=None, **kwargs): """ Either accept a settings dictionary as positional argument or keyword arguments. If both are present both are being used by updating the settings_dict with kwargs. The keyword arguments take precedence over the items in settings_dict if there are conflicting names. @param settings_dict: @param kwargs: @return: """ if not isinstance(settings_dict, dict): settings_dict = kwargs else: settings_dict.update(kwargs) # Force empty gate channel if fast counter is not gated if 'gate_channel' in settings_dict and not self.fast_counter_settings.get( 'is_gated'): settings_dict['gate_channel'] = '' self.sigSamplingSettingsChanged.emit(settings_dict) return @QtCore.Slot(str) @QtCore.Slot(str, dict) @QtCore.Slot(str, dict, bool) def generate_predefined_sequence(self, generator_method_name, kwarg_dict=None, sample_and_load=False): """ @param generator_method_name: @param kwarg_dict: @param sample_and_load: @return: """ if not isinstance(kwarg_dict, dict): kwarg_dict = dict() self.status_dict['predefined_generation_busy'] = True if sample_and_load: self.status_dict['sampload_busy'] = True self.sigGeneratePredefinedSequence.emit(generator_method_name, kwarg_dict) return @QtCore.Slot(object, bool) def predefined_sequence_generated(self, asset_name, is_sequence): self.status_dict['predefined_generation_busy'] = False if asset_name is None: self.status_dict['sampload_busy'] = False self.sigPredefinedSequenceGenerated.emit(asset_name, is_sequence) if self.status_dict['sampload_busy']: if is_sequence: self.sample_sequence(asset_name, True) else: self.sample_ensemble(asset_name, True) return def get_ensemble_info(self, ensemble): """ """ return self.sequencegeneratorlogic().get_ensemble_info( ensemble=ensemble) def get_sequence_info(self, sequence): """ """ return self.sequencegeneratorlogic().get_sequence_info( sequence=sequence)
class ResizingScrolledPanel(QW.QScrollArea): okSignal = QC.Signal() def __init__(self, parent): QW.QScrollArea.__init__(self, parent) self.setWidget(QW.QWidget(self)) self.setWidgetResizable(True) self.widget().installEventFilter(ResizingEventFilter(self)) def _OKParent(self): self.okSignal.emit() def CheckValid(self): pass def CleanBeforeDestroy(self): pass def sizeHint(self): if self.widget(): # just as a fun note, QScrollArea does a 12 x 8 character height sizeHint on its own here due to as-yet invalid widget size, wew lad frame_width = self.frameWidth() frame_size = QC.QSize(frame_width * 2, frame_width * 2) size_hint = self.widget().sizeHint() + frame_size #visible_size = self.widget().visibleRegion().boundingRect().size() #size_hint = self.widget().sizeHint() + self.size() - visible_size available_screen_size = QW.QApplication.desktop( ).availableGeometry(self).size() screen_fill_factor = 0.85 # don't let size hint be bigger than this percentage of the available screen width/height if size_hint.width( ) > screen_fill_factor * available_screen_size.width(): size_hint.setWidth(screen_fill_factor * available_screen_size.width()) if size_hint.height( ) > screen_fill_factor * available_screen_size.height(): size_hint.setHeight(screen_fill_factor * available_screen_size.height()) return size_hint else: return QW.QScrollArea.sizeHint(self) def UserIsOKToOK(self): return True def UserIsOKToCancel(self): return True def WidgetJustSized(self, width_larger, height_larger): widget_minimum_size_hint = self.widget().minimumSizeHint() widget_normal_size_hint = self.widget().sizeHint() widget_size_hint = QC.QSize( max(widget_minimum_size_hint.width(), widget_normal_size_hint.width()), max(widget_minimum_size_hint.height(), widget_normal_size_hint.height())) my_size = self.size() width_increase = 0 height_increase = 0 # + 2 because it is late and that seems to stop scrollbars lmao if width_larger: width_increase = max( 0, widget_size_hint.width() - my_size.width() + 2) if height_larger: height_increase = max( 0, widget_size_hint.height() - my_size.height() + 2) if width_increase > 0 or height_increase > 0: window = self.window() if isinstance(window, (ClientGUITopLevelWindows.DialogThatResizes, ClientGUITopLevelWindows.FrameThatResizes)): desired_size_delta = QC.QSize(width_increase, height_increase) ClientGUITopLevelWindows.ExpandTLWIfPossible( window, window._frame_key, desired_size_delta)
class ProgressBar(QtWidgets.QFrame): progress_signal = QtCore.Signal(int) stop_signal = QtCore.Signal() def __init__( self, app: QtWidgets.QApplication, tasks: List[Task], signal_task: bool = False, auto_run: bool = True, can_cancel: bool = False, ): super().__init__(None) self.app = app self.tasks = tasks self.signal_task = signal_task self.setObjectName("ProgressBar") self.setStyleSheet("#ProgressBar{border: 1px solid #aaa}") self.setMinimumWidth(400) self.setWindowFlags(QtCore.Qt.SplashScreen | QtCore.Qt.FramelessWindowHint) self.status = QtWidgets.QLabel() self.progress_bar = QtWidgets.QProgressBar(self) self.progress_bar.setGeometry(30, 40, 500, 75) self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.status) self.layout.addWidget(self.progress_bar) if can_cancel: cancel_button = QtWidgets.QPushButton(t("Cancel")) cancel_button.clicked.connect(self.cancel) self.layout.addWidget(cancel_button) self.setLayout(self.layout) self.show() if auto_run: self.run() def cancel(self): self.stop_signal.emit() self.close() @reusables.log_exception("fastflix") def run(self): ratio = 100 // len(self.tasks) self.progress_bar.setValue(0) if self.signal_task: self.status.setText(self.tasks[0].name) self.progress_signal.connect(self.update_progress) self.tasks[0].kwargs["signal"] = self.progress_signal self.tasks[0].kwargs["stop_signal"] = self.stop_signal self.tasks[0].command(config=self.app.fastflix.config, app=self.app, **self.tasks[0].kwargs) else: for i, task in enumerate(self.tasks, start=1): self.status.setText(task.name) self.app.processEvents() try: task.command(config=self.app.fastflix.config, app=self.app, **task.kwargs) except Exception: logger.exception( f"Could not run task {task.name} with config {self.app.fastflix.config}" ) raise self.progress_bar.setValue(int(i * ratio)) def update_progress(self, value): self.progress_bar.setValue(value) self.app.processEvents()
class NavigationGraphicsView(QtWidgets.QGraphicsView): """Graphics view for dataset navigation. The view usually displays an auto-scaled low resolution overview of the scene with a red box indicating the area currently displayed in the high resolution view. :SIGNALS: * :attr:`mousePressed` * :attr:`mouseMoved` """ BOXCOLOR = QtGui.QColor(QtCore.Qt.red) BOXWIDTH = 60 #: SIGNAL: it is emitted when a mouse button is presses on the view #: #: :param point: #: the scene position #: :param mousebutton: #: the ID of the pressed button #: :param dragmode: #: current darg mode #: #: :C++ signature: `void mousePressed(QPointF, Qt::MouseButtons, #: QGraphicsView::DragMode)` mousePressed = QtCore.Signal(QtCore.QPointF, QtCore.Qt.MouseButtons, QtWidgets.QGraphicsView.DragMode) #: SIGNAL: it is emitted when the mouse is moved on the view #: #: :param point: #: the scene position #: :param mousebutton: #: the ID of the pressed button #: :param dragmode: #: current drag mode #: #: :C++ signature: `void mouseMoved(QPointF, Qt::MouseButtons, #: QGraphicsView::DragMode)` mouseMoved = QtCore.Signal(QtCore.QPointF, QtCore.Qt.MouseButtons, QtWidgets.QGraphicsView.DragMode) def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self._viewbox = None self._autoscale = True self.setMouseTracking(True) # default pen self._pen = QtGui.QPen() self._pen.setColor(self.BOXCOLOR) self._pen.setWidth(self.BOXWIDTH) @property def viewbox(self): """Viewport box in scene coordinates""" return self._viewbox @viewbox.setter def viewbox(self, box): """Set the viewport box in scene coordinates""" assert isinstance(box, (QtCore.QRect, QtCore.QRectF)) self._viewbox = box if self.isVisible(): # @WARNING: calling "update" on the scene causes a repaint of # *all* attached views and for each view the entire # exposedRect is updated. # Using QGraphicsView.invalidateScene with the # QtWidgets.QGraphicsScene.ForegroundLayer parameter # should be faster and repaint only one layer of the # current view. # @TODO: check # self.invalidateScene(self.sceneRect(), # QtWidgets.QGraphicsScene.ForegroundLayer) self.scene().update() def drawForeground(self, painter, rect): if not self.viewbox: return pen = painter.pen() try: box = self.viewbox.intersected(self.sceneRect()) painter.setPen(self._pen) painter.drawRect(box) # painter.drawConvexPolygon(self.viewbox) #@TODO: check finally: painter.setPen(pen) def fitInView(self, rect=None, aspectRatioMode=QtCore.Qt.KeepAspectRatio): if not rect: scene = self.scene() if scene: rect = scene.sceneRect() else: return QtWidgets.QGraphicsView.fitInView(self, rect, aspectRatioMode) @property def autoscale(self): return self._autoscale @autoscale.setter def autoscale(self, flag): self._autoscale = bool(flag) if self._autoscale: self.fitInView() else: self.setTransform(QtGui.QTransform()) self.update() def resizeEvent(self, event): if self.autoscale: self.fitInView() return QtWidgets.QGraphicsView.resizeEvent(self, event) # @TODO: use event filters def mousePressEvent(self, event): pos = self.mapToScene(event.pos()) self.mousePressed.emit(pos, event.buttons(), self.dragMode()) return QtWidgets.QGraphicsView.mousePressEvent(self, event) def mouseMoveEvent(self, event): pos = self.mapToScene(event.pos()) self.mouseMoved.emit(pos, event.buttons(), self.dragMode()) return QtWidgets.QGraphicsView.mouseMoveEvent(self, event)
class SliceWidget(QtWidgets.QWidget): slice_changed = QtCore.Signal(int) def __init__(self, label='', world=None, lo=0, hi=10, parent=None, world_unit=None, world_warning=False): super(SliceWidget, self).__init__(parent) self.state = SliceState() self.state.label = label self.state.slice_center = (lo + hi) // 2 self._world = np.asarray(world) self._world_warning = world_warning self._world_unit = world_unit self.ui = load_ui('data_slice_widget.ui', self, directory=os.path.dirname(__file__)) autoconnect_callbacks_to_qt(self.state, self.ui) font = self.text_warning.font() font.setPointSize(font.pointSize() * 0.75) self.text_warning.setFont(font) self.button_first.setStyleSheet('border: 0px') self.button_first.setIcon(get_icon('playback_first')) self.button_prev.setStyleSheet('border: 0px') self.button_prev.setIcon(get_icon('playback_prev')) self.button_back.setStyleSheet('border: 0px') self.button_back.setIcon(get_icon('playback_back')) self.button_stop.setStyleSheet('border: 0px') self.button_stop.setIcon(get_icon('playback_stop')) self.button_forw.setStyleSheet('border: 0px') self.button_forw.setIcon(get_icon('playback_forw')) self.button_next.setStyleSheet('border: 0px') self.button_next.setIcon(get_icon('playback_next')) self.button_last.setStyleSheet('border: 0px') self.button_last.setIcon(get_icon('playback_last')) self.value_slice_center.setMinimum(lo) self.value_slice_center.setMaximum(hi) self.value_slice_center.valueChanged.connect(nonpartial(self.set_label_from_slider)) # Figure out the optimal format to use to show the world values. We do # this by figuring out the precision needed so that when converted to # a string, every string value is different. if world is not None: if np.max(np.abs(world)) > 1e5 or np.max(np.abs(world)) < 1e-5: fmt_type = 'e' else: fmt_type = 'f' relative = np.abs(np.diff(world) / world[:-1]) ndec = max(2, min(int(np.ceil(-np.log10(np.min(relative)))) + 1, 15)) self.label_fmt = "{:." + str(ndec) + fmt_type + "}" else: self.label_fmt = "{:g}" self.text_slider_label.setMinimumWidth(80) self.state.slider_label = self.label_fmt.format(self.value_slice_center.value()) self.text_slider_label.editingFinished.connect(nonpartial(self.set_slider_from_label)) self._play_timer = QtCore.QTimer() self._play_timer.setInterval(500) self._play_timer.timeout.connect(nonpartial(self._play_slice)) self.button_first.clicked.connect(nonpartial(self._browse_slice, 'first')) self.button_prev.clicked.connect(nonpartial(self._browse_slice, 'prev')) self.button_back.clicked.connect(nonpartial(self._adjust_play, 'back')) self.button_stop.clicked.connect(nonpartial(self._adjust_play, 'stop')) self.button_forw.clicked.connect(nonpartial(self._adjust_play, 'forw')) self.button_next.clicked.connect(nonpartial(self._browse_slice, 'next')) self.button_last.clicked.connect(nonpartial(self._browse_slice, 'last')) self.bool_use_world.toggled.connect(nonpartial(self.set_label_from_slider)) if world is None: self.state.use_world = False self.bool_use_world.hide() else: self.state.use_world = not world_warning if world_unit: self.state.slider_unit = world_unit else: self.state.slider_unit = '' self._play_speed = 0 self.set_label_from_slider() def set_label_from_slider(self): value = self.state.slice_center if self.state.use_world: value = self._world[value] if self._world_warning: self.text_warning.show() else: self.text_warning.hide() self.state.slider_unit = self._world_unit self.state.slider_label = self.label_fmt.format(value) else: self.text_warning.hide() self.state.slider_unit = '' self.state.slider_label = str(value) def set_slider_from_label(self): # Ignore recursive calls - we do this rather than ignore_callback # below when setting slider_label, otherwise we might be stopping other # subscribers to that event from being correctly updated if getattr(self, '_in_set_slider_from_label', False): return else: self._in_set_slider_from_label = True text = self.text_slider_label.text() if self.state.use_world: # Don't want to assume world is sorted, pick closest value value = np.argmin(np.abs(self._world - float(text))) self.state.slider_label = self.label_fmt.format(self._world[value]) else: value = int(text) self.value_slice_center.setValue(value) self._in_set_slider_from_label = False def _adjust_play(self, action): if action == 'stop': self._play_speed = 0 elif action == 'back': if self._play_speed > 0: self._play_speed = -1 else: self._play_speed -= 1 elif action == 'forw': if self._play_speed < 0: self._play_speed = +1 else: self._play_speed += 1 if self._play_speed == 0: self._play_timer.stop() else: self._play_timer.start() self._play_timer.setInterval(500 / abs(self._play_speed)) def _play_slice(self): if self._play_speed > 0: self._browse_slice('next', play=True) elif self._play_speed < 0: self._browse_slice('prev', play=True) def _browse_slice(self, action, play=False): imin = self.value_slice_center.minimum() imax = self.value_slice_center.maximum() value = self.value_slice_center.value() # If this was not called from _play_slice, we should stop the # animation. if not play: self._adjust_play('stop') if action == 'first': value = imin elif action == 'last': value = imax elif action == 'prev': value = value - 1 if value < imin: value = imax elif action == 'next': value = value + 1 if value > imax: value = imin else: raise ValueError("Action should be one of first/prev/next/last") self.value_slice_center.setValue(value)
class CounterLogic(GenericLogic): """ This logic module gathers data from a hardware counting device. @signal sigCounterUpdate: there is new counting data available @signal sigCountContinuousNext: used to simulate a loop in which the data acquisition runs. @sigmal sigCountGatedNext: ??? @return error: 0 is OK, -1 is error """ sigCounterUpdated = QtCore.Signal() sigCountDataNext = QtCore.Signal() sigGatedCounterFinished = QtCore.Signal() sigGatedCounterContinue = QtCore.Signal(bool) sigCountingSamplesChanged = QtCore.Signal(int) sigCountLengthChanged = QtCore.Signal(int) sigCountFrequencyChanged = QtCore.Signal(float) sigSavingStatusChanged = QtCore.Signal(bool) sigCountStatusChanged = QtCore.Signal(bool) sigCountingModeChanged = QtCore.Signal(CountingMode) _modclass = 'CounterLogic' _modtype = 'logic' ## declare connectors _connectors = { 'counter1': 'SlowCounterInterface', 'savelogic': 'SaveLogic' } def __init__(self, config, **kwargs): """ Create CounterLogic object with connectors. @param dict config: module configuration @param dict kwargs: optional parameters """ super().__init__(config=config, **kwargs) #locking for thread safety self.threadlock = Mutex() self.log.info('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.info('{0}: {1}'.format(key, config[key])) # in bins self._count_length = 300 self._smooth_window_length = 10 self._counting_samples = 1 # oversampling # in hertz self._count_frequency = 50 # self._binned_counting = True # UNUSED? self._counting_mode = CountingMode['CONTINUOUS'] self._saving = False return def on_activate(self): """ Initialisation performed during activation of the module. """ # Connect to hardware and save logic self._counting_device = self.get_connector('counter1') self._save_logic = self.get_connector('savelogic') # Recall saved app-parameters if 'count_length' in self._statusVariables: self._count_length = self._statusVariables['count_length'] if 'smooth_window_length' in self._statusVariables: self._smooth_window_length = self._statusVariables[ 'smooth_window_length'] if 'counting_samples' in self._statusVariables: self._counting_samples = self._statusVariables['counting_samples'] if 'count_frequency' in self._statusVariables: self._count_frequency = self._statusVariables['count_frequency'] if 'counting_mode' in self._statusVariables: self._counting_mode = CountingMode[ self._statusVariables['counting_mode']] if 'saving' in self._statusVariables: self._saving = self._statusVariables['saving'] constraints = self.get_hardware_constraints() number_of_detectors = constraints.max_detectors # initialize data arrays self.countdata = np.zeros( [len(self.get_channels()), self._count_length]) self.countdata_smoothed = np.zeros( [len(self.get_channels()), self._count_length]) self.rawdata = np.zeros( [len(self.get_channels()), self._counting_samples]) self._already_counted_samples = 0 # For gated counting self._data_to_save = [] # Flag to stop the loop self.stopRequested = False self._saving_start_time = time.time() # connect signals self.sigCountDataNext.connect(self.count_loop_body, QtCore.Qt.QueuedConnection) return def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ # Save parameters to disk self._statusVariables['count_length'] = self._count_length self._statusVariables[ 'smooth_window_length'] = self._smooth_window_length self._statusVariables['counting_samples'] = self._counting_samples self._statusVariables['count_frequency'] = self._count_frequency self._statusVariables['counting_mode'] = self._counting_mode.name self._statusVariables['saving'] = self._saving # Stop measurement if self.getState() == 'locked': self._stopCount_wait() self.sigCountDataNext.disconnect() return def get_hardware_constraints(self): """ Retrieve the hardware constrains from the counter device. @return SlowCounterConstraints: object with constraints for the counter """ return self._counting_device.get_constraints() def set_counting_samples(self, samples=1): """ Sets the length of the counted bins. The counter is stopped first and restarted afterwards. @param int samples: oversampling in units of bins (positive int ). @return int: oversampling in units of bins. """ # Determine if the counter has to be restarted after setting the parameter if self.getState() == 'locked': restart = True else: restart = False if samples > 0: self._stopCount_wait() self._counting_samples = int(samples) # if the counter was running, restart it if restart: self.startCount() else: self.log.warning( 'counting_samples has to be larger than 0! Command ignored!') self.sigCountingSamplesChanged.emit(self._counting_samples) return self._counting_samples def set_count_length(self, length=300): """ Sets the time trace in units of bins. @param int length: time trace in units of bins (positive int). @return int: length of time trace in units of bins This makes sure, the counter is stopped first and restarted afterwards. """ if self.getState() == 'locked': restart = True else: restart = False if length > 0: self._stopCount_wait() self._count_length = int(length) # if the counter was running, restart it if restart: self.startCount() else: self.log.warning( 'count_length has to be larger than 0! Command ignored!') self.sigCountLengthChanged.emit(self._count_length) return self._count_length def set_count_frequency(self, frequency=50): """ Sets the frequency with which the data is acquired. @param float frequency: the desired frequency of counting in Hz @return float: the actual frequency of counting in Hz This makes sure, the counter is stopped first and restarted afterwards. """ constraints = self.get_hardware_constraints() if self.getState() == 'locked': restart = True else: restart = False if constraints.min_count_frequency <= frequency <= constraints.max_count_frequency: self._stopCount_wait() self._count_frequency = frequency # if the counter was running, restart it if restart: self.startCount() else: self.log.warning('count_frequency not in range! Command ignored!') self.sigCountFrequencyChanged.emit(self._count_frequency) return self._count_frequency def get_count_length(self): """ Returns the currently set length of the counting array. @return int: count_length """ return self._count_length #FIXME: get from hardware def get_count_frequency(self): """ Returns the currently set frequency of counting (resolution). @return float: count_frequency """ return self._count_frequency def get_counting_samples(self): """ Returns the currently set number of samples counted per readout. @return int: counting_samples """ return self._counting_samples def get_saving_state(self): """ Returns if the data is saved in the moment. @return bool: saving state """ return self._saving def start_saving(self, resume=False): """ Sets up start-time and initializes data array, if not resuming, and changes saving state. If the counter is not running it will be started in order to have data to save. @return bool: saving state """ if not resume: self._data_to_save = [] self._saving_start_time = time.time() self._saving = True # If the counter is not running, then it should start running so there is data to save if self.getState() != 'locked': self.startCount() self.sigSavingStatusChanged.emit(self._saving) return self._saving def save_data(self, to_file=True, postfix=''): """ Save the counter trace data and writes it to a file. @param bool to_file: indicate, whether data have to be saved to file @param str postfix: an additional tag, which will be added to the filename upon save @return dict parameters: Dictionary which contains the saving parameters """ # stop saving thus saving state has to be set to False self._saving = False self._saving_stop_time = time.time() # write the parameters: parameters = OrderedDict() parameters['Start counting time'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_start_time)) parameters['Stop counting time'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_stop_time)) parameters['Count frequency (Hz)'] = self._count_frequency parameters['Oversampling (Samples)'] = self._counting_samples parameters[ 'Smooth Window Length (# of events)'] = self._smooth_window_length if to_file: # If there is a postfix then add separating underscore if postfix == '': filelabel = 'count_trace' else: filelabel = 'count_trace_' + postfix # prepare the data in a dict or in an OrderedDict: header = 'Time (s)' for i, detector in enumerate(self.get_channels()): header = header + ',Signal{0} (counts/s)'.format(i) data = {header: self._data_to_save} filepath = self._save_logic.get_path_for_module( module_name='Counter') fig = self.draw_figure(data=np.array(self._data_to_save)) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, plotfig=fig, delimiter='\t') self.log.info('Counter Trace saved to:\n{0}'.format(filepath)) self.sigSavingStatusChanged.emit(self._saving) return self._data_to_save, parameters def draw_figure(self, data): """ Draw figure to save with data file. @param: nparray data: a numpy array containing counts vs time for all detectors @return: fig fig: a matplotlib figure object to be saved to file. """ count_data = data[:, 1:len(self.get_channels()) + 1] time_data = data[:, 0] # Scale count values using SI prefix prefix = ['', 'k', 'M', 'G'] prefix_index = 0 while np.max(count_data) > 1000: count_data = count_data / 1000 prefix_index = prefix_index + 1 counts_prefix = prefix[prefix_index] # Use qudi style plt.style.use(self._save_logic.mpl_qd_style) # Create figure fig, ax = plt.subplots() ax.plot(time_data, count_data, linestyle=':', linewidth=0.5) ax.set_xlabel('Time (s)') ax.set_ylabel('Fluorescence (' + counts_prefix + 'c/s)') return fig def set_counting_mode(self, mode='CONTINUOUS'): """Set the counting mode, to change between continuous and gated counting. Possible options are: 'CONTINUOUS' = counts continuously 'GATED' = bins the counts according to a gate signal 'FINITE_GATED' = finite measurement with predefined number of samples @return str: counting mode """ constraints = self.get_hardware_constraints() if self.getState() != 'locked': if CountingMode[mode] in constraints.counting_mode: self._counting_mode = CountingMode[mode] self.log.debug('New counting mode: {}'.format( self._counting_mode)) else: self.log.warning( 'Counting mode not supported from hardware. Command ignored!' ) self.sigCountingModeChanged.emit(self._counting_mode) else: self.log.error( 'Cannot change counting mode while counter is still running.') return self._counting_mode def get_counting_mode(self): """ Retrieve the current counting mode. @return str: one of the possible counting options: 'CONTINUOUS' = counts continuously 'GATED' = bins the counts according to a gate signal 'FINITE_GATED' = finite measurement with predefined number of samples """ return self._counting_mode # FIXME: Not implemented for self._counting_mode == 'gated' def startCount(self): """ This is called externally, and is basically a wrapper that redirects to the chosen counting mode start function. @return error: 0 is OK, -1 is error """ # Sanity checks constraints = self.get_hardware_constraints() if self._counting_mode not in constraints.counting_mode: self.log.error( 'Unknown counting mode "{0}". Cannot start the counter.' ''.format(self._counting_mode)) self.sigCountStatusChanged.emit(False) return -1 with self.threadlock: # Lock module if self.getState() != 'locked': self.lock() else: self.log.warning( 'Counter already running. Method call ignored.') return 0 # Set up clock clock_status = self._counting_device.set_up_clock( clock_frequency=self._count_frequency) if clock_status < 0: self.unlock() self.sigCountStatusChanged.emit(False) return -1 # Set up counter if self._counting_mode == CountingMode['FINITE_GATED']: counter_status = self._counting_device.set_up_counter( counter_buffer=self._count_length) # elif self._counting_mode == CountingMode['GATED']: # else: counter_status = self._counting_device.set_up_counter() if counter_status < 0: self._counting_device.close_clock() self.unlock() self.sigCountStatusChanged.emit(False) return -1 # initialising the data arrays self.rawdata = np.zeros( [len(self.get_channels()), self._counting_samples]) self.countdata = np.zeros( [len(self.get_channels()), self._count_length]) self.countdata_smoothed = np.zeros( [len(self.get_channels()), self._count_length]) self._sampling_data = np.empty( [len(self.get_channels()), self._counting_samples]) # the sample index for gated counting self._already_counted_samples = 0 # Start data reader loop self.sigCountStatusChanged.emit(True) self.sigCountDataNext.emit() return def stopCount(self): """ Set a flag to request stopping counting. """ if self.getState() == 'locked': with self.threadlock: self.stopRequested = True return def count_loop_body(self): """ This method gets the count data from the hardware for the continuous counting mode (default). It runs repeatedly in the logic module event loop by being connected to sigCountContinuousNext and emitting sigCountContinuousNext through a queued connection. """ if self.getState() == 'locked': with self.threadlock: # check for aborts of the thread in break if necessary if self.stopRequested: # close off the actual counter cnt_err = self._counting_device.close_counter() clk_err = self._counting_device.close_clock() if cnt_err < 0 or clk_err < 0: self.log.error( 'Could not even close the hardware, giving up.') # switch the state variable off again self.stopRequested = False self.unlock() self.sigCounterUpdated.emit() return # read the current counter value self.rawdata = self._counting_device.get_counter( samples=self._counting_samples) if self.rawdata[0, 0] < 0: self.log.error( 'The counting went wrong, killing the counter.') self.stopRequested = True else: if self._counting_mode == CountingMode['CONTINUOUS']: self._process_data_continous() elif self._counting_mode == CountingMode['GATED']: self._process_data_gated() elif self._counting_mode == CountingMode['FINITE_GATED']: self._process_data_finite_gated() else: self.log.error( 'No valid counting mode set! Can not process counter data.' ) # call this again from event loop self.sigCounterUpdated.emit() self.sigCountDataNext.emit() return def save_current_count_trace(self, name_tag=''): """ The currently displayed counttrace will be saved. @param str name_tag: optional, personal description that will be appended to the file name @return: dict data: Data which was saved str filepath: Filepath dict parameters: Experiment parameters str filelabel: Filelabel This method saves the already displayed counts to file and does not accumulate them. The counttrace variable will be saved to file with the provided name! """ # If there is a postfix then add separating underscore if name_tag == '': filelabel = 'snapshot_count_trace' else: filelabel = 'snapshot_count_trace_' + name_tag stop_time = self._count_length / self._count_frequency time_step_size = stop_time / len(self.countdata) x_axis = np.arange(0, stop_time, time_step_size) # prepare the data in a dict or in an OrderedDict: data = OrderedDict() chans = self.get_channels() savearr = np.empty((len(chans) + 1, len(x_axis))) savearr[0] = x_axis datastr = 'Time (s)' for i, ch in enumerate(chans): savearr[i + 1] = self.countdata[i] datastr += ',Signal {0} (counts/s)'.format(i) data[datastr] = savearr.transpose() # write the parameters: parameters = OrderedDict() timestr = time.strftime('%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(time.time())) parameters['Saved at time'] = timestr parameters['Count frequency (Hz)'] = self._count_frequency parameters['Oversampling (Samples)'] = self._counting_samples parameters[ 'Smooth Window Length (# of events)'] = self._smooth_window_length filepath = self._save_logic.get_path_for_module(module_name='Counter') self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, delimiter='\t') self.log.debug('Current Counter Trace saved to: {0}'.format(filepath)) return data, filepath, parameters, filelabel def get_channels(self): """ Shortcut for hardware get_counter_channels. @return list(str): return list of active counter channel names """ return self._counting_device.get_counter_channels() def _process_data_continous(self): """ Processes the raw data from the counting device @return: """ for i, ch in enumerate(self.get_channels()): # remember the new count data in circular array self.countdata[i, 0] = np.average(self.rawdata[i]) # move the array to the left to make space for the new data self.countdata = np.roll(self.countdata, -1, axis=1) # also move the smoothing array self.countdata_smoothed = np.roll(self.countdata_smoothed, -1, axis=1) # calculate the median and save it window = -int(self._smooth_window_length / 2) - 1 for i, ch in enumerate(self.get_channels()): self.countdata_smoothed[i, window:] = np.median( self.countdata[i, -self._smooth_window_length:]) # save the data if necessary if self._saving: # if oversampling is necessary if self._counting_samples > 1: chans = self.get_channels() self._sampling_data = np.empty( [len(chans) + 1, self._counting_samples]) self._sampling_data[ 0, :] = time.time() - self._saving_start_time for i, ch in enumerate(chans): self._sampling_data[i + 1, 0] = self.rawdata[i] self._data_to_save.extend(list(self._sampling_data)) # if we don't want to use oversampling else: # append tuple to data stream (timestamp, average counts) chans = self.get_channels() newdata = np.empty((len(chans) + 1, )) newdata[0] = time.time() - self._saving_start_time for i, ch in enumerate(chans): newdata[i + 1] = self.countdata[i, -1] self._data_to_save.append(newdata) return def _process_data_gated(self): """ Processes the raw data from the counting device @return: """ # remember the new count data in circular array self.countdata[0] = np.average(self.rawdata[0]) # move the array to the left to make space for the new data self.countdata = np.roll(self.countdata, -1) # also move the smoothing array self.countdata_smoothed = np.roll(self.countdata_smoothed, -1) # calculate the median and save it self.countdata_smoothed[-int(self._smooth_window_length / 2) - 1:] = np.median( self. countdata[-self._smooth_window_length:]) # save the data if necessary if self._saving: # if oversampling is necessary if self._counting_samples > 1: self._sampling_data = np.empty((self._counting_samples, 2)) self._sampling_data[:, 0] = time.time() - self._saving_start_time self._sampling_data[:, 1] = self.rawdata[0] self._data_to_save.extend(list(self._sampling_data)) # if we don't want to use oversampling else: # append tuple to data stream (timestamp, average counts) self._data_to_save.append( np.array((time.time() - self._saving_start_time, self.countdata[-1]))) return def _process_data_finite_gated(self): """ Processes the raw data from the counting device @return: """ if self._already_counted_samples + len(self.rawdata[0]) >= len( self.countdata): needed_counts = len(self.countdata) - self._already_counted_samples self.countdata[0:needed_counts] = self.rawdata[0][0:needed_counts] self.countdata = np.roll(self.countdata, -needed_counts) self._already_counted_samples = 0 self.stopRequested = True else: # replace the first part of the array with the new data: self.countdata[0:len(self.rawdata[0])] = self.rawdata[0] # roll the array by the amount of data it had been inserted: self.countdata = np.roll(self.countdata, -len(self.rawdata[0])) # increment the index counter: self._already_counted_samples += len(self.rawdata[0]) return def _stopCount_wait(self, timeout=5.0): """ Stops the counter and waits until it actually has stopped. @param timeout: float, the max. time in seconds how long the method should wait for the process to stop. @return: error code """ self.stopCount() start_time = time.time() while self.getState() == 'locked': time.sleep(0.1) if time.time() - start_time >= timeout: self.log.error( 'Stopping the counter timed out after {0}s'.format( timeout)) return -1 return 0
class QuickEditView(QtWidgets.QWidget): error_signal = QtCore.Signal(object) def __init__(self, subcontext, parent=None, default_msg="All"): super(QuickEditView, self).__init__(parent) self._default_selector_msg = default_msg button_layout = QtWidgets.QHBoxLayout() self.plot_selector = QtWidgets.QComboBox() self.plot_selector.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) self.plot_selector.setSizeAdjustPolicy( QtWidgets.QComboBox.AdjustToContents) self.plot_selector.setMinimumContentsLength(12) self.plot_selector.setEditable(True) self.plot_selector.completer().setCompletionMode( QtWidgets.QCompleter.PopupCompletion) self.plot_selector.view().setMinimumWidth(100) self.plot_selector.completer().setFilterMode(QtCore.Qt.MatchContains) self.plot_selector.addItem(self._default_selector_msg) self.plot_selector.setEditable(False) self.x_axis_changer = AxisChangerWidget("X", self) self.autoscale = None self.autoscale = QtWidgets.QCheckBox("Autoscale y") self.autoscale.setToolTip( "While pan or zoom are enabled autoscale is disabled") self.y_axis_changer = AxisChangerWidget("Y", self) self.errors = QtWidgets.QCheckBox("Errors") self.errors.stateChanged.connect(self._emit_errors) button_layout.addWidget(self.plot_selector) button_layout.addWidget(self.x_axis_changer.view) button_layout.addWidget(self.autoscale) button_layout.addWidget(self.y_axis_changer.view) button_layout.addWidget(self.errors) self.setLayout(button_layout) @property def get_multiple_selection_name(self): return self._default_selector_msg """ plot selection """ def disable_plot_selection(self): self.plot_selector.setEnabled(False) def add_subplot(self, name): self.plot_selector.blockSignals(True) self.plot_selector.addItem(name) self.plot_selector.adjustSize() self.plot_selector.blockSignals(False) def rm_subplot(self, index): self.plot_selector.removeItem(index) self.plot_selector.adjustSize() def current_selection(self): return self.plot_selector.currentText() def find_subplot(self, name): return self.plot_selector.findText(name) def set_selection(self, index: int): self.plot_selector.setCurrentIndex(index) def get_selection_index(self) -> int: return self.plot_selector.currentIndex() def plot_at_index(self, index): return self.plot_selector.itemText(index) def number_of_plots(self): return self.plot_selector.count() def clear_subplots(self): self.plot_selector.blockSignals(True) self.plot_selector.clear() self.plot_selector.addItem(self._default_selector_msg) self.plot_selector.blockSignals(False) def connect_plot_selection(self, slot): self.plot_selector.currentIndexChanged.connect(slot) """ x axis selection """ def connect_x_range_changed(self, slot): self.x_axis_changer.on_range_changed(slot) def set_plot_x_range(self, limits): self.x_axis_changer.set_limits(limits) def get_x_bounds(self): return self.x_axis_changer.get_limits() """ y axis selection """ def connect_y_range_changed(self, slot): self.y_axis_changer.on_range_changed(slot) def set_plot_y_range(self, limits): self.y_axis_changer.set_limits(limits) def get_y_bounds(self): return self.y_axis_changer.get_limits() def disable_yaxis_changer(self): self.y_axis_changer.view.setEnabled(False) def enable_yaxis_changer(self): self.y_axis_changer.view.setEnabled(True) """ auto scale selection """ def connect_autoscale_changed(self, slot): self.autoscale.clicked.connect(slot) @property def autoscale_state(self): return self.autoscale.checkState() def disable_autoscale(self): self.autoscale.setEnabled(False) def enable_autoscale(self): self.autoscale.setEnabled(True) def set_autoscale(self, state: bool): self.autoscale.setChecked(state) def uncheck_autoscale(self): self.autoscale.setChecked(False) """ errors selection """ # need our own signal that sends a bool def _emit_errors(self): state = self.get_errors() self.error_signal.emit(state) def connect_errors_changed(self, slot): self.error_signal.connect(slot) def set_errors(self, state): self.errors.setChecked(state) def get_errors(self): return self.errors.isChecked()
class BetterListCtrl(QW.QTreeWidget): columnListContentsChanged = QC.Signal() columnListStatusChanged = QC.Signal() def __init__(self, parent, column_list_type, height_num_chars, data_to_tuples_func, use_simple_delete=False, delete_key_callback=None, activation_callback=None, style=None, column_types_to_name_overrides=None): QW.QTreeWidget.__init__(self, parent) self._have_shown_a_column_data_error = False self._creation_time = HydrusData.GetNow() self._column_list_type = column_list_type self._column_list_status: ClientGUIListStatus.ColumnListStatus = HG.client_controller.column_list_manager.GetStatus( self._column_list_type) self._original_column_list_status = self._column_list_status self.setAlternatingRowColors(True) self.setColumnCount(self._column_list_status.GetColumnCount()) self.setSortingEnabled( False ) # Keeping the custom sort implementation. It would be better to use Qt's native sorting in the future so sort indicators are displayed on the headers as expected. self.setSelectionMode(QW.QAbstractItemView.ExtendedSelection) self.setRootIsDecorated(False) self._initial_height_num_chars = height_num_chars self._forced_height_num_chars = None self._data_to_tuples_func = data_to_tuples_func self._use_simple_delete = use_simple_delete self._menu_callable = None (self._sort_column_type, self._sort_asc) = self._column_list_status.GetSort() self._indices_to_data_info = {} self._data_to_indices = {} # old way ''' #sizing_column_initial_width = self.fontMetrics().boundingRect( 'x' * sizing_column_initial_width_num_chars ).width() total_width = self.fontMetrics().boundingRect( 'x' * sizing_column_initial_width_num_chars ).width() resize_column = 1 for ( i, ( name, width_num_chars ) ) in enumerate( columns ): if width_num_chars == -1: width = -1 resize_column = i + 1 else: width = self.fontMetrics().boundingRect( 'x' * width_num_chars ).width() total_width += width self.headerItem().setText( i, name ) self.setColumnWidth( i, width ) # Technically this is the previous behavior, but the two commented lines might work better in some cases (?) self.header().setStretchLastSection( False ) self.header().setSectionResizeMode( resize_column - 1 , QW.QHeaderView.Stretch ) #self.setColumnWidth( resize_column - 1, sizing_column_initial_width ) #self.header().setStretchLastSection( True ) self.setMinimumWidth( total_width ) ''' main_tlw = HG.client_controller.GetMainTLW() # if last section is set too low, for instance 3, the column seems unable to ever shrink from initial (expanded to fill space) size # _ _ ___ _ _ __ __ ___ # ( \/\/ )( _)( \/\/ ) ( ) ( ) ( \ # \ / ) _) \ / )(__ /__\ ) ) ) # \/\/ (___) \/\/ (____)(_)(_)(___/ # # I think this is because of mismatch between set size and min size! So ensuring we never set smaller than that initially should fix this???!? MIN_SECTION_SIZE_CHARS = 3 MIN_LAST_SECTION_SIZE_CHARS = 10 self._min_section_width = ClientGUIFunctions.ConvertTextToPixelWidth( main_tlw, MIN_SECTION_SIZE_CHARS) self.header().setMinimumSectionSize(self._min_section_width) last_column_index = self._column_list_status.GetColumnCount() - 1 for (i, column_type) in enumerate( self._column_list_status.GetColumnTypes()): self.headerItem().setData(i, QC.Qt.UserRole, column_type) if column_types_to_name_overrides is not None and column_type in column_types_to_name_overrides: name = column_types_to_name_overrides[column_type] else: name = CGLC.column_list_column_name_lookup[ self._column_list_type][column_type] self.headerItem().setText(i, name) self.headerItem().setToolTip(i, name) if i == last_column_index: width_chars = MIN_SECTION_SIZE_CHARS else: width_chars = self._column_list_status.GetColumnWidth( column_type) width_chars = max(width_chars, MIN_SECTION_SIZE_CHARS) # ok this is a pain in the neck issue, but fontmetrics changes afte widget init. I guess font gets styled on top afterwards # this means that if I use this window's fontmetrics here, in init, then it is different later on, and we get creeping growing columns lmao # several other places in the client are likely affected in different ways by this also! width_pixels = ClientGUIFunctions.ConvertTextToPixelWidth( main_tlw, width_chars) self.setColumnWidth(i, width_pixels) self.header().setStretchLastSection(True) self._delete_key_callback = delete_key_callback self._activation_callback = activation_callback self._widget_event_filter = QP.WidgetEventFilter(self) self._widget_event_filter.EVT_KEY_DOWN(self.EventKeyDown) self.itemDoubleClicked.connect(self.EventItemActivated) self.header().setSectionsMovable( False) # can only turn this on when we move from data/sort tuples # self.header().setFirstSectionMovable( True ) # same self.header().setSectionsClickable(True) self.header().sectionClicked.connect(self.EventColumnClick) #self.header().sectionMoved.connect( self._DoStatusChanged ) # same self.header().sectionResized.connect(self._SectionsResized) def _AddDataInfo(self, data_info): (data, display_tuple, sort_tuple) = data_info if data in self._data_to_indices: return append_item = QW.QTreeWidgetItem() for i in range(len(display_tuple)): text = display_tuple[i] if len(text) > 0: text = text.splitlines()[0] append_item.setText(i, text) append_item.setToolTip(i, text) self.addTopLevelItem(append_item) index = self.topLevelItemCount() - 1 self._indices_to_data_info[index] = data_info self._data_to_indices[data] = index def _SectionsResized(self, logical_index, old_size, new_size): self._DoStatusChanged() self.updateGeometry() def _DoStatusChanged(self): self._column_list_status = self._GenerateCurrentStatus() HG.client_controller.column_list_manager.SaveStatus( self._column_list_status) def _GenerateCurrentStatus(self) -> ClientGUIListStatus.ColumnListStatus: status = ClientGUIListStatus.ColumnListStatus() status.SetColumnListType(self._column_list_type) main_tlw = HG.client_controller.GetMainTLW() columns = [] header = self.header() num_columns = header.count() last_column_index = num_columns - 1 # ok, the big pain in the ass situation here is getting a precise last column size that is reproduced on next dialog launch # ultimately, with fuzzy sizing, style padding, scrollbars appearing, and other weirdness, the more precisely we try to define it, the more we will get dialogs that grow/shrink by a pixel each time # *therefore*, the actual solution here is to move to snapping with a decent snap distance. the user loses size setting precision, but we'll snap back to a decent size every time, compensating for fuzz LAST_COLUMN_SNAP_DISTANCE_CHARS = 5 for visual_index in range(num_columns): logical_index = header.logicalIndex(visual_index) column_type = self.headerItem().data(logical_index, QC.Qt.UserRole) width_pixels = header.sectionSize(logical_index) shown = not header.isSectionHidden(logical_index) if visual_index == last_column_index: if self.verticalScrollBar().isVisible(): width_pixels += max( 0, min(self.verticalScrollBar().width(), 20)) width_chars = ClientGUIFunctions.ConvertPixelsToTextWidth( main_tlw, width_pixels) if visual_index == last_column_index: # here's the snap magic width_chars = round( width_chars // LAST_COLUMN_SNAP_DISTANCE_CHARS ) * LAST_COLUMN_SNAP_DISTANCE_CHARS columns.append((column_type, width_chars, shown)) status.SetColumns(columns) status.SetSort(self._sort_column_type, self._sort_asc) return status def _GetDisplayAndSortTuples(self, data): try: (display_tuple, sort_tuple) = self._data_to_tuples_func(data) except Exception as e: if not self._have_shown_a_column_data_error: HydrusData.ShowText( 'A multi-column list was unable to generate text or sort data for one or more rows! Please send hydrus dev the traceback!' ) HydrusData.ShowException(e) self._have_shown_a_column_data_error = True error_display_tuple = [ 'unable to display' for i in range(self._column_list_status.GetColumnCount()) ] return (error_display_tuple, None) better_sort = [] for item in sort_tuple: if isinstance(item, str): item = HydrusData.HumanTextSortKey(item) better_sort.append(item) sort_tuple = tuple(better_sort) return (display_tuple, sort_tuple) def _GetSelected(self): indices = [] for i in range(self.topLevelItemCount()): if self.topLevelItem(i).isSelected(): indices.append(i) return indices def _RecalculateIndicesAfterDelete(self): indices_and_data_info = sorted(self._indices_to_data_info.items()) self._indices_to_data_info = {} self._data_to_indices = {} for (index, (old_index, data_info)) in enumerate(indices_and_data_info): (data, display_tuple, sort_tuple) = data_info self._data_to_indices[data] = index self._indices_to_data_info[index] = data_info def _ShowMenu(self): try: menu = self._menu_callable() except HydrusExceptions.DataMissing: return CGC.core().PopupMenu(self, menu) def _SortDataInfo(self): sort_column_index = self._column_list_status.GetColumnIndexFromType( self._sort_column_type) data_infos = list(self._indices_to_data_info.values()) data_infos_good = [(data, display_tuple, sort_tuple) for (data, display_tuple, sort_tuple) in data_infos if sort_tuple is not None] data_infos_bad = [(data, display_tuple, sort_tuple) for (data, display_tuple, sort_tuple) in data_infos if sort_tuple is None] def sort_key(data_info): (data, display_tuple, sort_tuple) = data_info return (sort_tuple[sort_column_index], sort_tuple ) # add the sort tuple to get secondary sorting try: data_infos_good.sort(key=sort_key, reverse=not self._sort_asc) except Exception as e: HydrusData.ShowText( 'A multi-column list failed to sort! Please send hydrus dev the traceback!' ) HydrusData.ShowException(e) data_infos_bad.extend(data_infos_good) data_infos = data_infos_bad return data_infos def _SortAndRefreshRows(self): selected_data_quick = set(self.GetData(only_selected=True)) self.clearSelection() sorted_data_info = self._SortDataInfo() self._indices_to_data_info = {} self._data_to_indices = {} for (index, data_info) in enumerate(sorted_data_info): self._indices_to_data_info[index] = data_info (data, display_tuple, sort_tuple) = data_info self._data_to_indices[data] = index self._UpdateRow(index, display_tuple) if data in selected_data_quick: self.topLevelItem(index).setSelected(True) def _UpdateRow(self, index, display_tuple): for (column_index, value) in enumerate(display_tuple): if len(value) > 0: value = value.splitlines()[0] tree_widget_item = self.topLevelItem(index) existing_value = tree_widget_item.text(column_index) if existing_value != value: tree_widget_item.setText(column_index, value) tree_widget_item.setToolTip(column_index, value) def AddDatas(self, datas: typing.Iterable[object]): for data in datas: (display_tuple, sort_tuple) = self._GetDisplayAndSortTuples(data) self._AddDataInfo((data, display_tuple, sort_tuple)) self.columnListContentsChanged.emit() def AddMenuCallable(self, menu_callable): self._menu_callable = menu_callable self.setContextMenuPolicy(QC.Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.EventShowMenu) def DeleteDatas(self, datas: typing.Iterable[object]): deletees = [(self._data_to_indices[data], data) for data in datas] deletees.sort(reverse=True) # The below comment is most probably obsolote (from before the Qt port), but keeping it just in case it is not and also as an explanation. # # I am not sure, but I think if subsequent deleteitems occur in the same event, the event processing of the first is forced!! # this means that button checking and so on occurs for n-1 times on an invalid indices structure in this thing before correcting itself in the last one # if a button update then tests selected data against the invalid index and a selection is on the i+1 or whatever but just got bumped up into invalid area, we are exception city # this doesn't normally affect us because mostly we _are_ deleting selections when we do deletes, but 'try to link url stuff' auto thing hit this # I obviously don't want to recalc all indices for every delete # so I wrote a catch in getdata to skip the missing error, and now I'm moving the data deletion to a second loop, which seems to help for (index, data) in deletees: self.takeTopLevelItem(index) for (index, data) in deletees: del self._data_to_indices[data] del self._indices_to_data_info[index] self._RecalculateIndicesAfterDelete() self.columnListContentsChanged.emit() def DeleteSelected(self): indices = self._GetSelected() indices.sort(reverse=True) for index in indices: (data, display_tuple, sort_tuple) = self._indices_to_data_info[index] item = self.takeTopLevelItem(index) del item del self._data_to_indices[data] del self._indices_to_data_info[index] self._RecalculateIndicesAfterDelete() self.columnListContentsChanged.emit() def EventColumnClick(self, col): sort_column_type = self._column_list_status.GetColumnTypeFromIndex(col) if sort_column_type == self._sort_column_type: self._sort_asc = not self._sort_asc else: self._sort_column_type = sort_column_type self._sort_asc = True self._SortAndRefreshRows() self._DoStatusChanged() def EventItemActivated(self, item, column): if self._activation_callback is not None: self._activation_callback() def EventKeyDown(self, event): (modifier, key) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple(event) if key in ClientGUIShortcuts.DELETE_KEYS_QT: self.ProcessDeleteAction() elif key in (ord('A'), ord('a')) and modifier == QC.Qt.ControlModifier: self.selectAll() else: return True # was: event.ignore() def EventShowMenu(self): QP.CallAfter(self._ShowMenu) def ForceHeight(self, rows): self._forced_height_num_chars = rows self.updateGeometry() # +2 for the header row and * 1.25 for magic rough text-to-rowheight conversion #existing_min_width = self.minimumWidth() #( width_gumpf, ideal_client_height ) = ClientGUIFunctions.ConvertTextToPixels( self, ( 20, int( ( ideal_rows + 2 ) * 1.25 ) ) ) #QP.SetMinClientSize( self, ( existing_min_width, ideal_client_height ) ) def GetData(self, only_selected=False): if only_selected: indices = self._GetSelected() else: indices = list(self._indices_to_data_info.keys()) result = [] for index in indices: # this can get fired while indices are invalid, wew if index not in self._indices_to_data_info: continue (data, display_tuple, sort_tuple) = self._indices_to_data_info[index] result.append(data) return result def HasData(self, data: object): return data in self._data_to_indices def HasOneSelected(self): return len(self.selectedItems()) == 1 def HasSelected(self): return len(self.selectedItems()) > 0 def ProcessDeleteAction(self): if self._use_simple_delete: self.ShowDeleteSelectedDialog() elif self._delete_key_callback is not None: self._delete_key_callback() def SelectDatas(self, datas: typing.Iterable[object]): for data in datas: if data in self._data_to_indices: index = self._data_to_indices[data] self.topLevelItem(index).setSelected(True) def SetData(self, datas: typing.Iterable[object]): existing_datas = set(self._data_to_indices.keys()) # useful to preserve order here sometimes (e.g. export file path generation order) datas_to_add = [data for data in datas if data not in existing_datas] datas_to_update = [data for data in datas if data in existing_datas] datas_to_delete = existing_datas.difference(datas) if len(datas_to_delete) > 0: self.DeleteDatas(datas_to_delete) if len(datas_to_update) > 0: self.UpdateDatas(datas_to_update) if len(datas_to_add) > 0: self.AddDatas(datas_to_add) self._SortAndRefreshRows() self.columnListContentsChanged.emit() def ShowDeleteSelectedDialog(self): from hydrus.client.gui import ClientGUIDialogsQuick result = ClientGUIDialogsQuick.GetYesNo(self, 'Remove all selected?') if result == QW.QDialog.Accepted: self.DeleteSelected() def _GetRowHeightEstimate(self): if self.topLevelItemCount() > 0: height = self.rowHeight(self.indexFromItem(self.topLevelItem(0))) else: (width_gumpf, height) = ClientGUIFunctions.ConvertTextToPixels(self, (20, 1)) return height def minimumSizeHint(self): width = 0 for i in range(self.columnCount() - 1): width += self.columnWidth(i) width += self._min_section_width # the last column width += self.frameWidth() * 2 if self._forced_height_num_chars is None: min_num_rows = 4 else: min_num_rows = self._forced_height_num_chars header_size = self.header().sizeHint( ) # this is better than min size hint for some reason ?( 69, 69 )? data_area_height = self._GetRowHeightEstimate() * min_num_rows PADDING = 10 min_size_hint = QC.QSize( width, header_size.height() + data_area_height + PADDING) return min_size_hint def resizeEvent(self, event): self._DoStatusChanged() return QW.QTreeWidget.resizeEvent(self, event) def sizeHint(self): width = 0 # all but last column for i in range(self.columnCount() - 1): width += self.columnWidth(i) # # ok, we are going full slippery dippery doo now # the issue is: when we first boot up, we want to give a 'hey, it would be nice' size of the last actual recorded final column # HOWEVER, after that: we want to use the current size of the last column # so, if it is the first couple of seconds, lmao. after that, oaml # I later updated this to use the columnWidth, rather than hickery dickery text-to-pixel-width, since it was juddering resize around text width phase last_column_type = self._column_list_status.GetColumnTypes()[-1] if HydrusData.TimeHasPassed(self._creation_time + 2): width += self.columnWidth(self.columnCount() - 1) else: last_column_chars = self._original_column_list_status.GetColumnWidth( last_column_type) main_tlw = HG.client_controller.GetMainTLW() width += ClientGUIFunctions.ConvertTextToPixelWidth( main_tlw, last_column_chars) # width += self.frameWidth() * 2 if self._forced_height_num_chars is None: num_rows = self._initial_height_num_chars else: num_rows = self._forced_height_num_chars header_size = self.header().sizeHint() data_area_height = self._GetRowHeightEstimate() * num_rows PADDING = 10 size_hint = QC.QSize(width, header_size.height() + data_area_height + PADDING) return size_hint def Sort(self, sort_column_type=None, sort_asc=None): if sort_column_type is not None: self._sort_column_type = sort_column_type if sort_asc is not None: self._sort_asc = sort_asc self._SortAndRefreshRows() self.columnListContentsChanged.emit() self._DoStatusChanged() def UpdateDatas(self, datas: typing.Optional[typing.Iterable[object]] = None): if datas is None: # keep it sorted here, which is sometimes useful indices_and_datas = sorted( ((index, data) for (data, index) in self._data_to_indices.items())) datas = [data for (index, data) in indices_and_datas] sort_data_has_changed = False sort_index = self._column_list_status.GetColumnIndexFromType( self._sort_column_type) for data in datas: (display_tuple, sort_tuple) = self._GetDisplayAndSortTuples(data) data_info = (data, display_tuple, sort_tuple) index = self._data_to_indices[data] existing_data_info = self._indices_to_data_info[index] if data_info != existing_data_info: if not sort_data_has_changed: (existing_data, existing_display_tuple, existing_sort_tuple) = existing_data_info if existing_sort_tuple is not None and sort_tuple is not None: # this does not govern secondary sorts, but let's not spam sorts m8 if sort_tuple[sort_index] != existing_sort_tuple[ sort_index]: sort_data_has_changed = True self._indices_to_data_info[index] = data_info self._UpdateRow(index, display_tuple) self.columnListContentsChanged.emit() return sort_data_has_changed def SetNonDupeName(self, obj: object): current_names = {o.GetName() for o in self.GetData() if o is not obj} HydrusSerialisable.SetNonDupeName(obj, current_names) def ReplaceData(self, old_data: object, new_data: object): new_data = QP.ListsToTuples(new_data) data_index = self._data_to_indices[old_data] (display_tuple, sort_tuple) = self._GetDisplayAndSortTuples(new_data) data_info = (new_data, display_tuple, sort_tuple) self._indices_to_data_info[data_index] = data_info del self._data_to_indices[old_data] self._data_to_indices[new_data] = data_index self._UpdateRow(data_index, display_tuple)
class CounterGui(GUIBase): """ FIXME: Please document """ # declare connectors counterlogic1 = Connector(interface='CounterLogic') sigStartCounter = QtCore.Signal() sigStopCounter = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) def on_activate(self): """ Definition and initialisation of the GUI. """ self._counting_logic = self.counterlogic1() ##################### # Configuring the dock widgets # Use the inherited class 'CounterMainWindow' to create the GUI window self._mw = CounterMainWindow() # Setup dock widgets self._mw.centralwidget.hide() self._mw.trace_selection_DockWidget.hide() self._mw.setDockNestingEnabled(True) # Plot labels. self._pw = self._mw.counter_trace_PlotWidget self._pw.setLabel('left', 'Fluorescence', units='counts/s') self._pw.setLabel('bottom', 'Time', units='s') self.curves = [] for i, ch in enumerate(self._counting_logic.get_channels()): if i % 2 == 0: # Create an empty plot curve to be filled later, set its pen self.curves.append( pg.PlotDataItem(pen=pg.mkPen(palette.c1), symbol=None)) self._pw.addItem(self.curves[-1]) self.curves.append( pg.PlotDataItem(pen=pg.mkPen(palette.c2, width=3), symbol=None)) self._pw.addItem(self.curves[-1]) else: self.curves.append( pg.PlotDataItem(pen=pg.mkPen(palette.c3, style=QtCore.Qt.DotLine), symbol='s', symbolPen=palette.c3, symbolBrush=palette.c3, symbolSize=5)) self._pw.addItem(self.curves[-1]) self.curves.append( pg.PlotDataItem(pen=pg.mkPen(palette.c4, width=3), symbol=None)) self._pw.addItem(self.curves[-1]) # setting the x axis length correctly self._pw.setXRange( 0, self._counting_logic.get_count_length() / self._counting_logic.get_count_frequency()) ##################### # Setting default parameters self._mw.count_length_SpinBox.setValue( self._counting_logic.get_count_length()) self._mw.count_freq_SpinBox.setValue( self._counting_logic.get_count_frequency()) self._mw.oversampling_SpinBox.setValue( self._counting_logic.get_counting_samples()) self._display_trace = 1 self._trace_selection = [True, True, True, True] ##################### # Connecting user interactions self._mw.start_counter_Action.triggered.connect(self.start_clicked) self._mw.record_counts_Action.triggered.connect(self.save_clicked) self._mw.count_length_SpinBox.valueChanged.connect( self.count_length_changed) self._mw.count_freq_SpinBox.valueChanged.connect( self.count_frequency_changed) self._mw.oversampling_SpinBox.valueChanged.connect( self.oversampling_changed) if len(self.curves) >= 2: self._mw.trace_1_checkbox.setChecked(True) else: self._mw.trace_1_checkbox.setEnabled(False) self._mw.trace_1_radiobutton.setEnabled(False) if len(self.curves) >= 4: self._mw.trace_2_checkbox.setChecked(True) else: self._mw.trace_2_checkbox.setEnabled(False) self._mw.trace_2_radiobutton.setEnabled(False) if len(self.curves) >= 6: self._mw.trace_3_checkbox.setChecked(True) else: self._mw.trace_3_checkbox.setEnabled(False) self._mw.trace_3_radiobutton.setEnabled(False) if len(self.curves) >= 8: self._mw.trace_4_checkbox.setChecked(True) else: self._mw.trace_4_checkbox.setEnabled(False) self._mw.trace_4_radiobutton.setEnabled(False) self._mw.trace_1_checkbox.stateChanged.connect( self.trace_selection_changed) self._mw.trace_2_checkbox.stateChanged.connect( self.trace_selection_changed) self._mw.trace_3_checkbox.stateChanged.connect( self.trace_selection_changed) self._mw.trace_4_checkbox.stateChanged.connect( self.trace_selection_changed) self._mw.trace_1_radiobutton.setChecked(True) self._mw.trace_1_radiobutton.released.connect( self.trace_display_changed) self._mw.trace_2_radiobutton.released.connect( self.trace_display_changed) self._mw.trace_3_radiobutton.released.connect( self.trace_display_changed) self._mw.trace_4_radiobutton.released.connect( self.trace_display_changed) # Connect the default view action self._mw.restore_default_view_Action.triggered.connect( self.restore_default_view) ##################### # starting the physical measurement self.sigStartCounter.connect(self._counting_logic.startCount) self.sigStopCounter.connect(self._counting_logic.stopCount) ################## # Handling signals from the logic self._counting_logic.sigCounterUpdated.connect(self.updateData) # ToDo: # self._counting_logic.sigCountContinuousNext.connect() # self._counting_logic.sigCountGatedNext.connect() # self._counting_logic.sigCountFiniteGatedNext.connect() # self._counting_logic.sigGatedCounterFinished.connect() # self._counting_logic.sigGatedCounterContinue.connect() self._counting_logic.sigCountingSamplesChanged.connect( self.update_oversampling_SpinBox) self._counting_logic.sigCountLengthChanged.connect( self.update_count_length_SpinBox) self._counting_logic.sigCountFrequencyChanged.connect( self.update_count_freq_SpinBox) self._counting_logic.sigSavingStatusChanged.connect( self.update_saving_Action) self._counting_logic.sigCountingModeChanged.connect( self.update_counting_mode_ComboBox) self._counting_logic.sigCountStatusChanged.connect( self.update_count_status_Action) # Throw a deprecation warning pop-up to encourage users to switch to # TimeSeriesGui/TimeSeriesReaderLogic dialog = QtWidgets.QDialog(self._mw) dialog.setWindowTitle('Deprecation warning') label1 = QtWidgets.QLabel('Deprecation Warning:') label1.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) font = label1.font() font.setPointSize(12) label1.setFont(font) label2 = QtWidgets.QLabel( 'The modules CounterGui, CounterLogic and ' 'NationalInstrumentsXSeries are deprecated for time series ' 'streaming (also called "slow counting") and will be removed in ' 'the future.\nPlease consider switching to TimeSeriesGui, ' 'TimeSeriesReaderLogic and NIXSeriesInStreamer.\nSee default.cfg ' 'for a configuration template.') label2.setAlignment(QtCore.Qt.AlignVCenter) label2.setWordWrap(True) button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok) button_box.setCenterButtons(True) layout = QtWidgets.QVBoxLayout() layout.addWidget(label1) layout.addWidget(label2) layout.addWidget(button_box) button_box.accepted.connect(dialog.accept) dialog.setLayout(layout) dialog.exec() return 0 def show(self): """Make window visible and put it above all other windows. """ QtWidgets.QMainWindow.show(self._mw) self._mw.activateWindow() self._mw.raise_() return def on_deactivate(self): # FIXME: ! """ Deactivate the module """ # disconnect signals self._mw.start_counter_Action.triggered.disconnect() self._mw.record_counts_Action.triggered.disconnect() self._mw.count_length_SpinBox.valueChanged.disconnect() self._mw.count_freq_SpinBox.valueChanged.disconnect() self._mw.oversampling_SpinBox.valueChanged.disconnect() self._mw.trace_1_checkbox.stateChanged.disconnect() self._mw.trace_2_checkbox.stateChanged.disconnect() self._mw.trace_3_checkbox.stateChanged.disconnect() self._mw.trace_4_checkbox.stateChanged.disconnect() self._mw.restore_default_view_Action.triggered.disconnect() self.sigStartCounter.disconnect() self.sigStopCounter.disconnect() self._counting_logic.sigCounterUpdated.disconnect() self._counting_logic.sigCountingSamplesChanged.disconnect() self._counting_logic.sigCountLengthChanged.disconnect() self._counting_logic.sigCountFrequencyChanged.disconnect() self._counting_logic.sigSavingStatusChanged.disconnect() self._counting_logic.sigCountingModeChanged.disconnect() self._counting_logic.sigCountStatusChanged.disconnect() self._mw.close() return def updateData(self): """ The function that grabs the data and sends it to the plot. """ if self._counting_logic.module_state() == 'locked': if 0 < self._counting_logic.countdata_smoothed[( self._display_trace - 1), -1] < 10: self._mw.count_value_Label.setText('{0:,.6f}'.format( self._counting_logic.countdata_smoothed[( self._display_trace - 1), -1])) else: self._mw.count_value_Label.setText('{0:,.0f}'.format( self._counting_logic.countdata_smoothed[( self._display_trace - 1), -1])) x_vals = (np.arange(0, self._counting_logic.get_count_length()) / self._counting_logic.get_count_frequency()) ymax = -1 ymin = 2000000000 for i, ch in enumerate(self._counting_logic.get_channels()): self.curves[2 * i].setData(y=self._counting_logic.countdata[i], x=x_vals) self.curves[2 * i + 1].setData( y=self._counting_logic.countdata_smoothed[i], x=x_vals) if ymax < self._counting_logic.countdata[i].max( ) and self._trace_selection[i]: ymax = self._counting_logic.countdata[i].max() if ymin > self._counting_logic.countdata[i].min( ) and self._trace_selection[i]: ymin = self._counting_logic.countdata[i].min() if ymin == ymax: ymax += 0.1 self._pw.setYRange(0.95 * ymin, 1.05 * ymax) if self._counting_logic.get_saving_state(): self._mw.record_counts_Action.setText('Save') self._mw.count_freq_SpinBox.setEnabled(False) self._mw.oversampling_SpinBox.setEnabled(False) else: self._mw.record_counts_Action.setText('Start Saving Data') self._mw.count_freq_SpinBox.setEnabled(True) self._mw.oversampling_SpinBox.setEnabled(True) if self._counting_logic.module_state() == 'locked': self._mw.start_counter_Action.setText('Stop counter') self._mw.start_counter_Action.setChecked(True) else: self._mw.start_counter_Action.setText('Start counter') self._mw.start_counter_Action.setChecked(False) return 0 def start_clicked(self): """ Handling the Start button to stop and restart the counter. """ if self._counting_logic.module_state() == 'locked': self._mw.start_counter_Action.setText('Start counter') self.sigStopCounter.emit() else: self._mw.start_counter_Action.setText('Stop counter') self.sigStartCounter.emit() return self._counting_logic.module_state() def save_clicked(self): """ Handling the save button to save the data into a file. """ if self._counting_logic.get_saving_state(): self._mw.record_counts_Action.setText('Start Saving Data') self._mw.count_freq_SpinBox.setEnabled(True) self._mw.oversampling_SpinBox.setEnabled(True) self._counting_logic.save_data() else: self._mw.record_counts_Action.setText('Save') self._mw.count_freq_SpinBox.setEnabled(False) self._mw.oversampling_SpinBox.setEnabled(False) self._counting_logic.start_saving() return self._counting_logic.get_saving_state() ######## # Input parameters changed via GUI def trace_selection_changed(self): """ Handling any change to the selection of the traces to display. """ if self._mw.trace_1_checkbox.isChecked(): self._trace_selection[0] = True else: self._trace_selection[0] = False if self._mw.trace_2_checkbox.isChecked(): self._trace_selection[1] = True else: self._trace_selection[1] = False if self._mw.trace_3_checkbox.isChecked(): self._trace_selection[2] = True else: self._trace_selection[2] = False if self._mw.trace_4_checkbox.isChecked(): self._trace_selection[3] = True else: self._trace_selection[3] = False for i, ch in enumerate(self._counting_logic.get_channels()): if self._trace_selection[i]: self._pw.addItem(self.curves[2 * i]) self._pw.addItem(self.curves[2 * i + 1]) else: self._pw.removeItem(self.curves[2 * i]) self._pw.removeItem(self.curves[2 * i + 1]) def trace_display_changed(self): """ Handling of a change in teh selection of which counts should be shown. """ if self._mw.trace_1_radiobutton.isChecked(): self._display_trace = 1 elif self._mw.trace_2_radiobutton.isChecked(): self._display_trace = 2 elif self._mw.trace_3_radiobutton.isChecked(): self._display_trace = 3 elif self._mw.trace_4_radiobutton.isChecked(): self._display_trace = 4 else: self._display_trace = 1 def count_length_changed(self): """ Handling the change of the count_length and sending it to the measurement. """ self._counting_logic.set_count_length( self._mw.count_length_SpinBox.value()) self._pw.setXRange( 0, self._counting_logic.get_count_length() / self._counting_logic.get_count_frequency()) return self._mw.count_length_SpinBox.value() def count_frequency_changed(self): """ Handling the change of the count_frequency and sending it to the measurement. """ self._counting_logic.set_count_frequency( self._mw.count_freq_SpinBox.value()) self._pw.setXRange( 0, self._counting_logic.get_count_length() / self._counting_logic.get_count_frequency()) return self._mw.count_freq_SpinBox.value() def oversampling_changed(self): """ Handling the change of the oversampling and sending it to the measurement. """ self._counting_logic.set_counting_samples( samples=self._mw.oversampling_SpinBox.value()) self._pw.setXRange( 0, self._counting_logic.get_count_length() / self._counting_logic.get_count_frequency()) return self._mw.oversampling_SpinBox.value() ######## # Restore default values def restore_default_view(self): """ Restore the arrangement of DockWidgets to the default """ # Show any hidden dock widgets self._mw.counter_trace_DockWidget.show() # self._mw.slow_counter_control_DockWidget.show() self._mw.slow_counter_parameters_DockWidget.show() self._mw.trace_selection_DockWidget.hide() # re-dock any floating dock widgets self._mw.counter_trace_DockWidget.setFloating(False) self._mw.slow_counter_parameters_DockWidget.setFloating(False) self._mw.trace_selection_DockWidget.setFloating(True) # Arrange docks widgets self._mw.addDockWidget(QtCore.Qt.DockWidgetArea(1), self._mw.counter_trace_DockWidget) self._mw.addDockWidget(QtCore.Qt.DockWidgetArea(8), self._mw.slow_counter_parameters_DockWidget) self._mw.addDockWidget( QtCore.Qt.DockWidgetArea(QtCore.Qt.LeftDockWidgetArea), self._mw.trace_selection_DockWidget) # Set the toolbar to its initial top area self._mw.addToolBar(QtCore.Qt.TopToolBarArea, self._mw.counting_control_ToolBar) return 0 ########## # Handle signals from logic def update_oversampling_SpinBox(self, oversampling): """Function to ensure that the GUI displays the current value of the logic @param int oversampling: adjusted oversampling to update in the GUI in bins @return int oversampling: see above """ self._mw.oversampling_SpinBox.blockSignals(True) self._mw.oversampling_SpinBox.setValue(oversampling) self._mw.oversampling_SpinBox.blockSignals(False) return oversampling def update_count_freq_SpinBox(self, count_freq): """Function to ensure that the GUI displays the current value of the logic @param float count_freq: adjusted count frequency in Hz @return float count_freq: see above """ self._mw.count_freq_SpinBox.blockSignals(True) self._mw.count_freq_SpinBox.setValue(count_freq) self._pw.setXRange( 0, self._counting_logic.get_count_length() / count_freq) self._mw.count_freq_SpinBox.blockSignals(False) return count_freq def update_count_length_SpinBox(self, count_length): """Function to ensure that the GUI displays the current value of the logic @param int count_length: adjusted count length in bins @return int count_length: see above """ self._mw.count_length_SpinBox.blockSignals(True) self._mw.count_length_SpinBox.setValue(count_length) self._pw.setXRange( 0, count_length / self._counting_logic.get_count_frequency()) self._mw.count_length_SpinBox.blockSignals(False) return count_length def update_saving_Action(self, start): """Function to ensure that the GUI-save_action displays the current status @param bool start: True if the measurment saving is started @return bool start: see above """ if start: self._mw.record_counts_Action.setText('Save') self._mw.count_freq_SpinBox.setEnabled(False) self._mw.oversampling_SpinBox.setEnabled(False) else: self._mw.record_counts_Action.setText('Start Saving Data') self._mw.count_freq_SpinBox.setEnabled(True) self._mw.oversampling_SpinBox.setEnabled(True) return start def update_count_status_Action(self, running): """Function to ensure that the GUI-save_action displays the current status @param bool running: True if the counting is started @return bool running: see above """ if running: self._mw.start_counter_Action.setText('Stop counter') else: self._mw.start_counter_Action.setText('Start counter') return running # TODO: def update_counting_mode_ComboBox(self): self.log.warning('Not implemented yet') return 0 # TODO: def update_smoothing_ComboBox(self): self.log.warning('Not implemented yet') return 0
class DataViewer(ViewerBase, QtWidgets.QMainWindow): """ Base class for all Qt DataViewer widgets. This defines a minimal interface, and implemlements the following:: * An automatic call to unregister on window close * Drag and drop support for adding data """ window_closed = QtCore.Signal() _layer_artist_container_cls = QtLayerArtistContainer _layer_style_widget_cls = None LABEL = 'Override this' _toolbar_cls = None tools = [] def __init__(self, session, parent=None): """ :type session: :class:`~glue.core.Session` """ QtWidgets.QMainWindow.__init__(self, parent) ViewerBase.__init__(self, session) self.setWindowIcon(get_qapp().windowIcon()) self._view = LayerArtistWidget( layer_style_widget_cls=self._layer_style_widget_cls, hub=session.hub) self._view.layer_list.setModel(self._layer_artist_container.model) self._tb_vis = {} # store whether toolbars are enabled self.setAttribute(Qt.WA_DeleteOnClose) self.setAcceptDrops(True) self.setAnimated(False) self._toolbars = [] self._warn_close = True self.setContentsMargins(2, 2, 2, 2) self._mdi_wrapper = None # GlueMdiSubWindow that self is embedded in self.statusBar().setStyleSheet("QStatusBar{font-size:10px}") # close window when last plot layer deleted self._layer_artist_container.on_empty(lambda: self.close(warn=False)) self._layer_artist_container.on_changed(self.update_window_title) @property def selected_layer(self): return self._view.layer_list.current_artist() def remove_layer(self, layer): self._layer_artist_container.pop(layer) def dragEnterEvent(self, event): """ Accept the event if it has data layers""" if event.mimeData().hasFormat(LAYER_MIME_TYPE): event.accept() elif event.mimeData().hasFormat(LAYERS_MIME_TYPE): event.accept() else: event.ignore() def dropEvent(self, event): """ Add layers to the viewer if contained in mime data """ if event.mimeData().hasFormat(LAYER_MIME_TYPE): self.request_add_layer(event.mimeData().data(LAYER_MIME_TYPE)) assert event.mimeData().hasFormat(LAYERS_MIME_TYPE) for layer in event.mimeData().data(LAYERS_MIME_TYPE): self.request_add_layer(layer) event.accept() def mousePressEvent(self, event): """ Consume mouse press events, and prevent them from propagating down to the MDI area """ event.accept() apply_roi = set_cursor(Qt.WaitCursor)(ViewerBase.apply_roi) def close(self, warn=True): self._warn_close = warn QtWidgets.QMainWindow.close(self) ViewerBase.close(self) self._warn_close = True def mdi_wrap(self): """Wrap this object in a GlueMdiSubWindow""" from glue.app.qt.mdi_area import GlueMdiSubWindow sub = GlueMdiSubWindow() sub.setWidget(self) self.destroyed.connect(sub.close) sub.resize(self.size()) self._mdi_wrapper = sub return sub @property def position(self): target = self._mdi_wrapper or self pos = target.pos() return pos.x(), pos.y() @position.setter def position(self, xy): x, y = xy self.move(x, y) def move(self, x=None, y=None): """ Move the viewer to a new XY pixel location You can also set the position attribute to a new tuple directly. Parameters ---------- x : int (optional) New x position y : int (optional) New y position """ x0, y0 = self.position if x is None: x = x0 if y is None: y = y0 if self._mdi_wrapper is not None: self._mdi_wrapper.move(x, y) else: QtWidgets.QMainWindow.move(self, x, y) @property def viewer_size(self): if self._mdi_wrapper is not None: sz = self._mdi_wrapper.size() else: sz = self.size() return sz.width(), sz.height() @viewer_size.setter def viewer_size(self, value): width, height = value self.resize(width, height) if self._mdi_wrapper is not None: self._mdi_wrapper.resize(width, height) def closeEvent(self, event): """ Call unregister on window close """ if not self._confirm_close(): event.ignore() return if self._hub is not None: self.unregister(self._hub) self._layer_artist_container.clear_callbacks() self._layer_artist_container.clear() super(DataViewer, self).closeEvent(event) event.accept() self.window_closed.emit() def _confirm_close(self): """Ask for close confirmation :rtype: bool. True if user wishes to close. False otherwise """ if self._warn_close and ( not os.environ.get('GLUE_TESTING')) and self.isVisible(): buttons = QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel dialog = QtWidgets.QMessageBox.warning( self, "Confirm Close", "Do you want to close this window?", buttons=buttons, defaultButton=QtWidgets.QMessageBox.Cancel) return dialog == QtWidgets.QMessageBox.Ok return True def _confirm_large_data(self, data): if not settings.SHOW_LARGE_DATA_WARNING: # Ignoring large data warning return True else: warn_msg = ( "WARNING: Data set has %i points, and may render slowly." " Continue?" % data.size) title = "Add large data set?" ok = QtWidgets.QMessageBox.Ok cancel = QtWidgets.QMessageBox.Cancel buttons = ok | cancel result = QtWidgets.QMessageBox.question(self, title, warn_msg, buttons=buttons, defaultButton=cancel) return result == ok def layer_view(self): return self._view def options_widget(self): return QtWidgets.QWidget() def addToolBar(self, tb): super(DataViewer, self).addToolBar(tb) self._toolbars.append(tb) self._tb_vis[tb] = True def initialize_toolbar(self): from glue.config import viewer_tool self.toolbar = self._toolbar_cls(self) for tool_id in self.tools: mode_cls = viewer_tool.members[tool_id] mode = mode_cls(self) self.toolbar.add_tool(mode) self.addToolBar(self.toolbar) def show_toolbars(self): """Re-enable any toolbars that were hidden with `hide_toolbars()` Does not re-enable toolbars that were hidden by other means """ for tb in self._toolbars: if self._tb_vis.get(tb, False): tb.setEnabled(True) def hide_toolbars(self): """ Disable all the toolbars in the viewer. This action can be reversed by calling `show_toolbars()` """ for tb in self._toolbars: self._tb_vis[tb] = self._tb_vis.get(tb, False) or tb.isVisible() tb.setEnabled(False) def set_focus(self, state): if state: css = """ DataViewer { border: 2px solid; border-color: rgb(56, 117, 215); } """ self.setStyleSheet(css) self.show_toolbars() else: css = """ DataViewer { border: none; } """ self.setStyleSheet(css) self.hide_toolbars() def __str__(self): return self.LABEL def unregister(self, hub): """ Override to perform cleanup operations when disconnecting from hub """ pass @property def window_title(self): return str(self) def update_window_title(self): self.setWindowTitle(self.window_title) def set_status(self, message): sb = self.statusBar() sb.showMessage(message)
class NewFileInDirectoryWatcher(QtCore.QObject): """ This class watches a given filepath for any new files with a given file extension added to it. Typical usage:: def callback_fcn(path): print(path) watcher = NewFileInDirectoryWatcher(example_path, file_types = ['.tif', '.tiff']) watcher.file_added.connect(callback_fcn) """ file_added = QtCore.Signal(str) def __init__(self, path=None, file_types=None, activate=False): """ :param path: path to folder which will be watched :param file_types: list of file types which will be watched for, e.g. ['.tif', '.jpeg] :param activate: whether or not the Watcher will already emit signals """ super(NewFileInDirectoryWatcher, self).__init__() self._file_system_watcher = QtCore.QFileSystemWatcher() if path is None: path = os.getcwd() self._file_system_watcher.addPath(path) self._files_in_path = os.listdir(path) self._file_system_watcher.directoryChanged.connect( self._directory_changed) self._file_system_watcher.blockSignals(~activate) self._file_changed_watcher = QtCore.QFileSystemWatcher() self._file_changed_watcher.fileChanged.connect(self._file_changed) if file_types is None: self.file_types = set([]) else: self.file_types = set(file_types) @property def path(self): return self._file_system_watcher.directories()[0] @path.setter def path(self, new_path): if len(self._file_system_watcher.directories()): self._file_system_watcher.removePath( self._file_system_watcher.directories()[0]) self._file_system_watcher.addPath(new_path) self._files_in_path = os.listdir(new_path) def activate(self): """ activates the watcher to emit signals when a new file is added """ self._file_system_watcher.blockSignals(False) def deactivate(self): """ deactivates the watcher so it will not emit a signal when a new file is added """ self._file_system_watcher.blockSignals(True) def _directory_changed(self): """ internal function which determines whether the change in directory is an actual new file. If a new file was detected it looks if it has the right extension and checks the file size. When the file is not completely written yet it watches it for changes and will call the _file_changed function which wil acctually emit the signal. """ files_now = os.listdir(self.path) files_added = [f for f in files_now if not f in self._files_in_path] if len(files_added) > 0: new_file_path = os.path.join(str(self.path), files_added[-1]) # abort if the new_file added is actually a directory... if os.path.isdir(new_file_path): self._files_in_path = files_now return valid_file = False for file_type in self.file_types: if new_file_path.endswith(file_type): valid_file = True break if valid_file: if self._file_closed(new_file_path): self.file_added.emit(new_file_path) else: self._file_changed_watcher.addPath(new_file_path) self._files_in_path = files_now def _file_closed(self, path): """ Checks whether a file is used by other processes. """ # since it is hard to ask the operating system for this directly, the change in file size is checked. size1 = os.stat(path).st_size time.sleep(0.10) size2 = os.stat(path).st_size return size1 == size2 def _file_changed(self, path): """ internal function callback for the file_changed_watcher. The watcher is invoked if a new file is detected but the file is still below 100 bytes (basically only the file handle created, and no data yet). The _file_changed callback function is then invoked when the data is completely written into the file. To ensure that everything is correct this function also checks whether the file is above 100 byte after the system sends a file changed signal. :param path: file path of the watched file """ if self._file_closed(path): self.file_added.emit(path) self._file_changed_watcher.removePath(path)
class ClassWithSignal(QtCore.QObject): signal = QtCore.Signal()
class Model(QtCore.QAbstractItemModel): dataNeedsRefresh = QtCore.Signal() dataAboutToBeRefreshed = QtCore.Signal() dataRefreshed = QtCore.Signal() @property def rootItem(self): return self._rootItem @property def dataSource(self): return self._dataSource @property def sorter(self): return self._sorter def __init__(self): super(Model, self).__init__() self._columnCount = 0 self._dataSource = None self._sorter = None self._uuidLookup = {} self._totalItemCount = 0 self._sorter = ModelItemSorter(self) self._sorter.sortingChanged.connect(self.doSort) self._maintainSorted = False self._headerItem = self.newItem() self._rootItem = self.newItem() self._rootItem.setModel(self) self._rootItem.setTotalChildCount(-1) self.dataNeedsRefresh.connect(self.requestRefresh) def addToTotalItemCount(self, count): self._totalItemCount += count def num_items(self): return self._totalItemCount def setColumnCount(self, columnCount): countDelta = columnCount - self._columnCount if countDelta > 0: self.insertColumns(self._columnCount, countDelta) elif countDelta < 0: self.removeColumns(self._columnCount + countDelta, -countDelta) def rowCount(self, parentIndex=QtCore.QModelIndex()): return self.itemFromIndex(parentIndex).childCount() def columnCount(self, parentIndex=QtCore.QModelIndex()): return self._columnCount def index(self, row, column, parentIndex=QtCore.QModelIndex()): if self.hasIndex(row, column, parentIndex): childItem = self.itemFromIndex(parentIndex).child(row) if childItem: return self.createIndex(row, column, childItem) return QtCore.QModelIndex() def parent(self, index): if index.isValid(): parentItem = self.itemFromIndex(index, False).parent() return self.indexFromItem(parentItem) return QtCore.QModelIndex() def flags(self, index): if not index.isValid(): return self._rootItem.flags() return self.itemFromIndex(index, False).flags(index.column()) def data(self, index, role=QtCore.Qt.DisplayRole): if index.isValid(): return self.itemFromIndex(index, False).data(index.column(), role) return None def dataType(self, index): dataType = self.data(index, common.ROLE_TYPE) if dataType in common.TYPES: return dataType dataType = self.headerData(index.column(), QtCore.Qt.Horizontal, common.ROLE_TYPE) if dataType in common.TYPES: return dataType return common.TYPE_DEFAULT def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): if orientation == QtCore.Qt.Horizontal: return self._headerItem.data(section, role) return None def indexFromItem(self, item): if item and item.model is self and item.parent(): row = item.parent().childPosition(item) if row >= 0: return self.createIndex(row, 0, item) return QtCore.QModelIndex() def itemFromIndex(self, index, validate=True): if validate: if not index.isValid(): return self._rootItem return index.internalPointer() def uuidFromIndex(self, index): item = self.itemFromIndex(index) if item: return item.uuid return None def indexFromUuid(self, uuid): if uuid is not None: modelName = (self.dataSource or self).__class__.__name__ try: item = self._uuidLookup[uuid] except KeyError: pass else: if item.model is self and item.uuid == uuid: return self.indexFromItem(item) else: _LOGGER.debug("%s: removing invalid uniqueId key %s -> %s" % (modelName, uuid, item)) del self._uniqueIdLookup[uuid] # lookup failed, search for the item the slow way _LOGGER.debug("%s: uniqueId lookup failed; performing exhaustive search for %s" % (modelName, uuid)) for item in self.iterItems(): if item.uuid == uuid: _LOGGER.debug("%s: adding new uniqueId key %s -> %s" % (modelName, uuid, item)) self._uuidLookup[uuid] = item return self.indexFromItem(item) return QtCore.QModelIndex() def setData(self, index, value, role=QtCore.Qt.DisplayRole): return self.setItemData(index, {role: value}) def setItemData(self, index, roles): item = self.itemFromIndex(index) column = index.column() for (role, value) in roles.items(): item.setData(value, column, role) self.dataChanged.emit(index, index) return True def setHeaderData(self, section, orientation, value, role=QtCore.Qt.DisplayRole): if orientation == QtCore.Qt.Horizontal: if self._headerItem.setData(value, section, role): success = True if role == QtCore.Qt.DisplayRole and not self.headerData(section, orientation, common.ROLE_NAME): success = self._headerItem.setData(value, section, common.ROLE_NAME) if success: self.headerDataChanged.emit(orientation, section, section) return success return False def _emitDataChanged(self, itemList): for (parentItem, first, last) in self._splitContiguousSegments(itemList): _LOGGER.debug("emitting data changed: rows %d-%d of parent %s" % (first, last, parentItem)) parentIndex = self.indexFromItem(parentItem) startIndex = self.index(first, 0, parentIndex) endIndex = self.index(last, self._columnCount - 1, parentIndex) self.dataChanged.emit(startIndex, endIndex) def newItem(self): item = ModelItem(self._columnCount) return item def clear(self): self.beginResetModel() self._rootItem.removeAllChildren() self._rootItem.setTotalChildCount(-1) self._totalItemCount = 0 self._uuidLookup = {} self.endResetModel() def appendRow(self, parentIndex=QtCore.QModelIndex()): return self.appendRows(1, parentIndex) def appendRows(self, count, parentIndex=QtCore.QModelIndex()): return self.insertRows(self.rowCount(parentIndex), count, parentIndex) def insertRows(self, position, count, parentIndex=QtCore.QModelIndex()): return self.insertItems(position, [self.newItem() for i in range(count)], parentIndex) def insertItem(self, position, item, parentIndex=QtCore.QModelIndex()): return self.insertItems(position, [item], parentIndex) def insertItems(self, position, itemList, parentIndex=QtCore.QModelIndex()): parentItem = self.itemFromIndex(parentIndex) if not 0 <= position <= parentItem.childCount(): return False if not itemList: return True self.beginInsertRows(parentIndex, position, position + len(itemList) - 1) for item in itemList: uuid = item.uuid if uuid is not None: self._uuidLookup[uuid] = item if item.hasChildren(): for descendant in item.iterTree(includeRoot=False): uuid = descendant.uuid if uuid is not None: self._uuidLookup[uuid] = descendant assert parentItem.insertChildren(position, itemList) self.endInsertRows() return True def appendItem(self, item, parentIndex=QtCore.QModelIndex()): return self.appendItems([item], parentIndex) def appendItems(self, itemList, parentIndex=QtCore.QModelIndex()): return self.insertItems(self.rowCount(parentIndex), itemList, parentIndex) def setItems(self, itemList): self.beginResetModel() self._rootItem.removeAllChildren() self._uuidLookup = {} self._rootItem.appendChildren(itemList) for item in self.iterItems(): if item.uuid is not None: self._uuidLookup[item.uuid] = item self.endResetModel() self._rootItem.setTotalChildCount(len(itemList)) def removeRows(self, position, count, parentIndex=QtCore.QModelIndex()): if count < 0 or position < 0: return False if count == 0: return True parentItem = self.itemFromIndex(parentIndex) if position + count > parentItem.childCount(): return False return self.removeItems([parentItem.child(i) for i in range(position, position + count)]) def removeItem(self, item): return self.removeItems([item]) def removeItems(self, itemList): if not itemList: return True for item in itemList: if item.model is not self: return False itemsToRemove = set(itemList) for item in itemsToRemove: for descendant in item.iterTree(includeRoot=True): if descendant.uuid is not None: try: del self._uuidLookup[descendant.uuid] except KeyError: pass segments = list(self._splitContiguousSegments(itemsToRemove)) for (parentItem, first, last) in reversed(segments): if parentItem.model is self: _LOGGER.debug("removing: rows %d-%d of parent %s" % (first, last, parentItem)) parentIndex = self.indexFromItem(parentItem) self.beginRemoveRows(parentIndex, first, last) parentItem.removeChildren(first, last - first + 1) self._totalItemCount -= last - first + 1 self.endRemoveRows() return True def disableItem(self, item): return self.disableItems([item]) def disableItems(self, itemList): pass def reviveItem(self, item): return self.reviveItems([item]) def reviveItems(self, itemList): pass def enableItem(self, item): return self.enableItems([item]) def enableItems(self, itemList): pass def moveRow(self, fromPosition, toPosition, fromParent=QtCore.QModelIndex(), toParent=QtCore.QModelIndex()): return self.moveRows(fromPosition, 1, toPosition, fromParent, toParent) def moveRows(self, fromPosition, count, toPosition, fromParent=QtCore.QModelIndex(), toParent=QtCore.QModelIndex()): if fromPosition < 0 or count < 0: return False if count == 0: return True fromParentItem = self.itemFromIndex(fromParent) toParentItem = self.itemFromIndex(toParent) if fromPosition + count > fromParentItem.childCount() or toPosition > toParentItem.childCount(): return False if fromParentItem is toParentItem and fromPosition <= toPosition < fromPosition + count: return True itemsToMove = fromParentItem.children()[fromPosition:fromPosition + count] assert len(itemsToMove) == count sourceLast = fromPosition + count - 1 if fromPosition <= toPosition <= sourceLast and fromParent == toParent: return True if toPosition > sourceLast: toPosition += 1 if not self.beginMoveRows(fromParent, fromPosition, sourceLast, toParent, toPosition): return False assert fromParentItem.removeChildren(fromPosition, count) if fromParentItem is toParentItem and fromPosition + count < toPosition: toPosition -= count assert toParentItem.insertChildren(toPosition, itemsToMove) self.endMoveRows() return True def moveItem(self, item, toPosition, toParent=QtCore.QModelIndex()): return self.moveItems([item], toPosition, toParent) def moveItems(self, itemList, toPosition, toParent=QtCore.QModelIndex()): if not itemList: return True for item in itemList: if item.model is not self: return False toParentItem = self.itemFromIndex(toParent) if toPosition < 0: toPosition = toParentItem.childCount() - 1 elif toPosition > toParentItem.childCount() - 1: toPosition = 0 itemSet = set(itemList) parentItem = toParentItem while parentItem: if parentItem in itemSet: return False parentItem = parentItem.parent() segments = list(self._splitContiguousSegments(itemList)) for (fromParentItem, first, last) in reversed(segments): fromParentIndex = self.indexFromItem(fromParentItem) toParentIndex = self.indexFromItem(toParentItem) count = last - first + 1 if not self.moveRows(first, count, toPosition, fromParentIndex, toParentIndex): return False if fromParentItem is toParentItem and toPosition > last: toPosition -= count self._emitDataChanged(itemList) return True def insertColumns(self, position, count, parentIndex=QtCore.QModelIndex()): if position < 0 or position > self._columnCount: return False if count > 0: self.beginInsertColumns(QtCore.QModelIndex(), position, position + count - 1) self._headerItem.insertColumns(position, count) self._rootItem.insertColumns(position, count) self._columnCount += count self.endInsertColumns() return True def removeColumns(self, position, count, parentIndex=QtCore.QModelIndex()): if position < 0 or position + count > self._columnCount: return False if count > 0: self.beginRemoveColumns(QtCore.QModelIndex(), position, position + count - 1) self._headerItem.removeColumns(position, count) self._rootItem.removeColumns(position, count) self._columnCount -= count self.endRemoveColumns() return True def hasChildren(self, parentIndex=QtCore.QModelIndex()): item = self.itemFromIndex(parentIndex) if item.childCount() > 0: return True return False def doSort(self, refresh=True): """ """ # do the actual sort if self._dataSource: try: self._dataSource.sortByColumns(self._sorter.sortColumns, self._sorter.sortDirections, refresh=refresh) return except NotImplementedError: pass # save persistent indexes oldPersistentMap = dict([(self.itemFromIndex(idx), idx.row()) for idx in self.persistentIndexList()]) self.layoutAboutToBeChanged.emit() # sort all items self._sorter.sortItems([self._rootItem]) # update persistent indexes fromList = [] toList = [] for (item, oldRow) in oldPersistentMap.items(): newIdx = self.indexFromItem(item) for column in range(self._columnCount): fromList.append(self.createIndex(oldRow, column, newIdx.internalPointer())) toList.append(self.createIndex(newIdx.row(), column, newIdx.internalPointer())) self.changePersistentIndexList(fromList, toList) self.layoutChanged.emit() def beginResetModel(self): self.modelAboutToBeReset.emit() def endResetModel(self): persistentIndexList = self.persistentIndexList() for index in persistentIndexList: self.changePersistentIndex(index, QtCore.QModelIndex()) self.modelReset.emit() def setDataSource(self, dataSource): if self._dataSource: self._dataSource.dataNeedsRefresh.disconnect(self.dataNeedsRefresh) self._dataSource.setModel(None) self._dataSource.setParent(None) self._dataSource = dataSource self.clear() if self._dataSource: self._dataSource.setModel(self) self._dataSource.setParent(self) self.setColumnCount(len(self._dataSource.headerItem)) self._headerItem = self._dataSource.headerItem self._dataSource.dataNeedsRefresh.connect(self.dataNeedsRefresh) self.dataNeedsRefresh.emit() else: self.setColumnCount(0) def requestRefresh(self): if self._dataSource and (reload or self._dataSource.needToRefresh): self._dataSource.setNeedToRefresh(False) self.dataAboutToBeRefreshed.emit() itemList = self._dataSource.fetchItems(QtCore.QModelIndex()) self.setItems(itemList) self.dataRefreshed.emit() return True def iterItems(self, includeRoot=True): return self._rootItem.iterTree(includeRoot=includeRoot) def _splitContiguousSegments(self, itemList): partitions = {} for item in itemList: index = self.indexFromItem(item) parentIndex = index.parent() if parentIndex not in partitions: partitions[parentIndex] = set() parentIndex[parentIndex].add(index.row()) for parentIndex in sorted(partitions): parentItem = self.itemFromIndex(parentIndex) rowList = partitions[parentIndex] sequences = [map(operator.itemgetter(1), g) for k, g in itertools.groupby(enumerate(sorted(rowList)), calcGroupingKey)] for seq in sequences: yield (parentItem, seq[0], seq[-1])
class mpvWidget(QW.QWidget): launchMediaViewer = QC.Signal() def __init__(self, parent): QW.QWidget.__init__(self, parent) self._canvas_type = ClientGUICommon.CANVAS_PREVIEW self._stop_for_slideshow = False # This is necessary since PyQT stomps over the locale settings needed by libmpv. # This needs to happen after importing PyQT before creating the first mpv.MPV instance. locale.setlocale(locale.LC_NUMERIC, 'C') self.setAttribute(QC.Qt.WA_DontCreateNativeAncestors) self.setAttribute(QC.Qt.WA_NativeWindow) loglevel = 'debug' if HG.mpv_report_mode else 'fatal' # loglevels: fatal, error, debug self._player = mpv.MPV(wid=str(int(self.winId())), log_handler=log_handler, loglevel=loglevel) # hydev notes on OSC: # OSC is by default off, default input bindings are by default off # difficult to get this to intercept mouse/key events naturally, so you have to pipe them to the window with 'command', but this is not excellent # general recommendation when using libmpv is to just implement your own stuff anyway, so let's do that for prototype #self._player[ 'input-default-bindings' ] = True self.UpdateConf() self._player.loop = True # this makes black screen for audio (rather than transparent) self._player.force_window = True # this actually propagates up to the OS-level sound mixer lmao, otherwise defaults to ugly hydrus filename self._player.title = 'hydrus mpv player' # pass up un-button-pressed mouse moves to parent, which wants to do cursor show/hide self.setMouseTracking(True) #self.setFocusPolicy(QC.Qt.StrongFocus)#Needed to get key events self._player.input_cursor = False #Disable mpv mouse move/click event capture self._player.input_vo_keyboard = False #Disable mpv key event capture, might also need to set input_x11_keyboard self._media = None self._file_is_loaded = False self._disallow_seek_on_this_file = False self._times_to_play_gif = 0 self._current_seek_to_start_count = 0 self._InitialiseMPVCallbacks() self.destroyed.connect(self._player.terminate) HG.client_controller.sub(self, 'UpdateAudioMute', 'new_audio_mute') HG.client_controller.sub(self, 'UpdateAudioVolume', 'new_audio_volume') HG.client_controller.sub(self, 'UpdateConf', 'notify_new_options') HG.client_controller.sub(self, 'SetLogLevel', 'set_mpv_log_level') self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [], catch_mouse=True) def _GetAudioOptionNames(self): if self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER: if HG.client_controller.new_options.GetBoolean( 'media_viewer_uses_its_own_audio_volume'): return ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_MEDIA_VIEWER] elif self._canvas_type == ClientGUICommon.CANVAS_PREVIEW: if HG.client_controller.new_options.GetBoolean( 'preview_uses_its_own_audio_volume'): return ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_PREVIEW] return ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_GLOBAL] def _GetCorrectCurrentMute(self): (global_mute_option_name, global_volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_GLOBAL] mute_option_name = global_mute_option_name if self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER: (mute_option_name, volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_MEDIA_VIEWER] elif self._canvas_type == ClientGUICommon.CANVAS_PREVIEW: (mute_option_name, volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_PREVIEW] return HG.client_controller.new_options.GetBoolean( mute_option_name) or HG.client_controller.new_options.GetBoolean( global_mute_option_name) def _GetCorrectCurrentVolume(self): (mute_option_name, volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_GLOBAL] if self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER: if HG.client_controller.new_options.GetBoolean( 'media_viewer_uses_its_own_audio_volume'): (mute_option_name, volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_MEDIA_VIEWER] elif self._canvas_type == ClientGUICommon.CANVAS_PREVIEW: if HG.client_controller.new_options.GetBoolean( 'preview_uses_its_own_audio_volume'): (mute_option_name, volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_PREVIEW] return HG.client_controller.new_options.GetInteger(volume_option_name) def _InitialiseMPVCallbacks(self): def qt_file_loaded_event(): if not QP.isValid(self): return self._file_is_loaded = True def qt_seek_event(): if not QP.isValid(self): return if not self._file_is_loaded: return current_timestamp_s = self._player.time_pos if self._media is not None and current_timestamp_s is not None and current_timestamp_s <= 1.0: self._current_seek_to_start_count += 1 if self._stop_for_slideshow: self.Pause() if self._times_to_play_gif != 0 and self._current_seek_to_start_count >= self._times_to_play_gif: self.Pause() player = self._player @player.event_callback(mpv.MpvEventID.SEEK) def seek_event(event): QP.CallAfter(qt_seek_event) @player.event_callback(mpv.MpvEventID.FILE_LOADED) def file_loaded_event(event): QP.CallAfter(qt_file_loaded_event) def ClearMedia(self): self.SetMedia(None) def GetAnimationBarStatus(self): buffer_indices = None if self._media is None or not self._file_is_loaded: current_frame_index = 0 current_timestamp_ms = 0 paused = True else: current_timestamp_s = self._player.time_pos if current_timestamp_s is None: current_frame_index = 0 current_timestamp_ms = None else: current_timestamp_ms = current_timestamp_s * 1000 num_frames = self._media.GetNumFrames() if num_frames is None or num_frames == 1: current_frame_index = 0 else: current_frame_index = int( round( (current_timestamp_ms / self._media.GetDuration()) * num_frames)) current_frame_index = min(current_frame_index, num_frames - 1) current_timestamp_ms = min(current_timestamp_ms, self._media.GetDuration()) paused = self._player.pause return (current_frame_index, current_timestamp_ms, paused, buffer_indices) def GotoPreviousOrNextFrame(self, direction): if not self._file_is_loaded: return command = 'frame-step' if direction == 1: command = 'frame-step' elif direction == -1: command = 'frame-back-step' self._player.command(command) def HasPlayedOnceThrough(self): return self._current_seek_to_start_count > 0 def IsPlaying(self): return not self._player.pause def Pause(self): self._player.pause = True def PausePlay(self): self._player.pause = not self._player.pause def Play(self): self._player.pause = False def ProcessApplicationCommand(self, command: CAC.ApplicationCommand): command_processed = True if command.IsSimpleCommand(): action = command.GetSimpleAction() if action == CAC.SIMPLE_PAUSE_MEDIA: self.Pause() elif action == CAC.SIMPLE_PAUSE_PLAY_MEDIA: self.PausePlay() elif action == CAC.SIMPLE_MEDIA_SEEK_DELTA: (direction, duration_ms) = command.GetSimpleData() self.SeekDelta(direction, duration_ms) elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM: if self._media is not None: ClientGUIMedia.OpenExternally(self._media) elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER: self.window().close() elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER and self._canvas_type == ClientGUICommon.CANVAS_PREVIEW: self.launchMediaViewer.emit() else: command_processed = False else: command_processed = False return command_processed def Seek(self, time_index_ms): if not self._file_is_loaded: return if self._disallow_seek_on_this_file: return time_index_s = time_index_ms / 1000 try: self._player.seek(time_index_s, reference='absolute') except: self._disallow_seek_on_this_file = True # on some files, this seems to fail with a SystemError lmaoooo # with the same elegance, we will just pass all errors def SeekDelta(self, direction, duration_ms): if not self._file_is_loaded: return current_timestamp_s = self._player.time_pos new_timestamp_ms = max(0, (current_timestamp_s * 1000) + (direction * duration_ms)) if new_timestamp_ms > self._media.GetDuration(): new_timestamp_ms = 0 self.Seek(new_timestamp_ms) def SetCanvasType(self, canvas_type): self._canvas_type = canvas_type if self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER: shortcut_set = 'media_viewer_media_window' else: shortcut_set = 'preview_media_window' self._my_shortcut_handler.SetShortcuts([shortcut_set]) def SetLogLevel(self, level: str): self._player.set_loglevel(level) def SetMedia(self, media, start_paused=False): if media == self._media: return self._file_is_loaded = False self._disallow_seek_on_this_file = False self._media = media self._times_to_play_gif = 0 if self._media is not None and self._media.GetMime( ) == HC.IMAGE_GIF and not HG.client_controller.new_options.GetBoolean( 'always_loop_gifs'): hash = self._media.GetHash() path = HG.client_controller.client_files_manager.GetFilePath( hash, HC.IMAGE_GIF) self._times_to_play_gif = HydrusImageHandling.GetTimesToPlayGIF( path) self._current_seek_to_start_count = 0 if self._media is None: self._player.pause = True if len(self._player.playlist) > 0: try: self._player.command('playlist-remove', 'current') except: pass # sometimes happens after an error--screw it else: hash = self._media.GetHash() mime = self._media.GetMime() client_files_manager = HG.client_controller.client_files_manager path = client_files_manager.GetFilePath(hash, mime) self._player.visibility = 'always' self._stop_for_slideshow = False self._player.pause = True try: self._player.loadfile(path) except Exception as e: HydrusData.ShowException(e) self._player.volume = self._GetCorrectCurrentVolume() self._player.mute = self._GetCorrectCurrentMute() self._player.pause = start_paused def StopForSlideshow(self, value): self._stop_for_slideshow = value def UpdateAudioMute(self): self._player.mute = self._GetCorrectCurrentMute() def UpdateAudioVolume(self): self._player.volume = self._GetCorrectCurrentVolume() def UpdateConf(self): mpv_config_path = HG.client_controller.GetMPVConfPath() if not os.path.exists(mpv_config_path): default_mpv_config_path = HG.client_controller.GetDefaultMPVConfPath( ) if not os.path.exists(default_mpv_config_path): HydrusData.ShowText( 'There is no default mpv configuration file to load! Perhaps there is a problem with your install?' ) return else: HydrusPaths.MirrorFile(default_mpv_config_path, mpv_config_path) #To load an existing config file (by default it doesn't load the user/global config like standalone mpv does): load_f = getattr(mpv, '_mpv_load_config_file', None) if load_f is not None and callable(load_f): try: load_f(self._player.handle, mpv_config_path.encode('utf-8')) # pylint: disable=E1102 except Exception as e: HydrusData.ShowText( 'MPV could not load its configuration file! This was probably due to an invalid parameter value inside the conf. The error follows:' ) HydrusData.ShowException(e) else: HydrusData.Print( 'Was unable to load mpv.conf--has the MPV API changed?')
class FilterMenu(QtWidgets.QMenu): activate = QtCore.Signal(int) checkedItemsChanged = QtCore.Signal(list) def __init__(self, parent=None): super(QtWidgets.QMenu, self).__init__(parent) self._list_view = QtWidgets.QListView(parent) self._list_view.setFrameStyle(0) model = SequenceStandardItemModel() self._list_view.setModel(model) self._model = model self.addItem("(select all)") model[0].setTristate(True) action = QtWidgets.QWidgetAction(self) action.setDefaultWidget(self._list_view) self.addAction(action) self.installEventFilter(self) self._list_view.installEventFilter(self) self._list_view.window().installEventFilter(self) model.itemChanged.connect(self.on_model_item_changed) self._list_view.pressed.connect(self.on_list_view_pressed) self.activate.connect(self.on_activate) def on_list_view_pressed(self, index): item = self._model.itemFromIndex(index) # item is None when the button has not been used yet (and this is # triggered via enter) if item is not None: item.checked = not item.checked def on_activate(self, row): target_item = self._model[row] for item in self._model[1:]: item.checked = item is target_item def on_model_item_changed(self, item): model = self._model model.blockSignals(True) if item.index().row() == 0: # (un)check first => (un)check others for other in model[1:]: other.checked = item.checked items_checked = [item for item in model[1:] if item.checked] num_checked = len(items_checked) if num_checked == 0 or num_checked == len(model) - 1: model[0].checked = bool(num_checked) elif num_checked == 1: model[0].checked = 'partial' else: model[0].checked = 'partial' model.blockSignals(False) is_checked = [i for i, item in enumerate(model[1:]) if item.checked] self.checkedItemsChanged.emit(is_checked) def addItem(self, text): item = StandardItem(text) # not editable item.setFlags(QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled) item.checked = True self._model.appendRow(item) def addItems(self, items): for item in items: self.addItem(item) def eventFilter(self, obj, event): event_type = event.type() if event_type == QtCore.QEvent.KeyRelease: key = event.key() # tab key closes the popup if obj == self._list_view.window() and key == QtCore.Qt.Key_Tab: self.hide() # return key activates *one* item and closes the popup # first time the key is sent to the menu, afterwards to # list_view elif (obj == self._list_view and key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return)): self.activate.emit(self._list_view.currentIndex().row()) self.hide() return True return False
class Measurement(QtCore.QObject): """ Base class for ScopeFoundry Measurement objects to subclass, implement :meth:`setup`, :meth:`run` for measurements with graphical interfaces, subclass and additionally implement :meth:`setup_figure`, :meth:`update_display` """ measurement_sucessfully_completed = QtCore.Signal(()) """signal sent when full measurement is complete""" measurement_interrupted = QtCore.Signal(()) """signal sent when measurement is complete due to an interruption""" #measurement_state_changed = QtCore.Signal(bool) # signal sent when measurement started or stopped def __init__(self, app, name=None): """ :type app: BaseMicroscopeApp """ QtCore.QObject.__init__(self) self.log = get_logger_from_class(self) if not hasattr(self, 'name'): self.name = self.__class__.__name__ if name is not None: self.name = name self.app = app self.display_update_period = 0.1 # seconds self.display_update_timer = QtCore.QTimer(self) self.display_update_timer.timeout.connect( self._on_display_update_timer) self.acq_thread = None self.interrupt_measurement_called = False #self.logged_quantities = OrderedDict() self.settings = LQCollection() self.operations = OrderedDict() self.activation = self.settings.New( 'activation', dtype=bool, ro=False) # does the user want to the thread to be running self.running = self.settings.New( 'running', dtype=bool, ro=True) # is the thread actually running? self.progress = self.settings.New('progress', dtype=float, unit="%", si=False, ro=True) self.settings.New( 'profile', dtype=bool, initial=False ) # Run a profile on the run to find performance problems self.activation.updated_value[bool].connect(self.start_stop) self.add_operation("start", self.start) self.add_operation("interrupt", self.interrupt) #self.add_operation('terminate', self.terminate) #self.add_operation("setup", self.setup) #self.add_operation("setup_figure", self.setup_figure) self.add_operation("update_display", self.update_display) self.add_operation('show_ui', self.show_ui) if hasattr(self, 'ui_filename'): self.load_ui() self.setup() def setup(self): """Override this to set up logged quantities and gui connections Runs during __init__, before the hardware connection is established Should generate desired LoggedQuantities""" pass #raise NotImplementedError() def setup_figure(self): """ Overide setup_figure to build graphical interfaces. This function is run on ScopeFoundry startup. """ self.log.info("Empty setup_figure called") pass @QtCore.Slot() def start(self): """ Starts the measurement calls *pre_run* creates acquisition thread runs thread starts display timer which calls update_display periodically calls post run when thread is finished """ #self.start_stop(True) self.activation.update_value(True) def _start(self): """ Starts the measurement calls *pre_run* creates acquisition thread runs thread starts display timer which calls update_display periodically calls post run when thread is finished """ self.log.info("measurement {} start".format(self.name)) self.interrupt_measurement_called = False if (self.acq_thread is not None) and self.is_measuring(): raise RuntimeError( "Cannot start a new measurement while still measuring") #self.acq_thread = threading.Thread(target=self._thread_run) self.acq_thread = MeasurementQThread(self) self.acq_thread.finished.connect(self.post_run) #self.measurement_state_changed.emit(True) self.running.update_value(True) self.pre_run() self.acq_thread.start() self.t_start = time.time() self.display_update_timer.start(self.display_update_period * 1000) def pre_run(self): """Override this method to enable main-thread initialization prior to measurement thread start""" pass def run(self): """ *run* method runs in an separate thread and is used for data acquisition No GUI updates should occur within the *run* function, any Qt related GUI work should occur in :meth:`update_display` """ if hasattr(self, '_run'): self.log.warning("warning _run is deprecated, use run") self._run() else: raise NotImplementedError( "Measurement {}.run() not defined".format(self.name)) def post_run(self): """Override this method to enable main-thread finalization after to measurement thread completes""" pass def _thread_run(self): """ This function governs the behavior of the measurement thread. """ self.set_progress( 50.) # set progress bars to default run position at 50% try: if self.settings['profile']: import cProfile profile = cProfile.Profile() profile.enable() self.run() #except Exception as err: # self.interrupt_measurement_called = True # raise err finally: self.running.update_value(False) self.activation.update_value(False) self.set_progress(0.) # set progress bars back to zero #self.measurement_state_changed.emit(False) if self.interrupt_measurement_called: self.measurement_interrupted.emit() self.interrupt_measurement_called = False else: self.measurement_sucessfully_completed.emit() if self.settings['profile']: profile.disable() profile.print_stats(sort='time') @property def gui(self): self.log.warning( "Measurement.gui is deprecated, use Measurement.app " + repr(DeprecationWarning)) return self.app def set_progress(self, pct): """ This function updates the logged quantity progress which is used for the display of progress bars in the UI. ============== ============================================================================================== **Arguments:** pct The percentage of progress given by a measurement module ============== ============================================================================================== """ self.progress.update_value(pct) @QtCore.Slot() def interrupt(self): """ Kindly ask the measurement to stop. This raises the :attr:`interrupt_measurement_called` flag To actually stop, the threaded :meth:`run` method must check for this flag and exit """ self.log.info("measurement {} interrupt".format(self.name)) self.interrupt_measurement_called = True self.activation.update_value(False) #Make sure display is up to date #self._on_display_update_timer() def terminate(self): """ Terminate MeasurementQThread. Usually a bad idea: This will not clean up the thread correctly and usually requires a reboot of the App """ self.acq_thread.terminate() def start_stop(self, start): """ Use boolean *start* to either start (True) or interrupt (False) measurement. Test. """ self.log.info("{} start_stop {}".format(self.name, start)) if start: self._start() else: self.interrupt() def is_measuring(self): """ Returns whether the acquisition thread is running """ if self.acq_thread is None: self.running.update_value(False) self.activation.update_value(False) self.settings['progress'] = 0.0 return False else: #resp = self.acq_thread.is_alive() resp = self.acq_thread.isRunning() self.running.update_value(resp) return resp def update_display(self): "Override this function to provide figure updates when the display timer runs" pass @QtCore.Slot() def _on_display_update_timer(self): try: self.update_display() except Exception as err: exc_type, exc_value, exc_traceback = sys.exc_info() self.log.error("{} Failed to update figure1: {}. {}".format( self.name, err, traceback.format_exception(exc_type, exc_value, exc_traceback))) finally: if not self.is_measuring(): self.display_update_timer.stop() def add_logged_quantity(self, name, **kwargs): """ Create a new :class:`LoggedQuantity` and adds it to the measurement's :attr:`settings` (:class:`LQCollection`) """ lq = self.settings.New(name=name, **kwargs) return lq def add_operation(self, name, op_func): """ Used to create a logged quantity connection between a button in the Measurement tree and a function. ============== ================= **type name:** **type op_func:** str QtCore.Slot ============== ================= """ self.operations[name] = op_func def start_nested_measure_and_wait(self, measure, nested_interrupt=True, polling_func=None, polling_time=0.1, start_time=0.0): """ Start another nested measurement *measure* and wait until completion. Optionally it can call a polling function *polling_func* with no arguments at an interval *polling_time* in seconds. if *nested_interrupt* is True then interrupting the nested *measure* will also interrupt the outer measurement. *nested_interrupt* defaults to True start_time is how the time period after starting the nested measurement that checks for completion of nested measurement """ measure.settings['activation'] = True # wait until measurement has started t0 = time.time() while not measure.settings['running']: # check for startup timeout if (time.time() - t0) > 1.0: break time.sleep(0.001) time.sleep(start_time) last_polling = time.time() while measure.is_measuring(): if self.interrupt_measurement_called: measure.interrupt() if measure.interrupt_measurement_called and nested_interrupt: #print("nested interrupt bubbling up") self.interrupt() time.sleep(0.010) # polling if polling_func: t = time.time() if t - last_polling > polling_time: #print("poll", t - last_polling ) polling_func() last_polling = t def load_ui(self, ui_fname=None): """ Loads and shows user interface. ============== =============================================================== **Arguments:** ui_fname filename of user interface file (usually made with Qt Designer) ============== =============================================================== """ # TODO destroy and rebuild UI if it already exists if ui_fname is not None: self.ui_filename = ui_fname # Load Qt UI from .ui file self.ui = load_qt_ui_file(self.ui_filename) #self.show_ui() def show_ui(self): """ Shows the graphical user interface of this measurement. :attr:`ui` """ self.app.bring_measure_ui_to_front(self) # if self.app.mdi and self.ui.parent(): # self.ui.parent().raise_() # return # self.ui.show() # self.ui.activateWindow() # self.ui.raise_() #just to be sure it's on top # if self.app.mdi and self.ui.parent(): # self.ui.parent().raise_() def new_control_widgets(self): self.controls_groupBox = QtWidgets.QGroupBox(self.name) self.controls_formLayout = QtWidgets.QFormLayout() self.controls_groupBox.setLayout(self.controls_formLayout) self.control_widgets = OrderedDict() for lqname, lq in self.settings.as_dict().items(): #: :type lq: LoggedQuantity if lq.choices is not None: widget = QtWidgets.QComboBox() elif lq.dtype in [int, float]: if lq.si: widget = pg.SpinBox() else: widget = QtWidgets.QDoubleSpinBox() elif lq.dtype in [bool]: widget = QtWidgets.QCheckBox() elif lq.dtype in [str]: widget = QtWidgets.QLineEdit() lq.connect_bidir_to_widget(widget) # Add to formlayout self.controls_formLayout.addRow(lqname, widget) self.control_widgets[lqname] = widget self.op_buttons = OrderedDict() for op_name, op_func in self.operations.items(): op_button = QtWidgets.QPushButton(op_name) op_button.clicked.connect(op_func) self.controls_formLayout.addRow(op_name, op_button) return self.controls_groupBox def add_widgets_to_tree(self, tree): """ Adds Measurement items and their controls to Measurements tree in the user interface. """ #if tree is None: # tree = self.app.ui.measurements_treeWidget tree.setColumnCount(2) tree.setHeaderLabels(["Measurements", "Value"]) self.tree_item = QtWidgets.QTreeWidgetItem(tree, [self.name, ""]) tree.insertTopLevelItem(0, self.tree_item) #self.tree_item.setFirstColumnSpanned(True) self.tree_progressBar = QtWidgets.QProgressBar() tree.setItemWidget(self.tree_item, 1, self.tree_progressBar) self.progress.updated_value.connect(self.tree_progressBar.setValue) # Add logged quantities to tree self.settings.add_widgets_to_subtree(self.tree_item) # Add operation buttons to tree self.op_buttons = OrderedDict() for op_name, op_func in self.operations.items(): op_button = QtWidgets.QPushButton(op_name) op_button.clicked.connect(op_func) self.op_buttons[op_name] = op_button #self.controls_formLayout.addRow(op_name, op_button) op_tree_item = QtWidgets.QTreeWidgetItem(self.tree_item, [op_name, ""]) tree.setItemWidget(op_tree_item, 1, op_button) def web_ui(self): return "Hardware {}".format(self.name)
class FFTView(QtWidgets.QWidget): """ creates the layout for the FFT GUI """ # signals buttonSignal = QtCore.Signal() tableClickSignal = QtCore.Signal(object, object) phaseCheckSignal = QtCore.Signal() def __init__(self, parent=None): super(FFTView, self).__init__(parent) self.grid = QtWidgets.QGridLayout(self) # add splitter for resizing splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical) # make table self.FFTTable = QtWidgets.QTableWidget(self) self.FFTTable.resize(800, 800) self.FFTTable.setRowCount(6) self.FFTTable.setColumnCount(2) self.FFTTable.setColumnWidth(0, 300) self.FFTTable.setColumnWidth(1, 300) self.FFTTable.verticalHeader().setVisible(False) self.FFTTable.horizontalHeader().setStretchLastSection(True) self.FFTTable.setHorizontalHeaderLabels( ("FFT Property;Value").split(";")) # populate table options = ['test'] table_utils.setRowName(self.FFTTable, 0, "Workspace") self.ws = table_utils.addComboToTable(self.FFTTable, 0, options) self.Im_box_row = 1 table_utils.setRowName(self.FFTTable, self.Im_box_row, "Imaginary Data") self.Im_box = table_utils.addCheckBoxToTable(self.FFTTable, True, self.Im_box_row) table_utils.setRowName(self.FFTTable, 2, "Imaginary Workspace") self.Im_ws = table_utils.addComboToTable(self.FFTTable, 2, options) self.shift_box_row = 3 table_utils.setRowName(self.FFTTable, self.shift_box_row, "Auto shift") self.shift_box = table_utils.addCheckBoxToTable( self.FFTTable, True, self.shift_box_row) table_utils.setRowName(self.FFTTable, 4, "Shift") self.shift = table_utils.addDoubleToTable(self.FFTTable, 0.0, 4) self.FFTTable.hideRow(4) table_utils.setRowName(self.FFTTable, 5, "Use Raw data") self.Raw_box = table_utils.addCheckBoxToTable(self.FFTTable, True, 5) self.FFTTable.resizeRowsToContents() # make advanced table options self.advancedLabel = QtWidgets.QLabel("\n Advanced Options") self.FFTTableA = QtWidgets.QTableWidget(self) self.FFTTableA.resize(800, 800) self.FFTTableA.setRowCount(4) self.FFTTableA.setColumnCount(2) self.FFTTableA.setColumnWidth(0, 300) self.FFTTableA.setColumnWidth(1, 300) self.FFTTableA.verticalHeader().setVisible(False) self.FFTTableA.horizontalHeader().setStretchLastSection(True) self.FFTTableA.setHorizontalHeaderLabels( ("Advanced Property;Value").split(";")) table_utils.setRowName(self.FFTTableA, 0, "Apodization Function") options = ["Lorentz", "Gaussian", "None"] self.apodization = table_utils.addComboToTable(self.FFTTableA, 0, options) table_utils.setRowName(self.FFTTableA, 1, "Decay Constant (micro seconds)") self.decay = table_utils.addDoubleToTable(self.FFTTableA, 4.4, 1) table_utils.setRowName(self.FFTTableA, 2, "Negative Padding") self.negativePadding = table_utils.addCheckBoxToTable( self.FFTTableA, True, 2) table_utils.setRowName(self.FFTTableA, 3, "Padding") self.padding = table_utils.addSpinBoxToTable(self.FFTTableA, 1, 3) self.FFTTableA.resizeRowsToContents() # make button self.button = QtWidgets.QPushButton('Calculate FFT', self) self.button.setStyleSheet("background-color:lightgrey") # connects self.FFTTable.cellClicked.connect(self.tableClick) self.button.clicked.connect(self.buttonClick) self.ws.currentIndexChanged.connect(self.phaseCheck) # add to layout self.FFTTable.setMinimumSize(40, 158) self.FFTTableA.setMinimumSize(40, 127) table_utils.setTableHeaders(self.FFTTable) table_utils.setTableHeaders(self.FFTTableA) # add to layout splitter.addWidget(self.FFTTable) splitter.addWidget(self.advancedLabel) splitter.addWidget(self.FFTTableA) self.grid.addWidget(splitter) self.grid.addWidget(self.button) def getLayout(self): return self.grid def addItems(self, options): self.ws.clear() self.ws.addItems(options) self.Im_ws.clear() self.Im_ws.addItems(options) self.phaseQuadChanged() def removeIm(self, pattern): index = self.Im_ws.findText(pattern) self.Im_ws.removeItem(index) def removeRe(self, pattern): index = self.ws.findText(pattern) self.ws.removeItem(index) # connect signals def phaseCheck(self): self.phaseCheckSignal.emit() def tableClick(self, row, col): self.tableClickSignal.emit(row, col) def buttonClick(self): self.buttonSignal.emit() # responses to commands def activateButton(self): self.button.setEnabled(True) def deactivateButton(self): self.button.setEnabled(False) def setPhaseBox(self): self.FFTTable.setRowHidden(8, "PhaseQuad" not in self.workspace) def changed(self, box, row): self.FFTTable.setRowHidden(row, box.checkState() == QtCore.Qt.Checked) def changedHideUnTick(self, box, row): self.FFTTable.setRowHidden(row, box.checkState() != QtCore.Qt.Checked) def phaseQuadChanged(self): # hide complex ws self.FFTTable.setRowHidden( 2, "PhaseQuad" in self.workspace or self.Im_box.checkState() != QtCore.Qt.Checked) def set_raw_checkbox_state(self, state): if state: self.Raw_box.setCheckState(QtCore.Qt.Checked) else: self.Raw_box.setCheckState(QtCore.Qt.Unchecked) def setup_raw_checkbox_changed(self, slot): self.FFTTable.itemChanged.connect(self.raw_checkbox_changed) self.signal_raw_option_changed = slot def raw_checkbox_changed(self, table_item): if table_item == self.Raw_box: self.signal_raw_option_changed() def getImBoxRow(self): return self.Im_box_row def getShiftBoxRow(self): return self.shift_box_row def getImBox(self): return self.Im_box def getShiftBox(self): return self.shift_box def warning_popup(self, message): warning(message, parent=self) @property def workspace(self): return str(self.ws.currentText()) @workspace.setter def workspace(self, name): index = self.ws.findText(name) if index == -1: return self.ws.setCurrentIndex(index) @property def imaginary_workspace(self): return str(self.Im_ws.currentText()) @imaginary_workspace.setter def imaginary_workspace(self, name): index = self.Im_ws.findText(name) if index == -1: return self.Im_ws.setCurrentIndex(index) @property def imaginary_data(self): return self.Im_box.checkState() == QtCore.Qt.Checked @imaginary_data.setter def imaginary_data(self, value): if value: self.Im_box.setCheckState(QtCore.Qt.Checked) else: self.Im_box.setCheckState(QtCore.Qt.Unchecked) @property def auto_shift(self): return self.shift_box.checkState() == QtCore.Qt.Checked @property def use_raw_data(self): return self.Raw_box.checkState() == QtCore.Qt.Checked @property def apodization_function(self): return str(self.apodization.currentText()) @property def decay_constant(self): return float(self.decay.text()) @property def negative_padding(self): return self.negativePadding.checkState() == QtCore.Qt.Checked @property def padding_value(self): return int(self.padding.text())
class PowerScannerLogic(GenericLogic): """This logic module controls scans of DC voltage on the third analog output channel of the NI Card. It collects count-rate as a function of voltage. """ sig_data_updated = QtCore.Signal() # declare connectors confocalscanner1 = Connector(interface='ConfocalScannerInterface') savelogic = Connector(interface='SaveLogic') scan_range = StatusVar('scan_range', [0, 1]) number_of_repeats = StatusVar(default=10) resolution = StatusVar('resolution', 100) _scan_speed = StatusVar('scan_speed', 1) _static_v = StatusVar('goto_voltage', 0) sigChangeVoltage = QtCore.Signal(float) sigVoltageChanged = QtCore.Signal(float) sigScanNextLine = QtCore.Signal() signal_change_position = QtCore.Signal(str) sigUpdatePlots = QtCore.Signal() sigScanFinished = QtCore.Signal() sigScanStarted = QtCore.Signal() def __init__(self, **kwargs): """ Create VoltageScanningLogic object with connectors. @param dict kwargs: optional parameters """ super().__init__(**kwargs) # locking for thread safety self.threadlock = Mutex() self.stopRequested = False self.fit_x = [] self.fit_y = [] self.plot_x = [] self.plot_y = [] self.plot_y2 = [] def on_activate(self): """ Initialisation performed during activation of the module. """ self._scanning_device = self.confocalscanner1() self._save_logic = self.savelogic() # Reads in the maximal scanning range. The unit of that scan range is # micrometer! self.z_range = self._scanning_device.get_position_range()[2] # Initialise the current position of all four scanner channels. self.current_position = self._scanning_device.get_scanner_position() # initialise the range for scanning self.scan_range = [0.0, 1.0] self.set_scan_range(self.scan_range) self._static_v = 1.0 # Keep track of the current static voltage even while a scan may cause the real-time # voltage to change. self.goto_voltage(self._static_v) # Sets connections between signals and functions self.sigChangeVoltage.connect(self._change_voltage, QtCore.Qt.QueuedConnection) self.sigScanNextLine.connect(self._do_next_line, QtCore.Qt.QueuedConnection) # Initialization of internal counter for scanning self._scan_counter_up = 0 self._scan_counter_down = 0 # Keep track of scan direction self.upwards_scan = True # calculated number of points in a scan, depends on speed and max step size self._num_of_steps = 50 # initialising. This is calculated for a given ramp. ############################# # TODO: allow configuration with respect to measurement duration self.acquire_time = 20 # seconds self._scan_speed = 5 # default values for clock frequency and slowness # slowness: steps during retrace line self.set_resolution(self.resolution) self._goto_speed = 10 # 0.01 # volt / second self.set_scan_speed(self._scan_speed) self._smoothing_steps = 10 # steps to accelerate between 0 and scan_speed self._max_step = 0.01 # volt self._current_z = 0 self._change_position('activation') ############################## # Initialie data matrix self._initialise_data_matrix(100) def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self.set_voltage(0) self.stopRequested = True @QtCore.Slot(float) def goto_voltage(self, volts=None): """Forwarding the desired output voltage to the scanning device. @param float volts: desired voltage (volts) @return int: error code (0:OK, -1:error) """ # print(tag, x, y, z) # Changes the respective value if volts is not None: self._static_v = volts # Checks if the scanner is still running if (self.module_state() == 'locked' or self._scanning_device.module_state() == 'locked'): self.log.error('Cannot goto, because scanner is locked!') return -1 else: self.sigChangeVoltage.emit(volts) return 0 def _change_voltage(self, new_voltage): """ Threaded method to change the hardware voltage for a goto. @return int: error code (0:OK, -1:error) """ ramp_scan = self._generate_ramp(self.get_current_voltage(), new_voltage, self._goto_speed) self._initialise_scanner() ignored_counts = self._scan_line(ramp_scan) self._close_scanner() self.sigVoltageChanged.emit(new_voltage) return 0 def _goto_during_scan(self, voltage=None): if voltage is None: return -1 goto_ramp = self._generate_ramp(self.get_current_voltage(), voltage, self._goto_speed) ignored_counts = self._scan_line(goto_ramp) return 0 def set_clock_frequency(self, clock_frequency): """Sets the frequency of the clock @param int clock_frequency: desired frequency of the clock @return int: error code (0:OK, -1:error) """ self._clock_frequency = float(clock_frequency) # checks if scanner is still running if self.module_state() == 'locked': return -1 else: return 0 def set_resolution(self, resolution): """ Calculate clock rate from scan speed and desired number of pixels """ self.resolution = resolution scan_range = abs(self.scan_range[1] - self.scan_range[0]) duration = scan_range / self._scan_speed new_clock = resolution / duration return self.set_clock_frequency(new_clock) def set_scan_range(self, scan_range): """ Set the scan range """ r_max = np.clip(scan_range[1], self.z_range[0], self.z_range[1]) r_min = np.clip(scan_range[0], self.z_range[0], r_max) self.scan_range = [r_min, r_max] def set_voltage(self, volts): """ Set the channel idle voltage """ self._static_v = np.clip(volts, self.z_range[0], self.z_range[1]) self.goto_voltage(self._static_v) def set_scan_speed(self, scan_speed): """ Set scan speed in volt per second """ self._scan_speed = np.clip(scan_speed, 1e-9, 1e6) self._goto_speed = self._scan_speed def set_scan_lines(self, scan_lines): self.number_of_repeats = int(np.clip(scan_lines, 1, 1e6)) def _initialise_data_matrix(self, scan_length): """ Initializing the ODMR matrix plot. """ self.scan_matrix = np.zeros((self.number_of_repeats, scan_length)) self.scan_matrix2 = np.zeros((self.number_of_repeats, scan_length)) self.plot_x = np.linspace(self.scan_range[0], self.scan_range[1], scan_length) self.plot_y = np.zeros(scan_length) self.plot_y2 = np.zeros(scan_length) self.fit_x = np.linspace(self.scan_range[0], self.scan_range[1], scan_length) self.fit_y = np.zeros(scan_length) def get_current_voltage(self): """returns current voltage of hardware device(atm NIDAQ 3rd output)""" return self._scanning_device.get_scanner_position()[2] def _initialise_scanner(self): """Initialise the clock and locks for a scan""" self.module_state.lock() self._scanning_device.module_state.lock() returnvalue = self._scanning_device.set_up_scanner_clock( clock_frequency=self._clock_frequency) if returnvalue < 0: self._scanning_device.module_state.unlock() self.module_state.unlock() self.set_position('scanner') return -1 returnvalue = self._scanning_device.set_up_scanner() if returnvalue < 0: self._scanning_device.module_state.unlock() self.module_state.unlock() self.set_position('scanner') return -1 return 0 def _change_position(self, tag): """ Threaded method to change the hardware position. @return int: error code (0:OK, -1:error) """ ch_array = ['z'] pos_array = [self._current_z] pos_dict = {} # for i, ch in enumerate(self.get_scanner_axes()): pos_dict[ch_array[0]] = pos_array[0] self._scanning_device.scanner_set_position(**pos_dict) return 0 def get_scanner_axes(self): """ Get axes from scanning device. @return list(str): names of scanner axes """ return self._scanning_device.get_scanner_axes() def set_position(self, tag, z=None): """Forwarding the desired new position from the GUI to the scanning device. @param string tag: TODO @param float a: if defined, changes to postion in a-direction (microns) @return int: error code (0:OK, -1:error) """ # Changes the respective value if z is not None: self._current_z = z # Checks if the scanner is still running if self.module_state( ) == 'locked' or self._scanning_device.module_state() == 'locked': return -1 else: self._change_position(tag) self.signal_change_position.emit(tag) return 0 def start_scanning(self, v_min=None, v_max=None): """Setting up the scanner device and starts the scanning procedure @return int: error code (0:OK, -1:error) """ self.current_position = self._scanning_device.get_scanner_position() print(self.current_position) if v_min is not None: self.scan_range[0] = v_min else: v_min = self.scan_range[0] if v_max is not None: self.scan_range[1] = v_max else: v_max = self.scan_range[1] self._scan_counter_up = 0 self._scan_counter_down = 0 self.upwards_scan = True # TODO: Generate Ramps self._upwards_ramp = self._generate_ramp(v_min, v_max, self._scan_speed) self._downwards_ramp = self._generate_ramp(v_max, v_min, self._scan_speed) self._initialise_data_matrix(len(self._upwards_ramp[3])) # Lock and set up scanner returnvalue = self._initialise_scanner() if returnvalue < 0: # TODO: error message return -1 self.sigScanNextLine.emit() self.sigScanStarted.emit() return 0 def stop_scanning(self): """Stops the scan @return int: error code (0:OK, -1:error) """ with self.threadlock: if self.module_state() == 'locked': self.stopRequested = True return 0 def _close_scanner(self): """Close the scanner and unlock""" with self.threadlock: self.kill_scanner() self.stopRequested = False if self.module_state.can('unlock'): self.module_state.unlock() def _do_next_line(self): """ If stopRequested then finish the scan, otherwise perform next repeat of the scan line """ # stops scanning if self.stopRequested or self._scan_counter_down >= self.number_of_repeats: print(self.current_position) self._goto_during_scan(self._static_v) self._close_scanner() self.sigScanFinished.emit() return if self._scan_counter_up == 0: # move from current voltage to start of scan range. self._goto_during_scan(self.scan_range[0]) if self.upwards_scan: counts = self._scan_line(self._upwards_ramp) self.scan_matrix[self._scan_counter_up] = counts self.plot_y += counts self._scan_counter_up += 1 self.upwards_scan = False else: counts = self._scan_line(self._downwards_ramp) self.scan_matrix2[self._scan_counter_down] = counts self.plot_y2 += counts self._scan_counter_down += 1 self.upwards_scan = True self.sigUpdatePlots.emit() self.sigScanNextLine.emit() def _generate_ramp(self, voltage1, voltage2, speed): """Generate a ramp vrom voltage1 to voltage2 that satisfies the speed, step, smoothing_steps parameters. Smoothing_steps=0 means that the ramp is just linear. @param float voltage1: voltage at start of ramp. @param float voltage2: voltage at end of ramp. """ # It is much easier to calculate the smoothed ramp for just one direction (upwards), # and then to reverse it if a downwards ramp is required. v_min = min(voltage1, voltage2) v_max = max(voltage1, voltage2) if v_min == v_max: ramp = np.array([v_min, v_max]) else: # These values help simplify some of the mathematical expressions linear_v_step = speed / self._clock_frequency smoothing_range = self._smoothing_steps + 1 # Sanity check in case the range is too short # The voltage range covered while accelerating in the smoothing steps v_range_of_accel = sum(n * linear_v_step / smoothing_range for n in range(0, smoothing_range)) # Obtain voltage bounds for the linear part of the ramp v_min_linear = v_min + v_range_of_accel v_max_linear = v_max - v_range_of_accel if v_min_linear > v_max_linear: self.log.warning( 'Voltage ramp too short to apply the ' 'configured smoothing_steps. A simple linear ramp ' 'was created instead.') num_of_linear_steps = np.rint((v_max - v_min) / linear_v_step) ramp = np.linspace(v_min, v_max, int(num_of_linear_steps)) else: num_of_linear_steps = np.rint( (v_max_linear - v_min_linear) / linear_v_step) # Calculate voltage step values for smooth acceleration part of ramp smooth_curve = np.array([ sum(n * linear_v_step / smoothing_range for n in range(1, N)) for N in range(1, smoothing_range) ]) accel_part = v_min + smooth_curve decel_part = v_max - smooth_curve[::-1] linear_part = np.linspace(v_min_linear, v_max_linear, int(num_of_linear_steps)) ramp = np.hstack((accel_part, linear_part, decel_part)) # Reverse if downwards ramp is required if voltage2 < voltage1: ramp = ramp[::-1] # Put the voltage ramp into a scan line for the hardware (4-dimension) spatial_pos = self._scanning_device.get_scanner_position() ##################### WARNING ############################# # the position of the ramp in the array defines the channel of the NI card scan_line = np.vstack((np.ones( (len(ramp), )) * spatial_pos[0], np.ones( (len(ramp), )) * spatial_pos[1], ramp, np.ones( (len(ramp), )) * spatial_pos[3])) return scan_line def _scan_line(self, line_to_scan=None): """do a single voltage scan from voltage1 to voltage2 """ if line_to_scan is None: self.log.error('Voltage scanning logic needs a line to scan!') return -1 try: # scan of a single line counts_on_scan_line = self._scanning_device.scan_line(line_to_scan) return counts_on_scan_line.transpose()[0] except Exception as e: self.log.error('The scan went wrong, killing the scanner.') self.stop_scanning() self.sigScanNextLine.emit() raise e def kill_scanner(self): """Closing the scanner device. @return int: error code (0:OK, -1:error) """ try: self._scanning_device.close_scanner() self._scanning_device.close_scanner_clock() except Exception as e: self.log.exception('Could not even close the scanner, giving up.') raise e try: if self._scanning_device.module_state.can('unlock'): self._scanning_device.module_state.unlock() except: self.log.exception('Could not unlock scanning device.') return 0 def save_data(self, tag=None, colorscale_range=None, percentile_range=None): """ Save the counter trace data and writes it to a file. @return int: error code (0:OK, -1:error) """ if tag is None: tag = '' self._saving_stop_time = time.time() filepath = self._save_logic.get_path_for_module( module_name='PowerScanning') filepath2 = self._save_logic.get_path_for_module( module_name='PowerScanning') filepath3 = self._save_logic.get_path_for_module( module_name='PowerScanning') timestamp = datetime.datetime.now() if len(tag) > 0: filelabel = tag + '_volt_data' filelabel2 = tag + '_volt_data_raw_trace' filelabel3 = tag + '_volt_data_raw_retrace' else: filelabel = 'volt_data' filelabel2 = 'volt_data_raw_trace' filelabel3 = 'volt_data_raw_retrace' # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data['frequency (Hz)'] = self.plot_x data['trace count data (counts/s)'] = self.plot_y data['retrace count data (counts/s)'] = self.plot_y2 data2 = OrderedDict() data2['count data (counts/s)'] = self.scan_matrix[:self. _scan_counter_up, :] data3 = OrderedDict() data3[ 'count data (counts/s)'] = self.scan_matrix2[:self. _scan_counter_down, :] parameters = OrderedDict() parameters['Number of frequency sweeps (#)'] = self._scan_counter_up parameters['Start Voltage (V)'] = self.scan_range[0] parameters['Stop Voltage (V)'] = self.scan_range[1] parameters['Scan speed [V/s]'] = self._scan_speed parameters['Clock Frequency (Hz)'] = self._clock_frequency fig = self.draw_figure(self.scan_matrix, self.plot_x, self.plot_y, self.fit_x, self.fit_y, cbar_range=colorscale_range, percentile_range=percentile_range) fig2 = self.draw_figure(self.scan_matrix2, self.plot_x, self.plot_y2, self.fit_x, self.fit_y, cbar_range=colorscale_range, percentile_range=percentile_range) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, fmt='%.6e', delimiter='\t', timestamp=timestamp) self._save_logic.save_data(data2, filepath=filepath2, parameters=parameters, filelabel=filelabel2, fmt='%.6e', delimiter='\t', timestamp=timestamp, plotfig=fig) self._save_logic.save_data(data3, filepath=filepath3, parameters=parameters, filelabel=filelabel3, fmt='%.6e', delimiter='\t', timestamp=timestamp, plotfig=fig2) self.log.info('Power Scan saved to:\n{0}'.format(filepath)) return 0 def draw_figure(self, matrix_data, freq_data, count_data, fit_freq_vals, fit_count_vals, cbar_range=None, percentile_range=None): """ Draw the summary figure to save with the data. @param: list cbar_range: (optional) [color_scale_min, color_scale_max]. If not supplied then a default of data_min to data_max will be used. @param: list percentile_range: (optional) Percentile range of the chosen cbar_range. @return: fig fig: a matplotlib figure object to be saved to file. """ # If no colorbar range was given, take full range of data if cbar_range is None: cbar_range = np.array([np.min(matrix_data), np.max(matrix_data)]) else: cbar_range = np.array(cbar_range) prefix = ['', 'k', 'M', 'G', 'T'] prefix_index = 0 # Rescale counts data with SI prefix while np.max(count_data) > 1000: count_data = count_data / 1000 fit_count_vals = fit_count_vals / 1000 prefix_index = prefix_index + 1 counts_prefix = prefix[prefix_index] # Rescale frequency data with SI prefix prefix_index = 0 while np.max(freq_data) > 1000: freq_data = freq_data / 1000 fit_freq_vals = fit_freq_vals / 1000 prefix_index = prefix_index + 1 mw_prefix = prefix[prefix_index] # Rescale matrix counts data with SI prefix prefix_index = 0 while np.max(matrix_data) > 1000: matrix_data = matrix_data / 1000 cbar_range = cbar_range / 1000 prefix_index = prefix_index + 1 cbar_prefix = prefix[prefix_index] # Use qudi style plt.style.use(self._save_logic.mpl_qd_style) # Create figure fig, (ax_mean, ax_matrix) = plt.subplots(nrows=2, ncols=1) ax_mean.plot(freq_data, count_data, linestyle=':', linewidth=0.5) # Do not include fit curve if there is no fit calculated. if max(fit_count_vals) > 0: ax_mean.plot(fit_freq_vals, fit_count_vals, marker='None') ax_mean.set_ylabel('Fluorescence (' + counts_prefix + 'c/s)') ax_mean.set_xlim(np.min(freq_data), np.max(freq_data)) matrixplot = ax_matrix.imshow( matrix_data, cmap=plt.get_cmap('inferno'), # reference the right place in qd origin='lower', vmin=cbar_range[0], vmax=cbar_range[1], extent=[ np.min(freq_data), np.max(freq_data), 0, self.number_of_repeats ], aspect='auto', interpolation='nearest') ax_matrix.set_xlabel('Frequency (' + mw_prefix + 'Hz)') ax_matrix.set_ylabel('Scan #') # Adjust subplots to make room for colorbar fig.subplots_adjust(right=0.8) # Add colorbar axis to figure cbar_ax = fig.add_axes([0.85, 0.15, 0.02, 0.7]) # Draw colorbar cbar = fig.colorbar(matrixplot, cax=cbar_ax) cbar.set_label('Fluorescence (' + cbar_prefix + 'c/s)') # remove ticks from colorbar for cleaner image cbar.ax.tick_params(which='both', length=0) # If we have percentile information, draw that to the figure if percentile_range is not None: cbar.ax.annotate(str(percentile_range[0]), xy=(-0.3, 0.0), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) cbar.ax.annotate(str(percentile_range[1]), xy=(-0.3, 1.0), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) cbar.ax.annotate('(percentile)', xy=(-0.3, 0.5), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) return fig
class DataSlice(QtWidgets.QWidget): """ A DatSlice widget provides an inteface for selection slices through an N-dimensional dataset QtCore.Signals ------- slice_changed : triggered when the slice through the data changes """ slice_changed = QtCore.Signal() def __init__(self, data=None, parent=None): """ :param data: :class:`~glue.core.data.Data` instance, or None """ super(DataSlice, self).__init__(parent) self._slices = [] self._data = None layout = QtWidgets.QVBoxLayout() layout.setSpacing(4) layout.setContentsMargins(0, 3, 0, 3) self.layout = layout self.setLayout(layout) self.set_data(data) @property def ndim(self): return len(self.shape) @property def shape(self): return tuple() if self._data is None else self._data.shape def _clear(self): for _ in range(self.layout.count()): self.layout.takeAt(0) for s in self._slices: s.close() self._slices = [] def set_data(self, data): """ Change datasets :parm data: :class:`~glue.core.data.Data` instance """ # remove old widgets self._clear() self._data = data if data is None or data.ndim < 3: return # create slider widget for each dimension... for i, s in enumerate(data.shape): # TODO: For now we simply pass a single set of world coordinates, # but we will need to generalize this in future. We deliberately # check the type of data.coords here since we want to treat # subclasses differently. if type(data.coords) != Coordinates: world = data.coords.world_axis(data, i) world_unit = data.coords.world_axis_unit(i) world_warning = len(data.coords.dependent_axes(i)) > 1 else: world = None world_unit = None world_warning = False slider = SliceWidget(data.get_world_component_id(i).label, hi=s - 1, world=world, world_unit=world_unit, world_warning=world_warning) if i == self.ndim - 1: slider.mode = 'x' elif i == self.ndim - 2: slider.mode = 'y' else: slider.mode = 'slice' self._slices.append(slider) # save ref to prevent PySide segfault self.__on_slice = partial(self._on_slice, i) self.__on_mode = partial(self._on_mode, i) slider.slice_changed.connect(self.__on_slice) slider.mode_changed.connect(self.__on_mode) if s == 1: slider.freeze() # ... and add to the layout for s in self._slices[::-1]: self.layout.addWidget(s) if s is not self._slices[0]: line = QtWidgets.QFrame() line.setFrameShape(QtWidgets.QFrame.HLine) line.setFrameShadow(QtWidgets.QFrame.Sunken) self.layout.addWidget(line) s.show() # this somehow fixes #342 self.layout.addStretch(5) def _on_slice(self, index, slice_val): self.slice_changed.emit() def _on_mode(self, index, mode_index): s = self.slice def isok(ss): # valid slice description: 'x' and 'y' both appear c = Counter(ss) return c['x'] == 1 and c['y'] == 1 if isok(s): self.slice_changed.emit() return for i in range(len(s)): if i == index: continue if self._slices[i].frozen: continue for mode in 'x', 'y', 'slice': if self._slices[i].mode == mode: continue ss = list(s) ss[i] = mode if isok(ss): self._slices[i].mode = mode return else: raise RuntimeError("Corrupted Data Slice") @property def slice(self): """ A description of the slice through the dataset A tuple of lenght equal to the dimensionality of the data Each element is an integer, 'x', or 'y' 'x' and 'y' indicate the horizontal and vertical orientation of the slice """ if self.ndim < 3: return {0: tuple(), 1: ('x', ), 2: ('y', 'x')}[self.ndim] return tuple(s.mode if s.mode != 'slice' else s.slice_center for s in self._slices) @slice.setter def slice(self, value): for v, s in zip(value, self._slices): if v in ['x', 'y']: s.mode = v else: s.mode = 'slice' s.slice_center = v
class BasicToolbar(QtWidgets.QToolBar): tool_activated = QtCore.Signal() tool_deactivated = QtCore.Signal() def __init__(self, parent, default_mouse_mode_cls=None): """ Create a new toolbar object """ super(BasicToolbar, self).__init__(parent=parent) self.actions = {} self.tools = {} self.setIconSize(QtCore.QSize(25, 25)) self.layout().setSpacing(1) self.setFocusPolicy(Qt.StrongFocus) self._active_tool = None self._default_mouse_mode_cls = default_mouse_mode_cls self._default_mouse_mode = None self.setup_default_modes() def setup_default_modes(self): if self._default_mouse_mode_cls is not None: self._default_mouse_mode = self._default_mouse_mode_cls( self.parent()) self._default_mouse_mode.activate() @property def active_tool(self): return self._active_tool @active_tool.setter def active_tool(self, new_tool): old_tool = self._active_tool # If the tool is as before, we don't need to do anything if old_tool is new_tool: return # Otheriwse, if the tool changes, then we need to disable the previous # tool... if old_tool is not None: self.deactivate_tool(old_tool) if isinstance(old_tool, CheckableTool): button = self.actions[old_tool.tool_id] if button.isChecked(): button.blockSignals(True) button.setChecked(False) button.blockSignals(False) # ... and enable the new one if new_tool is not None: self.activate_tool(new_tool) if isinstance(new_tool, CheckableTool): button = self.actions[new_tool.tool_id] if not button.isChecked(): button.blockSignals(True) button.setChecked(True) button.blockSignals(False) if isinstance(new_tool, CheckableTool): self._active_tool = new_tool self.parent().set_status(new_tool.status_tip) self.tool_activated.emit() else: self._active_tool = None self.parent().set_status('') self.tool_deactivated.emit() def activate_tool(self, tool): if self._default_mouse_mode is not None: self._default_mouse_mode.deactivate() tool.activate() def deactivate_tool(self, tool): if isinstance(tool, CheckableTool): tool.deactivate() if self._default_mouse_mode is not None: self._default_mouse_mode.activate() def add_tool(self, tool): parent = QtWidgets.QToolBar.parent(self) if isinstance(tool.icon, six.string_types): if os.path.exists(tool.icon): icon = QtGui.QIcon(tool.icon) else: icon = get_icon(tool.icon) else: icon = tool.icon action = QtWidgets.QAction(icon, tool.action_text, parent) def toggle(checked): if checked: self.active_tool = tool else: self.active_tool = None def trigger(checked): self.active_tool = tool parent.addAction(action) if isinstance(tool, CheckableTool): action.toggled.connect(toggle) else: action.triggered.connect(trigger) shortcut = None if tool.shortcut is not None: # Make sure that the keyboard shortcut is unique for m in self.tools.values(): if tool.shortcut == m.shortcut: warnings.warn( "Tools '{0}' and '{1}' have the same shortcut " "('{2}'). Ignoring shortcut for " "'{1}'".format(m.tool_id, tool.tool_id, tool.shortcut)) break else: shortcut = tool.shortcut action.setShortcut(tool.shortcut) action.setShortcutContext(Qt.WidgetShortcut) if shortcut is None: action.setToolTip(tool.tool_tip) else: action.setToolTip(tool.tool_tip + " [shortcut: {0}]".format(shortcut)) action.setCheckable(isinstance(tool, CheckableTool)) self.actions[tool.tool_id] = action menu_actions = tool.menu_actions() if len(menu_actions) > 0: menu = QtWidgets.QMenu(self) for ma in tool.menu_actions(): ma.setParent(self) menu.addAction(ma) action.setMenu(menu) menu.triggered.connect(trigger) self.addAction(action) # Bind tool visibility to tool.enabled def toggle(state): action.setVisible(state) action.setEnabled(state) add_callback(tool, 'enabled', toggle) self.tools[tool.tool_id] = tool return action def cleanup(self): # We need to make sure we set _default_mouse_mode to None otherwise # we keep a reference to the viewer (parent) inside the mouse mode, # creating a circular reference. self._default_mouse_mode = None self.active_tool = None
class Canvas(QtWidgets.QWidget): zoomRequest = QtCore.Signal(int, QtCore.QPoint) scrollRequest = QtCore.Signal(int, int) newShape = QtCore.Signal() selectionChanged = QtCore.Signal(bool) shapeMoved = QtCore.Signal() drawingPolygon = QtCore.Signal(bool) edgeSelected = QtCore.Signal(bool) CREATE, EDIT = 0, 1 epsilon = 11.0 def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) # Initialise local state. self.mode = self.EDIT self.shapes = [] self.shapesBackups = [] self.current = None self.selectedShape = None # save the selected shape here self.selectedShapeCopy = None self.lineColor = QtGui.QColor(0, 0, 255) self.line = Shape(line_color=self.lineColor) self.prevPoint = QtCore.QPointF() self.prevMovePoint = QtCore.QPointF() self.offsets = QtCore.QPointF(), QtCore.QPointF() self.scale = 1.0 self.pixmap = QtGui.QPixmap() self.visible = {} self._hideBackround = False self.hideBackround = False self.hShape = None self.hVertex = None self.hEdge = None self.movingShape = False self._painter = QtGui.QPainter() self._cursor = CURSOR_DEFAULT # Menus: self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu()) # Set widget options. self.setMouseTracking(True) self.setFocusPolicy(QtCore.Qt.WheelFocus) def storeShapes(self): shapesBackup = [] for shape in self.shapes: shapesBackup.append(shape.copy()) if len(self.shapesBackups) >= 10: self.shapesBackups = self.shapesBackups[-9:] self.shapesBackups.append(shapesBackup) @property def isShapeRestorable(self): if len(self.shapesBackups) < 2: return False return True def restoreShape(self): if not self.isShapeRestorable: return self.shapesBackups.pop() # latest shapesBackup = self.shapesBackups.pop() self.shapes = shapesBackup self.storeShapes() self.repaint() def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() def unHighlight(self): if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def selectedVertex(self): return self.hVertex is not None def mouseMoveEvent(self, ev): """Update line with last point and current coordinates.""" if QT5: pos = self.transformPos(ev.pos()) else: pos = self.transformPos(ev.posF()) self.prevMovePoint = pos self.restoreCursor() # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: color = self.lineColor if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Project the point to the pixmap's edges. pos = self.intersectionPoint(self.current[-1], pos) elif len(self.current) > 1 and \ self.closeEnough(pos, self.current[0]): # Attract line to starting point and # colorise to alert the user. pos = self.current[0] color = self.current.line_color self.overrideCursor(CURSOR_POINT) self.current.highlightVertex(0, Shape.NEAR_VERTEX) self.line[0] = self.current[-1] self.line[1] = pos self.line.line_color = color self.repaint() self.current.highlightClear() return # Polygon copy moving. if QtCore.Qt.RightButton & ev.buttons(): if self.selectedShapeCopy and self.prevPoint: self.overrideCursor(CURSOR_MOVE) self.boundedMoveShape(self.selectedShapeCopy, pos) self.repaint() elif self.selectedShape: self.selectedShapeCopy = self.selectedShape.copy() self.repaint() return # Polygon/Vertex moving. self.movingShape = False if QtCore.Qt.LeftButton & ev.buttons(): if self.selectedVertex(): self.boundedMoveVertex(pos) self.repaint() self.movingShape = True elif self.selectedShape and self.prevPoint: self.overrideCursor(CURSOR_MOVE) self.boundedMoveShape(self.selectedShape, pos) self.repaint() self.movingShape = True return # Just hovering over the canvas, 2 posibilities: # - Highlight shapes # - Highlight vertex # Update shape/vertex fill and tooltip value accordingly. self.setToolTip("Image") for shape in reversed([s for s in self.shapes if self.isVisible(s)]): # Look for a nearby vertex to highlight. If that fails, # check if we happen to be inside a shape. index = shape.nearestVertex(pos, self.epsilon) index_edge = shape.nearestEdge(pos, self.epsilon) if index is not None: if self.selectedVertex(): self.hShape.highlightClear() self.hVertex = index self.hShape = shape self.hEdge = index_edge shape.highlightVertex(index, shape.MOVE_VERTEX) self.overrideCursor(CURSOR_POINT) self.setToolTip("Click & drag to move point") self.setStatusTip(self.toolTip()) self.update() break elif shape.containsPoint(pos): if self.selectedVertex(): self.hShape.highlightClear() self.hVertex = None self.hShape = shape self.hEdge = index_edge self.setToolTip("Click & drag to move shape '%s'" % shape.label) self.setStatusTip(self.toolTip()) self.overrideCursor(CURSOR_GRAB) self.update() break else: # Nothing found, clear highlights, reset state. if self.hShape: self.hShape.highlightClear() self.update() self.hVertex, self.hShape, self.hEdge = None, None, None self.edgeSelected.emit(self.hEdge is not None) def addPointToEdge(self): if (self.hShape is None and self.hEdge is None and self.prevMovePoint is None): return shape = self.hShape index = self.hEdge point = self.prevMovePoint shape.insertPoint(index, point) shape.highlightVertex(index, shape.MOVE_VERTEX) self.hShape = shape self.hVertex = index self.hEdge = None def mousePressEvent(self, ev): if QT5: pos = self.transformPos(ev.pos()) else: pos = self.transformPos(ev.posF()) if ev.button() == QtCore.Qt.LeftButton: if self.drawing(): if self.current: self.current.addPoint(self.line[1]) self.line[0] = self.current[-1] if self.current.isClosed(): self.finalise() elif not self.outOfPixmap(pos): self.current = Shape() self.current.addPoint(pos) self.line.points = [pos, pos] self.setHiding() self.drawingPolygon.emit(True) self.update() else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == QtCore.Qt.RightButton and self.editing(): self.selectShapePoint(pos) self.prevPoint = pos self.repaint() def mouseReleaseEvent(self, ev): if ev.button() == QtCore.Qt.RightButton: menu = self.menus[bool(self.selectedShapeCopy)] self.restoreCursor() if not menu.exec_(self.mapToGlobal(ev.pos()))\ and self.selectedShapeCopy: # Cancel the move by deleting the shadow copy. self.selectedShapeCopy = None self.repaint() elif ev.button() == QtCore.Qt.LeftButton and self.selectedShape: self.overrideCursor(CURSOR_GRAB) if self.movingShape: self.storeShapes() self.shapeMoved.emit() def endMove(self, copy=False): assert self.selectedShape and self.selectedShapeCopy shape = self.selectedShapeCopy # del shape.fill_color # del shape.line_color if copy: self.shapes.append(shape) self.selectedShape.selected = False self.selectedShape = shape self.repaint() else: shape.label = self.selectedShape.label self.deleteSelected() self.shapes.append(shape) self.storeShapes() self.selectedShapeCopy = None def hideBackroundShapes(self, value): self.hideBackround = value if self.selectedShape: # Only hide other shapes if there is a current selection. # Otherwise the user will not be able to select a shape. self.setHiding(True) self.repaint() def setHiding(self, enable=True): self._hideBackround = self.hideBackround if enable else False def canCloseShape(self): return self.drawing() and self.current and len(self.current) > 2 def mouseDoubleClickEvent(self, ev): # We need at least 4 points here, since the mousePress handler # adds an extra one before this handler is called. if self.canCloseShape() and len(self.current) > 3: self.current.popPoint() self.finalise() def selectShape(self, shape): self.deSelectShape() shape.selected = True self.selectedShape = shape self.setHiding() self.selectionChanged.emit(True) self.update() def selectShapePoint(self, point): """Select the first shape created which contains this point.""" self.deSelectShape() if self.selectedVertex(): # A vertex is marked for selection. index, shape = self.hVertex, self.hShape shape.highlightVertex(index, shape.MOVE_VERTEX) return for shape in reversed(self.shapes): if self.isVisible(shape) and shape.containsPoint(point): shape.selected = True self.selectedShape = shape self.calculateOffsets(shape, point) self.setHiding() self.selectionChanged.emit(True) return def calculateOffsets(self, shape, point): rect = shape.boundingRect() x1 = rect.x() - point.x() y1 = rect.y() - point.y() x2 = (rect.x() + rect.width()) - point.x() y2 = (rect.y() + rect.height()) - point.y() self.offsets = QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2) def boundedMoveVertex(self, pos): index, shape = self.hVertex, self.hShape point = shape[index] if self.outOfPixmap(pos): pos = self.intersectionPoint(point, pos) shape.moveVertexBy(index, pos - point) def boundedMoveShape(self, shape, pos): if self.outOfPixmap(pos): return False # No need to move o1 = pos + self.offsets[0] if self.outOfPixmap(o1): pos -= QtCore.QPointF(min(0, o1.x()), min(0, o1.y())) o2 = pos + self.offsets[1] if self.outOfPixmap(o2): pos += QtCore.QPointF(min(0, self.pixmap.width() - o2.x()), min(0, self.pixmap.height() - o2.y())) # XXX: The next line tracks the new position of the cursor # relative to the shape, but also results in making it # a bit "shaky" when nearing the border and allows it to # go outside of the shape's area for some reason. # self.calculateOffsets(self.selectedShape, pos) dp = pos - self.prevPoint if dp: shape.moveBy(dp) self.prevPoint = pos return True return False def deSelectShape(self): if self.selectedShape: self.selectedShape.selected = False self.selectedShape = None self.setHiding(False) self.selectionChanged.emit(False) self.update() def deleteSelected(self): if self.selectedShape: shape = self.selectedShape self.shapes.remove(self.selectedShape) self.storeShapes() self.selectedShape = None self.update() return shape def copySelectedShape(self): if self.selectedShape: shape = self.selectedShape.copy() self.deSelectShape() self.shapes.append(shape) self.storeShapes() shape.selected = True self.selectedShape = shape self.boundedShiftShape(shape) return shape def boundedShiftShape(self, shape): # Try to move in one direction, and if it fails in another. # Give up if both fail. point = shape[0] offset = QtCore.QPointF(2.0, 2.0) self.calculateOffsets(shape, point) self.prevPoint = point if not self.boundedMoveShape(shape, point - offset): self.boundedMoveShape(shape, point + offset) def paintEvent(self, event): if not self.pixmap: return super(Canvas, self).paintEvent(event) p = self._painter p.begin(self) p.setRenderHint(QtGui.QPainter.Antialiasing) p.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) p.setRenderHint(QtGui.QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawPixmap(0, 0, self.pixmap) Shape.scale = self.scale for shape in self.shapes: if (shape.selected or not self._hideBackround) and \ self.isVisible(shape): shape.fill = shape.selected or shape == self.hShape shape.paint(p) if self.current: self.current.paint(p) self.line.paint(p) if self.selectedShapeCopy: self.selectedShapeCopy.paint(p) p.end() def transformPos(self, point): """Convert from widget-logical coordinates to painter-logical ones.""" return point / self.scale - self.offsetToCenter() def offsetToCenter(self): s = self.scale area = super(Canvas, self).size() w, h = self.pixmap.width() * s, self.pixmap.height() * s aw, ah = area.width(), area.height() x = (aw - w) / (2 * s) if aw > w else 0 y = (ah - h) / (2 * s) if ah > h else 0 if QT5: return QtCore.QPoint(x, y) else: return QtCore.QPointF(x, y) def outOfPixmap(self, p): w, h = self.pixmap.width(), self.pixmap.height() return not (0 <= p.x() <= w and 0 <= p.y() <= h) def finalise(self): assert self.current self.current.close() self.shapes.append(self.current) self.storeShapes() self.current = None self.setHiding(False) self.newShape.emit() self.update() def closeEnough(self, p1, p2): # d = distance(p1 - p2) # m = (p1-p2).manhattanLength() # print "d %.2f, m %d, %.2f" % (d, m, d - m) return distance(p1 - p2) < self.epsilon def intersectionPoint(self, p1, p2): # Cycle through each image edge in clockwise fashion, # and find the one intersecting the current line segment. # http://paulbourke.net/geometry/lineline2d/ size = self.pixmap.size() points = [(0, 0), (size.width(), 0), (size.width(), size.height()), (0, size.height())] x1, y1 = p1.x(), p1.y() x2, y2 = p2.x(), p2.y() d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points)) x3, y3 = points[i] x4, y4 = points[(i + 1) % 4] if (x, y) == (x1, y1): # Handle cases where previous point is on one of the edges. if x3 == x4: return QtCore.QPointF(x3, min(max(0, y2), max(y3, y4))) else: # y3 == y4 return QtCore.QPointF(min(max(0, x2), max(x3, x4)), y3) return QtCore.QPointF(x, y) def intersectingEdges(self, point1, point2, points): """Find intersecting edges. For each edge formed by `points', yield the intersection with the line segment `(x1,y1) - (x2,y2)`, if it exists. Also return the distance of `(x2,y2)' to the middle of the edge along with its index, so that the one closest can be chosen. """ (x1, y1) = point1 (x2, y2) = point2 for i in range(4): x3, y3 = points[i] x4, y4 = points[(i + 1) % 4] denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3) nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3) if denom == 0: # This covers two cases: # nua == nub == 0: Coincident # otherwise: Parallel continue ua, ub = nua / denom, nub / denom if 0 <= ua <= 1 and 0 <= ub <= 1: x = x1 + ua * (x2 - x1) y = y1 + ua * (y2 - y1) m = QtCore.QPointF((x3 + x4) / 2, (y3 + y4) / 2) d = distance(m - QtCore.QPointF(x2, y2)) yield d, i, (x, y) # These two, along with a call to adjustSize are required for the # scroll area. def sizeHint(self): return self.minimumSizeHint() def minimumSizeHint(self): if self.pixmap: return self.scale * self.pixmap.size() return super(Canvas, self).minimumSizeHint() def wheelEvent(self, ev): if QT5: mods = ev.modifiers() delta = ev.angleDelta() if QtCore.Qt.ControlModifier == int(mods): # with Ctrl/Command key # zoom self.zoomRequest.emit(delta.y(), ev.pos()) else: # scroll self.scrollRequest.emit(delta.x(), QtCore.Qt.Horizontal) self.scrollRequest.emit(delta.y(), QtCore.Qt.Vertical) else: if ev.orientation() == QtCore.Qt.Vertical: mods = ev.modifiers() if QtCore.Qt.ControlModifier == int(mods): # with Ctrl/Command key self.zoomRequest.emit(ev.delta(), ev.pos()) else: self.scrollRequest.emit( ev.delta(), QtCore.Qt.Horizontal if (QtCore.Qt.ShiftModifier == int(mods)) else QtCore.Qt.Vertical) else: self.scrollRequest.emit(ev.delta(), QtCore.Qt.Horizontal) ev.accept() def keyPressEvent(self, ev): key = ev.key() if key == QtCore.Qt.Key_Escape and self.current: self.current = None self.drawingPolygon.emit(False) self.update() elif key == QtCore.Qt.Key_Return and self.canCloseShape(): self.finalise() def setLastLabel(self, text): assert text self.shapes[-1].label = text self.shapesBackups.pop() self.storeShapes() return self.shapes[-1] def undoLastLine(self): assert self.shapes self.current = self.shapes.pop() self.current.setOpen() self.line.points = [self.current[-1], self.current[0]] self.drawingPolygon.emit(True) def undoLastPoint(self): if not self.current or self.current.isClosed(): return self.current.popPoint() if len(self.current) > 0: self.line[0] = self.current[-1] else: self.current = None self.drawingPolygon.emit(False) self.repaint() def loadPixmap(self, pixmap): self.pixmap = pixmap self.shapes = [] self.repaint() def loadShapes(self, shapes): self.shapes = list(shapes) self.storeShapes() self.current = None self.repaint() def setShapeVisible(self, shape, value): self.visible[shape] = value self.repaint() def overrideCursor(self, cursor): self.restoreCursor() self._cursor = cursor QtWidgets.QApplication.setOverrideCursor(cursor) def restoreCursor(self): QtWidgets.QApplication.restoreOverrideCursor() def resetState(self): self.restoreCursor() self.pixmap = None self.shapesBackups = [] self.update()
class ElementPickerWidget(FigureWidget): """ Tool window for picking elements of an interactive periodic table. Takes a signal in the constructor, and a parent control. """ element_toggled = QtCore.Signal(str) def __init__(self, main_window, parent): super(ElementPickerWidget, self).__init__(main_window, parent) self.signal = None self.create_controls() self.table.element_toggled.connect(self._toggle_element) self.set_signal(self.signal) self._only_lines = ['Ka', 'La', 'Ma', 'Kb', 'Lb1', 'Mb'] def _on_figure_change(self, figure): super(ElementPickerWidget, self)._on_figure_change(figure) signal = win2sig(figure, plotting_signal=self.ui._plotting_signal) self.set_signal(signal) def set_signal(self, signal): self.signal = signal self.setWindowTitle(tr("Element picker")) if self.isEDS(): self.map_btn.show() else: self.map_btn.hide() if signal is None or signal.signal is None: self.set_enabled(False) return else: self.set_enabled(True) # Enable markers if plot has any with block_signals(self.chk_markers): markers = (hasattr(signal.signal, '_xray_markers') and bool(signal.signal._xray_markers)) self.chk_markers.setChecked(markers) # Make sure we have the Sample node, and Sample.elements if not hasattr(signal.signal.metadata, 'Sample'): signal.signal.metadata.add_node('Sample') if not hasattr(signal.signal.metadata.Sample, 'elements'): signal.signal.metadata.Sample.elements = [] self._set_elements(signal.signal.metadata.Sample.elements) # Disable elements which hyperspy does not accept hsyp_elem = list(elements_db.keys()) for w in self.table.children(): if isinstance(w, ExClickLabel): elem = w.text() if elem not in hsyp_elem: self.table.disable_element(elem) else: self.table.enable_element(elem) def set_enabled(self, value): self.setEnabled(value) @property def markers(self): return self.chk_markers.isChecked() def isEDS(self): if self.signal is None or self.signal.signal is None: return False return isinstance(self.signal.signal, edstypes) def isEELS(self): if self.signal is None or self.signal.signal is None: return False return isinstance(self.signal.signal, hyperspy.signals.EELSSpectrum) def _toggle_element(self, element): """ Makes sure the element is toggled correctly for both EDS and EELS. Dependent on hyperspy implementation, as there are currently no remove_element functions. """ if self.isEELS(): self._toggle_element_eels(element) elif self.isEDS(): self._toggle_element_eds(element) def _toggle_element_eds(self, element): s = self.signal.signal lines_added = [] lines_removed = [] self.ui.record_code("signal = ui.get_selected_signal()") if element in s.metadata.Sample.elements: # Element present, we're removing it s.metadata.Sample.elements.remove(element) self.ui.record_code( "signal.metadata.Sample.elements.remove('%s')" % element) if 'Sample.xray_lines' in s.metadata: for line in reversed(s.metadata.Sample.xray_lines): if line.startswith(element): s.metadata.Sample.xray_lines.remove(line) lines_removed.append(line) if len(s.metadata.Sample.xray_lines) < 1: del s.metadata.Sample.xray_lines else: lines_removed.extend( s._get_lines_from_elements([element], only_one=False, only_lines=self._only_lines)) else: lines_added = s._get_lines_from_elements( [element], only_one=False, only_lines=self._only_lines) if 'Sample.xray_lines' in s.metadata: s.add_lines(lines_added) # Will also add element self.ui.record_code("signal.add_lines(%s)" % str(lines_added)) else: s.add_elements((element, )) self.ui.record_code("signal.add_elements(%s)" % str([element])) if self.markers: if lines_added: s.add_xray_lines_markers(lines_added) if lines_removed: s.remove_xray_lines_markers(lines_removed) def _toggle_element_eels(self, element): s = self.signal.signal self.ui.record_code("signal = ui.get_selected_signal()") if element in s.metadata.Sample.elements: s.elements.remove(element) s.subshells = set() s.add_elements([]) # Will set metadata.Sample.elements self.ui.record_code("signal.elements.remove('%s')" % str(element)) self.ui.record_code("signal.subshells = set()") self.ui.record_code("signal.add_elements([])") else: s.add_elements((element, )) self.ui.record_code("signal.add_elements(%s)" % str([element])) def _toggle_subshell(self, subshell, checked): if not self.isEDS(): return s = self.signal.signal element, ss = subshell.split('_') # Figure out whether element should be toggled active, _ = self._get_element_subshells(element) if checked: any_left = True elif ss in active: active.remove(ss) any_left = len(active) > 0 # If any(subshells toggled) != element toggled, we should toggle # element if self.table.toggled[element] != any_left: # Update table toggle self.table.toggle_element(element) # Update signal state if not any_left: # Remove element self._toggle_element(element) self.ui.record_code("signal = ui.get_selected_signal()") if 'Sample.xray_lines' not in s.metadata and len(active) > 0: lines = [element + '_' + a for a in active] s.add_lines(lines) self.ui.record_code("signal.add_lines(%s)" % str(lines)) if self.markers: if checked: s.add_xray_lines_markers(lines) else: s.remove_xray_lines_markers([subshell]) else: if checked: s.add_lines([subshell]) self.ui.record_code("signal.add_lines(%s)" % str([subshell])) if self.markers: s.add_xray_lines_markers([subshell]) elif 'Sample.xray_lines' in s.metadata: if subshell in s.metadata.Sample.xray_lines: s.metadata.Sample.xray_lines.remove(subshell) self.ui.record_code( "signal.metadata.Sample.xray_lines.remove('%s')" % str(subshell)) if self.markers: s.remove_xray_lines_markers([subshell]) # If all lines are disabled, fall back to element defined # (Not strictly needed) if len(s.metadata.Sample.xray_lines) < 1: del s.metadata.Sample.xray_lines self.ui.record_code( "del signal.metadata.Sample.xray_lines") def _set_elements(self, elements): """ Sets the table elements to passed parameter. Does not modify elements in signal! That is handled by the _toggle_element* functions """ self.table.set_elements(elements) def set_element(self, element, value): """Sets the state of element in table and adds/removes in signal if necessary """ self.table.set_element(element, value) s = self.signal.signal if (element in s.metadata.Sample.elements) != value: self._toggle_element(element) def _on_toggle_markers(self, value): """Toggles peak markers on the plot, i.e. adds/removes markers for all elements added on signal. """ w = self.signal s = self.signal.signal if value: if self.isEDS(): w.keep_on_close = True s._plot_xray_lines(xray_lines=True) w.update_figures() w.keep_on_close = False else: if self.isEDS(): for m in reversed(s._plot.signal_plot.ax_markers): m.close(render_figure=False) if hasattr(s, '_xray_markers'): s._xray_markers.clear() def make_map(self): """ Make integrated intensity maps for the defines elements. Currently only implemented for EDS signals. """ if self.isEELS(): pass # TODO: EELS maps elif self.isEDS(): imgs = self.signal.signal.get_lines_intensity(only_one=False) for im in imgs: im.plot() def _get_element_subshells(self, element, include_pre_edges=False): s = self.signal.signal subshells = [] possible_subshells = [] if self.isEELS(): subshells[:] = [ss.split('_')[0] for ss in s.subshells] Eaxis = s.axes_manager.signal_axes[0].axis if not include_pre_edges: start_energy = Eaxis[0] else: start_energy = 0. end_energy = Eaxis[-1] for shell in elements_db[element]['Atomic_properties'][ 'Binding_energies']: if shell[-1] != 'a': if start_energy <= \ elements_db[element]['Atomic_properties'][ 'Binding_energies'][shell]['onset_energy (eV)'] \ <= end_energy: possible_subshells.add(shell) elif self.isEDS(): if 'Sample.xray_lines' in s.metadata: xray_lines = s.metadata.Sample.xray_lines for line in xray_lines: c_element, subshell = line.split("_") if c_element == element: subshells.append(subshell) elif ('Sample.elements' in s.metadata and element in s.metadata.Sample.elements): xray_lines = s._get_lines_from_elements( [element], only_one=False, only_lines=self._only_lines) for line in xray_lines: _, subshell = line.split("_") subshells.append(subshell) possible_xray_lines = \ s._get_lines_from_elements([element], only_one=False, only_lines=self._only_lines) for line in possible_xray_lines: _, subshell = line.split("_") possible_subshells.append(subshell) return (subshells, possible_subshells) def element_context(self, widget, point): if not self.isEDS(): return cm = QtWidgets.QMenu() element = widget.text() active, possible = self._get_element_subshells(element) for ss in possible: key = element + '_' + ss ac = cm.addAction(ss) ac.setCheckable(True) ac.setChecked(ss in active) f = partial(self._toggle_subshell, key) ac.toggled[bool].connect(f) if possible: cm.exec_(widget.mapToGlobal(point)) def create_controls(self): """ Create UI controls. """ self.table = PeriodicTableWidget(self) self.table.element_toggled.connect(self.element_toggled) # Forward for w in self.table.children(): if not isinstance(w, ExClickLabel): continue w.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) f = partial(self.element_context, w) w.customContextMenuRequested[QtCore.QPoint].connect(f) self.chk_markers = QtWidgets.QCheckBox(tr("Markers")) self.chk_markers.toggled[bool].connect(self._on_toggle_markers) self.map_btn = QtWidgets.QPushButton(tr("Map")) self.map_btn.clicked.connect(self.make_map) vbox = QtWidgets.QVBoxLayout() vbox.addWidget(self.table) hbox = QtWidgets.QHBoxLayout() hbox.addWidget(self.chk_markers) hbox.addWidget(self.map_btn) vbox.addLayout(hbox) w = QtWidgets.QWidget() w.setLayout(vbox) self.setWidget(w)