def qapplication(translate=True, test_time=3): """ Return QApplication instance Creates it if it doesn't already exist test_time: Time to maintain open the application when testing. It's given in seconds """ if running_in_mac_app(): SpyderApplication = MacApplication else: SpyderApplication = QApplication app = SpyderApplication.instance() if app is None: # Set Application name for Gnome 3 # https://groups.google.com/forum/#!topic/pyside/24qxvwfrRDs app = SpyderApplication(['Spyder']) # Set application name for KDE (See issue 2207) app.setApplicationName('Spyder') if translate: install_translator(app) test_travis = os.environ.get('TEST_CI_WIDGETS', None) if test_travis is not None: timer_shutdown = QTimer(app) timer_shutdown.timeout.connect(app.quit) timer_shutdown.start(test_time*1000) return app
def test_calltip(main_window, qtbot): """Hide the calltip in the editor when a matching ')' is found.""" # Load test file text = 'a = [1,2,3]\n(max' main_window.editor.new(fname="test.py", text=text) code_editor = main_window.editor.get_focus_widget() # Set text to start code_editor.set_text(text) code_editor.go_to_line(2) code_editor.move_cursor(5) calltip = code_editor.calltip_widget assert not calltip.isVisible() qtbot.keyPress(code_editor, Qt.Key_ParenLeft, delay=3000) qtbot.keyPress(code_editor, Qt.Key_A, delay=1000) qtbot.waitUntil(lambda: calltip.isVisible(), timeout=1000) qtbot.keyPress(code_editor, Qt.Key_ParenRight, delay=1000) qtbot.keyPress(code_editor, Qt.Key_Space) assert not calltip.isVisible() qtbot.keyPress(code_editor, Qt.Key_ParenRight, delay=1000) qtbot.keyPress(code_editor, Qt.Key_Enter, delay=1000) QTimer.singleShot(1000, lambda: close_save_message_box(qtbot)) main_window.editor.close_file()
def free_memory(self): """Free memory signal.""" self.main.free_memory() QTimer.singleShot(self.INITIAL_FREE_MEMORY_TIME_TRIGGER, lambda: self.main.free_memory()) QTimer.singleShot(self.SECONDARY_FREE_MEMORY_TIME_TRIGGER, lambda: self.main.free_memory())
def blink_figure(self): """Blink figure once.""" if self.fig: self._blink_flag = not self._blink_flag self.repaint() if self._blink_flag: timer = QTimer() timer.singleShot(40, self.blink_figure)
def start_video(self): timer = QTimer() self.timer = timer timer.timeout.connect(self._wait_for_frame) self.cam.start_live_video(**self.settings) timer.start(0) # Run full throttle self.is_live = True self.needs_resize = True self.videoStarted.emit()
def set_font_size(self, old, new): current_font = self.app.font() current_font.setPointSizeF(current_font.pointSizeF()/old*new) QApplication.instance().setFont(current_font) for w in self.app.allWidgets(): w_c_f = w.font() w_c_f.setPointSizeF(w_c_f.pointSizeF()/old*new) w.setFont(w_c_f) QTimer.singleShot(0, self.resizeForNewDisplayWidget)
def test_sort_dataframe_with_category_dtypes(qtbot): # cf. issue 5361 df = DataFrame({'A': [1, 2, 3, 4], 'B': ['a', 'b', 'c', 'd']}) df = df.astype(dtype={'B': 'category'}) df_cols = df.dtypes editor = DataFrameEditor(None) editor.setup_and_check(df_cols) dfm = editor.dataModel QTimer.singleShot(1000, lambda: close_message_box(qtbot)) editor.dataModel.sort(0) assert data(dfm, 0, 0) == 'int64' assert data(dfm, 1, 0) == 'category'
def revert(self): """ Takes the data stored in the models and displays them in the widgets. """ # make sure it is called from main thread if (not QThread.currentThread() == QCoreApplication.instance( ).thread()): QTimer.singleShot(0, self.revert) return for key in self._mappings: self._on_model_notification(key)
def environ_dialog(qtbot): "Setup the Environment variables Dialog taking into account the os." QTimer.singleShot(1000, lambda: close_message_box(qtbot)) if os.name == 'nt': from spyder.utils.environ import WinUserEnvDialog dialog = WinUserEnvDialog() else: from spyder.utils.environ import EnvDialog dialog = EnvDialog() qtbot.addWidget(dialog) return dialog
def focusOutEvent(self, event): """Handle focus out event restoring the last valid selected path.""" # Calling asynchronously the 'add_current_text' to avoid crash # https://groups.google.com/group/spyderlib/browse_thread/thread/2257abf530e210bd if not self.is_valid(): lineedit = self.lineEdit() QTimer.singleShot(50, lambda: lineedit.setText(self.selected_text)) hide_status = getattr(self.lineEdit(), 'hide_status_icon', None) if hide_status: hide_status() QComboBox.focusOutEvent(self, event)
def start(self): """ Start this tester. """ self.timer = QTimer() # Connecting self.idle to a QTimer with 0 time delay # makes it called when there are no events to process self.timer.timeout.connect(self._idle) self.timer.start() # This calls __call__() method QTimer.singleShot(0, self) # Start the event loop self.app.exec_()
def test_sort_dataframe_with_duplicate_column(qtbot): df = DataFrame({'A': [1, 3, 2], 'B': [4, 6, 5]}) df = concat((df, df.A), axis=1) editor = DataFrameEditor(None) editor.setup_and_check(df) dfm = editor.dataModel QTimer.singleShot(1000, lambda: close_message_box(qtbot)) editor.dataModel.sort(0) assert [data(dfm, row, 0) for row in range(len(df))] == ['1', '3', '2'] assert [data(dfm, row, 1) for row in range(len(df))] == ['4', '6', '5'] editor.dataModel.sort(1) assert [data(dfm, row, 0) for row in range(len(df))] == ['1', '2', '3'] assert [data(dfm, row, 1) for row in range(len(df))] == ['4', '5', '6']
def set_display_widget(self, new_widget): if new_widget == self._display_widget: return self.clear_display_widget() if not new_widget.layout(): new_widget.setMinimumSize(new_widget.size()) self._new_widget_size = new_widget.size() self._display_widget = new_widget self.setCentralWidget(self._display_widget) self.update_window_title() # Resizing to the new widget's dimensions needs to be # done on the event loop for some reason - you can't # just do it here. QTimer.singleShot(0, self.resizeForNewDisplayWidget)
def setup(self, icon_painter, painter, rect): if self.parent_widget not in self.info: timer = QTimer() timer.timeout.connect(lambda: self._update(self.parent_widget)) self.info[self.parent_widget] = [timer, 0, self.step] timer.start(self.interval) else: timer, angle, self.step = self.info[self.parent_widget] x_center = rect.width() * 0.5 y_center = rect.height() * 0.5 painter.translate(x_center, y_center) painter.rotate(angle) painter.translate(-x_center, -y_center)
def follow_directories_loaded(self, fname): """Follow directories loaded during startup""" if self._to_be_loaded is None: return path = osp.normpath(to_text_string(fname)) if path in self._to_be_loaded: self._to_be_loaded.remove(path) if self._to_be_loaded is not None and len(self._to_be_loaded) == 0 \ and not is_pyqt46: self.fsmodel.directoryLoaded.disconnect( self.follow_directories_loaded) if self._scrollbar_positions is not None: # The tree view need some time to render branches: QTimer.singleShot(50, self.restore_scrollbar_positions)
class ConnectionInspector(QWidget): def __init__(self, parent=None): super(ConnectionInspector, self).__init__(parent, Qt.Window) connections = self.fetch_data() self.table_view = ConnectionTableView(connections, self) self.setLayout(QVBoxLayout(self)) self.layout().addWidget(self.table_view) button_layout = QHBoxLayout() self.layout().addItem(button_layout) self.save_status_label = QLabel(self) button_layout.addWidget(self.save_status_label) button_layout.addStretch() self.save_button = QPushButton(self) self.save_button.setText("Save list to file...") self.save_button.clicked.connect(self.save_list_to_file) button_layout.addWidget(self.save_button) self.update_timer = QTimer(parent=self) self.update_timer.setInterval(1500) self.update_timer.timeout.connect(self.update_data) self.update_timer.start() def update_data(self): self.table_view.model().connections = self.fetch_data() def fetch_data(self): plugins = data_plugins.plugin_modules return [connection for p in plugins.values() for connection in p.connections.values() ] @Slot() def save_list_to_file(self): filename, filters = QFileDialog.getSaveFileName(self, "Save connection list", "", "Text Files (*.txt)") try: with open(filename, "w") as f: for conn in self.table_view.model().connections: f.write( "{p}://{a}\n".format(p=conn.protocol, a=conn.address)) self.save_status_label.setText("File saved to {}".format(filename)) except Exception as e: msgBox = QMessageBox() msgBox.setText("Couldn't save connection list to file.") msgBox.setInformativeText("Error: {}".format(str(e))) msgBox.setStandardButtons(QMessageBox.Ok) msgBox.exec_()
def setup(): qapp = QApplication(sys.argv) # qapp.setGraphicsSystem('native') qapp.setWindowIcon(QIcon(os.path.join(ICON_PATH, 'application', 'icon.png'))) #http://stackoverflow.com/questions/4938723/what-is-the-correct-way-to-make-my-pyqt-application-quit-when-killed-from-the-co timer = QTimer() timer.start(500) # You may change this if you wish. timer.timeout.connect(lambda: None) # Let the interpreter run each 500 ms. app = App(sys.argv) app.viewer.main_window.show() return qapp, app
def __init__(self, cmd_list, environ=None): """ Process worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. environ : dict Process environment, """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._fired = False self._communicate_first = False self._partial_stdout = None self._started = False self._timer = QTimer() self._process = QProcess() self._set_environment(environ) self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial)
def __init__(self, parent, statusbar): """Status bar widget base for widgets that update based on timers.""" super(BaseTimerStatus, self).__init__(parent, statusbar) # Widgets self.label = QLabel(self.TITLE) self.value = QLabel() # Widget setup self.setToolTip(self.TIP) self.value.setAlignment(Qt.AlignRight) self.value.setFont(self.label_font) fm = self.value.fontMetrics() self.value.setMinimumWidth(fm.width('000%')) # Layout layout = self.layout() layout.addWidget(self.label) layout.addWidget(self.value) layout.addSpacing(20) # Setup if self.is_supported(): self.timer = QTimer() self.timer.timeout.connect(self.update_label) self.timer.start(2000) else: self.timer = None self.hide()
def qapplication(translate=True, test_time=3): """Return QApplication instance Creates it if it doesn't already exist""" app = QApplication.instance() if app is None: app = QApplication(['Conda-Manager']) app.setApplicationName('Conda-Manager') if translate: install_translator(app) test_travis = os.environ.get('TEST_CI', None) if test_travis is not None: timer_shutdown = QTimer(app) timer_shutdown.timeout.connect(app.quit) timer_shutdown.start(test_time*1000) return app
def __init__(self, channel, pv, protocol=None, parent=None): """ Instantiate Pv object and set up the channel access connections. :param channel: :class:`PyDMChannel` object as the first listener. :type channel: :class:`PyDMChannel` :param pv: Name of the pv to connect to. :type pv: str :param parent: PyQt widget that this widget is inside of. :type parent: QWidget """ super(Connection, self).__init__(channel, pv, protocol, parent) self.python_type = None self.pv = setup_pv(pv, con_cb=self.connected_cb, mon_cb=self.monitor_cb, rwaccess_cb=self.rwaccess_cb) self.enums = None self.sevr = None self.ctrl_llim = None self.ctrl_hlim = None self.units = None self.prec = None self.count = None self.epics_type = None # Auxilliary info to help with throttling self.scan_pv = setup_pv(pv + ".SCAN", mon_cb=self.scan_pv_cb, mon_cb_once=True) self.throttle = QTimer(self) self.throttle.timeout.connect(self.throttle_cb) self.add_listener(channel)
def __init__(self, connections=[], parent=None): super(ConnectionTableModel, self).__init__(parent=parent) self._column_names = ("protocol", "address", "connected") self.update_timer = QTimer(self) self.update_timer.setInterval(1000) self.update_timer.timeout.connect(self.update_values) self.connections = connections
class BaseTimerStatus(StatusBarWidget): """Status bar widget base for widgets that update based on timers.""" def __init__(self, parent, statusbar): """Status bar widget base for widgets that update based on timers.""" self.timer = None # Needs to come before parent call super(BaseTimerStatus, self).__init__(parent, statusbar) self._interval = 2000 # Widget setup fm = self.label_value.fontMetrics() self.label_value.setMinimumWidth(fm.width('000%')) # Setup if self.is_supported(): self.timer = QTimer() self.timer.timeout.connect(self.update_status) self.timer.start(self._interval) else: self.hide() def setVisible(self, value): """Override Qt method to stops timers if widget is not visible.""" if self.timer is not None: if value: self.timer.start(self._interval) else: self.timer.stop() super(BaseTimerStatus, self).setVisible(value) def set_interval(self, interval): """Set timer interval (ms).""" self._interval = interval if self.timer is not None: self.timer.setInterval(interval) def import_test(self): """Raise ImportError if feature is not supported.""" raise NotImplementedError def is_supported(self): """Return True if feature is supported.""" try: self.import_test() return True except ImportError: return False def get_value(self): """Return formatted text value.""" raise NotImplementedError def update_status(self): """Update status label widget, if widget is visible.""" if self.isVisible(): self.label_value.setText(self.get_value())
def submit(self): """ Submits the current values stored in the widgets to the models. """ # make sure it is called from main thread if (not QThread.currentThread() == QCoreApplication.instance( ).thread()): QTimer.singleShot(0, self.submit) return submit_policy = self._submit_policy self.submit_policy = SUBMIT_POLICY_AUTO try: for key in self._mappings: self._on_widget_property_notification(key) finally: self.submit_policy = submit_policy
class BaseTimerStatus(StatusBarWidget): TITLE = None TIP = None def __init__(self, parent, statusbar): StatusBarWidget.__init__(self, parent, statusbar) self.setToolTip(self.TIP) layout = self.layout() layout.addWidget(QLabel(self.TITLE)) self.label = QLabel() self.label.setFont(self.label_font) layout.addWidget(self.label) layout.addSpacing(20) if self.is_supported(): self.timer = QTimer() self.timer.timeout.connect(self.update_label) self.timer.start(2000) else: self.timer = None self.hide() def set_interval(self, interval): """Set timer interval (ms)""" if self.timer is not None: self.timer.setInterval(interval) def import_test(self): """Raise ImportError if feature is not supported""" raise NotImplementedError def is_supported(self): """Return True if feature is supported""" try: self.import_test() return True except ImportError: return False def get_value(self): """Return value (e.g. CPU or memory usage)""" raise NotImplementedError def update_label(self): """Update status label widget, if widget is visible""" if self.isVisible(): self.label.setText('%d %%' % self.get_value())
def start_master(self): """Start the master thread, used to retrieve the results.""" if self.use_master_thread: self._thread_master = _start_thread(self._retrieve) else: self._timer_master = QTimer(self) self._timer_master.setInterval(int(TIMER_MASTER_DELAY * 1000)) self._timer_master.timeout.connect(self._retrieve) self._timer_master.start()
def __init__(self, widget, address, protocol=None, parent=None): super(Connection, self).__init__(widget, address, protocol, parent) self.add_listener(widget) self.value = address self.rand = 0 self.timer = QTimer(self) self.timer.timeout.connect(self.send_new_value) self.timer.start(1000) self.connected = True
def test_load_kernel_file_from_id(ipyconsole, qtbot): """ Test that a new client is created using its id """ shell = ipyconsole.get_current_shellwidget() client = ipyconsole.get_current_client() qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) connection_file = osp.basename(client.connection_file) id_ = connection_file.split('kernel-')[-1].split('.json')[0] QTimer.singleShot(2000, lambda: open_client_from_connection_info( id_, qtbot)) ipyconsole.create_client_for_kernel() qtbot.wait(1000) new_client = ipyconsole.get_clients()[1] assert new_client.id_ == dict(int_id='1', str_id='B')
class MatplotlibDataViewer(MatplotlibViewerMixin, DataViewer): _state_cls = MatplotlibDataViewerState tools = ['mpl:home', 'mpl:pan', 'mpl:zoom'] subtools = {'save': ['mpl:save']} def __init__(self, session, parent=None, wcs=None, state=None): super(MatplotlibDataViewer, self).__init__(session, parent=parent, state=state) # Use MplWidget to set up a Matplotlib canvas inside the Qt window self.mpl_widget = MplWidget() self.setCentralWidget(self.mpl_widget) # TODO: shouldn't have to do this self.central_widget = self.mpl_widget self.figure, self.axes = init_mpl(self.mpl_widget.canvas.fig, wcs=wcs) MatplotlibViewerMixin.setup_callbacks(self) self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) self._monitor_computation = QTimer() self._monitor_computation.setInterval(500) self._monitor_computation.timeout.connect(self._update_computation) def _update_computation(self, message=None): # If we get a ComputationStartedMessage and the timer isn't currently # active, then we start the timer but we then return straight away. # This is to avoid showing the 'Computing' message straight away in the # case of reasonably fast operations. if isinstance(message, ComputationStartedMessage): if not self._monitor_computation.isActive(): self._monitor_computation.start() return for layer_artist in self.layers: if layer_artist.is_computing: self.loading_rectangle.set_visible(True) text = self.loading_text.get_text() if text.count('.') > 2: text = 'Computing' else: text += '.' self.loading_text.set_text(text) self.loading_text.set_visible(True) self.redraw() return self.loading_rectangle.set_visible(False) self.loading_text.set_visible(False) self.redraw() # If we get here, the computation has stopped so we can stop the timer self._monitor_computation.stop()
def test_restart_kernel(ipyconsole, qtbot): """ Test that kernel is restarted correctly """ shell = ipyconsole.get_current_shellwidget() client = ipyconsole.get_current_client() qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) # Do an assigment to verify that it's not there after restarting with qtbot.waitSignal(shell.executed): shell.execute('a = 10') # Restart kernel and wait until it's up again shell._prompt_html = None QTimer.singleShot(1000, lambda: close_message_box(qtbot)) client.restart_kernel() qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) assert not shell.is_defined('a')
class ScrollFlagArea(Panel): """Source code editor's scroll flag area""" WIDTH = 24 if sys.platform == 'darwin' else 12 FLAGS_DX = 4 FLAGS_DY = 2 def __init__(self): Panel.__init__(self) self.setAttribute(Qt.WA_OpaquePaintEvent) self.scrollable = True self.setMouseTracking(True) # Define some attributes to be used for unit testing. self._unit_testing = False self._range_indicator_is_visible = False self._alt_key_is_down = False self._slider_range_color = QColor(Qt.gray) self._slider_range_color.setAlphaF(.85) self._slider_range_brush = QColor(Qt.gray) self._slider_range_brush.setAlphaF(.5) self._update_list_timer = QTimer(self) self._update_list_timer.setSingleShot(True) self._update_list_timer.timeout.connect(self.update_flags) # Dictionary with flag lists self._dict_flag_list = {} def on_install(self, editor): """Manages install setup of the pane.""" super().on_install(editor) # Define permanent Qt colors that are needed for painting the flags # and the slider range. self._facecolors = { 'warning': QColor(editor.warning_color), 'error': QColor(editor.error_color), 'todo': QColor(editor.todo_color), 'breakpoint': QColor(editor.breakpoint_color), 'occurrence': QColor(editor.occurrence_color), 'found_results': QColor(editor.found_results_color) } self._edgecolors = { key: color.darker(120) for key, color in self._facecolors.items() } # Signals editor.sig_focus_changed.connect(self.update) editor.sig_key_pressed.connect(self.keyPressEvent) editor.sig_key_released.connect(self.keyReleaseEvent) editor.sig_alt_left_mouse_pressed.connect(self.mousePressEvent) editor.sig_alt_mouse_moved.connect(self.mouseMoveEvent) editor.sig_leave_out.connect(self.update) editor.sig_flags_changed.connect(self.delayed_update_flags) editor.sig_theme_colors_changed.connect(self.update_flag_colors) @property def slider(self): """This property holds whether the vertical scrollbar is visible.""" return self.editor.verticalScrollBar().isVisible() def sizeHint(self): """Override Qt method""" return QSize(self.WIDTH, 0) def update_flag_colors(self, color_dict): """ Update the permanent Qt colors that are used for painting the flags and the slider range with the new colors defined in the given dict. """ for name, color in color_dict.items(): self._facecolors[name] = QColor(color) self._edgecolors[name] = self._facecolors[name].darker(120) def delayed_update_flags(self): """ This function is called every time a flag is changed. There is no need of updating the flags thousands of time by second, as it is quite resources-heavy. This limits the calls to REFRESH_RATE. """ if self._update_list_timer.isActive(): return self._update_list_timer.start(REFRESH_RATE) def update_flags(self): """ Update flags list. This parses the entire file, which can take a lot of time for large files. Save all the flags in lists for painting during paint events. """ self._dict_flag_list = { 'error': [], 'warning': [], 'todo': [], 'breakpoint': [], } editor = self.editor block = editor.document().firstBlock() while block.isValid(): # Parse all lines in the file looking for something to flag. data = block.userData() if data: if data.code_analysis: # Paint the errors and warnings for _, _, severity, _ in data.code_analysis: if severity == DiagnosticSeverity.ERROR: flag_type = 'error' break else: flag_type = 'warning' elif data.todo: flag_type = 'todo' elif data.breakpoint: flag_type = 'breakpoint' else: flag_type = None if flag_type is not None: self._dict_flag_list[flag_type].append(block.blockNumber()) block = block.next() self.update() def paintEvent(self, event): """ Override Qt method. Painting the scroll flag area There is two cases: - The scroll bar is moving, in which case paint all flags. - The scroll bar is not moving, only paint flags corresponding to visible lines. """ # The area in which the slider handle of the scrollbar may move. groove_rect = self.get_scrollbar_groove_rect() # The scrollbar's scale factor ratio between pixel span height and # value span height scale_factor = groove_rect.height() / self.get_scrollbar_value_height() # The vertical offset of the scroll flag area relative to the # top of the text editor. offset = groove_rect.y() # Note that we calculate the pixel metrics required to draw the flags # here instead of using the convenience methods of the ScrollFlagArea # for performance reason. rect_x = ceil(self.FLAGS_DX / 2) rect_w = self.WIDTH - self.FLAGS_DX rect_h = self.FLAGS_DY # Fill the whole painting area painter = QPainter(self) painter.fillRect(event.rect(), self.editor.sideareas_color) editor = self.editor # Define compute_flag_ypos to position the flags: # Paint flags for the entire document last_line = editor.document().lastBlock().firstLineNumber() # The 0.5 offset is used to align the flags with the center of # their corresponding text edit block before scaling. first_y_pos = self.value_to_position(0.5, scale_factor, offset) - self.FLAGS_DY / 2 last_y_pos = self.value_to_position(last_line + 0.5, scale_factor, offset) - self.FLAGS_DY / 2 def compute_flag_ypos(block): line_number = block.firstLineNumber() if editor.verticalScrollBar().maximum() == 0: geometry = editor.blockBoundingGeometry(block) pos = geometry.y() + geometry.height() / 2 + self.FLAGS_DY / 2 elif last_line != 0: frac = line_number / last_line pos = first_y_pos + frac * (last_y_pos - first_y_pos) else: pos = first_y_pos return ceil(pos) # All the lists of block numbers for flags dict_flag_lists = { "occurrence": editor.occurrences, "found_results": editor.found_results } dict_flag_lists.update(self._dict_flag_list) for flag_type in dict_flag_lists: painter.setBrush(self._facecolors[flag_type]) painter.setPen(self._edgecolors[flag_type]) for block_number in dict_flag_lists[flag_type]: # Find the block block = editor.document().findBlockByNumber(block_number) if not block.isValid(): continue # paint if everything else is fine rect_y = compute_flag_ypos(block) painter.drawRect(rect_x, rect_y, rect_w, rect_h) # Paint the slider range if not self._unit_testing: alt = QApplication.queryKeyboardModifiers() & Qt.AltModifier else: alt = self._alt_key_is_down if self.slider: cursor_pos = self.mapFromGlobal(QCursor().pos()) is_over_self = self.rect().contains(cursor_pos) is_over_editor = editor.rect().contains( editor.mapFromGlobal(QCursor().pos())) # We use QRect.contains instead of QWidget.underMouse method to # determined if the cursor is over the editor or the flag scrollbar # because the later gives a wrong result when a mouse button # is pressed. if is_over_self or (alt and is_over_editor): painter.setPen(self._slider_range_color) painter.setBrush(self._slider_range_brush) x, y, width, height = self.make_slider_range( cursor_pos, scale_factor, offset, groove_rect) painter.drawRect(x, y, width, height) self._range_indicator_is_visible = True else: self._range_indicator_is_visible = False def enterEvent(self, event): """Override Qt method""" self.update() def leaveEvent(self, event): """Override Qt method""" self.update() def mouseMoveEvent(self, event): """Override Qt method""" self.update() def mousePressEvent(self, event): """Override Qt method""" if self.slider and event.button() == Qt.LeftButton: vsb = self.editor.verticalScrollBar() value = self.position_to_value(event.pos().y()) vsb.setValue(int(value - vsb.pageStep() / 2)) def keyReleaseEvent(self, event): """Override Qt method.""" if event.key() == Qt.Key_Alt: self._alt_key_is_down = False self.update() def keyPressEvent(self, event): """Override Qt method""" if event.key() == Qt.Key_Alt: self._alt_key_is_down = True self.update() def get_vertical_offset(self): """ Return the vertical offset of the scroll flag area relative to the top of the text editor. """ groove_rect = self.get_scrollbar_groove_rect() return groove_rect.y() def get_slider_min_height(self): """ Return the minimum height of the slider range based on that set for the scroll bar's slider. """ return QApplication.instance().style().pixelMetric( QStyle.PM_ScrollBarSliderMin) def get_scrollbar_groove_rect(self): """Return the area in which the slider handle may move.""" vsb = self.editor.verticalScrollBar() style = QApplication.instance().style() opt = QStyleOptionSlider() vsb.initStyleOption(opt) # Get the area in which the slider handle may move. groove_rect = style.subControlRect(QStyle.CC_ScrollBar, opt, QStyle.SC_ScrollBarGroove, self) return groove_rect def get_scrollbar_position_height(self): """Return the pixel span height of the scrollbar area in which the slider handle may move""" groove_rect = self.get_scrollbar_groove_rect() return float(groove_rect.height()) def get_scrollbar_value_height(self): """Return the value span height of the scrollbar""" vsb = self.editor.verticalScrollBar() return vsb.maximum() - vsb.minimum() + vsb.pageStep() def get_scale_factor(self): """Return scrollbar's scale factor: ratio between pixel span height and value span height""" return (self.get_scrollbar_position_height() / self.get_scrollbar_value_height()) def value_to_position(self, y, scale_factor, offset): """Convert value to position in pixels""" vsb = self.editor.verticalScrollBar() return int((y - vsb.minimum()) * scale_factor + offset) def position_to_value(self, y): """Convert position in pixels to value""" vsb = self.editor.verticalScrollBar() offset = self.get_vertical_offset() return vsb.minimum() + max([0, (y - offset) / self.get_scale_factor()]) def make_slider_range(self, cursor_pos, scale_factor, offset, groove_rect): """ Return the slider x and y positions and the slider width and height. """ # The slider range indicator position follows the mouse vertical # position while its height corresponds to the part of the file that # is currently visible on screen. vsb = self.editor.verticalScrollBar() slider_height = self.value_to_position(vsb.pageStep(), scale_factor, offset) - offset slider_height = max(slider_height, self.get_slider_min_height()) # Calculate the minimum and maximum y-value to constraint the slider # range indicator position to the height span of the scrollbar area # where the slider may move. min_ypos = offset max_ypos = groove_rect.height() + offset - slider_height # Determine the bounded y-position of the slider rect. slider_y = max(min_ypos, min(max_ypos, ceil(cursor_pos.y() - slider_height / 2))) return 1, slider_y, self.WIDTH - 2, slider_height def wheelEvent(self, event): """Override Qt method""" self.editor.wheelEvent(event) def set_enabled(self, state): """Toggle scroll flag area visibility""" self.enabled = state self.setVisible(state)
class CondaDependenciesModel(QAbstractTableModel): """ """ def __init__(self, parent, dic, packages_sizes=None): super(CondaDependenciesModel, self).__init__(parent) self._parent = parent self._packages = dic self._packages_sizes = packages_sizes self._rows = [] self._bold_rows = [] self._timer = QTimer() self._timer.timeout.connect(self._timer_update) self._timer_dots = ['. ', '.. ', '...', ' '] self._timer_counter = 0 if len(dic) == 0: self._timer.start(650) self._rows = [[_(u'Resolving dependencies '), u'', u'', u'']] self._bold_rows.append(0) else: if 'actions' in dic: dic = dic['actions'] titles = [_('Name'), _('Unlink'), _('Link'), _('Download')] order = ['UNLINK', 'LINK', 'FETCH'] packages_dic = self._build_packages_table(dic) total_size = packages_dic.pop('TOTAL##', '') packages_order = sorted(list(packages_dic)) rows = [titles] self._bold_rows.append(0) for package in packages_order: row = [package] item = packages_dic[package] for section in order: row.append(item.get(section, u'-')) rows.append(row) if total_size: rows.append([u'', u'', u'', u'TOTAL']) rows.append([u'', u'', u'', total_size]) self._bold_rows.append(len(rows) - 2) self._bold_rows.append(len(rows) - 1) for row in rows: self._rows.append(row) def _timer_update(self): """Add some moving points to the dependency resolution text.""" self._timer_counter += 1 dot = self._timer_dots.pop(0) self._timer_dots = self._timer_dots + [dot] self._rows = [[_(u'Resolving dependencies') + dot, u'', u'', u'']] index = self.createIndex(0, 0) self.dataChanged.emit(index, index) if self._timer_counter > 150: self._timer.stop() self._timer_counter = 0 def _build_packages_table(self, dic): """ """ self._timer.stop() sections = {'FETCH': None, 'EXTRACT': None, 'LINK': None, 'UNLINK': None} packages = {} for section in sections: sections[section] = dic.get(section, ()) if sections[section]: for item in sections[section]: i = item.split(' ')[0] name, version, build = split_canonical_name(i) packages[name] = {} for section in sections: pkgs = sections[section] for item in pkgs: i = item.split(' ')[0] name, version, build = split_canonical_name(i) packages[name][section] = version total = 0 for pkg in packages: val = packages[pkg] if u'FETCH' in val: v = val['FETCH'] if pkg in self._packages_sizes: size = self._packages_sizes[pkg].get(v, '-') if size != '-': total += size size = human_bytes(size) packages[pkg]['FETCH'] = size packages['TOTAL##'] = human_bytes(total) return packages def flags(self, index): """Override Qt method""" if not index.isValid(): return Qt.ItemIsEnabled column = index.column() if column in [0, 1, 2, 3]: return Qt.ItemFlags(Qt.ItemIsEnabled) else: return Qt.ItemFlags(Qt.NoItemFlags) def data(self, index, role=Qt.DisplayRole): """Override Qt method""" if not index.isValid() or not 0 <= index.row() < len(self._rows): return to_qvariant() row = index.row() column = index.column() # Carefull here with the order, this has to be adjusted manually if self._rows[row] == row: name, unlink, link, fetch = [u'', u'', u'', u''] else: name, unlink, link, fetch = self._rows[row] if role == Qt.DisplayRole: if column == 0: return to_qvariant(name) elif column == 1: return to_qvariant(unlink) elif column == 2: return to_qvariant(link) elif column == 3: return to_qvariant(fetch) elif role == Qt.TextAlignmentRole: if column in [0]: return to_qvariant(int(Qt.AlignLeft | Qt.AlignVCenter)) elif column in [1, 2, 3]: return to_qvariant(int(Qt.AlignHCenter | Qt.AlignVCenter)) elif role == Qt.ForegroundRole: return to_qvariant() elif role == Qt.FontRole: font = QFont() if row in self._bold_rows: font.setBold(True) return to_qvariant(font) else: font.setBold(False) return to_qvariant(font) return to_qvariant() def rowCount(self, index=QModelIndex()): """Override Qt method.""" return len(self._rows) def columnCount(self, index=QModelIndex()): """Override Qt method.""" return 4 def row(self, row_index): """Get a row by row index.""" return self._rows[row_index]
class PyDMTimePlot(BasePlot): """ PyDMWaveformPlot is a widget to plot one or more waveforms. Each curve can plot either a Y-axis waveform vs. its indices, or a Y-axis waveform against an X-axis waveform. Parameters ---------- parent : optional The parent of this widget. init_y_channels : list A list of scalar channels to plot vs time. plot_by_timestamps : bool If True, the x-axis shows timestamps as ticks, and those timestamps scroll to the left as time progresses. If False, the x-axis tick marks show time relative to the current time. background: optional The background color for the plot. Accepts any arguments that pyqtgraph.mkColor will accept. """ SynchronousMode = 1 AsynchronousMode = 2 plot_redrawn_signal = Signal(TimePlotCurveItem) def __init__(self, parent=None, init_y_channels=[], plot_by_timestamps=True, background='default'): """ Parameters ---------- parent : Widget The parent widget of the chart. init_y_channels : list A list of scalar channels to plot vs time. plot_by_timestamps : bool If True, the x-axis shows timestamps as ticks, and those timestamps scroll to the left as time progresses. If False, the x-axis tick marks show time relative to the current time. background : str, optional The background color for the plot. Accepts any arguments that pyqtgraph.mkColor will accept. """ self._plot_by_timestamps = plot_by_timestamps self._left_axis = AxisItem("left") if plot_by_timestamps: self._bottom_axis = TimeAxisItem('bottom') else: self.starting_epoch_time = time.time() self._bottom_axis = AxisItem('bottom') super(PyDMTimePlot, self).__init__(parent=parent, background=background, axisItems={ "bottom": self._bottom_axis, "left": self._left_axis }) # Removing the downsampling while PR 763 is not merged at pyqtgraph # Reference: https://github.com/pyqtgraph/pyqtgraph/pull/763 # self.setDownsampling(ds=True, auto=True, mode="mean") if self._plot_by_timestamps: self.plotItem.disableAutoRange(ViewBox.XAxis) self.getViewBox().setMouseEnabled(x=False) else: self.plotItem.setRange(xRange=[DEFAULT_X_MIN, 0], padding=0) self.plotItem.setLimits(xMax=0) self._bufferSize = DEFAULT_BUFFER_SIZE self._time_span = DEFAULT_TIME_SPAN # This is in seconds self._update_interval = DEFAULT_UPDATE_INTERVAL self.update_timer = QTimer(self) self.update_timer.setInterval(self._update_interval) self._update_mode = PyDMTimePlot.SynchronousMode self._needs_redraw = True self.labels = {"left": None, "right": None, "bottom": None} self.units = {"left": None, "right": None, "bottom": None} for channel in init_y_channels: self.addYChannel(channel) def initialize_for_designer(self): # If we are in Qt Designer, don't update the plot continuously. # This function gets called by PyDMTimePlot's designer plugin. self.redraw_timer.setSingleShot(True) def addYChannel(self, y_channel=None, name=None, color=None, lineStyle=None, lineWidth=None, symbol=None, symbolSize=None): """ Adds a new curve to the current plot Parameters ---------- y_channel : str The PV address name : str The name of the curve (usually made the same as the PV address) color : QColor The color for the curve lineStyle : str The line style of the curve, i.e. solid, dash, dot, etc. lineWidth : int How thick the curve line should be symbol : str The symbols as markers along the curve, i.e. circle, square, triangle, star, etc. symbolSize : int How big the symbols should be Returns ------- new_curve : TimePlotCurveItem The newly created curve. """ plot_opts = dict() plot_opts['symbol'] = symbol if symbolSize is not None: plot_opts['symbolSize'] = symbolSize if lineStyle is not None: plot_opts['lineStyle'] = lineStyle if lineWidth is not None: plot_opts['lineWidth'] = lineWidth # Add curve new_curve = TimePlotCurveItem( y_channel, plot_by_timestamps=self._plot_by_timestamps, name=name, color=color, **plot_opts) new_curve.setUpdatesAsynchronously(self.updatesAsynchronously) new_curve.setBufferSize(self._bufferSize) self.update_timer.timeout.connect(new_curve.asyncUpdate) self.addCurve(new_curve, curve_color=color) new_curve.data_changed.connect(self.set_needs_redraw) self.redraw_timer.start() return new_curve def removeYChannel(self, curve): """ Remove a curve from the graph. This also stops update the timer associated with the curve. Parameters ---------- curve : TimePlotCurveItem The curve to be removed. """ self.update_timer.timeout.disconnect(curve.asyncUpdate) self.removeCurve(curve) if len(self._curves) < 1: self.redraw_timer.stop() def removeYChannelAtIndex(self, index): """ Remove a curve from the graph, given its index in the graph's curve list. Parameters ---------- index : int The curve's index from the graph's curve list. """ curve = self._curves[index] self.removeYChannel(curve) @Slot() def set_needs_redraw(self): self._needs_redraw = True @Slot() def redrawPlot(self): """ Redraw the graph """ if not self._needs_redraw: return self.updateXAxis() for curve in self._curves: curve.redrawCurve() self.plot_redrawn_signal.emit(curve) self._needs_redraw = False def updateXAxis(self, update_immediately=False): """ Update the x-axis for every graph redraw. Parameters ---------- update_immediately : bool Update the axis range(s) immediately if True, or defer until the next rendering. """ if len(self._curves) == 0: return if self._plot_by_timestamps: if self._update_mode == PyDMTimePlot.SynchronousMode: maxrange = max([curve.max_x() for curve in self._curves]) else: maxrange = time.time() minrange = maxrange - self._time_span self.plotItem.setXRange(minrange, maxrange, padding=0.0, update=update_immediately) else: diff_time = self.starting_epoch_time - max( [curve.max_x() for curve in self._curves]) if diff_time > DEFAULT_X_MIN: diff_time = DEFAULT_X_MIN self.getViewBox().setLimits(minXRange=diff_time) def clearCurves(self): """ Remove all curves from the graph. """ super(PyDMTimePlot, self).clear() def getCurves(self): """ Dump the current list of curves and each curve's settings into a list of JSON-formatted strings. Returns ------- settings : list A list of JSON-formatted strings, each containing a curve's settings """ return [json.dumps(curve.to_dict()) for curve in self._curves] def setCurves(self, new_list): """ Add a list of curves into the graph. Parameters ---------- new_list : list A list of JSON-formatted strings, each contains a curve and its settings """ try: new_list = [json.loads(str(i)) for i in new_list] except ValueError as e: logger.exception("Error parsing curve json data: {}".format(e)) return self.clearCurves() for d in new_list: color = d.get('color') if color: color = QColor(color) self.addYChannel(d['channel'], name=d.get('name'), color=color, lineStyle=d.get('lineStyle'), lineWidth=d.get('lineWidth'), symbol=d.get('symbol'), symbolSize=d.get('symbolSize')) curves = Property("QStringList", getCurves, setCurves, designable=False) def findCurve(self, pv_name): """ Find a curve from a graph's curve list. Parameters ---------- pv_name : str The curve's PV address. Returns ------- curve : TimePlotCurveItem The found curve, or None. """ for curve in self._curves: if curve.address == pv_name: return curve def refreshCurve(self, curve): """ Remove a curve currently being plotted on the timeplot, then redraw that curve, which could have been updated with a new symbol, line style, line width, etc. Parameters ---------- curve : TimePlotCurveItem The curve to be re-added. """ curve = self.findCurve(curve.channel) if curve: self.removeYChannel(curve) self.addYChannel(y_channel=curve.address, color=curve.color, name=curve.address, lineStyle=curve.lineStyle, lineWidth=curve.lineWidth, symbol=curve.symbol, symbolSize=curve.symbolSize) def addLegendItem(self, item, pv_name, force_show_legend=False): """ Add an item into the graph's legend. Parameters ---------- item : TimePlotCurveItem A curve being plotted in the graph pv_name : str The PV channel force_show_legend : bool True to make the legend to be displayed; False to just add the item, but do not display the legend. """ self._legend.addItem(item, pv_name) self.setShowLegend(force_show_legend) def removeLegendItem(self, pv_name): """ Remove an item from the legend. Parameters ---------- pv_name : str The PV channel, used to search for the legend item to remove. """ self._legend.removeItem(pv_name) if len(self._legend.items) == 0: self.setShowLegend(False) def getBufferSize(self): """ Get the size of the data buffer for the entire chart. Returns ------- size : int The chart's data buffer size. """ return int(self._bufferSize) def setBufferSize(self, value): """ Set the size of the data buffer of the entire chart. This will also update the same value for each of the data buffer of each chart's curve. Parameters ---------- value : int The new buffer size for the chart. """ if self._bufferSize != int(value): # Originally, the bufferSize is the max between the user's input and 1, and 1 doesn't make sense. # So, I'm comparing the user's input with the minimum buffer size, and pick the max between the two self._bufferSize = max(int(value), MINIMUM_BUFFER_SIZE) for curve in self._curves: curve.setBufferSize(value) def resetBufferSize(self): """ Reset the data buffer size of the chart, and each of the chart's curve's data buffer, to the minimum """ if self._bufferSize != DEFAULT_BUFFER_SIZE: self._bufferSize = DEFAULT_BUFFER_SIZE for curve in self._curves: curve.resetBufferSize() bufferSize = Property("int", getBufferSize, setBufferSize, resetBufferSize) def getUpdatesAsynchronously(self): return self._update_mode == PyDMTimePlot.AsynchronousMode def setUpdatesAsynchronously(self, value): for curve in self._curves: curve.setUpdatesAsynchronously(value) if value is True: self._update_mode = PyDMTimePlot.AsynchronousMode self.update_timer.start() else: self._update_mode = PyDMTimePlot.SynchronousMode self.update_timer.stop() def resetUpdatesAsynchronously(self): self._update_mode = PyDMTimePlot.SynchronousMode self.update_timer.stop() for curve in self._curves: curve.resetUpdatesAsynchronously() updatesAsynchronously = Property("bool", getUpdatesAsynchronously, setUpdatesAsynchronously, resetUpdatesAsynchronously) def getTimeSpan(self): """ The extent of the x-axis of the chart, in seconds. In other words, how long a data point stays on the plot before falling off the left edge. Returns ------- time_span : float The extent of the x-axis of the chart, in seconds. """ return float(self._time_span) def setTimeSpan(self, value): """ Set the extent of the x-axis of the chart, in seconds. In aynchronous mode, the chart will allocate enough buffer for the new time span duration. Data arriving after each duration will be recorded into the buffer having been rotated. Parameters ---------- value : float The time span duration, in seconds, to allocate enough buffer to collect data for, before rotating the buffer. """ value = float(value) if self._time_span != value: self._time_span = value if self.getUpdatesAsynchronously(): self.setBufferSize( int((self._time_span * 1000.0) / self._update_interval)) self.updateXAxis(update_immediately=True) def resetTimeSpan(self): """ Reset the timespan to the default value. """ if self._time_span != DEFAULT_TIME_SPAN: self._time_span = DEFAULT_TIME_SPAN if self.getUpdatesAsynchronously(): self.setBufferSize( int((self._time_span * 1000.0) / self._update_interval)) self.updateXAxis(update_immediately=True) timeSpan = Property(float, getTimeSpan, setTimeSpan, resetTimeSpan) def getUpdateInterval(self): """ Get the update interval for the chart. Returns ------- interval : float The update interval of the chart. """ return float(self._update_interval) / 1000.0 def setUpdateInterval(self, value): """ Set a new update interval for the chart and update its data buffer size. Parameters ---------- value : float The new update interval value. """ value = abs(int(1000.0 * value)) if self._update_interval != value: self._update_interval = value self.update_timer.setInterval(self._update_interval) if self.getUpdatesAsynchronously(): self.setBufferSize( int((self._time_span * 1000.0) / self._update_interval)) def resetUpdateInterval(self): """ Reset the chart's update interval to the default. """ if self._update_interval != DEFAULT_UPDATE_INTERVAL: self._update_interval = DEFAULT_UPDATE_INTERVAL self.update_timer.setInterval(self._update_interval) if self.getUpdatesAsynchronously(): self.setBufferSize( int((self._time_span * 1000.0) / self._update_interval)) updateInterval = Property(float, getUpdateInterval, setUpdateInterval, resetUpdateInterval) def getAutoRangeX(self): if self._plot_by_timestamps: return False else: super(PyDMTimePlot, self).getAutoRangeX() def setAutoRangeX(self, value): if self._plot_by_timestamps: self._auto_range_x = False self.plotItem.enableAutoRange(ViewBox.XAxis, enable=self._auto_range_x) else: super(PyDMTimePlot, self).setAutoRangeX(value) def channels(self): return [curve.channel for curve in self._curves] # The methods for autoRangeY, minYRange, and maxYRange are # all defined in BasePlot, but we don't expose them as properties there, because not all plot # subclasses necessarily want them to be user-configurable in Designer. autoRangeY = Property(bool, BasePlot.getAutoRangeY, BasePlot.setAutoRangeY, BasePlot.resetAutoRangeY, doc=""" Whether or not the Y-axis automatically rescales to fit the data. If true, the values in minYRange and maxYRange are ignored. """) minYRange = Property(float, BasePlot.getMinYRange, BasePlot.setMinYRange, doc=""" Minimum Y-axis value visible on the plot.""") maxYRange = Property(float, BasePlot.getMaxYRange, BasePlot.setMaxYRange, doc=""" Maximum Y-axis value visible on the plot.""") def enableCrosshair(self, is_enabled, starting_x_pos=DEFAULT_X_MIN, starting_y_pos=DEFAULT_Y_MIN, vertical_angle=90, horizontal_angle=0, vertical_movable=False, horizontal_movable=False): """ Display a crosshair on the graph. Parameters ---------- is_enabled : bool True is to display the crosshair; False is to hide it. starting_x_pos : float The x position where the vertical line will cross starting_y_pos : float The y position where the horizontal line will cross vertical_angle : int The angle of the vertical line horizontal_angle : int The angle of the horizontal line vertical_movable : bool True if the user can move the vertical line; False if not horizontal_movable : bool True if the user can move the horizontal line; False if not """ super(PyDMTimePlot, self).enableCrosshair(is_enabled, starting_x_pos, starting_y_pos, vertical_angle, horizontal_angle, vertical_movable, horizontal_movable)
def __alive_loopback(self): alive = self.is_alive() if not alive: self.terminal_closed.emit() else: QTimer.singleShot(250, self.__alive_loopback)
class Restarter(QWidget): """Widget in charge of displaying the splash information screen and the error messages. """ def __init__(self): super(Restarter, self).__init__() self.ellipsis = ['', '.', '..', '...', '..', '.'] # Widgets self.timer_ellipsis = QTimer(self) self.splash = QSplashScreen(QPixmap(get_image_path('splash.svg'), 'svg')) # Widget setup self.setVisible(False) font = self.splash.font() font.setPixelSize(10) self.splash.setFont(font) self.splash.show() self.timer_ellipsis.timeout.connect(self.animate_ellipsis) def _show_message(self, text): """Show message on splash screen.""" self.splash.showMessage(text, int(Qt.AlignBottom | Qt.AlignCenter | Qt.AlignAbsolute), QColor(Qt.white)) def animate_ellipsis(self): """Animate dots at the end of the splash screen message.""" ellipsis = self.ellipsis.pop(0) text = ' '*len(ellipsis) + self.splash_text + ellipsis self.ellipsis.append(ellipsis) self._show_message(text) def set_splash_message(self, text): """Sets the text in the bottom of the Splash screen.""" self.splash_text = text self._show_message(text) self.timer_ellipsis.start(500) def launch_error_message(self, error_type, error=None): """Launch a message box with a predefined error message. Parameters ---------- error_type : int [CLOSE_ERROR, RESET_ERROR, RESTART_ERROR] Possible error codes when restarting/reseting spyder. error : Exception Actual Python exception error caught. """ messages = {CLOSE_ERROR: _("It was not possible to close the previous " "Spyder instance.\nRestart aborted."), RESET_ERROR: _("Spyder could not reset to factory " "defaults.\nRestart aborted."), RESTART_ERROR: _("It was not possible to restart Spyder.\n" "Operation aborted.")} titles = {CLOSE_ERROR: _("Spyder exit error"), RESET_ERROR: _("Spyder reset error"), RESTART_ERROR: _("Spyder restart error")} if error: e = error.__repr__() message = messages[error_type] + "\n\n{0}".format(e) else: message = messages[error_type] title = titles[error_type] self.splash.hide() QMessageBox.warning(self, title, message, QMessageBox.Ok) raise RuntimeError(message)
def __init__(self, parent=None, opts=None, ui_file=None, stylesheet=None, maximize=False, fullscreen=False, position=None, size=None, confirm_exit=True, title=None, menu='default'): super(VCPMainWindow, self).__init__(parent) if opts is None: opts = qtpyvcp.OPTIONS self.setWindowTitle(title) self.app = QApplication.instance() self.status = getPlugin('status') self.confirm_exit = confirm_exit if opts.confirm_exit is None else opts.confirm_exit # Load the UI file AFTER defining variables, otherwise the values # set in QtDesigner get overridden by the default values if ui_file is not None: self.loadUi(ui_file) self.initUi() if menu is not None: try: # delete any preexisting menuBar added in QtDesigner # because it shadows the QMainWindow.menuBar() method del self.menuBar except AttributeError: pass if menu == 'default': menu = qtpyvcp.CONFIG.get('default_menubar', []) self.setMenuBar(self.buildMenuBar(menu)) if title is not None: self.setWindowTitle(title) if stylesheet is not None: self.loadStylesheet(stylesheet) maximize = opts.maximize if opts.maximize is not None else maximize if maximize: QTimer.singleShot(0, self.showMaximized) fullscreen = opts.fullscreen if opts.fullscreen is not None else fullscreen if fullscreen: QTimer.singleShot(0, self.showFullScreen) if opts.hide_menu_bar: self.menuBar().hide() size = opts.size or size if size: try: width, height = size.lower().split('x') self.resize(int(width), int(height)) except: LOG.exception('Error parsing --size argument: %s', size) pos = opts.position or position if pos: try: xpos, ypos = pos.lower().split('x') self.move(int(xpos), int(ypos)) except: LOG.exception('Error parsing --position argument: %s', pos) QShortcut(QKeySequence("F11"), self, self.toggleFullscreen) self.app.focusChanged.connect(self.focusChangedEvent)
class FilterWidget(filterBase): __clsName = "LocFilter" filterChangeDelay = 200 varNameRex = re.compile(r"\{(\w*)\}") def tr(self, string): return QCoreApplication.translate(self.__clsName, string) def __init__(self, parent=None): super().__init__(parent) self._ui = filterClass() self._ui.setupUi(self) self._delayTimer = QTimer(self) self._delayTimer.setInterval(self.filterChangeDelay) self._delayTimer.setSingleShot(True) if not (qtpy.PYQT4 or qtpy.PYSIDE): self._delayTimer.setTimerType(Qt.PreciseTimer) self._delayTimer.timeout.connect(self.filterChanged) self._ui.filterEdit.textChanged.connect(self._delayTimer.start) self._menu = QMenu() self._menu.triggered.connect(self._addVariable) filterChanged = Signal() @Slot(list) def setVariables(self, var): self._menu.clear() for v in var: self._menu.addAction(v) def setFilterString(self, filt): self._ui.filterEdit.setPlainText(filt) @Property(str, fset=setFilterString, doc="String describing the filter") def filterString(self): s = self._ui.filterEdit.toPlainText() return self.varNameRex.subn("\\1", s)[0] def getFilter(self): filterStr = self.filterString filterStrList = filterStr.split("\n") def filterFunc(data): filter = np.ones(len(data), dtype=bool) for f in filterStrList: with suppress(Exception): filter &= data.eval(f, local_dict={}, global_dict={}) return filter return filterFunc @Slot(QAction) def _addVariable(self, act): self._ui.filterEdit.textCursor().insertText(act.text()) @Slot(str) def on_showVarLabel_linkActivated(self, link): if not self._menu.isEmpty(): self._menu.exec(QCursor.pos())
class PluginManager(QObject): introspection_complete = Signal(object) def __init__(self, executable): super(PluginManager, self).__init__() plugins = OrderedDict() for name in PLUGINS: try: plugin = PluginClient(name, executable) plugin.run() except Exception as e: debug_print('Introspection Plugin Failed: %s' % name) debug_print(str(e)) continue debug_print('Introspection Plugin Loaded: %s' % name) plugins[name] = plugin plugin.received.connect(self.handle_response) self.plugins = plugins self.timer = QTimer() self.desired = [] self.ids = dict() self.info = None self.request = None self.pending = None self.pending_request = None self.waiting = False def send_request(self, info): """Handle an incoming request from the user.""" if self.waiting: if info.serialize() != self.info.serialize(): self.pending_request = info else: debug_print('skipping duplicate request') return debug_print('%s request' % info.name) desired = None self.info = info editor = info.editor if (info.name == 'completion' and 'jedi' not in self.plugins and info.line.lstrip().startswith(('import ', 'from '))): desired = 'fallback' if ((not editor.is_python_like()) or sourcecode.is_keyword(info.obj) or (editor.in_comment_or_string() and info.name != 'info')): desired = 'fallback' plugins = self.plugins.values() if desired: plugins = [self.plugins[desired]] self.desired = [desired] elif (info.name == 'definition' and not info.editor.is_python() or info.name == 'info'): self.desired = list(self.plugins.keys()) else: # Use all but the fallback plugins = list(self.plugins.values())[:-1] self.desired = list(self.plugins.keys())[:-1] self._start_time = time.time() self.waiting = True method = 'get_%s' % info.name value = info.serialize() self.ids = dict() for plugin in plugins: request_id = plugin.request(method, value) self.ids[request_id] = plugin.name self.timer.stop() self.timer.singleShot(LEAD_TIME_SEC * 1000, self._handle_timeout) def validate(self): for plugin in self.plugins.values(): plugin.request('validate') def handle_response(self, response): name = self.ids.get(response['request_id'], None) if not name: return if response.get('error', None): debug_print('Response error:', response['error']) return if name == self.desired[0] or not self.waiting: if response.get('result', None): self._finalize(response) else: self.pending = response def close(self): [plugin.close() for plugin in self.plugins] def _finalize(self, response): self.waiting = False self.pending = None if self.info: delta = time.time() - self._start_time debug_print('%s request from %s finished: "%s" in %.1f sec' % (self.info.name, response['name'], str(response['result'])[:100], delta)) response['info'] = self.info self.introspection_complete.emit(response) self.info = None if self.pending_request: info = self.pending_request self.pending_request = None self.send_request(info) def _handle_timeout(self): self.waiting = False if self.pending: self._finalize(self.pending) else: debug_print('No valid responses acquired')
class AddDataTableMixin(object): """ Mixin providing validation and type casting for a table for adding cycle data. Should be used as part of a widget that has a `table` attribute. If it also has a `okButton`, this will be enabled/diabled when validation is performed. """ invalid = Signal(int, int) """ **signal** invalid(int `row`, int `col`) Emitted if the data in cell `row`,`col` is invalid. """ def __init__(self, *args, emptyDateValid=True, **kwargs): super().__init__(*args, **kwargs) self.headerLabels = ['Date', 'Time', 'Distance (km)', 'Calories', 'Gear'] self.headerLabelColumnOffset = 0 self.table = QTableWidget(0, len(self.headerLabels)) self.table.setHorizontalHeaderLabels(self.headerLabels) self.table.verticalHeader().setVisible(False) # dict of methods to validate and cast types for input data in each column isDatePartial = partial(isDate, allowEmpty=emptyDateValid) validateMethods = [isDatePartial, isDuration, isFloat, isFloat, isInt] parseDatePartial = partial(parseDate, pd_timestamp=True) castMethods = [parseDatePartial, parseDuration, float, float, int] self.mthds = {name:{'validate':validateMethods[i], 'cast':castMethods[i]} for i, name in enumerate(self.headerLabels)} self.validateTimer = QTimer() self.validateTimer.setInterval(100) self.validateTimer.setSingleShot(True) self.validateTimer.timeout.connect(self._validate) self.table.cellChanged.connect(self.validateTimer.start) self.invalid.connect(self._invalid) @property def defaultBrush(self): # made this a property rather than setting in constructor as the mixin # doesn't have a `table` attribute when __init__ is called return self.table.item(0,0).background() @property def invalidBrush(self): return QBrush(QColor("#910404")) @Slot(int, int) def _invalid(self, row, col): """ Set the background of cell `row`,`col` to the `invalidBrush` and disable the 'Ok' button. """ self.table.item(row, col).setBackground(self.invalidBrush) if hasattr(self, "okButton"): self.okButton.setEnabled(False) @Slot() def _validate(self): """ Validate all data in the table. If data is valid and the widget has an `okButton`, it will be enabled. """ # it would be more efficient to only validate a single cell, after its # text has been changed, but this table is unlikely to ever be more # than a few rows long, so this isn't too inefficient allValid = True for row in range(self.table.rowCount()): for col, name in enumerate(self.headerLabels): col += self.headerLabelColumnOffset item = self.table.item(row, col) value = item.text() mthd = self.mthds[name]['validate'] valid = mthd(value) if not valid: if hasattr(self, "_clicked"): if (row, col) not in self._clicked: continue self.invalid.emit(row, col) allValid = False elif valid and self.table.item(row, col).background() == self.invalidBrush: self.table.item(row, col).setBackground(self.defaultBrush) if allValid and hasattr(self, "okButton"): self.okButton.setEnabled(True) return allValid def _getValues(self): values = {name:[] for name in self.headerLabels} self.table.sortItems(0, Qt.AscendingOrder) for row in range(self.table.rowCount()): for col, name in enumerate(self.headerLabels): item = self.table.item(row, col) value = item.text() mthd = self.mthds[name]['cast'] value = mthd(value) values[name].append(value) return values
class AsyncClient(QObject): """ A class which handles a connection to a client through a QProcess. """ # Emitted when the client has initialized. initialized = Signal() # Emitted when the client errors. errored = Signal() # Emitted when a request response is received. received = Signal(object) def __init__(self, target, executable=None, name=None, extra_args=None, libs=None, cwd=None, env=None, extra_path=None): super(AsyncClient, self).__init__() self.executable = executable or sys.executable self.extra_args = extra_args self.target = target self.name = name or self self.libs = libs self.cwd = cwd self.env = env self.extra_path = extra_path self.is_initialized = False self.closing = False self.notifier = None self.process = None self.context = zmq.Context() QApplication.instance().aboutToQuit.connect(self.close) # Set up the heartbeat timer. self.timer = QTimer(self) self.timer.timeout.connect(self._heartbeat) def run(self): """Handle the connection with the server. """ # Set up the zmq port. self.socket = self.context.socket(zmq.PAIR) self.port = self.socket.bind_to_random_port('tcp://*') # Set up the process. self.process = QProcess(self) if self.cwd: self.process.setWorkingDirectory(self.cwd) p_args = ['-u', self.target, str(self.port)] if self.extra_args is not None: p_args += self.extra_args # Set up environment variables. processEnvironment = QProcessEnvironment() env = self.process.systemEnvironment() if (self.env and 'PYTHONPATH' not in self.env) or self.env is None: python_path = osp.dirname(get_module_path('trex')) # Add the libs to the python path. for lib in self.libs: try: path = osp.dirname(imp.find_module(lib)[1]) python_path = osp.pathsep.join([python_path, path]) except ImportError: pass if self.extra_path: try: python_path = osp.pathsep.join([python_path] + self.extra_path) except Exception as e: debug_print("Error when adding extra_path to plugin env") debug_print(e) env.append("PYTHONPATH=%s" % python_path) if self.env: env.update(self.env) for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) # Start the process and wait for started. self.process.start(self.executable, p_args) self.process.finished.connect(self._on_finished) running = self.process.waitForStarted() if not running: raise IOError('Could not start %s' % self) # Set up the socket notifer. fid = self.socket.getsockopt(zmq.FD) self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self) self.notifier.activated.connect(self._on_msg_received) def request(self, func_name, *args, **kwargs): """Send a request to the server. The response will be a dictionary the 'request_id' and the 'func_name' as well as a 'result' field with the object returned by the function call or or an 'error' field with a traceback. """ if not self.is_initialized: return request_id = uuid.uuid4().hex request = dict(func_name=func_name, args=args, kwargs=kwargs, request_id=request_id) self._send(request) return request_id def close(self): """Cleanly close the connection to the server. """ self.closing = True self.is_initialized = False self.timer.stop() if self.notifier is not None: self.notifier.activated.disconnect(self._on_msg_received) self.notifier.setEnabled(False) self.notifier = None self.request('server_quit') if self.process is not None: self.process.waitForFinished(1000) self.process.close() self.context.destroy() def _on_finished(self): """Handle a finished signal from the process. """ if self.closing: return if self.is_initialized: debug_print('Restarting %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.is_initialized = False self.notifier.setEnabled(False) self.run() else: debug_print('Errored %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.errored.emit() def _on_msg_received(self): """Handle a message trigger from the socket. """ self.notifier.setEnabled(False) while 1: try: resp = self.socket.recv_pyobj(flags=zmq.NOBLOCK) except zmq.ZMQError: self.notifier.setEnabled(True) return if not self.is_initialized: self.is_initialized = True debug_print('Initialized %s' % self.name) self.initialized.emit() self.timer.start(HEARTBEAT) continue resp['name'] = self.name self.received.emit(resp) def _heartbeat(self): """Send a heartbeat to keep the server alive. """ self._send(dict(func_name='server_heartbeat')) def _send(self, obj): """Send an object to the server. """ try: self.socket.send_pyobj(obj, zmq.NOBLOCK) except Exception as e: debug_print(e) self.is_initialized = False self._on_finished()
class ShellBaseWidget(ConsoleBaseWidget, SaveHistoryMixin, BrowseHistoryMixin): """ Shell base widget """ redirect_stdio = Signal(bool) sig_keyboard_interrupt = Signal() execute = Signal(str) append_to_history = Signal(str, str) def __init__(self, parent, history_filename, profile=False, initial_message=None, default_foreground_color=None, error_foreground_color=None, traceback_foreground_color=None, prompt_foreground_color=None, background_color=None): """ parent : specifies the parent widget """ ConsoleBaseWidget.__init__(self, parent) SaveHistoryMixin.__init__(self, history_filename) BrowseHistoryMixin.__init__(self) # Prompt position: tuple (line, index) self.current_prompt_pos = None self.new_input_line = True # History assert is_text_string(history_filename) self.history = self.load_history() # Session self.historylog_filename = CONF.get('main', 'historylog_filename', get_conf_path('history.log')) # Context menu self.menu = None self.setup_context_menu() # Simple profiling test self.profile = profile # Buffer to increase performance of write/flush operations self.__buffer = [] if initial_message: self.__buffer.append(initial_message) self.__timestamp = 0.0 self.__flushtimer = QTimer(self) self.__flushtimer.setSingleShot(True) self.__flushtimer.timeout.connect(self.flush) # Give focus to widget self.setFocus() # Cursor width self.setCursorWidth(CONF.get('main', 'cursor/width')) def toggle_wrap_mode(self, enable): """Enable/disable wrap mode""" self.set_wrap_mode('character' if enable else None) def set_font(self, font): """Set shell styles font""" self.setFont(font) self.set_pythonshell_font(font) cursor = self.textCursor() cursor.select(QTextCursor.Document) charformat = QTextCharFormat() charformat.setFontFamily(font.family()) charformat.setFontPointSize(font.pointSize()) cursor.mergeCharFormat(charformat) #------ Context menu def setup_context_menu(self): """Setup shell context menu""" self.menu = QMenu(self) self.cut_action = create_action(self, _("Cut"), shortcut=keybinding('Cut'), icon=ima.icon('editcut'), triggered=self.cut) self.copy_action = create_action(self, _("Copy"), shortcut=keybinding('Copy'), icon=ima.icon('editcopy'), triggered=self.copy) paste_action = create_action(self, _("Paste"), shortcut=keybinding('Paste'), icon=ima.icon('editpaste'), triggered=self.paste) save_action = create_action(self, _("Save history log..."), icon=ima.icon('filesave'), tip=_( "Save current history log (i.e. all " "inputs and outputs) in a text file"), triggered=self.save_historylog) self.delete_action = create_action(self, _("Delete"), shortcut=keybinding('Delete'), icon=ima.icon('editdelete'), triggered=self.delete) selectall_action = create_action(self, _("Select All"), shortcut=keybinding('SelectAll'), icon=ima.icon('selectall'), triggered=self.selectAll) add_actions( self.menu, (self.cut_action, self.copy_action, paste_action, self.delete_action, None, selectall_action, None, save_action)) def contextMenuEvent(self, event): """Reimplement Qt method""" state = self.has_selected_text() self.copy_action.setEnabled(state) self.cut_action.setEnabled(state) self.delete_action.setEnabled(state) self.menu.popup(event.globalPos()) event.accept() #------ Input buffer def get_current_line_from_cursor(self): return self.get_text('cursor', 'eof') def _select_input(self): """Select current line (without selecting console prompt)""" line, index = self.get_position('eof') if self.current_prompt_pos is None: pline, pindex = line, index else: pline, pindex = self.current_prompt_pos self.setSelection(pline, pindex, line, index) @Slot() def clear_terminal(self): """ Clear terminal window Child classes reimplement this method to write prompt """ self.clear() # The buffer being edited def _set_input_buffer(self, text): """Set input buffer""" if self.current_prompt_pos is not None: self.replace_text(self.current_prompt_pos, 'eol', text) else: self.insert(text) self.set_cursor_position('eof') def _get_input_buffer(self): """Return input buffer""" input_buffer = '' if self.current_prompt_pos is not None: input_buffer = self.get_text(self.current_prompt_pos, 'eol') input_buffer = input_buffer.replace(os.linesep, '\n') return input_buffer input_buffer = Property("QString", _get_input_buffer, _set_input_buffer) #------ Prompt def new_prompt(self, prompt): """ Print a new prompt and save its (line, index) position """ if self.get_cursor_line_column()[1] != 0: self.write('\n') self.write(prompt, prompt=True) # now we update our cursor giving end of prompt self.current_prompt_pos = self.get_position('cursor') self.ensureCursorVisible() self.new_input_line = False def check_selection(self): """ Check if selected text is r/w, otherwise remove read-only parts of selection """ if self.current_prompt_pos is None: self.set_cursor_position('eof') else: self.truncate_selection(self.current_prompt_pos) #------ Copy / Keyboard interrupt @Slot() def copy(self): """Copy text to clipboard... or keyboard interrupt""" if self.has_selected_text(): ConsoleBaseWidget.copy(self) elif not sys.platform == 'darwin': self.interrupt() def interrupt(self): """Keyboard interrupt""" self.sig_keyboard_interrupt.emit() @Slot() def cut(self): """Cut text""" self.check_selection() if self.has_selected_text(): ConsoleBaseWidget.cut(self) @Slot() def delete(self): """Remove selected text""" self.check_selection() if self.has_selected_text(): ConsoleBaseWidget.remove_selected_text(self) @Slot() def save_historylog(self): """Save current history log (all text in console)""" title = _("Save history log") self.redirect_stdio.emit(False) filename, _selfilter = getsavefilename( self, title, self.historylog_filename, "%s (*.log)" % _("History logs")) self.redirect_stdio.emit(True) if filename: filename = osp.normpath(filename) try: encoding.write(to_text_string(self.get_text_with_eol()), filename) self.historylog_filename = filename CONF.set('main', 'historylog_filename', filename) except EnvironmentError: pass #------ Basic keypress event handler def on_enter(self, command): """on_enter""" self.execute_command(command) def execute_command(self, command): self.execute.emit(command) self.add_to_history(command) self.new_input_line = True def on_new_line(self): """On new input line""" self.set_cursor_position('eof') self.current_prompt_pos = self.get_position('cursor') self.new_input_line = False @Slot() def paste(self): """Reimplemented slot to handle multiline paste action""" if self.new_input_line: self.on_new_line() ConsoleBaseWidget.paste(self) def keyPressEvent(self, event): """ Reimplement Qt Method Basic keypress event handler (reimplemented in InternalShell to add more sophisticated features) """ if self.preprocess_keyevent(event): # Event was accepted in self.preprocess_keyevent return self.postprocess_keyevent(event) def preprocess_keyevent(self, event): """Pre-process keypress event: return True if event is accepted, false otherwise""" # Copy must be done first to be able to copy read-only text parts # (otherwise, right below, we would remove selection # if not on current line) ctrl = event.modifiers() & Qt.ControlModifier meta = event.modifiers() & Qt.MetaModifier # meta=ctrl in OSX if event.key() == Qt.Key_C and \ ((Qt.MetaModifier | Qt.ControlModifier) & event.modifiers()): if meta and sys.platform == 'darwin': self.interrupt() elif ctrl: self.copy() event.accept() return True if self.new_input_line and ( len(event.text()) or event.key() in \ (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right) ): self.on_new_line() return False def postprocess_keyevent(self, event): """Post-process keypress event: in InternalShell, this is method is called when shell is ready""" event, text, key, ctrl, shift = restore_keyevent(event) # Is cursor on the last line? and after prompt? if len(text): #XXX: Shouldn't it be: `if len(unicode(text).strip(os.linesep))` ? if self.has_selected_text(): self.check_selection() self.restrict_cursor_position(self.current_prompt_pos, 'eof') cursor_position = self.get_position('cursor') if key in (Qt.Key_Return, Qt.Key_Enter): if self.is_cursor_on_last_line(): self._key_enter() # add and run selection else: self.insert_text(self.get_selected_text(), at_end=True) elif key == Qt.Key_Insert and not shift and not ctrl: self.setOverwriteMode(not self.overwriteMode()) elif key == Qt.Key_Delete: if self.has_selected_text(): self.check_selection() self.remove_selected_text() elif self.is_cursor_on_last_line(): self.stdkey_clear() elif key == Qt.Key_Backspace: self._key_backspace(cursor_position) elif key == Qt.Key_Tab: self._key_tab() elif key == Qt.Key_Space and ctrl: self._key_ctrl_space() elif key == Qt.Key_Left: if self.current_prompt_pos == cursor_position: # Avoid moving cursor on prompt return method = self.extend_selection_to_next if shift \ else self.move_cursor_to_next method('word' if ctrl else 'character', direction='left') elif key == Qt.Key_Right: if self.is_cursor_at_end(): return method = self.extend_selection_to_next if shift \ else self.move_cursor_to_next method('word' if ctrl else 'character', direction='right') elif (key == Qt.Key_Home) or ((key == Qt.Key_Up) and ctrl): self._key_home(shift, ctrl) elif (key == Qt.Key_End) or ((key == Qt.Key_Down) and ctrl): self._key_end(shift, ctrl) elif key == Qt.Key_Up: if not self.is_cursor_on_last_line(): self.set_cursor_position('eof') y_cursor = self.get_coordinates(cursor_position)[1] y_prompt = self.get_coordinates(self.current_prompt_pos)[1] if y_cursor > y_prompt: self.stdkey_up(shift) else: self.browse_history(backward=True) elif key == Qt.Key_Down: if not self.is_cursor_on_last_line(): self.set_cursor_position('eof') y_cursor = self.get_coordinates(cursor_position)[1] y_end = self.get_coordinates('eol')[1] if y_cursor < y_end: self.stdkey_down(shift) else: self.browse_history(backward=False) elif key in (Qt.Key_PageUp, Qt.Key_PageDown): #XXX: Find a way to do this programmatically instead of calling # widget keyhandler (this won't work if the *event* is coming from # the event queue - i.e. if the busy buffer is ever implemented) ConsoleBaseWidget.keyPressEvent(self, event) elif key == Qt.Key_Escape and shift: self.clear_line() elif key == Qt.Key_Escape: self._key_escape() elif key == Qt.Key_L and ctrl: self.clear_terminal() elif key == Qt.Key_V and ctrl: self.paste() elif key == Qt.Key_X and ctrl: self.cut() elif key == Qt.Key_Z and ctrl: self.undo() elif key == Qt.Key_Y and ctrl: self.redo() elif key == Qt.Key_A and ctrl: self.selectAll() elif key == Qt.Key_Question and not self.has_selected_text(): self._key_question(text) elif key == Qt.Key_ParenLeft and not self.has_selected_text(): self._key_parenleft(text) elif key == Qt.Key_Period and not self.has_selected_text(): self._key_period(text) elif len(text) and not self.isReadOnly(): self.hist_wholeline = False self.insert_text(text) self._key_other(text) else: # Let the parent widget handle the key press event ConsoleBaseWidget.keyPressEvent(self, event) #------ Key handlers def _key_enter(self): command = self.input_buffer self.insert_text('\n', at_end=True) self.on_enter(command) self.flush() def _key_other(self, text): raise NotImplementedError def _key_backspace(self, cursor_position): raise NotImplementedError def _key_tab(self): raise NotImplementedError def _key_ctrl_space(self): raise NotImplementedError def _key_home(self, shift, ctrl): if self.is_cursor_on_last_line(): self.stdkey_home(shift, ctrl, self.current_prompt_pos) def _key_end(self, shift, ctrl): if self.is_cursor_on_last_line(): self.stdkey_end(shift, ctrl) def _key_pageup(self): raise NotImplementedError def _key_pagedown(self): raise NotImplementedError def _key_escape(self): raise NotImplementedError def _key_question(self, text): raise NotImplementedError def _key_parenleft(self, text): raise NotImplementedError def _key_period(self, text): raise NotImplementedError #------ History Management def load_history(self): """Load history from a .py file in user home directory""" if osp.isfile(self.history_filename): rawhistory, _ = encoding.readlines(self.history_filename) rawhistory = [line.replace('\n', '') for line in rawhistory] if rawhistory[1] != self.INITHISTORY[1]: rawhistory[1] = self.INITHISTORY[1] else: rawhistory = self.INITHISTORY history = [line for line in rawhistory \ if line and not line.startswith('#')] # Truncating history to X entries: while len(history) >= CONF.get('historylog', 'max_entries'): del history[0] while rawhistory[0].startswith('#'): del rawhistory[0] del rawhistory[0] # Saving truncated history: try: encoding.writelines(rawhistory, self.history_filename) except EnvironmentError: pass return history #------ Simulation standards input/output def write_error(self, text): """Simulate stderr""" self.flush() self.write(text, flush=True, error=True) if get_debug_level(): STDERR.write(text) def write(self, text, flush=False, error=False, prompt=False): """Simulate stdout and stderr""" if prompt: self.flush() if not is_string(text): # This test is useful to discriminate QStrings from decoded str text = to_text_string(text) self.__buffer.append(text) ts = time.time() if flush or prompt: self.flush(error=error, prompt=prompt) elif ts - self.__timestamp > 0.05: self.flush(error=error) self.__timestamp = ts # Timer to flush strings cached by last write() operation in series self.__flushtimer.start(50) def flush(self, error=False, prompt=False): """Flush buffer, write text to console""" # Fix for Issue 2452 if PY3: try: text = "".join(self.__buffer) except TypeError: text = b"".join(self.__buffer) try: text = text.decode(locale.getdefaultlocale()[1]) except: pass else: text = "".join(self.__buffer) self.__buffer = [] self.insert_text(text, at_end=True, error=error, prompt=prompt) QCoreApplication.processEvents() self.repaint() # Clear input buffer: self.new_input_line = True #------ Text Insertion def insert_text(self, text, at_end=False, error=False, prompt=False): """ Insert text at the current cursor position or at the end of the command line """ if at_end: # Insert text at the end of the command line self.append_text_to_shell(text, error, prompt) else: # Insert text at current cursor position ConsoleBaseWidget.insert_text(self, text) #------ Re-implemented Qt Methods def focusNextPrevChild(self, next): """ Reimplemented to stop Tab moving to the next window """ if next: return False return ConsoleBaseWidget.focusNextPrevChild(self, next) #------ Drag and drop def dragEnterEvent(self, event): """Drag and Drop - Enter event""" event.setAccepted(event.mimeData().hasFormat("text/plain")) def dragMoveEvent(self, event): """Drag and Drop - Move event""" if (event.mimeData().hasFormat("text/plain")): event.setDropAction(Qt.MoveAction) event.accept() else: event.ignore() def dropEvent(self, event): """Drag and Drop - Drop event""" if (event.mimeData().hasFormat("text/plain")): text = to_text_string(event.mimeData().text()) if self.new_input_line: self.on_new_line() self.insert_text(text, at_end=True) self.setFocus() event.setDropAction(Qt.MoveAction) event.accept() else: event.ignore() def drop_pathlist(self, pathlist): """Drop path list""" raise NotImplementedError
class ToolTipWidget(QLabel): """ Shows tooltips that can be styled with the different themes. """ sig_completion_help_requested = Signal(str, str) sig_help_requested = Signal(str) def __init__(self, parent=None, as_tooltip=False): """ Shows tooltips that can be styled with the different themes. """ super(ToolTipWidget, self).__init__(parent, Qt.ToolTip) # Variables self.completion_doc = None self._url = '' self.app = QCoreApplication.instance() self.as_tooltip = as_tooltip self.tip = None self._timer_hide = QTimer() self._text_edit = parent # Setup # This keeps the hints below other applications if sys.platform == 'darwin': self.setWindowFlags(Qt.SplashScreen) else: self.setWindowFlags(Qt.ToolTip | Qt.FramelessWindowHint) self._timer_hide.setInterval(500) self.setTextInteractionFlags(Qt.TextSelectableByMouse) self.setTextInteractionFlags(Qt.TextBrowserInteraction) self.setOpenExternalLinks(False) self.setForegroundRole(QPalette.ToolTipText) self.setBackgroundRole(QPalette.ToolTipBase) self.setPalette(QToolTip.palette()) self.setAlignment(Qt.AlignLeft) self.setIndent(1) self.setFrameStyle(QFrame.NoFrame) style = self.style() delta_margin = style.pixelMetric(QStyle.PM_ToolTipLabelFrameWidth, None, self) self.setMargin(1 + delta_margin) # Signals self.linkHovered.connect(self._update_hover_html_link_style) self._timer_hide.timeout.connect(self.hide) def paintEvent(self, event): """Reimplemented to paint the background panel.""" painter = QStylePainter(self) option = QStyleOptionFrame() option.initFrom(self) painter.drawPrimitive(QStyle.PE_PanelTipLabel, option) painter.end() super(ToolTipWidget, self).paintEvent(event) def _update_hover_html_link_style(self, url): """Update style of labels that include rich text and html links.""" link = 'text-decoration:none;' link_hovered = 'text-decoration:underline;' self._url = url if url: QApplication.setOverrideCursor(QCursor(Qt.PointingHandCursor)) new_text, old_text = link_hovered, link else: new_text, old_text = link, link_hovered QApplication.restoreOverrideCursor() text = self.text() new_text = text.replace(old_text, new_text) self.setText(new_text) # ------------------------------------------------------------------------ # --- 'ToolTipWidget' interface # ------------------------------------------------------------------------ def show_basic_tip(self, point, tip): """Show basic tip.""" self.tip = tip self.setText(tip) self.resize(self.sizeHint()) y = point.y() - self.height() self.move(point.x(), y) self.show() return True def show_tip(self, point, tip, cursor=None, completion_doc=None): """ Attempts to show the specified tip at the current cursor location. """ # Don't attempt to show it if it's already visible and the text # to be displayed is the same as the one displayed before. if self.tip == tip: if not self.isVisible(): self.show() return # Set the text and resize the widget accordingly. self.tip = tip self.setText(tip) self.resize(self.sizeHint()) self.completion_doc = completion_doc padding = 0 text_edit = self._text_edit if cursor is None: cursor_rect = text_edit.cursorRect() else: cursor_rect = text_edit.cursorRect(cursor) screen_rect = self.app.desktop().screenGeometry(text_edit) point.setY(point.y() + padding) tip_height = self.size().height() tip_width = self.size().width() vertical = 'bottom' horizontal = 'Right' if point.y() + tip_height > screen_rect.height() + screen_rect.y(): point_ = text_edit.mapToGlobal(cursor_rect.topRight()) # If tip is still off screen, check if point is in top or bottom # half of screen. if point_.y() - tip_height < padding: # If point is in upper half of screen, show tip below it. # otherwise above it. if 2 * point.y() < screen_rect.height(): vertical = 'bottom' else: vertical = 'top' else: vertical = 'top' if point.x() + tip_width > screen_rect.width() + screen_rect.x(): point_ = text_edit.mapToGlobal(cursor_rect.topRight()) # If tip is still off-screen, check if point is in the right or # left half of the screen. if point_.x() - tip_width < padding: if 2 * point.x() < screen_rect.width(): horizontal = 'Right' else: horizontal = 'Left' else: horizontal = 'Left' pos = getattr(cursor_rect, '%s%s' % (vertical, horizontal)) adjusted_point = text_edit.mapToGlobal(pos()) if vertical == 'top': if os.name == 'nt': padding = -7 else: padding = -4.5 point.setY(adjusted_point.y() - tip_height - padding) if horizontal == 'Left': point.setX(adjusted_point.x() - tip_width - padding) self.move(point) if not self.isVisible(): self.show() return True def mousePressEvent(self, event): """ Reimplemented to hide it when focus goes out of the main window. """ QApplication.restoreOverrideCursor() if self.completion_doc: name = self.completion_doc.get('name', '') signature = self.completion_doc.get('signature', '') self.sig_completion_help_requested.emit(name, signature) else: self.sig_help_requested.emit(self._url) super(ToolTipWidget, self).mousePressEvent(event) self._hide() def focusOutEvent(self, event): """ Reimplemented to hide it when focus goes out of the main window. """ self.hide() def leaveEvent(self, event): """Override Qt method to hide the tooltip on leave.""" super(ToolTipWidget, self).leaveEvent(event) self.hide() def _hide(self): """Hide method helper.""" QApplication.restoreOverrideCursor() self._timer_hide.start() def hide(self): """Override Qt method to add timer and restore cursor.""" super(ToolTipWidget, self).hide() self._timer_hide.stop()
# Update upper section of plot (old) if np.any(move_bool) or (new_t0 != rs['latest_t0']): # Update old x_vals = rs['old_timestamps'] % self.x_lim y_vals = 1.1 * np.ones_like(x_vals) for row_ix in range(1, self.plot_config['y_range']): row_cutoff = new_t0 - (row_ix * self.x_lim) y_vals[rs['old_timestamps'] < row_cutoff] += 1 x_vals = np.repeat(x_vals, 2) y_vals = np.repeat(y_vals, 2) y_vals[1::2] += 0.8 rs['old'].setData(x=x_vals, y=y_vals) # Save some variables rs['latest_t0'] = new_t0 rs['start_time'] = max(rs['start_time'], rs['last_spike_time'] - (self.x_lim * self.plot_config['y_range'])) # Update frate annotation. samples_elapsed = rs['last_spike_time'] - rs['start_time'] if samples_elapsed > 0: frate = self.rasters[line_label]['count'] * self.samplingRate / samples_elapsed self.modify_frate(line_label, frate) if __name__ == '__main__': qapp = QApplication(sys.argv) aw = RasterGUI() timer = QTimer() timer.timeout.connect(aw.update) timer.start(1) if (sys.flags.interactive != 1) or not hasattr(qtpy.QtCore, 'PYQT_VERSION'): QApplication.instance().exec_()
class RunDialog(QDialog): simulation_done = Signal(bool, str) simulation_termination_request = Signal() def __init__(self, config_file, run_model, simulation_arguments, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setModal(True) self.setWindowModality(Qt.WindowModal) self.setWindowTitle("Simulations - {}".format(config_file)) self._snapshot_model = SnapshotModel(self) self._run_model = run_model self._isDetailedDialog = False self._minimum_width = 1200 ert = None if isinstance(run_model, BaseRunModel): ert = run_model.ert() self._simulations_argments = simulation_arguments self._ticker = QTimer(self) self._ticker.timeout.connect(self._on_ticker) progress_proxy_model = ProgressProxyModel(self._snapshot_model, parent=self) self._total_progress_label = QLabel(_TOTAL_PROGRESS_TEMPLATE.format(0), self) self._total_progress_bar = QProgressBar(self) self._total_progress_bar.setRange(0, 100) self._total_progress_bar.setTextVisible(False) self._iteration_progress_label = QLabel(self) self._progress_view = ProgressView(self) self._progress_view.setModel(progress_proxy_model) self._progress_view.setIndeterminate(True) legend_view = LegendView(self) legend_view.setModel(progress_proxy_model) self._tab_widget = QTabWidget(self) self._snapshot_model.rowsInserted.connect(self.on_new_iteration) self._job_label = QLabel(self) self._job_model = JobListProxyModel(self, 0, 0, 0, 0) self._job_model.setSourceModel(self._snapshot_model) self._job_view = QTableView(self) self._job_view.clicked.connect(self._job_clicked) self._open_files = {} self._job_view.setModel(self._job_model) self.running_time = QLabel("") self.plot_tool = PlotTool(config_file) self.plot_tool.setParent(self) self.plot_button = QPushButton(self.plot_tool.getName()) self.plot_button.clicked.connect(self.plot_tool.trigger) self.plot_button.setEnabled(ert is not None) self.kill_button = QPushButton("Kill Simulations") self.done_button = QPushButton("Done") self.done_button.setHidden(True) self.restart_button = QPushButton("Restart") self.restart_button.setHidden(True) self.show_details_button = QPushButton("Show Details") self.show_details_button.setCheckable(True) size = 20 spin_movie = resourceMovie("ide/loading.gif") spin_movie.setSpeed(60) spin_movie.setScaledSize(QSize(size, size)) spin_movie.start() self.processing_animation = QLabel() self.processing_animation.setMaximumSize(QSize(size, size)) self.processing_animation.setMinimumSize(QSize(size, size)) self.processing_animation.setMovie(spin_movie) button_layout = QHBoxLayout() button_layout.addWidget(self.processing_animation) button_layout.addWidget(self.running_time) button_layout.addStretch() button_layout.addWidget(self.show_details_button) button_layout.addWidget(self.plot_button) button_layout.addWidget(self.kill_button) button_layout.addWidget(self.done_button) button_layout.addWidget(self.restart_button) button_widget_container = QWidget() button_widget_container.setLayout(button_layout) layout = QVBoxLayout() layout.addWidget(self._total_progress_label) layout.addWidget(self._total_progress_bar) layout.addWidget(self._iteration_progress_label) layout.addWidget(self._progress_view) layout.addWidget(legend_view) layout.addWidget(self._tab_widget) layout.addWidget(self._job_label) layout.addWidget(self._job_view) layout.addWidget(button_widget_container) self.setLayout(layout) self.kill_button.clicked.connect(self.killJobs) self.done_button.clicked.connect(self.accept) self.restart_button.clicked.connect(self.restart_failed_realizations) self.show_details_button.clicked.connect(self.toggle_detailed_progress) self.simulation_done.connect(self._on_simulation_done) self.setMinimumWidth(self._minimum_width) self._setSimpleDialog() def _setSimpleDialog(self) -> None: self._isDetailedDialog = False self._tab_widget.setVisible(False) self._job_label.setVisible(False) self._job_view.setVisible(False) self.show_details_button.setText("Show Details") def _setDetailedDialog(self) -> None: self._isDetailedDialog = True self._tab_widget.setVisible(True) self._job_label.setVisible(True) self._job_view.setVisible(True) self.show_details_button.setText("Hide Details") @Slot(QModelIndex, int, int) def on_new_iteration(self, parent: QModelIndex, start: int, end: int) -> None: if not parent.isValid(): iter = start self._iteration_progress_label.setText( f"Progress for iteration {iter}") widget = RealizationWidget(iter) widget.setSnapshotModel(self._snapshot_model) widget.currentChanged.connect(self._select_real) self._tab_widget.addTab(widget, f"Realizations for iteration {iter}") @Slot(QModelIndex) def _job_clicked(self, index): if not index.isValid(): return selected_file = index.data(FileRole) if selected_file and selected_file not in self._open_files: job_name = index.siblingAtColumn(0).data() viewer = FileDialog( selected_file, job_name, index.row(), index.model().get_real(), index.model().get_iter(), self, ) self._open_files[selected_file] = viewer viewer.finished.connect( lambda _, f=selected_file: self._open_files.pop(f)) elif selected_file in self._open_files: self._open_files[selected_file].raise_() @Slot(QModelIndex) def _select_real(self, index): step = 0 stage = 0 real = index.row() iter_ = index.model().get_iter() self._job_model.set_step(iter_, real, stage, step) self._job_label.setText( f"Realization id {index.data(RealIens)} in iteration {iter_}") self._job_view.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) # Clear the selection in the other tabs for i in range(0, self._tab_widget.count()): if i != self._tab_widget.currentIndex(): self._tab_widget.widget(i).clearSelection() def reject(self): return def closeEvent(self, QCloseEvent): if self._run_model.isFinished(): self.simulation_done.emit(self._run_model.hasRunFailed(), self._run_model.getFailMessage()) else: # Kill jobs if dialog is closed if self.killJobs() != QMessageBox.Yes: QCloseEvent.ignore() def startSimulation(self): self._run_model.reset() self._snapshot_model.reset() self._tab_widget.clear() def run(): self._run_model.startSimulations(self._simulations_argments) simulation_thread = Thread(name="ert_gui_simulation_thread") simulation_thread.setDaemon(True) simulation_thread.run = run simulation_thread.start() self._ticker.start(1000) tracker = create_tracker( self._run_model, num_realizations=self._simulations_argments["active_realizations"]. count(), ee_config=self._simulations_argments.get("ee_config", None), ) worker = TrackerWorker(tracker) worker_thread = QThread() worker.done.connect(worker_thread.quit) worker.consumed_event.connect(self._on_tracker_event) worker.moveToThread(worker_thread) self.simulation_done.connect(worker.stop) self._worker = worker self._worker_thread = worker_thread worker_thread.started.connect(worker.consume_and_emit) self._worker_thread.start() def killJobs(self): msg = "Are you sure you want to kill the currently running simulations?" if self._run_model.getQueueStatus().get( JobStatusType.JOB_QUEUE_UNKNOWN, 0) > 0: msg += "\n\nKilling a simulation with unknown status will not kill the realizations already submitted!" kill_job = QMessageBox.question(self, "Kill simulations?", msg, QMessageBox.Yes | QMessageBox.No) if kill_job == QMessageBox.Yes: # Normally this slot would be invoked by the signal/slot system, # but the worker is busy tracking the evaluation. self._worker.request_termination() self.reject() return kill_job @Slot(bool, str) def _on_simulation_done(self, failed, failed_msg): self.processing_animation.hide() self.kill_button.setHidden(True) self.done_button.setHidden(False) self.restart_button.setVisible(self.has_failed_realizations()) self.restart_button.setEnabled(self._run_model.support_restart) self._total_progress_bar.setValue(100) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format(100)) if failed: QMessageBox.critical( self, "Simulations failed!", f"The simulation failed with the following error:\n\n{failed_msg}", ) @Slot() def _on_ticker(self): runtime = self._run_model.get_runtime() self.running_time.setText(format_running_time(runtime)) @Slot(object) def _on_tracker_event(self, event): if isinstance(event, EndEvent): self.simulation_done.emit(event.failed, event.failed_msg) self._worker.stop() self._ticker.stop() elif isinstance(event, FullSnapshotEvent): if event.snapshot is not None: self._snapshot_model._add_snapshot(event.snapshot, event.iteration) self._progress_view.setIndeterminate(event.indeterminate) progress = int(event.progress * 100) self._total_progress_bar.setValue(progress) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format(progress)) elif isinstance(event, SnapshotUpdateEvent): if event.partial_snapshot is not None: self._snapshot_model._add_partial_snapshot( event.partial_snapshot, event.iteration) self._progress_view.setIndeterminate(event.indeterminate) progress = int(event.progress * 100) self._total_progress_bar.setValue(progress) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format(progress)) def has_failed_realizations(self): completed = self._run_model.completed_realizations_mask initial = self._run_model.initial_realizations_mask for (index, successful) in enumerate(completed): if initial[index] and not successful: return True return False def count_successful_realizations(self): """ Counts the realizations completed in the prevoius ensemble run :return: """ completed = self._run_model.completed_realizations_mask return completed.count(True) def create_mask_from_failed_realizations(self): """ Creates a BoolVector mask representing the failed realizations :return: Type BoolVector """ completed = self._run_model.completed_realizations_mask initial = self._run_model.initial_realizations_mask inverted_mask = BoolVector(default_value=False) for (index, successful) in enumerate(completed): inverted_mask[index] = initial[index] and not successful return inverted_mask def restart_failed_realizations(self): msg = QMessageBox(self) msg.setIcon(QMessageBox.Information) msg.setText( "Note that workflows will only be executed on the restarted realizations and that this might have unexpected consequences." ) msg.setWindowTitle("Restart Failed Realizations") msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) result = msg.exec_() if result == QMessageBox.Ok: self.restart_button.setVisible(False) self.kill_button.setVisible(True) self.done_button.setVisible(False) active_realizations = self.create_mask_from_failed_realizations() self._simulations_argments[ "active_realizations"] = active_realizations self._simulations_argments[ "prev_successful_realizations"] = self._simulations_argments.get( "prev_successful_realizations", 0) self._simulations_argments[ "prev_successful_realizations"] += self.count_successful_realizations( ) self.startSimulation() @Slot() def toggle_detailed_progress(self): if self._isDetailedDialog: self._setSimpleDialog() else: self._setDetailedDialog() self.adjustSize()
def focusChangedEvent(self, old_w, new_w): if issubclass(new_w.__class__, QLineEdit): #print("QLineEdit got focus: ", new_w) QTimer.singleShot(0, new_w.selectAll)
class PyDMApplication(QApplication): """ PyDMApplication handles loading PyDM display files, opening new windows, and most importantly, establishing and managing connections to channels via data plugins. Parameters ---------- ui_file : str, optional The file path to a PyDM display file (.ui or .py). command_line_args : list, optional A list of strings representing arguments supplied at the command line. All arguments in this list are handled by QApplication, in addition to PyDMApplication. display_args : list, optional A list of command line arguments that should be forwarded to the Display class. This is only useful if a Related Display Button is opening up a .py file with extra arguments specified, and probably isn't something you will ever need to use when writing code that instantiates PyDMApplication. perfmon : bool, optional Whether or not to enable performance monitoring using 'psutil'. When enabled, CPU load information on a per-thread basis is periodically printed to the terminal. hide_nav_bar : bool, optional Whether or not to display the navigation bar (forward/back/home buttons) when the main window is first displayed. hide_menu_bar: bool, optional Whether or not to display the menu bar (File, View) when the main window is first displayed. hide_status_bar: bool, optional Whether or not to display the status bar (general messages and errors) when the main window is first displayed. read_only: bool, optional Whether or not to launch PyDM in a read-only state. macros : dict, optional A dictionary of macro variables to be forwarded to the display class being loaded. use_main_window : bool, optional If ui_file is note given, this parameter controls whether or not to create a PyDMMainWindow in the initialization (Default is True). fullscreen : bool, optional Whether or not to launch PyDM in a full screen mode. """ # Instantiate our plugins. plugins = data_plugins.plugin_modules def __init__(self, ui_file=None, command_line_args=[], display_args=[], perfmon=False, hide_nav_bar=False, hide_menu_bar=False, hide_status_bar=False, read_only=False, macros=None, use_main_window=True, stylesheet_path=None, fullscreen=False): super(PyDMApplication, self).__init__(command_line_args) # Enable High DPI display, if available. if hasattr(Qt, 'AA_UseHighDpiPixmaps'): self.setAttribute(Qt.AA_UseHighDpiPixmaps) # The macro and directory stacks are needed for nested displays (usually PyDMEmbeddedDisplays). # During the process of loading a display (whether from a .ui file, or a .py file), the application's # 'open_file' method will be called recursively. Inside open_file, the last item on the stack represents # the parent widget's file path and macro variables. Any file paths are joined to the end of the parent's # file path, and any macros are merged with the parent's macros. This system depends on open_file always # being called hierarchially (i.e., parent calls it first, then on down the ancestor tree, with no unrelated # calls in between). If something crazy happens and PyDM somehow gains the ability to open files in a # multi-threaded way, for example, this system will fail. data_plugins.set_read_only(read_only) self.main_window = None self.directory_stack = [''] self.macro_stack = [{}] self.windows = {} self.display_args = display_args self.hide_nav_bar = hide_nav_bar self.hide_menu_bar = hide_menu_bar self.hide_status_bar = hide_status_bar self.fullscreen = fullscreen # Open a window if required. if ui_file is not None: self.make_main_window(stylesheet_path=stylesheet_path) self.make_window(ui_file, macros, command_line_args) elif use_main_window: self.make_main_window(stylesheet_path=stylesheet_path) self.had_file = ui_file is not None # Re-enable sigint (usually blocked by pyqt) signal.signal(signal.SIGINT, signal.SIG_DFL) # Performance monitoring if perfmon: import psutil self.perf = psutil.Process() self.perf_timer = QTimer() self.perf_timer.setInterval(2000) self.perf_timer.timeout.connect(self.get_CPU_usage) self.perf_timer.start() def get_string_encoding(self): return os.getenv("PYDM_STRING_ENCODING", "utf_8") def exec_(self): """ Execute the QApplication. """ return super(PyDMApplication, self).exec_() def is_read_only(self): warnings.warn("'PyDMApplication.is_read_only' is deprecated, " "use 'pydm.data_plugins.is_read_only' instead.") return data_plugins.is_read_only() @Slot() def get_CPU_usage(self): """ Prints total CPU usage (in percent), as well as per-thread usage, to the terminal. """ with self.perf.oneshot(): total_percent = self.perf.cpu_percent(interval=None) total_time = sum(self.perf.cpu_times()) usage = [ total_percent * ((t.system_time + t.user_time) / total_time) for t in self.perf.threads() ] print("Total: {tot}, Per Thread: {percpu}".format(tot=total_percent, percpu=usage)) def new_pydm_process(self, ui_file, macros=None, command_line_args=None): """ Spawn a new PyDM process and open the supplied file. Commands to open new windows in PyDM typically actually spawn an entirely new PyDM process. This keeps each window isolated, so that one window cannot slow down or crash another. Parameters ---------- ui_file : str The path to a .ui or .py file to open in the new process. macros : dict, optional A dictionary of macro variables to supply to the display file to be opened. command_line_args : list, optional A list of command line arguments to pass to the new process. Typically, this argument is used by related display buttons to pass in extra arguments. It is probably rare that code you write needs to use this argument. """ # Expand user (~ or ~user) and environment variables. ui_file = os.path.expanduser(os.path.expandvars(ui_file)) base_dir, fname, args = path_info(str(ui_file)) filepath = os.path.join(base_dir, fname) filepath_args = args pydm_display_app_path = which("pydm") if pydm_display_app_path is None: if os.environ.get("PYDM_PATH") is not None: pydm_display_app_path = os.path.join(os.environ["PYDM_PATH"], "pydm") else: # Not in the PATH and no ENV VAR pointing to it... # Let's try the script folder... pydm_display_app_path = os.path.join( os.path.split(os.path.realpath(__file__))[0], "..", "scripts", "pydm") args = [pydm_display_app_path] if self.hide_nav_bar: args.extend(["--hide-nav-bar"]) if self.hide_menu_bar: args.extend(["--hide-menu-bar"]) if self.hide_status_bar: args.extend(["--hide-status-bar"]) if self.fullscreen: args.extend(["--fullscreen"]) if macros is not None: args.extend(["-m", json.dumps(macros)]) args.append(filepath) args.extend(self.display_args) args.extend(filepath_args) if command_line_args is not None: args.extend(command_line_args) subprocess.Popen(args, shell=False) def new_window(self, ui_file, macros=None, command_line_args=None): """ Make a new window and open the supplied file. Currently, this method just calls `new_pydm_process`. This is an internal method that typically will not be needed by users. Parameters ---------- ui_file : str The path to a .ui or .py file to open in the new process. macros : dict, optional A dictionary of macro variables to supply to the display file to be opened. command_line_args : list, optional A list of command line arguments to pass to the new process. Typically, this argument is used by related display buttons to pass in extra arguments. It is probably rare that code you write needs to use this argument. """ # All new windows are spawned as new processes. self.new_pydm_process(ui_file, macros, command_line_args) def make_main_window(self, stylesheet_path=None): """ Instantiate a new PyDMMainWindow, add it to the application's list of windows. Typically, this function is only called as part of starting up a new process, because PyDMApplications only have one window per process. """ main_window = PyDMMainWindow(hide_nav_bar=self.hide_nav_bar, hide_menu_bar=self.hide_menu_bar, hide_status_bar=self.hide_status_bar) self.main_window = main_window apply_stylesheet(stylesheet_path, widget=self.main_window) self.main_window.update_tools_menu() if self.fullscreen: main_window.enter_fullscreen() else: main_window.show() # If we are launching a new window, we don't want it to sit right on top of an existing window. if len(self.windows) > 1: main_window.move(main_window.x() + 10, main_window.y() + 10) def make_window(self, ui_file, macros=None, command_line_args=None): """ Open the ui_file in the window. Parameters ---------- ui_file : str The path to a .ui or .py file to open in the new process. macros : dict, optional A dictionary of macro variables to supply to the display file to be opened. command_line_args : list, optional A list of command line arguments to pass to the new process. Typically, this argument is used by related display buttons to pass in extra arguments. It is probably rare that code you write needs to use this argument. """ if ui_file is not None: self.main_window.open_file(ui_file, macros, command_line_args) self.windows[self.main_window] = path_info(ui_file)[0] def close_window(self, window): try: del self.windows[window] except KeyError: # If window is no longer at self.windows # it means that we already closed it. pass def open_file(self, ui_file, macros=None, command_line_args=None, defer_connections=False, **kwargs): """ Open a .ui or .py file, and return a widget from the loaded file. This method is the entry point for all opening of new displays, and manages handling macros and relative file paths when opening nested displays. Parameters ---------- ui_file : str The path to a .ui or .py file to open in the new process. macros : dict, optional A dictionary of macro variables to supply to the display file to be opened. command_line_args : list, optional A list of command line arguments to pass to the new process. Typically, this argument is used by related display buttons to pass in extra arguments. It is probably rare that code you write needs to use this argument. Returns ------- QWidget """ if 'establish_connection' in kwargs: logger.warning( "Ignoring 'establish_connection' parameter at " "open_relative. The connection is now handled by the" " widgets.") # First split the ui_file string into a filepath and arguments args = command_line_args if command_line_args is not None else [] dir_name, file_name, extra_args = path_info(ui_file) args.extend(extra_args) filepath = os.path.join(dir_name, file_name) self.directory_stack.append(dir_name) (filename, extension) = os.path.splitext(file_name) if macros is None: macros = {} merged_macros = self.macro_stack[-1].copy() merged_macros.update(macros) self.macro_stack.append(merged_macros) with data_plugins.connection_queue( defer_connections=defer_connections): if extension == '.ui': widget = load_ui_file(filepath, merged_macros) elif extension == '.py': widget = load_py_file(filepath, args, merged_macros) else: self.directory_stack.pop() self.macro_stack.pop() raise ValueError("Invalid file type: {}".format(extension)) # Add on the macros to the widget after initialization. This is # done for both ui files and python files. widget.base_macros = merged_macros self.directory_stack.pop() self.macro_stack.pop() return widget # get_path gives you the path to ui_file relative to where you are running pydm from. # Many widgets handle file paths (related display, embedded display, and drawing image come to mind) # and the standard is that they expect paths to be given relative to the .ui or .py file in which the # widget lives. But, python and Qt want the file path relative to the directory you are running # pydm from. This function does that translation. def get_path(self, ui_file): """ Gives you the path to ui_file relative to where you are running pydm from. Many widgets handle file paths (related display, embedded display, and drawing image come to mind) and the standard is that they expect paths to be given relative to the .ui or .py file in which the widget lives. But, python and Qt want the file path relative to the directory you are running pydm from. This function does that translation. Parameters ---------- ui_file : str Returns ------- str """ dirname = self.directory_stack[-1] full_path = os.path.join(dirname, str(ui_file)) return full_path def open_relative(self, ui_file, widget, macros=None, command_line_args=[], **kwargs): """ open_relative opens a ui file with a relative path. This is really only used by embedded displays. """ if 'establish_connection' in kwargs: logger.warning( "Ignoring 'establish_connection' parameter at " "open_relative. The connection is now handled by the" " widgets.") full_path = self.get_path(ui_file) if not os.path.exists(full_path): new_fname = find_display_in_path(ui_file) if new_fname is not None and new_fname != "": full_path = new_fname return self.open_file(full_path, macros=macros, command_line_args=command_line_args) def plugin_for_channel(self, channel): """ Given a PyDMChannel object, determine the appropriate plugin to use. Parameters ---------- channel : PyDMChannel Returns ------- PyDMPlugin """ warnings.warn("'PyDMApplication.plugin_for_channel' is deprecated, " "use 'pydm.data_plugins.plugin_for_address' instead.") if channel.address is None or channel.address == "": return None return data_plugins.plugin_for_address(channel.address) def add_connection(self, channel): """ Add a new connection to a channel. Parameters ---------- channel : PyDMChannel """ warnings.warn("'PyDMApplication.add_connection' is deprecated, " "use PyDMConnection.connect()") channel.connect() def remove_connection(self, channel): """ Remove a connection to a channel. Parameters ---------- channel : PyDMChannel """ warnings.warn("'PyDMApplication.remove_connection' is deprecated, " "use PyDMConnection.disconnect()") channel.disconnect() def eventFilter(self, obj, event): warnings.warn("'PyDMApplication.eventFilter' is deprecated, " " this function is now found on PyDMWidget") obj.eventFilter(obj, event) def show_address_tooltip(self, obj, event): warnings.warn("'PyDMApplication.show_address_tooltip' is deprecated, " " this function is now found on PyDMWidget") obj.show_address_tooltip(event) def establish_widget_connections(self, widget): """ Given a widget to start from, traverse the tree of child widgets, and try to establish connections to any widgets with channels. Display subclasses which dynamically create widgets may need to use this method. Parameters ---------- widget : QWidget """ warnings.warn( "'PyDMApplication.establish_widget_connections' is deprecated, " "this function is now found on `utilities.establish_widget_connections`." ) connection.establish_widget_connections(widget) def close_widget_connections(self, widget): """ Given a widget to start from, traverse the tree of child widgets, and try to close connections to any widgets with channels. Parameters ---------- widget : QWidget """ warnings.warn( "'PyDMApplication.close_widget_connections' is deprecated, " "this function is now found on `utilities.close_widget_connections`." ) connection.close_widget_connections(widget) def open_template(self, template_filename): fname = os.path.expanduser(os.path.expandvars(template_filename)) if os.path.isabs(fname): full_path = fname else: full_path = self.get_path(template_filename) if not os.path.exists(full_path): new_fname = find_display_in_path(template_filename) if new_fname is not None and new_fname != "": full_path = new_fname template = macro.template_for_file(full_path) return template def widget_from_template(self, template, macros): if macros is None: macros = {} merged_macros = self.macro_stack[-1].copy() merged_macros.update(macros) f = macro.replace_macros_in_template(template, merged_macros) w = load_ui_file(f) w.base_macros = merged_macros return w
return legend = self.plotWidget.plotItem.legend print('Legend:', legend) legend.removeItem('%d' % sweepIndex) print('Removing item:', curve) self.plotWidget.removeItem(curve) print('items in plotWidget.plotItem:', self.plotWidget.plotItem.items) del self._sweepMap[sweepIndex] print('Removed sweep') if __name__ == '__main__': from qtpy.QtGui import QApplication from qtpy.QtCore import QTimer path = 'ExampleData/' fileName = path + 'TES2_IV_20180117_090049.h5' app = QApplication([]) print('Loading data') sweepCollection = IvSweepCollection(fileName) print('making widget') mw = IvGraphWidget(sweepCollection) for i in range(0, len(sweepCollection), 4): mw.showSweep(i) mw.show() QTimer.singleShot(3000, lambda: mw.setSelection('up+/up-')) app.exec_()
class ProcessWorker(QObject): """ """ sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs={}): super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs self._timer = QTimer() self._process = QProcess() self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) # self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) json_stdout = stdout.replace('\n\x00', '') try: json_stdout = json.loads(json_stdout) except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """ """ if not self._communicate_first: if self._process.state() == QProcess.NotRunning: self.communicate() elif self._fired: self._timer.stop() def communicate(self): """ """ self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda cloud api site' not in stderr.lower(): if stderr.strip() and self._conda: raise Exception('{0}:\n' 'STDERR:\n{1}\nEND' ''.format(' '.join(self._cmd_list), stderr)) # elif stderr.strip() and self._pip: # raise PipError(self._cmd_list) else: result[-1] = '' if self._parse and stdout: try: result = json.loads(stdout), result[-1] except ValueError as error: result = stdout, error if 'error' in result[0]: error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['error']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: logger.error(str(('error', result[-1]))) self._fired = True return result def close(self): """ """ self._process.close() def is_finished(self): """ """ return self._process.state() == QProcess.NotRunning and self._fired def start(self): """ """ logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class StageUI(QMainWindow): col_connected = 0 col_port = 1 col_position = 2 col_moving = 3 col_engaged = 4 col_limit = 5 def __init__(self): """ Stage Controller PyQt UI Object """ super(QMainWindow, self).__init__() self.module_path = visual_behavior.__path__ self.config = visual_behavior.source_project_configuration('visual_behavior_v1.yml') self.admin_digest = b'!#/)zW\xa5\xa7C\x89J\x0eJ\x80\x1f\xc3' # obviously get this from db self.admin_enabled = False self.hw_proxy = hardware_proxy.RemoteStageController() self.stage = None self.daq = None self.ui = uic.loadUi(self.module_path[0] + '/resources/remote_stage_controller.ui') self.setCentralWidget(self.ui) self.setup_ui() self.log('Stage Controller Ready') self.table_timer = QTimer() self.table_timer.timeout.connect(self.update_table) self.coordinates = {'home': None, 'working': None, 'load': None, 'mouse': None} self._ignore_limits = False self.db = self.setup_db() self._spout_state = 0 self.signal_connect_to_rig('localhost') def log(self, message): logging.info(message) self.ui.statusBar.showMessage(message) def setup_db(self): db = {} return db def load_stage_coordinates(self): """ :return: """ key = f'{self.stage.serial}' safe_coords = f'{key}_working' if safe_coords in self.db: self.coordinates['working'] = yaml.load(self.db[safe_coords]) self.ui.lbl_coords_safe.setText(f'({coords[0]}, {coords[1]}, {coords[2]})') origin_coords = f'{key}_load' if origin_coords in self.db: self.coordinates['load'] = yaml.load(self.db[origin_coords]) self.ui.lbl_coords_origin.setText(f'({coords[0]}, {coords[1]},{coords[2]})') def load_mouse_coordinates(self): """ :return: """ key = f'{self.stage.serial}_{self.ui.le_mouse_id}' if key in self.db: self.coordinates['mouse'] = yaml.load(self.db[key]) self.log(f'loaded mouse offset: ({coords[0]}, {coords[1]}, {coords[2]})') else: self.log(f'Could not find mouse offset: {key}') def setup_ui(self): """ :return: """ self.ui.btn_moveto.clicked.connect(self.signal_move_to) self.ui.btn_air_1.clicked.connect(self.signal_extend_lickspout) self.ui.btn_air_2.clicked.connect(self.signal_retract_lickspout) self.ui.btn_water_1.clicked.connect(self.signal_water_sol_1) self.ui.btn_water_2.clicked.connect(self.signal_water_sol_2) self.ui.btn_home.clicked.connect(self.signal_home_stage) self.ui.btn_z_plus.clicked.connect(partial(self.axis_step, 2, 1)) self.ui.btn_z_minus.clicked.connect(partial(self.axis_step, 2, -1)) self.ui.btn_y_plus.clicked.connect(partial(self.axis_step, 1, 1)) self.ui.btn_y_minus.clicked.connect(partial(self.axis_step, 1, -1)) self.ui.btn_x_plus.clicked.connect(partial(self.axis_step, 0, 1)) self.ui.btn_x_minus.clicked.connect(partial(self.axis_step, 0, -1)) self.ui.btn_stop.clicked.connect(self.signal_stop) self.ui.btn_emergency.clicked.connect(self.signal_emergency) self.ui.btn_register_working.clicked.connect(partial(self.register_coordinates, 'working')) self.ui.btn_register_load.clicked.connect(partial(self.register_coordinates, 'load')) self.ui.btn_register_mouse.clicked.connect(partial(self.register_coordinates, 'mouse')) self.ui.le_mouse_id.editingFinished.connect(self.load_mouse_coordinates) self.ui.btn_working.clicked.connect(partial(self.move_to_coordinates, 'working')) self.ui.btn_load.clicked.connect(partial(self.move_to_coordinates, 'load')) self.ui.btn_mouse.clicked.connect(partial(self.move_to_coordinates, 'mouse')) self.icon_clear = QIcon(self.module_path[0] + '/resources/led_clear.png') self.icon_red = QIcon(self.module_path[0] + '/resources/led_red.png') self.icon_blue = QIcon(self.module_path[0] + '/resources/led_blue.png') self.icon_green = QIcon(self.module_path[0] + '/resources/led_green.png') self.ui.btn_x_release.clicked.connect(partial(self.set_clamp, 0)) self.ui.btn_x_release.clicked.connect(partial(self.set_clamp, 1)) self.ui.btn_x_release.clicked.connect(partial(self.set_clamp, 2)) self.ui.btn_reset.clicked.connect(partial(self.set_clamp, 'reset')) self.ui.cb_limits.toggled.connect(self.signal_ignore_limits) for c, r in product([self.col_port, self.col_position], range(3)): item = QTableWidgetItem() item.setTextAlignment(QtCore.Qt.AlignCenter) self.ui.tbl_stage.setItem(r, c, item) for c, r in product([self.col_connected, self.col_moving, self.col_engaged, self.col_limit], range(3)): button = QPushButton() button.setFlat(True) button.setIcon(self.icon_clear) self.ui.tbl_stage.setCellWidget(r, c, button) channels = self.config.phidget.channels self.ui.tbl_stage.item(0, self.col_port).setText(str(channels.x)) self.ui.tbl_stage.item(1, self.col_port).setText(str(channels.y)) self.ui.tbl_stage.item(2, self.col_port).setText(str(channels.z)) self.disable_user_controls() def set_clamp(self, axis): self.hw_proxy.clamp_signal(axis) def signal_emergency(self): self.signal_retract_lickspout() self.hw_proxy.stop_motion() def signal_ignore_limits(self, state): if self.ui.cb_limits.isChecked(): self._ignore_limits = True self.hw_proxy.ignore_limits(True) else: self._ignore_limits = False self.hw_proxy.ignore_limits(False) def update_table(self): moving = self.stage.is_moving engaged = self.stage.is_engaged limits = self.stage.limits position = self.stage.position #self.ui.lbl_temperature.setText('Temp: ', signal_temp_value) 'need a refresh frequency' # if any(motion): # self.ui.lbl_encoder_position.setText('Position: ', signal_encoder_velocity) # else: # self.ui.lbl_encoder_position.setText('Position: ') #if any(motion): #self.ui.lbl_encoder_velocity.setText('Velocity: ', signal_encoder_velocity) #else: #self.ui.lbl_encoder_velocity.setText('Velocity: ') # if any(motion): # self.ui.lbl_encoder_acceleration.setText('Velocity: ', signal_encoder_velocity) # else: # self.ui.lbl_encoder_acceleration.setText('Velocity: ') if any(moving): self.ui.lbl_status.setText('Status: Stage Moving') else: self.ui.lbl_status.setText('Status:') for i in range(3): if moving[i]: self.ui.tbl_stage.cellWidget(i, self.col_moving).setIcon(self.icon_blue) else: self.ui.tbl_stage.cellWidget(i, self.col_moving).setIcon(self.icon_clear) if engaged[i]: self.ui.tbl_stage.cellWidget(i, self.col_engaged).setIcon(self.icon_blue) else: self.ui.tbl_stage.cellWidget(i, self.col_engaged).setIcon(self.icon_clear) if limits[i]: self.ui.tbl_stage.cellWidget(i, self.col_limit).setIcon(self.icon_red) else: self.ui.tbl_stage.cellWidget(i, self.col_limit).setIcon(self.icon_blue) self.ui.tbl_stage.item(i, self.col_position).setText(str(position[i])) def axis_step(self, axis, sign=1): """ Step the axis by a value defined by sb_step. If the axis has hit it's limit switch, send the analog signal to release the break before sending a move_stage :param axis: :param sign: :param sb_step: :return: """ if not self.stage: return if self.ui.le_step_size.text() != '': step = float(self.ui.le_step_size.text()) else: step = self.config.phidget.step_size position = self.stage.position position[axis] += step * sign self.stage.append_move(position) def register_coordinates(self, name): """ :param name: :param label: :return: """ try: key = f'{self.stage.serial}_{name}' coords = list(self.stage.position) self.coordinates[name] = coords self.log('setting {name} to {coords}') self.db[key] = yaml.dump(coords) '''There is a hardware mixup preventing proper registration o Load/Unload. For now it is set as an x-translation o working''' if name == 'working': coords[0] += self.config.load_x_translation self.log(f'setting load to {coords}') self.coordinates['load'] = coords self.db[f'{self.hw_proxy.serial}_load'] = yaml.dump(coords) ''' b/c we can get stuck on the monument, we will retract the lickspout, move about, and extend again''' self.signal_retract_lickspout() coords = list(self.stage.position) coords[2] -= 10 self.move_to_coordinates(coords) QTimer.singleShot(200, self.signal_extend_lickspout) except Exception as err: self.log(f'Error recording {name} coordinates to the db') self.log(f'{err}') def move_to_coordinates(self, name): coords = self.coordinates[name] try: self.log(f'Driving to {name}: {coords}') if coords: self.hw_proxy.move_to(coords) except KeyError as err: self.log(f'Error: {key} has not been registered.') def signal_stop(self): self.how_proxy.stop_motion() def signal_home_stage(self): self.hw_proxy.home_stage() def signal_retract_lickspout(self): self.hw_proxy.retract_lickspout() def signal_extend_lickspout(self): self.hw_proxy.extend_lickspout() def signal_water_sol_1(self): self.hw_proxy.signal_water_sol_1() def signal_water_sol_2(self): self.hw_proxy.signal_water_sol_2() def signal_temp_value(self): self.hw_proxy.signal_temp() def signal_encoder_movement(self): self.hw_proxy.signal_encoder_movement() def signal_move_to(self): try: x = float(self.ui.le_x.text()) y = float(self.ui.le_y.text()) z = float(self.ui.le_z.text()) self.stage.append_move([x, y, z]) except Exception as err: print('error:', err) pass def signal_connect_to_rig(self, host): host = 'localhost' self.stage = self.hw_proxy # self.daq = self.hw_proxy.daq # self.load_stage_coordinates() self.log(f'Connected to stage: {self.hw_proxy.stage_serial}') self.enable_user_controls() for i in range(3): self.ui.tbl_stage.cellWidget(i, self.col_connected).setIcon(self.icon_blue) self.table_timer.start(500) def disable_user_controls(self): user_controls = [self.ui.btn_x_minus, self.ui.btn_x_plus, self.ui.btn_y_minus, self.ui.btn_y_plus, self.ui.btn_z_minus, self.ui.btn_z_plus, self.ui.btn_home, self.ui.btn_load, self.ui.btn_working, self.ui.btn_mouse, self.ui.btn_stop ] for control in user_controls: control.setEnabled(False) def enable_user_controls(self): user_controls = [self.ui.btn_x_minus, self.ui.btn_x_plus, self.ui.btn_y_minus, self.ui.btn_y_plus, self.ui.btn_z_minus, self.ui.btn_z_plus, self.ui.btn_home, self.ui.btn_load, self.ui.btn_working, self.ui.btn_mouse, self.ui.btn_stop] for control in user_controls: control.setEnabled(True) def keyPressEvent(self, event): """ :param event: :return: """ modifiers = QApplication.keyboardModifiers() if event.key() == QtCore.Qt.Key_A: self.axis_step(0, -1) if event.key() == QtCore.Qt.Key_Space: if not self.admin_enabled: return if self._spout_state: self.signal_retract_lickspout() self._spout_state = 0 else: self.signal_extend_lickspout() self._spout_state = 1 if event.key() == QtCore.Qt.Key_D: if modifiers == (QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier): if self.admin_enabled: self.admin_enabled = False else: if self.authenticate_admin(): logging.info('Admin mode enabled.') self.admin_enabled = True else: self.axis_step(0, 1) elif event.key() == QtCore.Qt.Key_W: self.axis_step(1, 1) elif event.key() == QtCore.Qt.Key_S: self.axis_step(1, -1) elif event.key() == QtCore.Qt.Key_Q: self.axis_step(2, -1) elif event.key() == QtCore.Qt.Key_E: self.axis_step(2, 1) def authenticate_admin(self): """ :return: """ text, ok = QInputDialog.getText(self, 'Administration Panel', 'Password:'******'Invalid Password') dialog.setText('Unable to enable administration mode. Invalid Password.') dialog.exec_() return False
class _CondaAPI(QObject): """ """ ROOT_PREFIX = None ENCODING = 'ascii' UTF8 = 'utf-8' DEFAULT_CHANNELS = [ 'https://repo.continuum.io/pkgs/pro', 'https://repo.continuum.io/pkgs/free' ] def __init__(self, parent=None): super(_CondaAPI, self).__init__() self._parent = parent self._queue = deque() self._timer = QTimer() self._current_worker = None self._workers = [] self._timer.setInterval(1000) self._timer.timeout.connect(self._clean) self.set_root_prefix() def _clean(self): """ Periodically check for inactive workers and remove their references. """ if self._workers: for w in self._workers: if w.is_finished(): self._workers.remove(w) else: self._current_worker = None self._timer.stop() def _start(self): """ """ if len(self._queue) == 1: self._current_worker = self._queue.popleft() self._workers.append(self._current_worker) self._current_worker.start() self._timer.start() def is_active(self): """ Check if a worker is still active. """ return len(self._workers) == 0 def terminate_all_processes(self): """ Kill all working processes. """ for worker in self._workers: worker.close() # --- Conda api # ------------------------------------------------------------------------- def _call_conda(self, extra_args, abspath=True, parse=False, callback=None): """ Call conda with the list of extra arguments, and return the worker. The result can be force by calling worker.communicate(), which returns the tuple (stdout, stderr). """ if abspath: if sys.platform == 'win32': python = join(self.ROOT_PREFIX, 'python.exe') conda = join(self.ROOT_PREFIX, 'Scripts', 'conda-script.py') else: python = join(self.ROOT_PREFIX, 'bin/python') conda = join(self.ROOT_PREFIX, 'bin/conda') cmd_list = [python, conda] else: # Just use whatever conda is on the path cmd_list = ['conda'] cmd_list.extend(extra_args) process_worker = ProcessWorker(cmd_list, parse=parse, callback=callback) process_worker.sig_finished.connect(self._start) self._queue.append(process_worker) self._start() return process_worker def _call_and_parse(self, extra_args, abspath=True, callback=None): """ """ return self._call_conda(extra_args, abspath=abspath, parse=True, callback=callback) def _setup_install_commands_from_kwargs(self, kwargs, keys=tuple()): cmd_list = [] if kwargs.get('override_channels', False) and 'channel' not in kwargs: raise TypeError('conda search: override_channels requires channel') if 'env' in kwargs: cmd_list.extend(['--name', kwargs.pop('env')]) if 'prefix' in kwargs: cmd_list.extend(['--prefix', kwargs.pop('prefix')]) if 'channel' in kwargs: channel = kwargs.pop('channel') if isinstance(channel, str): cmd_list.extend(['--channel', channel]) else: cmd_list.append('--channel') cmd_list.extend(channel) for key in keys: if key in kwargs and kwargs[key]: cmd_list.append('--' + key.replace('_', '-')) return cmd_list def set_root_prefix(self, prefix=None): """ Set the prefix to the root environment (default is /opt/anaconda). This function should only be called once (right after importing conda_api). """ if prefix: self.ROOT_PREFIX = prefix else: # Find some conda instance, and then use info to get 'root_prefix' worker = self._call_and_parse(['info', '--json'], abspath=False) info = worker.communicate()[0] self.ROOT_PREFIX = info['root_prefix'] def get_conda_version(self): """ Return the version of conda being used (invoked) as a string. """ return self._call_conda(['--version'], callback=self._get_conda_version) def _get_conda_version(self, stdout, stderr): # argparse outputs version to stderr in Python < 3.4. # http://bugs.python.org/issue18920 pat = re.compile(r'conda:?\s+(\d+\.\d\S+|unknown)') m = pat.match(stderr.decode().strip()) if m is None: m = pat.match(stdout.decode().strip()) if m is None: raise Exception('output did not match: {0}'.format(stderr)) return m.group(1) def get_envs(self): """ Return all of the (named) environment (this does not include the root environment), as a list of absolute path to their prefixes. """ logger.debug('') # return self._call_and_parse(['info', '--json'], # callback=lambda o, e: o['envs']) envs = os.listdir(os.sep.join([self.ROOT_PREFIX, 'envs'])) envs = [os.sep.join([self.ROOT_PREFIX, 'envs', i]) for i in envs] valid_envs = [ e for e in envs if os.path.isdir(e) and self.environment_exists(prefix=e) ] return valid_envs def get_prefix_envname(self, name): """ Given the name of an environment return its full prefix path, or None if it cannot be found. """ prefix = None if name == 'root': prefix = self.ROOT_PREFIX # envs, error = self.get_envs().communicate() envs = self.get_envs() for p in envs: if basename(p) == name: prefix = p return prefix def linked(self, prefix): """ Return the (set of canonical names) of linked packages in `prefix`. """ logger.debug(str(prefix)) if not isdir(prefix): raise Exception('no such directory: {0}'.format(prefix)) meta_dir = join(prefix, 'conda-meta') if not isdir(meta_dir): # We might have nothing in linked (and no conda-meta directory) return set() return set(fn[:-5] for fn in os.listdir(meta_dir) if fn.endswith('.json')) def split_canonical_name(self, cname): """ Split a canonical package name into (name, version, build) strings. """ return tuple(cname.rsplit('-', 2)) def info(self, abspath=True): """ Return a dictionary with configuration information. No guarantee is made about which keys exist. Therefore this function should only be used for testing and debugging. """ logger.debug(str('')) return self._call_and_parse(['info', '--json'], abspath=abspath) def package_info(self, package, abspath=True): """ Return a dictionary with package information. """ return self._call_and_parse(['info', package, '--json'], abspath=abspath) def search(self, regex=None, spec=None, **kwargs): """ Search for packages. """ cmd_list = ['search', '--json'] if regex and spec: raise TypeError('conda search: only one of regex or spec allowed') if regex: cmd_list.append(regex) if spec: cmd_list.extend(['--spec', spec]) if 'platform' in kwargs: cmd_list.extend(['--platform', kwargs.pop('platform')]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('canonical', 'unknown', 'use_index_cache', 'outdated', 'override_channels'))) return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def create(self, name=None, prefix=None, pkgs=None, channels=None): """ Create an environment either by name or path with a specified set of packages. """ logger.debug(str((prefix, pkgs, channels))) # TODO: Fix temporal hack if not pkgs or not isinstance(pkgs, (list, tuple, str)): raise TypeError('must specify a list of one or more packages to ' 'install into new environment') cmd_list = ['create', '--yes', '--quiet', '--json', '--mkdir'] if name: ref = name search = [ os.path.join(d, name) for d in self.info().communicate()[0]['envs_dirs'] ] cmd_list.extend(['--name', name]) elif prefix: ref = prefix search = [prefix] cmd_list.extend(['--prefix', prefix]) else: raise TypeError('must specify either an environment name or a ' 'path for new environment') if any(os.path.exists(prefix) for prefix in search): raise CondaEnvExistsError('Conda environment {0} already ' 'exists'.format(ref)) # TODO: Fix temporal hack if isinstance(pkgs, (list, tuple)): cmd_list.extend(pkgs) elif isinstance(pkgs, str): cmd_list.extend(['--file', pkgs]) # TODO: Check if correct if channels: cmd_list.extend(['--override-channels']) for channel in channels: cmd_list.extend(['--channel']) cmd_list.extend([channel]) return self._call_and_parse(cmd_list) def parse_token_channel(self, channel, token): """ Adapt a channel to include the authentication token of the logged user. Ignore default channels """ if token and channel not in self.DEFAULT_CHANNELS: url_parts = channel.split('/') start = url_parts[:-1] middle = 't/{0}'.format(token) end = url_parts[-1] token_channel = '{0}/{1}/{2}'.format('/'.join(start), middle, end) return token_channel else: return channel def install(self, name=None, prefix=None, pkgs=None, dep=True, channels=None, token=None): """ Install packages into an environment either by name or path with a specified set of packages. If token is specified, the channels different from the defaults will get the token appended. """ logger.debug(str((prefix, pkgs, channels))) # TODO: Fix temporal hack if not pkgs or not isinstance(pkgs, (list, tuple, str)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--yes', '--json', '--force-pscheck'] if name: cmd_list.extend(['--name', name]) elif prefix: cmd_list.extend(['--prefix', prefix]) else: # Just install into the current environment, whatever that is pass # TODO: Check if correct if channels: cmd_list.extend(['--override-channels']) for channel in channels: cmd_list.extend(['--channel']) channel = self.parse_token_channel(channel, token) cmd_list.extend([channel]) # TODO: Fix temporal hack if isinstance(pkgs, (list, tuple)): cmd_list.extend(pkgs) elif isinstance(pkgs, str): cmd_list.extend(['--file', pkgs]) if not dep: cmd_list.extend(['--no-deps']) return self._call_and_parse(cmd_list) def update(self, *pkgs, **kwargs): """ Update package(s) (in an environment) by name. """ cmd_list = ['update', '--json', '--quiet', '--yes'] if not pkgs and not kwargs.get('all'): raise TypeError("Must specify at least one package to update, or " "all=True.") cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'no_deps', 'override_channels', 'no_pin', 'force', 'all', 'use_index_cache', 'use_local', 'alt_hint'))) cmd_list.extend(pkgs) return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def remove(self, name=None, prefix=None, pkgs=None, all_=False): """ Remove a package (from an environment) by name. Returns { success: bool, (this is always true), (other information) } """ logger.debug(str((prefix, pkgs))) cmd_list = ['remove', '--json', '--quiet', '--yes'] if not pkgs and not all_: raise TypeError("Must specify at least one package to remove, or " "all=True.") if name: cmd_list.extend(['--name', name]) elif prefix: cmd_list.extend(['--prefix', prefix]) else: raise TypeError('must specify either an environment name or a ' 'path for package removal') if all_: cmd_list.extend(['--all']) else: cmd_list.extend(pkgs) return self._call_and_parse(cmd_list) def remove_environment(self, name=None, path=None, **kwargs): """ Remove an environment entirely. See ``remove``. """ return self.remove(name=name, path=path, all=True, **kwargs) def clone_environment(self, clone, name=None, prefix=None, **kwargs): """ Clone the environment `clone` into `name` or `prefix`. """ cmd_list = ['create', '--json', '--quiet'] if (name and prefix) or not (name or prefix): raise TypeError("conda clone_environment: exactly one of `name` " "or `path` required") if name: cmd_list.extend(['--name', name]) if prefix: cmd_list.extend(['--prefix', prefix]) cmd_list.extend(['--clone', clone]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'unknown', 'use_index_cache', 'use_local', 'no_pin', 'force', 'all', 'channel', 'override_channels', 'no_default_packages'))) return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) # FIXME: def process(self, name=None, prefix=None, cmd=None): """ Create a Popen process for cmd using the specified args but in the conda environment specified by name or prefix. The returned object will need to be invoked with p.communicate() or similar. """ if bool(name) == bool(prefix): raise TypeError('exactly one of name or prefix must be specified') if not cmd: raise TypeError('cmd to execute must be specified') if not args: args = [] if name: prefix = self.get_prefix_envname(name) conda_env = dict(os.environ) sep = os.pathsep if sys.platform == 'win32': conda_env['PATH'] = join(prefix, 'Scripts') + sep + conda_env['PATH'] else: # Unix conda_env['PATH'] = join(prefix, 'bin') + sep + conda_env['PATH'] conda_env['PATH'] = prefix + os.pathsep + conda_env['PATH'] cmd_list = [cmd] cmd_list.extend(args) # = self.subprocess.process(cmd_list, env=conda_env, stdin=stdin, # stdout=stdout, stderr=stderr) def _setup_config_from_kwargs(self, kwargs): cmd_list = ['--json', '--force'] if 'file' in kwargs: cmd_list.extend(['--file', kwargs['file']]) if 'system' in kwargs: cmd_list.append('--system') return cmd_list def config_path(self, **kwargs): """ Get the path to the config file. """ cmd_list = ['config', '--get'] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o['rc_path']) def config_get(self, *keys, **kwargs): """ Get the values of configuration keys. Returns a dictionary of values. Note, the key may not be in the dictionary if the key wasn't set in the configuration file. """ cmd_list = ['config', '--get'] cmd_list.extend(keys) cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o['get']) def config_set(self, key, value, **kwargs): """ Set a key to a (bool) value. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--set', key, str(value)] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse( cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o.get('warnings', [])) def config_add(self, key, value, **kwargs): """ Add a value to a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--add', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse( cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o.get('warnings', [])) def config_remove(self, key, value, **kwargs): """ Remove a value from a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse( cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o.get('warnings', [])) def config_delete(self, key, **kwargs): """ Remove a key entirely. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove-key', key] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse( cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o.get('warnings', [])) def run(self, command, abspath=True): """ Launch the specified app by name or full package name. Returns a dictionary containing the key "fn", whose value is the full package (ending in ``.tar.bz2``) of the app. """ cmd_list = ['run', '--json', command] return self._call_and_parse(cmd_list, abspath=abspath) # --- Additional methods # ----------------------------------------------------------------------------- def dependencies(self, name=None, prefix=None, pkgs=None, channels=None, dep=True): """ Get dependenciy list for packages to be installed into an environment defined either by 'name' or 'prefix'. """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--dry-run', '--json', '--force-pscheck'] if not dep: cmd_list.extend(['--no-deps']) if name: cmd_list.extend(['--name', name]) elif prefix: cmd_list.extend(['--prefix', prefix]) else: pass cmd_list.extend(pkgs) # TODO: Check if correct if channels: cmd_list.extend(['--override-channels']) for channel in channels: cmd_list.extend(['--channel']) cmd_list.extend([channel]) return self._call_and_parse(cmd_list) def environment_exists(self, name=None, prefix=None, abspath=True): """ Check if an environment exists by 'name' or by 'prefix'. If query is by 'name' only the default conda environments directory is searched. """ logger.debug(str((name, prefix))) if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") if name: prefix = self.get_prefix_envname(name) if prefix is None: prefix = self.ROOT_PREFIX return os.path.isdir(os.path.join(prefix, 'conda-meta')) def clear_lock(self, abspath=True): """ Clean any conda lock in the system. """ cmd_list = ['clean', '--lock', '--json'] return self._call_and_parse(cmd_list, abspath=abspath) def package_version(self, prefix=None, name=None, pkg=None): """ """ package_versions = {} if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") if name: prefix = self.get_prefix_envname(name) if self.environment_exists(prefix=prefix): for package in self.linked(prefix): if pkg in package: n, v, b = self.split_canonical_name(package) package_versions[n] = v return package_versions.get(pkg, None) def get_platform(self): """ Get platform of current system (system and bitness). """ _sys_map = { 'linux2': 'linux', 'linux': 'linux', 'darwin': 'osx', 'win32': 'win', 'openbsd5': 'openbsd' } non_x86_linux_machines = {'armv6l', 'armv7l', 'ppc64le'} sys_platform = _sys_map.get(sys.platform, 'unknown') bits = 8 * tuple.__itemsize__ if (sys_platform == 'linux' and platform.machine() in non_x86_linux_machines): arch_name = platform.machine() subdir = 'linux-{0}'.format(arch_name) else: arch_name = {64: 'x86_64', 32: 'x86'}[bits] subdir = '{0}-{1}'.format(sys_platform, bits) return subdir def get_condarc_channels(self): """ Returns all the channel urls defined in .condarc using the defined `channel_alias`. If no condarc file is found, use the default channels. """ # First get the location of condarc file and parse it to get # the channel alias and the channels. default_channel_alias = 'https://conda.anaconda.org' default_urls = [ 'https://repo.continuum.io/pkgs/free', 'https://repo.continuum.io/pkgs/pro' ] condarc_path = os.path.abspath(os.path.expanduser('~/.condarc')) channels = default_urls[:] if not os.path.isfile(condarc_path): condarc = None channel_alias = default_channel_alias else: with open(condarc_path, 'r') as f: data = f.read() condarc = yaml.load(data) channels += condarc.get('channels', []) channel_alias = condarc.get('channel_alias', default_channel_alias) if channel_alias[-1] == '/': template = "{0}{1}" else: template = "{0}/{1}" if 'defaults' in channels: channels.remove('defaults') channel_urls = [] for channel in channels: if not channel.startswith('http'): channel_url = template.format(channel_alias, channel) else: channel_url = channel channel_urls.append(channel_url) return channel_urls # --- Pip commands # ------------------------------------------------------------------------- def _call_pip(self, name=None, prefix=None, extra_args=None, callback=None): """ """ cmd_list = self._pip_cmd(name=name, prefix=prefix) cmd_list.extend(extra_args) process_worker = ProcessWorker(cmd_list, pip=True, callback=callback) process_worker.sig_finished.connect(self._start) self._queue.append(process_worker) self._start() return process_worker def _pip_cmd(self, name=None, prefix=None): """ Get pip location based on environment `name` or `prefix`. """ if (name and prefix) or not (name or prefix): raise TypeError("conda pip: exactly one of 'name' " "or 'prefix' " "required.") if name and self.environment_exists(name=name): prefix = self.get_prefix_envname(name) if sys.platform == 'win32': python = join(prefix, 'python.exe') # FIXME: pip = join(prefix, 'pip.exe') # FIXME: else: python = join(prefix, 'bin/python') pip = join(prefix, 'bin/pip') cmd_list = [python, pip] return cmd_list def pip_list(self, name=None, prefix=None, abspath=True): """ Get list of pip installed packages. """ if (name and prefix) or not (name or prefix): raise TypeError("conda pip: exactly one of 'name' " "or 'prefix' " "required.") if name: prefix = self.get_prefix_envname(name) pip_command = os.sep.join([prefix, 'bin', 'python']) cmd_list = [pip_command, PIP_LIST_SCRIPT] process_worker = ProcessWorker(cmd_list, pip=True, parse=True, callback=self._pip_list, extra_kwargs={'prefix': prefix}) process_worker.sig_finished.connect(self._start) self._queue.append(process_worker) self._start() return process_worker # if name: # cmd_list = ['list', '--name', name] # if prefix: # cmd_list = ['list', '--prefix', prefix] # return self._call_conda(cmd_list, abspath=abspath, # callback=self._pip_list) def _pip_list(self, stdout, stderr, prefix=None): """ """ result = stdout # A dict linked = self.linked(prefix) pip_only = [] linked_names = [self.split_canonical_name(l)[0] for l in linked] for pkg in result: name = self.split_canonical_name(pkg)[0] if name not in linked_names: pip_only.append(pkg) # FIXME: NEED A MORE ROBUST WAY! # if '<pip>' in line and '#' not in line: # temp = line.split()[:-1] + ['pip'] # temp = '-'.join(temp) # if '-(' in temp: # start = temp.find('-(') # end = temp.find(')') # substring = temp[start:end+1] # temp = temp.replace(substring, '') # result.append(temp) return pip_only def pip_remove(self, name=None, prefix=None, pkgs=None): """ Remove a pip package in given environment by `name` or `prefix`. """ logger.debug(str((prefix, pkgs))) if isinstance(pkgs, list) or isinstance(pkgs, tuple): pkg = ' '.join(pkgs) else: pkg = pkgs extra_args = ['uninstall', '--yes', pkg] return self._call_pip(name=name, prefix=prefix, extra_args=extra_args) def pip_search(self, search_string=None): """ Search for pip installable python packages in PyPI matching `search_string`. """ extra_args = ['search', search_string] return self._call_pip(name='root', extra_args=extra_args, callback=self._pip_search) # if stderr: # raise PipError(stderr) # You are using pip version 7.1.2, however version 8.0.2 is available. # You should consider upgrading via the 'pip install --upgrade pip' # command. def _pip_search(self, stdout, stderr): result = {} lines = to_text_string(stdout).split('\n') while '' in lines: lines.remove('') for line in lines: if ' - ' in line: parts = line.split(' - ') name = parts[0].strip() description = parts[1].strip() result[name] = description return result
def main(): """Execute QDarkStyle example.""" parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('--qt_from', default='qtpy', type=str, choices=['pyqt5', 'pyqt', 'pyside2', 'pyside', 'qtpy', 'pyqtgraph', 'qt.py'], help="Choose which binding and/or abstraction is to be used to run the example.") parser.add_argument('--no_dark', action='store_true', help="Exihibts the original window (without qdarkstyle).") parser.add_argument('--test', action='store_true', help="Auto close window after 2s.") parser.add_argument('--reset', action='store_true', help="Reset GUI settings (position, size) then opens.") parser.add_argument('--screenshots', action='store_true', help="Generate screenshots on images folder.") # Parsing arguments from command line args = parser.parse_args() # To avoid problems when testing without screen if args.test or args.screenshots: os.environ['QT_QPA_PLATFORM'] = 'offscreen' # Set QT_API variable before importing QtPy if args.qt_from in ['pyqt', 'pyqt5', 'pyside', 'pyside2']: os.environ['QT_API'] = args.qt_from elif args.qt_from == 'pyqtgraph': os.environ['QT_API'] = os.environ['PYQTGRAPH_QT_LIB'] elif args.qt_from in ['qt.py', 'qt']: try: import Qt except ImportError: print('Could not import Qt (Qt.Py)') else: os.environ['QT_API'] = Qt.__binding__ # QtPy imports from qtpy import API_NAME, QT_VERSION, PYQT_VERSION, PYSIDE_VERSION from qtpy import __version__ as QTPY_VERSION from qtpy.QtWidgets import (QApplication, QMainWindow, QDockWidget, QStatusBar, QLabel, QPushButton, QMenu) from qtpy.QtCore import QTimer, Qt, QSettings # Set API_VERSION variable API_VERSION = '' if PYQT_VERSION: API_VERSION = PYQT_VERSION elif PYSIDE_VERSION: API_VERSION = PYSIDE_VERSION else: API_VERSION = 'Not found' # Import examples UI from mw_menus_ui import Ui_MainWindow as ui_main from dw_buttons_ui import Ui_DockWidget as ui_buttons from dw_displays_ui import Ui_DockWidget as ui_displays from dw_inputs_fields_ui import Ui_DockWidget as ui_inputs_fields from dw_inputs_no_fields_ui import Ui_DockWidget as ui_inputs_no_fields from dw_widgets_ui import Ui_DockWidget as ui_widgets from dw_views_ui import Ui_DockWidget as ui_views from dw_containers_tabs_ui import Ui_DockWidget as ui_containers_tabs from dw_containers_no_tabs_ui import Ui_DockWidget as ui_containers_no_tabs # create the application app = QApplication(sys.argv) app.setOrganizationName('QDarkStyle') app.setApplicationName('QDarkStyle Example') style = '' if not args.no_dark: style = qdarkstyle.load_stylesheet() app.setStyleSheet(style) # create main window window = QMainWindow() window.setObjectName('mainwindow') ui = ui_main() ui.setupUi(window) title = ("QDarkStyle Example - " + "(QDarkStyle=v" + qdarkstyle.__version__ + ", QtPy=v" + QTPY_VERSION + ", " + API_NAME + "=v" + API_VERSION + ", Qt=v" + QT_VERSION + ", Python=v" + platform.python_version() + ")") _logger.info(title) window.setWindowTitle(title) # Create docks for buttons dw_buttons = QDockWidget() dw_buttons.setObjectName('buttons') ui_buttons = ui_buttons() ui_buttons.setupUi(dw_buttons) window.addDockWidget(Qt.RightDockWidgetArea, dw_buttons) # Add actions on popup toolbuttons menu = QMenu() for action in ['Action A', 'Action B', 'Action C']: menu.addAction(action) ui_buttons.toolButtonDelayedPopup.setMenu(menu) ui_buttons.toolButtonInstantPopup.setMenu(menu) ui_buttons.toolButtonMenuButtonPopup.setMenu(menu) # Create docks for buttons dw_displays = QDockWidget() dw_displays.setObjectName('displays') ui_displays = ui_displays() ui_displays.setupUi(dw_displays) window.addDockWidget(Qt.RightDockWidgetArea, dw_displays) # Create docks for inputs - no fields dw_inputs_no_fields = QDockWidget() dw_inputs_no_fields.setObjectName('inputs_no_fields') ui_inputs_no_fields = ui_inputs_no_fields() ui_inputs_no_fields.setupUi(dw_inputs_no_fields) window.addDockWidget(Qt.RightDockWidgetArea, dw_inputs_no_fields) # Create docks for inputs - fields dw_inputs_fields = QDockWidget() dw_inputs_fields.setObjectName('inputs_fields') ui_inputs_fields = ui_inputs_fields() ui_inputs_fields.setupUi(dw_inputs_fields) window.addDockWidget(Qt.RightDockWidgetArea, dw_inputs_fields) # Create docks for widgets dw_widgets = QDockWidget() dw_widgets.setObjectName('widgets') ui_widgets = ui_widgets() ui_widgets.setupUi(dw_widgets) window.addDockWidget(Qt.LeftDockWidgetArea, dw_widgets) # Create docks for views dw_views = QDockWidget() dw_views.setObjectName('views') ui_views = ui_views() ui_views.setupUi(dw_views) window.addDockWidget(Qt.LeftDockWidgetArea, dw_views) # Create docks for containers - no tabs dw_containers_no_tabs = QDockWidget() dw_containers_no_tabs.setObjectName('containers_no_tabs') ui_containers_no_tabs = ui_containers_no_tabs() ui_containers_no_tabs.setupUi(dw_containers_no_tabs) window.addDockWidget(Qt.LeftDockWidgetArea, dw_containers_no_tabs) # Create docks for containters - tabs dw_containers_tabs = QDockWidget() dw_containers_tabs.setObjectName('containers_tabs') ui_containers_tabs = ui_containers_tabs() ui_containers_tabs.setupUi(dw_containers_tabs) window.addDockWidget(Qt.LeftDockWidgetArea, dw_containers_tabs) # Tabify right docks window.tabifyDockWidget(dw_buttons, dw_displays) window.tabifyDockWidget(dw_displays, dw_inputs_fields) window.tabifyDockWidget(dw_inputs_fields, dw_inputs_no_fields) # Tabify left docks window.tabifyDockWidget(dw_containers_no_tabs, dw_containers_tabs) window.tabifyDockWidget(dw_containers_tabs, dw_widgets) window.tabifyDockWidget(dw_widgets, dw_views) # Issues #9120, #9121 on Spyder qstatusbar = QStatusBar() qstatusbar.addWidget(QLabel('Issue Spyder #9120, #9121 - background not matching.')) qstatusbar.addWidget(QPushButton('OK')) # Add info also in status bar for screenshots get it qstatusbar.addWidget(QLabel('INFO: ' + title)) window.setStatusBar(qstatusbar) # Todo: add report info and other info in HELP graphical # Auto quit after 2s when in test mode if args.test: QTimer.singleShot(2000, app.exit) # Save screenshots for different displays and quit if args.screenshots: window.showFullScreen() create_screenshots(app, window, args.no_dark) # Do not read settings when taking screenshots - like reset else: _read_settings(window, args.reset, QSettings) window.showMaximized() app.exec_() _write_settings(window, QSettings)
class FindReplace(QWidget): """Find widget""" STYLE = {False: "background-color:rgb(255, 175, 90);", True: "", None: ""} visibility_changed = Signal(bool) def __init__(self, parent, enable_replace=False): QWidget.__init__(self, parent) self.enable_replace = enable_replace self.editor = None self.is_code_editor = None glayout = QGridLayout() glayout.setContentsMargins(0, 0, 0, 0) self.setLayout(glayout) self.close_button = create_toolbutton( self, triggered=self.hide, icon=ima.icon('DialogCloseButton')) glayout.addWidget(self.close_button, 0, 0) # Find layout self.search_text = PatternComboBox(self, tip=_("Search string"), adjust_to_minimum=False) self.search_text.valid.connect(lambda state: self.find( changed=False, forward=True, rehighlight=False)) self.search_text.lineEdit().textEdited.connect( self.text_has_been_edited) self.previous_button = create_toolbutton(self, triggered=self.find_previous, icon=ima.icon('ArrowUp')) self.next_button = create_toolbutton(self, triggered=self.find_next, icon=ima.icon('ArrowDown')) self.next_button.clicked.connect(self.update_search_combo) self.previous_button.clicked.connect(self.update_search_combo) self.re_button = create_toolbutton(self, icon=ima.icon('advanced'), tip=_("Regular expression")) self.re_button.setCheckable(True) self.re_button.toggled.connect(lambda state: self.find()) self.case_button = create_toolbutton(self, icon=get_icon("upper_lower.png"), tip=_("Case Sensitive")) self.case_button.setCheckable(True) self.case_button.toggled.connect(lambda state: self.find()) self.words_button = create_toolbutton(self, icon=get_icon("whole_words.png"), tip=_("Whole words")) self.words_button.setCheckable(True) self.words_button.toggled.connect(lambda state: self.find()) self.highlight_button = create_toolbutton( self, icon=get_icon("highlight.png"), tip=_("Highlight matches")) self.highlight_button.setCheckable(True) self.highlight_button.toggled.connect(self.toggle_highlighting) hlayout = QHBoxLayout() self.widgets = [ self.close_button, self.search_text, self.previous_button, self.next_button, self.re_button, self.case_button, self.words_button, self.highlight_button ] for widget in self.widgets[1:]: hlayout.addWidget(widget) glayout.addLayout(hlayout, 0, 1) # Replace layout replace_with = QLabel(_("Replace with:")) self.replace_text = PatternComboBox(self, adjust_to_minimum=False, tip=_('Replace string')) self.replace_button = create_toolbutton( self, text=_('Replace/find'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find, text_beside_icon=True) self.replace_button.clicked.connect(self.update_replace_combo) self.replace_button.clicked.connect(self.update_search_combo) self.all_check = QCheckBox(_("Replace all")) self.replace_layout = QHBoxLayout() widgets = [ replace_with, self.replace_text, self.replace_button, self.all_check ] for widget in widgets: self.replace_layout.addWidget(widget) glayout.addLayout(self.replace_layout, 1, 1) self.widgets.extend(widgets) self.replace_widgets = widgets self.hide_replace() self.search_text.setTabOrder(self.search_text, self.replace_text) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.shortcuts = self.create_shortcuts(parent) self.highlight_timer = QTimer(self) self.highlight_timer.setSingleShot(True) self.highlight_timer.setInterval(1000) self.highlight_timer.timeout.connect(self.highlight_matches) def create_shortcuts(self, parent): """Create shortcuts for this widget""" # Configurable findnext = config_shortcut(self.find_next, context='_', name='Find next', parent=parent) findprev = config_shortcut(self.find_previous, context='_', name='Find previous', parent=parent) togglefind = config_shortcut(self.show, context='_', name='Find text', parent=parent) togglereplace = config_shortcut(self.toggle_replace_widgets, context='_', name='Replace text', parent=parent) hide = config_shortcut(self.hide, context='_', name='hide find and replace', parent=self) return [findnext, findprev, togglefind, togglereplace, hide] def get_shortcut_data(self): """ Returns shortcut data, a list of tuples (shortcut, text, default) shortcut (QShortcut or QAction instance) text (string): action/shortcut description default (string): default key sequence """ return [sc.data for sc in self.shortcuts] def update_search_combo(self): self.search_text.lineEdit().returnPressed.emit() def update_replace_combo(self): self.replace_text.lineEdit().returnPressed.emit() def toggle_replace_widgets(self): if self.enable_replace: # Toggle replace widgets if self.replace_widgets[0].isVisible(): self.hide_replace() self.hide() else: self.show_replace() self.replace_text.setFocus() @Slot(bool) def toggle_highlighting(self, state): """Toggle the 'highlight all results' feature""" if self.editor is not None: if state: self.highlight_matches() else: self.clear_matches() def show(self): """Overrides Qt Method""" QWidget.show(self) self.visibility_changed.emit(True) if self.editor is not None: text = self.editor.get_selected_text() highlighted = True # If no text is highlighted for search, use whatever word is under # the cursor if not text: highlighted = False try: cursor = self.editor.textCursor() cursor.select(QTextCursor.WordUnderCursor) text = to_text_string(cursor.selectedText()) except AttributeError: # We can't do this for all widgets, e.g. WebView's pass # Now that text value is sorted out, use it for the search if text and not self.search_text.currentText() or highlighted: self.search_text.setEditText(text) self.search_text.lineEdit().selectAll() self.refresh() else: self.search_text.lineEdit().selectAll() self.search_text.setFocus() @Slot() def hide(self): """Overrides Qt Method""" for widget in self.replace_widgets: widget.hide() QWidget.hide(self) self.visibility_changed.emit(False) if self.editor is not None: self.editor.setFocus() self.clear_matches() def show_replace(self): """Show replace widgets""" self.show() for widget in self.replace_widgets: widget.show() def hide_replace(self): """Hide replace widgets""" for widget in self.replace_widgets: widget.hide() def refresh(self): """Refresh widget""" if self.isHidden(): if self.editor is not None: self.clear_matches() return state = self.editor is not None for widget in self.widgets: widget.setEnabled(state) if state: self.find() def set_editor(self, editor, refresh=True): """ Set associated editor/web page: codeeditor.base.TextEditBaseWidget browser.WebView """ self.editor = editor # Note: This is necessary to test widgets/editor.py # in Qt builds that don't have web widgets try: from qtpy.QtWebEngineWidgets import QWebEngineView except ImportError: QWebEngineView = type(None) self.words_button.setVisible(not isinstance(editor, QWebEngineView)) self.re_button.setVisible(not isinstance(editor, QWebEngineView)) from spyder.widgets.sourcecode.codeeditor import CodeEditor self.is_code_editor = isinstance(editor, CodeEditor) self.highlight_button.setVisible(self.is_code_editor) if refresh: self.refresh() if self.isHidden() and editor is not None: self.clear_matches() @Slot() def find_next(self): """Find next occurrence""" state = self.find(changed=False, forward=True, rehighlight=False) self.editor.setFocus() self.search_text.add_current_text() return state @Slot() def find_previous(self): """Find previous occurrence""" state = self.find(changed=False, forward=False, rehighlight=False) self.editor.setFocus() return state def text_has_been_edited(self, text): """Find text has been edited (this slot won't be triggered when setting the search pattern combo box text programmatically""" self.find(changed=True, forward=True, start_highlight_timer=True) def highlight_matches(self): """Highlight found results""" if self.is_code_editor and self.highlight_button.isChecked(): text = self.search_text.currentText() words = self.words_button.isChecked() regexp = self.re_button.isChecked() self.editor.highlight_found_results(text, words=words, regexp=regexp) def clear_matches(self): """Clear all highlighted matches""" if self.is_code_editor: self.editor.clear_found_results() def find(self, changed=True, forward=True, rehighlight=True, start_highlight_timer=False): """Call the find function""" text = self.search_text.currentText() if len(text) == 0: self.search_text.lineEdit().setStyleSheet("") if not self.is_code_editor: # Clears the selection for WebEngine self.editor.find_text('') return None else: case = self.case_button.isChecked() words = self.words_button.isChecked() regexp = self.re_button.isChecked() found = self.editor.find_text(text, changed, forward, case=case, words=words, regexp=regexp) self.search_text.lineEdit().setStyleSheet(self.STYLE[found]) if self.is_code_editor and found: if rehighlight or not self.editor.found_results: self.highlight_timer.stop() if start_highlight_timer: self.highlight_timer.start() else: self.highlight_matches() else: self.clear_matches() return found
def __init__(self, plugin, id_, history_filename, config_options, additional_options, interpreter_versions, connection_file=None, hostname=None, menu_actions=None, slave=False, external_kernel=False, given_name=None, options_button=None, show_elapsed_time=False, reset_warning=True, ask_before_restart=True, css_path=None): super(ClientWidget, self).__init__(plugin) SaveHistoryMixin.__init__(self, history_filename) # --- Init attrs self.plugin = plugin self.id_ = id_ self.connection_file = connection_file self.hostname = hostname self.menu_actions = menu_actions self.slave = slave self.external_kernel = external_kernel self.given_name = given_name self.show_elapsed_time = show_elapsed_time self.reset_warning = reset_warning self.ask_before_restart = ask_before_restart # --- Other attrs self.options_button = options_button self.stop_button = None self.reset_button = None self.stop_icon = ima.icon('stop') self.history = [] self.allow_rename = True self.stderr_dir = None self.is_error_shown = False if css_path is None: self.css_path = CSS_PATH else: self.css_path = css_path # --- Widgets self.shellwidget = ShellWidget( config=config_options, ipyclient=self, additional_options=additional_options, interpreter_versions=interpreter_versions, external_kernel=external_kernel, local_kernel=True) self.infowidget = plugin.infowidget self.blank_page = self._create_blank_page() self.loading_page = self._create_loading_page() # To keep a reference to the page to be displayed # in infowidget self.info_page = None self._show_loading_page() # Elapsed time self.time_label = None self.t0 = time.monotonic() self.timer = QTimer(self) self.show_time_action = create_action( self, _("Show elapsed time"), toggled=self.set_elapsed_time_visible) # --- Layout self.layout = QVBoxLayout() toolbar_buttons = self.get_toolbar_buttons() hlayout = QHBoxLayout() hlayout.addWidget(self.create_time_label()) hlayout.addStretch(0) for button in toolbar_buttons: hlayout.addWidget(button) self.layout.addLayout(hlayout) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.shellwidget) self.layout.addWidget(self.infowidget) self.setLayout(self.layout) # --- Exit function self.exit_callback = lambda: plugin.close_client(client=self) # --- Dialog manager self.dialog_manager = DialogManager() # Show timer self.update_time_label_visibility()
class QtFrameRate(QLabel): """A frame rate label with green/yellow/red LEDs. The LED values are logarithmic, so a lot of detail between 60Hz and 10Hz but then the highest LED is many seconds long. """ def __init__(self): super().__init__() self.leds = LedState() # The per-LED config and state. # The last time we were updated, either from mouse move or our # timer. We update _last_time in both cases. self._last_time: Optional[float] = None # The bitmap image we draw into. self._image = np.zeros(BITMAP_SHAPE, dtype=np.uint8) # We animate on camera movements, but then we use a timer to # animate the display. When all the LEDs go off, we stop the timer # so that we use zero CPU until another camera movement. self._timer = QTimer() self._timer.setSingleShot(False) self._timer.setInterval(33) self._timer.timeout.connect(self._on_timer) # _print_calibration() # Debugging. def _on_timer(self) -> None: """Animate the LEDs.""" now = time.time() self._draw(now) # Just animate and draw, no new peak. # Stop timer if nothing more to animation, save CPU. if self.leds.all_off(): self._timer.stop() def _draw(self, now: float) -> None: """Animate the LEDs. Parameters ---------- now : float The current time in seconds. """ self.leds.update(now) # Animates the LEDs. self._update_image(now) # Draws our internal self._image self._update_bitmap() # Writes self._image into the QLabel bitmap. # We always update _last_time whether this was from a camera move # or the timer. This is the right thing to do since in either case # it means a frame was drawn. self._last_time = now def on_camera_move(self) -> None: """Update our display to show the new framerate.""" # Only count this frame if the timer is active. This avoids # displaying a potentially super long frame since it might have # been many seconds or minutes since the last camera movement. # # Ideally we should display the draw time of even that first frame, # but there's no easy/obvious way to do that today. And this will # show everthing except one frame. first_time = self._last_time is None use_delta = self._timer.isActive() and not first_time now = time.time() if use_delta: delta_seconds = now - self._last_time if monitor: monitor.add({"frame_time": delta_seconds}) self.leds.set_peak(now, delta_seconds) self._draw(now) # Draw the whole meter. # Since there was activity, we need to start the timer so we can # animate the decay of the LEDs. The timer will be shut off when # all the LEDs go idle. self._timer.start() def _update_image(self, now: float) -> None: """Update our self._image with the latest meter display. Parameters ---------- now : float The current time in seconds. """ self._image.fill(0) # Start fresh each time. # Get colors with latest alpha values accord to decay. colors = self.leds.get_colors(now) # Draw each segment with the right color and alpha (due to decay). for index in range(NUM_SEGMENTS): x0 = int(index * SEGMENT_SPACING) x1 = int(x0 + SEGMENT_WIDTH) y0, y1 = 0, BITMAP_SHAPE[0] # The whole height of the bitmap. self._image[y0:y1, x0:x1] = colors[index] def _update_bitmap(self) -> None: """Update the bitmap with latest image data.""" height, width = BITMAP_SHAPE[:2] image = QImage(self._image, width, height, QImage.Format_RGBA8888) self.setPixmap(QPixmap.fromImage(image))
def __init__(self, parent=None, init_y_channels=[], plot_by_timestamps=True, background='default'): """ Parameters ---------- parent : Widget The parent widget of the chart. init_y_channels : list A list of scalar channels to plot vs time. plot_by_timestamps : bool If True, the x-axis shows timestamps as ticks, and those timestamps scroll to the left as time progresses. If False, the x-axis tick marks show time relative to the current time. background : str, optional The background color for the plot. Accepts any arguments that pyqtgraph.mkColor will accept. """ self._plot_by_timestamps = plot_by_timestamps self._left_axis = AxisItem("left") if plot_by_timestamps: self._bottom_axis = TimeAxisItem('bottom') else: self.starting_epoch_time = time.time() self._bottom_axis = AxisItem('bottom') super(PyDMTimePlot, self).__init__(parent=parent, background=background, axisItems={ "bottom": self._bottom_axis, "left": self._left_axis }) # Removing the downsampling while PR 763 is not merged at pyqtgraph # Reference: https://github.com/pyqtgraph/pyqtgraph/pull/763 # self.setDownsampling(ds=True, auto=True, mode="mean") if self._plot_by_timestamps: self.plotItem.disableAutoRange(ViewBox.XAxis) self.getViewBox().setMouseEnabled(x=False) else: self.plotItem.setRange(xRange=[DEFAULT_X_MIN, 0], padding=0) self.plotItem.setLimits(xMax=0) self._bufferSize = DEFAULT_BUFFER_SIZE self._time_span = DEFAULT_TIME_SPAN # This is in seconds self._update_interval = DEFAULT_UPDATE_INTERVAL self.update_timer = QTimer(self) self.update_timer.setInterval(self._update_interval) self._update_mode = PyDMTimePlot.SynchronousMode self._needs_redraw = True self.labels = {"left": None, "right": None, "bottom": None} self.units = {"left": None, "right": None, "bottom": None} for channel in init_y_channels: self.addYChannel(channel)
class QtPerformance(QWidget): """Dockable widget to show performance info. Notes ----- 1) The progress bar doesn't show "progress", we use it as a bar graph to show the average duration of recent "UpdateRequest" events. This is actually not the total draw time, but it's generally the biggest part of each frame. 2) We log any event whose duration is longer than the threshold. 3) We show uptime so you can tell if this window is being updated at all. Attributes ---------- start_time : float Time is seconds when widget was created. bar : QProgressBar The progress bar we use as your draw time indicator. thresh_ms : float Log events whose duration is longer then this. timer_label : QLabel We write the current "uptime" into this label. timer : QTimer To update our window every UPDATE_MS. """ # We log events slower than some threshold (in milliseconds). THRESH_DEFAULT = 100 THRESH_OPTIONS = [ "1", "5", "10", "15", "20", "30", "40", "50", "100", "200", ] # Update at 250ms / 4Hz for now. The more we update more alive our # display will look, but the more we will slow things down. UPDATE_MS = 250 def __init__(self): """Create our windgets.""" super().__init__() layout = QVBoxLayout() # For our "uptime" timer. self.start_time = time.time() # Label for our progress bar. bar_label = QLabel(trans._("Draw Time:")) layout.addWidget(bar_label) # Progress bar is not used for "progress", it's just a bar graph to show # the "draw time", the duration of the "UpdateRequest" event. bar = QProgressBar() bar.setRange(0, 100) bar.setValue(50) bar.setFormat("%vms") layout.addWidget(bar) self.bar = bar # We let the user set the "slow event" threshold. self.thresh_ms = self.THRESH_DEFAULT self.thresh_combo = QComboBox() self.thresh_combo.addItems(self.THRESH_OPTIONS) self.thresh_combo.activated[str].connect(self._change_thresh) self.thresh_combo.setCurrentText(str(self.thresh_ms)) combo_layout = QHBoxLayout() combo_layout.addWidget(QLabel(trans._("Show Events Slower Than:"))) combo_layout.addWidget(self.thresh_combo) combo_layout.addWidget(QLabel(trans._("milliseconds"))) combo_layout.addItem( QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) ) layout.addLayout(combo_layout) # We log slow events to this window. self.log = TextLog() layout.addWidget(self.log) # Uptime label. To indicate if the widget is getting updated. label = QLabel('') layout.addWidget(label) self.timer_label = label self.setLayout(layout) # Update us with a timer. self.timer = QTimer(self) self.timer.timeout.connect(self.update) self.timer.setInterval(self.UPDATE_MS) self.timer.start() def _change_thresh(self, text): """Threshold combo box change.""" self.thresh_ms = float(text) self.log.clear() # start fresh with this new threshold def _get_timer_info(self): """Get the information from the timers that we want to display.""" average = None long_events = [] # We don't update any GUI/widgets while iterating over the timers. # Updating widgets can create immediate Qt Events which would modify the # timers out from under us! for name, timer in perf.timers.timers.items(): # The Qt Event "UpdateRequest" is the main "draw" event, so # that's what we use for our progress bar. if name == "UpdateRequest": average = timer.average # Log any "long" events to the text window. if timer.max >= self.thresh_ms: long_events.append((name, timer.max)) return average, long_events def update(self): """Update our label and progress bar and log any new slow events.""" # Update our timer label. elapsed = time.time() - self.start_time self.timer_label.setText( trans._("Uptime: {elapsed:.2f}", elapsed=elapsed) ) average, long_events = self._get_timer_info() # Now safe to update the GUI: progress bar first. if average is not None: self.bar.setValue(int(average)) # And log any new slow events. for name, time_ms in long_events: self.log.append(name, time_ms) # Clear all the timers since we've displayed them. They will immediately # start accumulating numbers for the next update. perf.timers.clear()
class FindReplace(QWidget): """Find widget""" STYLE = { False: "background-color:rgb(255, 175, 90);", True: "", None: "", 'regexp_error': "background-color:rgb(255, 80, 80);", } TOOLTIP = { False: _("No matches"), True: _("Search string"), None: _("Search string"), 'regexp_error': _("Regular expression error") } visibility_changed = Signal(bool) return_shift_pressed = Signal() return_pressed = Signal() def __init__(self, parent, enable_replace=False): QWidget.__init__(self, parent) self.enable_replace = enable_replace self.editor = None self.is_code_editor = None glayout = QGridLayout() glayout.setContentsMargins(0, 0, 0, 0) self.setLayout(glayout) self.close_button = create_toolbutton( self, triggered=self.hide, icon=ima.icon('DialogCloseButton')) glayout.addWidget(self.close_button, 0, 0) # Find layout self.search_text = PatternComboBox(self, tip=_("Search string"), adjust_to_minimum=False) self.return_shift_pressed.connect( lambda: self.find(changed=False, forward=False, rehighlight=False, multiline_replace_check=False)) self.return_pressed.connect( lambda: self.find(changed=False, forward=True, rehighlight=False, multiline_replace_check=False)) self.search_text.lineEdit().textEdited.connect( self.text_has_been_edited) self.number_matches_text = QLabel(self) self.replace_on = False self.replace_text_button = create_toolbutton( self, toggled=self.change_replace_state, icon=ima.icon('replace'), tip=_("Replace text")) self.previous_button = create_toolbutton(self, triggered=self.find_previous, icon=ima.icon('ArrowUp'), tip=_("Find previous")) self.next_button = create_toolbutton(self, triggered=self.find_next, icon=ima.icon('ArrowDown'), tip=_("Find next")) self.next_button.clicked.connect(self.update_search_combo) self.previous_button.clicked.connect(self.update_search_combo) self.re_button = create_toolbutton(self, icon=ima.icon('regex'), tip=_("Regular expression")) self.re_button.setCheckable(True) self.re_button.toggled.connect(lambda state: self.find()) self.case_button = create_toolbutton( self, icon=ima.icon("format_letter_case"), tip=_("Case Sensitive")) self.case_button.setCheckable(True) self.case_button.toggled.connect(lambda state: self.find()) self.words_button = create_toolbutton(self, icon=get_icon("whole_words.png"), tip=_("Whole words")) self.words_button.setCheckable(True) self.words_button.toggled.connect(lambda state: self.find()) self.highlight_button = create_toolbutton( self, icon=get_icon("highlight.png"), tip=_("Highlight matches")) self.highlight_button.setCheckable(True) self.highlight_button.toggled.connect(self.toggle_highlighting) hlayout = QHBoxLayout() self.widgets = [ self.close_button, self.search_text, self.number_matches_text, self.replace_text_button, self.previous_button, self.next_button, self.re_button, self.case_button, self.words_button, self.highlight_button ] for widget in self.widgets[1:]: hlayout.addWidget(widget) glayout.addLayout(hlayout, 0, 1) # Replace layout replace_with = QLabel(_("Replace with:")) self.replace_text = PatternComboBox(self, adjust_to_minimum=False, tip=_('Replace string')) self.replace_text.valid.connect( lambda _: self.replace_find(focus_replace_text=True)) self.replace_button = create_toolbutton( self, text=_('Find next'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find, text_beside_icon=True) self.replace_sel_button = create_toolbutton( self, text=_('In selection'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find_selection, text_beside_icon=True) self.replace_sel_button.clicked.connect(self.update_replace_combo) self.replace_sel_button.clicked.connect(self.update_search_combo) self.replace_all_button = create_toolbutton( self, text=_('All'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find_all, text_beside_icon=True) self.replace_all_button.clicked.connect(self.update_replace_combo) self.replace_all_button.clicked.connect(self.update_search_combo) self.replace_layout = QHBoxLayout() widgets = [ replace_with, self.replace_text, self.replace_button, self.replace_sel_button, self.replace_all_button ] for widget in widgets: self.replace_layout.addWidget(widget) glayout.addLayout(self.replace_layout, 1, 1) self.widgets.extend(widgets) self.replace_widgets = widgets self.hide_replace() self.search_text.setTabOrder(self.search_text, self.replace_text) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.shortcuts = self.create_shortcuts(parent) self.highlight_timer = QTimer(self) self.highlight_timer.setSingleShot(True) self.highlight_timer.setInterval(1000) self.highlight_timer.timeout.connect(self.highlight_matches) self.search_text.installEventFilter(self) def eventFilter(self, widget, event): """Event filter for search_text widget. Emits signals when presing Enter and Shift+Enter. This signals are used for search forward and backward. Also, a crude hack to get tab working in the Find/Replace boxes. """ if event.type() == QEvent.KeyPress: key = event.key() shift = event.modifiers() & Qt.ShiftModifier if key == Qt.Key_Return: if shift: self.return_shift_pressed.emit() else: self.return_pressed.emit() if key == Qt.Key_Tab: if self.search_text.hasFocus(): self.replace_text.set_current_text( self.search_text.currentText()) self.focusNextChild() return super(FindReplace, self).eventFilter(widget, event) def create_shortcuts(self, parent): """Create shortcuts for this widget""" # Configurable findnext = CONF.config_shortcut(self.find_next, context='find_replace', name='Find next', parent=parent) findprev = CONF.config_shortcut(self.find_previous, context='find_replace', name='Find previous', parent=parent) togglefind = CONF.config_shortcut(self.show, context='find_replace', name='Find text', parent=parent) togglereplace = CONF.config_shortcut(self.show_replace, context='find_replace', name='Replace text', parent=parent) hide = CONF.config_shortcut(self.hide, context='find_replace', name='hide find and replace', parent=self) return [findnext, findprev, togglefind, togglereplace, hide] def get_shortcut_data(self): """ Returns shortcut data, a list of tuples (shortcut, text, default) shortcut (QShortcut or QAction instance) text (string): action/shortcut description default (string): default key sequence """ return [sc.data for sc in self.shortcuts] def update_search_combo(self): self.search_text.lineEdit().returnPressed.emit() def update_replace_combo(self): self.replace_text.lineEdit().returnPressed.emit() @Slot(bool) def toggle_highlighting(self, state): """Toggle the 'highlight all results' feature""" if self.editor is not None: if state: self.highlight_matches() else: self.clear_matches() def show(self, hide_replace=True): """Overrides Qt Method""" QWidget.show(self) self.visibility_changed.emit(True) self.change_number_matches() if self.editor is not None: if hide_replace: if self.replace_widgets[0].isVisible(): self.hide_replace() text = self.editor.get_selected_text() # When selecting several lines, and replace box is activated the # text won't be replaced for the selection if hide_replace or len(text.splitlines()) <= 1: highlighted = True # If no text is highlighted for search, use whatever word is # under the cursor if not text: highlighted = False try: cursor = self.editor.textCursor() cursor.select(QTextCursor.WordUnderCursor) text = to_text_string(cursor.selectedText()) except AttributeError: # We can't do this for all widgets, e.g. WebView's pass # Now that text value is sorted out, use it for the search if text and not self.search_text.currentText() or highlighted: self.search_text.setEditText(text) self.search_text.lineEdit().selectAll() self.refresh() else: self.search_text.lineEdit().selectAll() self.search_text.setFocus() @Slot() def replace_widget(self, replace_on): """Show and hide replace widget""" if replace_on: self.show_replace() else: self.hide_replace() def change_replace_state(self): """Handle the change of the replace state widget.""" self.replace_on = not self.replace_on self.replace_text_button.setChecked(self.replace_on) self.replace_widget(self.replace_on) def hide(self): """Overrides Qt Method""" for widget in self.replace_widgets: widget.hide() QWidget.hide(self) self.visibility_changed.emit(False) if self.editor is not None: self.editor.setFocus() self.clear_matches() def show_replace(self): """Show replace widgets""" if self.enable_replace: self.show(hide_replace=False) for widget in self.replace_widgets: widget.show() def hide_replace(self): """Hide replace widgets""" for widget in self.replace_widgets: widget.hide() def refresh(self): """Refresh widget""" if self.isHidden(): if self.editor is not None: self.clear_matches() return state = self.editor is not None for widget in self.widgets: widget.setEnabled(state) if state: self.find() def set_editor(self, editor, refresh=True): """ Set associated editor/web page: codeeditor.base.TextEditBaseWidget browser.WebView """ self.editor = editor # Note: This is necessary to test widgets/editor.py # in Qt builds that don't have web widgets try: from qtpy.QtWebEngineWidgets import QWebEngineView except ImportError: QWebEngineView = type(None) self.words_button.setVisible(not isinstance(editor, QWebEngineView)) self.re_button.setVisible(not isinstance(editor, QWebEngineView)) from spyder.plugins.editor.widgets.codeeditor import CodeEditor self.is_code_editor = isinstance(editor, CodeEditor) self.highlight_button.setVisible(self.is_code_editor) if refresh: self.refresh() if self.isHidden() and editor is not None: self.clear_matches() @Slot() def find_next(self, set_focus=True): """Find next occurrence""" state = self.find(changed=False, forward=True, rehighlight=False, multiline_replace_check=False) if set_focus: self.editor.setFocus() self.search_text.add_current_text() return state @Slot() def find_previous(self, set_focus=True): """Find previous occurrence""" state = self.find(changed=False, forward=False, rehighlight=False, multiline_replace_check=False) if set_focus: self.editor.setFocus() return state def text_has_been_edited(self, text): """Find text has been edited (this slot won't be triggered when setting the search pattern combo box text programmatically)""" self.find(changed=True, forward=True, start_highlight_timer=True) def highlight_matches(self): """Highlight found results""" if self.is_code_editor and self.highlight_button.isChecked(): text = self.search_text.currentText() case = self.case_button.isChecked() word = self.words_button.isChecked() regexp = self.re_button.isChecked() self.editor.highlight_found_results(text, word=word, regexp=regexp, case=case) def clear_matches(self): """Clear all highlighted matches""" if self.is_code_editor: self.editor.clear_found_results() def find(self, changed=True, forward=True, rehighlight=True, start_highlight_timer=False, multiline_replace_check=True): """Call the find function""" # When several lines are selected in the editor and replace box is # activated, dynamic search is deactivated to prevent changing the # selection. Otherwise we show matching items. if multiline_replace_check and self.replace_widgets[0].isVisible(): sel_text = self.editor.get_selected_text() if len(to_text_string(sel_text).splitlines()) > 1: return None text = self.search_text.currentText() if len(text) == 0: self.search_text.lineEdit().setStyleSheet("") if not self.is_code_editor: # Clears the selection for WebEngine self.editor.find_text('') self.change_number_matches() return None else: case = self.case_button.isChecked() word = self.words_button.isChecked() regexp = self.re_button.isChecked() found = self.editor.find_text(text, changed, forward, case=case, word=word, regexp=regexp) stylesheet = self.STYLE[found] tooltip = self.TOOLTIP[found] if not found and regexp: error_msg = regexp_error_msg(text) if error_msg: # special styling for regexp errors stylesheet = self.STYLE['regexp_error'] tooltip = self.TOOLTIP['regexp_error'] + ': ' + error_msg self.search_text.lineEdit().setStyleSheet(stylesheet) self.search_text.setToolTip(tooltip) if self.is_code_editor and found: cursor = QTextCursor(self.editor.textCursor()) TextHelper(self.editor).unfold_if_colapsed(cursor) if rehighlight or not self.editor.found_results: self.highlight_timer.stop() if start_highlight_timer: self.highlight_timer.start() else: self.highlight_matches() else: self.clear_matches() number_matches = self.editor.get_number_matches(text, case=case, regexp=regexp, word=word) if hasattr(self.editor, 'get_match_number'): match_number = self.editor.get_match_number(text, case=case, regexp=regexp, word=word) else: match_number = 0 self.change_number_matches(current_match=match_number, total_matches=number_matches) return found
def __init__(self, config_file, run_model, simulation_arguments, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setModal(True) self.setWindowModality(Qt.WindowModal) self.setWindowTitle("Simulations - {}".format(config_file)) self._snapshot_model = SnapshotModel(self) self._run_model = run_model self._isDetailedDialog = False self._minimum_width = 1200 ert = None if isinstance(run_model, BaseRunModel): ert = run_model.ert() self._simulations_argments = simulation_arguments self._ticker = QTimer(self) self._ticker.timeout.connect(self._on_ticker) progress_proxy_model = ProgressProxyModel(self._snapshot_model, parent=self) self._total_progress_label = QLabel(_TOTAL_PROGRESS_TEMPLATE.format(0), self) self._total_progress_bar = QProgressBar(self) self._total_progress_bar.setRange(0, 100) self._total_progress_bar.setTextVisible(False) self._iteration_progress_label = QLabel(self) self._progress_view = ProgressView(self) self._progress_view.setModel(progress_proxy_model) self._progress_view.setIndeterminate(True) legend_view = LegendView(self) legend_view.setModel(progress_proxy_model) self._tab_widget = QTabWidget(self) self._snapshot_model.rowsInserted.connect(self.on_new_iteration) self._job_label = QLabel(self) self._job_model = JobListProxyModel(self, 0, 0, 0, 0) self._job_model.setSourceModel(self._snapshot_model) self._job_view = QTableView(self) self._job_view.clicked.connect(self._job_clicked) self._open_files = {} self._job_view.setModel(self._job_model) self.running_time = QLabel("") self.plot_tool = PlotTool(config_file) self.plot_tool.setParent(self) self.plot_button = QPushButton(self.plot_tool.getName()) self.plot_button.clicked.connect(self.plot_tool.trigger) self.plot_button.setEnabled(ert is not None) self.kill_button = QPushButton("Kill Simulations") self.done_button = QPushButton("Done") self.done_button.setHidden(True) self.restart_button = QPushButton("Restart") self.restart_button.setHidden(True) self.show_details_button = QPushButton("Show Details") self.show_details_button.setCheckable(True) size = 20 spin_movie = resourceMovie("ide/loading.gif") spin_movie.setSpeed(60) spin_movie.setScaledSize(QSize(size, size)) spin_movie.start() self.processing_animation = QLabel() self.processing_animation.setMaximumSize(QSize(size, size)) self.processing_animation.setMinimumSize(QSize(size, size)) self.processing_animation.setMovie(spin_movie) button_layout = QHBoxLayout() button_layout.addWidget(self.processing_animation) button_layout.addWidget(self.running_time) button_layout.addStretch() button_layout.addWidget(self.show_details_button) button_layout.addWidget(self.plot_button) button_layout.addWidget(self.kill_button) button_layout.addWidget(self.done_button) button_layout.addWidget(self.restart_button) button_widget_container = QWidget() button_widget_container.setLayout(button_layout) layout = QVBoxLayout() layout.addWidget(self._total_progress_label) layout.addWidget(self._total_progress_bar) layout.addWidget(self._iteration_progress_label) layout.addWidget(self._progress_view) layout.addWidget(legend_view) layout.addWidget(self._tab_widget) layout.addWidget(self._job_label) layout.addWidget(self._job_view) layout.addWidget(button_widget_container) self.setLayout(layout) self.kill_button.clicked.connect(self.killJobs) self.done_button.clicked.connect(self.accept) self.restart_button.clicked.connect(self.restart_failed_realizations) self.show_details_button.clicked.connect(self.toggle_detailed_progress) self.simulation_done.connect(self._on_simulation_done) self.setMinimumWidth(self._minimum_width) self._setSimpleDialog()
def __init__(self, parent, enable_replace=False): QWidget.__init__(self, parent) self.enable_replace = enable_replace self.editor = None self.is_code_editor = None glayout = QGridLayout() glayout.setContentsMargins(0, 0, 0, 0) self.setLayout(glayout) self.close_button = create_toolbutton( self, triggered=self.hide, icon=ima.icon('DialogCloseButton')) glayout.addWidget(self.close_button, 0, 0) # Find layout self.search_text = PatternComboBox(self, tip=_("Search string"), adjust_to_minimum=False) self.search_text.valid.connect(lambda state: self.find( changed=False, forward=True, rehighlight=False)) self.search_text.lineEdit().textEdited.connect( self.text_has_been_edited) self.previous_button = create_toolbutton(self, triggered=self.find_previous, icon=ima.icon('ArrowUp')) self.next_button = create_toolbutton(self, triggered=self.find_next, icon=ima.icon('ArrowDown')) self.next_button.clicked.connect(self.update_search_combo) self.previous_button.clicked.connect(self.update_search_combo) self.re_button = create_toolbutton(self, icon=ima.icon('advanced'), tip=_("Regular expression")) self.re_button.setCheckable(True) self.re_button.toggled.connect(lambda state: self.find()) self.case_button = create_toolbutton(self, icon=get_icon("upper_lower.png"), tip=_("Case Sensitive")) self.case_button.setCheckable(True) self.case_button.toggled.connect(lambda state: self.find()) self.words_button = create_toolbutton(self, icon=get_icon("whole_words.png"), tip=_("Whole words")) self.words_button.setCheckable(True) self.words_button.toggled.connect(lambda state: self.find()) self.highlight_button = create_toolbutton( self, icon=get_icon("highlight.png"), tip=_("Highlight matches")) self.highlight_button.setCheckable(True) self.highlight_button.toggled.connect(self.toggle_highlighting) hlayout = QHBoxLayout() self.widgets = [ self.close_button, self.search_text, self.previous_button, self.next_button, self.re_button, self.case_button, self.words_button, self.highlight_button ] for widget in self.widgets[1:]: hlayout.addWidget(widget) glayout.addLayout(hlayout, 0, 1) # Replace layout replace_with = QLabel(_("Replace with:")) self.replace_text = PatternComboBox(self, adjust_to_minimum=False, tip=_('Replace string')) self.replace_button = create_toolbutton( self, text=_('Replace/find'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find, text_beside_icon=True) self.replace_button.clicked.connect(self.update_replace_combo) self.replace_button.clicked.connect(self.update_search_combo) self.all_check = QCheckBox(_("Replace all")) self.replace_layout = QHBoxLayout() widgets = [ replace_with, self.replace_text, self.replace_button, self.all_check ] for widget in widgets: self.replace_layout.addWidget(widget) glayout.addLayout(self.replace_layout, 1, 1) self.widgets.extend(widgets) self.replace_widgets = widgets self.hide_replace() self.search_text.setTabOrder(self.search_text, self.replace_text) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.shortcuts = self.create_shortcuts(parent) self.highlight_timer = QTimer(self) self.highlight_timer.setSingleShot(True) self.highlight_timer.setInterval(1000) self.highlight_timer.timeout.connect(self.highlight_matches)
class NapariQtNotification(QDialog): """Notification dialog frame, appears at the bottom right of the canvas. By default, only the first line of the notification is shown, and the text is elided. Double-clicking on the text (or clicking the chevron icon) will expand to show the full notification. The dialog will autmatically disappear in ``DISMISS_AFTER`` milliseconds, unless hovered or clicked. Parameters ---------- message : str The message that will appear in the notification severity : str or NotificationSeverity, optional Severity level {'error', 'warning', 'info', 'none'}. Will determine the icon associated with the message. by default NotificationSeverity.WARNING. source : str, optional A source string for the notifcation (intended to show the module and or package responsible for the notification), by default None actions : list of tuple, optional A sequence of 2-tuples, where each tuple is a string and a callable. Each tuple will be used to create button in the dialog, where the text on the button is determine by the first item in the tuple, and a callback function to call when the button is pressed is the second item in the tuple. by default () """ MAX_OPACITY = 0.9 FADE_IN_RATE = 220 FADE_OUT_RATE = 120 DISMISS_AFTER = 4000 MIN_WIDTH = 400 message: MultilineElidedLabel source_label: QLabel severity_icon: QLabel def __init__( self, message: str, severity: Union[str, NotificationSeverity] = 'WARNING', source: Optional[str] = None, actions: ActionSequence = (), ): """[summary]""" super().__init__(None) # FIXME: this does not work with multiple viewers. # we need a way to detect the viewer in which the error occured. for wdg in QApplication.topLevelWidgets(): if isinstance(wdg, QMainWindow): try: # TODO: making the canvas the parent makes it easier to # move/resize, but also means that the notification can get # clipped on the left if the canvas is too small. canvas = wdg.centralWidget().children()[1].canvas.native self.setParent(canvas) canvas.resized.connect(self.move_to_bottom_right) break except Exception: pass self.setupUi() self.setAttribute(Qt.WA_DeleteOnClose) self.setup_buttons(actions) self.setMouseTracking(True) self.severity_icon.setText(NotificationSeverity(severity).as_icon()) self.message.setText(message) if source: self.source_label.setText( trans._('Source: {source}', source=source)) self.close_button.clicked.connect(self.close) self.expand_button.clicked.connect(self.toggle_expansion) self.timer = QTimer() self.opacity = QGraphicsOpacityEffect() self.setGraphicsEffect(self.opacity) self.opacity_anim = QPropertyAnimation(self.opacity, b"opacity", self) self.geom_anim = QPropertyAnimation(self, b"geometry", self) self.move_to_bottom_right() def move_to_bottom_right(self, offset=(8, 8)): """Position widget at the bottom right edge of the parent.""" if not self.parent(): return sz = self.parent().size() - self.size() - QSize(*offset) self.move(QPoint(sz.width(), sz.height())) def slide_in(self): """Run animation that fades in the dialog with a slight slide up.""" geom = self.geometry() self.geom_anim.setDuration(self.FADE_IN_RATE) self.geom_anim.setStartValue(geom.translated(0, 20)) self.geom_anim.setEndValue(geom) self.geom_anim.setEasingCurve(QEasingCurve.OutQuad) # fade in self.opacity_anim.setDuration(self.FADE_IN_RATE) self.opacity_anim.setStartValue(0) self.opacity_anim.setEndValue(self.MAX_OPACITY) self.geom_anim.start() self.opacity_anim.start() def show(self): """Show the message with a fade and slight slide in from the bottom.""" super().show() self.slide_in() if self.DISMISS_AFTER > 0: self.timer.setInterval(self.DISMISS_AFTER) self.timer.setSingleShot(True) self.timer.timeout.connect(self.close) self.timer.start() def mouseMoveEvent(self, event): """On hover, stop the self-destruct timer""" self.timer.stop() def mouseDoubleClickEvent(self, event): """Expand the notification on double click.""" self.toggle_expansion() def close(self): """Fade out then close.""" self.opacity_anim.setDuration(self.FADE_OUT_RATE) self.opacity_anim.setStartValue(self.MAX_OPACITY) self.opacity_anim.setEndValue(0) self.opacity_anim.start() self.opacity_anim.finished.connect(super().close) def toggle_expansion(self): """Toggle the expanded state of the notification frame.""" self.contract() if self.property('expanded') else self.expand() self.timer.stop() def expand(self): """Expanded the notification so that the full message is visible.""" curr = self.geometry() self.geom_anim.setDuration(100) self.geom_anim.setStartValue(curr) new_height = self.sizeHint().height() delta = new_height - curr.height() self.geom_anim.setEndValue( QRect(curr.x(), curr.y() - delta, curr.width(), new_height)) self.geom_anim.setEasingCurve(QEasingCurve.OutQuad) self.geom_anim.start() self.setProperty('expanded', True) self.style().unpolish(self.expand_button) self.style().polish(self.expand_button) def contract(self): """Contract notification to a single elided line of the message.""" geom = self.geometry() self.geom_anim.setDuration(100) self.geom_anim.setStartValue(geom) dlt = geom.height() - self.minimumHeight() self.geom_anim.setEndValue( QRect(geom.x(), geom.y() + dlt, geom.width(), geom.height() - dlt)) self.geom_anim.setEasingCurve(QEasingCurve.OutQuad) self.geom_anim.start() self.setProperty('expanded', False) self.style().unpolish(self.expand_button) self.style().polish(self.expand_button) def setupUi(self): """Set up the UI during initialization.""" self.setWindowFlags(Qt.SubWindow) self.setMinimumWidth(self.MIN_WIDTH) self.setMaximumWidth(self.MIN_WIDTH) self.setMinimumHeight(40) self.setSizeGripEnabled(False) self.setModal(False) self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setContentsMargins(2, 2, 2, 2) self.verticalLayout.setSpacing(0) self.row1_widget = QWidget(self) self.row1 = QHBoxLayout(self.row1_widget) self.row1.setContentsMargins(12, 12, 12, 8) self.row1.setSpacing(4) self.severity_icon = QLabel(self.row1_widget) self.severity_icon.setObjectName("severity_icon") self.severity_icon.setMinimumWidth(30) self.severity_icon.setMaximumWidth(30) self.row1.addWidget(self.severity_icon, alignment=Qt.AlignTop) self.message = MultilineElidedLabel(self.row1_widget) self.message.setMinimumWidth(self.MIN_WIDTH - 200) self.message.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.row1.addWidget(self.message, alignment=Qt.AlignTop) self.expand_button = QPushButton(self.row1_widget) self.expand_button.setObjectName("expand_button") self.expand_button.setCursor(Qt.PointingHandCursor) self.expand_button.setMaximumWidth(20) self.expand_button.setFlat(True) self.row1.addWidget(self.expand_button, alignment=Qt.AlignTop) self.close_button = QPushButton(self.row1_widget) self.close_button.setObjectName("close_button") self.close_button.setCursor(Qt.PointingHandCursor) self.close_button.setMaximumWidth(20) self.close_button.setFlat(True) self.row1.addWidget(self.close_button, alignment=Qt.AlignTop) self.verticalLayout.addWidget(self.row1_widget, 1) self.row2_widget = QWidget(self) self.row2_widget.hide() self.row2 = QHBoxLayout(self.row2_widget) self.source_label = QLabel(self.row2_widget) self.source_label.setObjectName("source_label") self.row2.addWidget(self.source_label, alignment=Qt.AlignBottom) self.row2.addStretch() self.row2.setContentsMargins(12, 2, 16, 12) self.row2_widget.setMaximumHeight(34) self.row2_widget.setStyleSheet('QPushButton{' 'padding: 4px 12px 4px 12px; ' 'font-size: 11px;' 'min-height: 18px; border-radius: 0;}') self.verticalLayout.addWidget(self.row2_widget, 0) self.setProperty('expanded', False) self.resize(self.MIN_WIDTH, 40) def setup_buttons(self, actions: ActionSequence = ()): """Add buttons to the dialog. Parameters ---------- actions : tuple, optional A sequence of 2-tuples, where each tuple is a string and a callable. Each tuple will be used to create button in the dialog, where the text on the button is determine by the first item in the tuple, and a callback function to call when the button is pressed is the second item in the tuple. by default () """ if isinstance(actions, dict): actions = list(actions.items()) for text, callback in actions: btn = QPushButton(text) def call_back_with_self(callback, self): """ we need a higher order function this to capture the reference to self. """ def _inner(): return callback(self) return _inner btn.clicked.connect(call_back_with_self(callback, self)) btn.clicked.connect(self.close) self.row2.addWidget(btn) if actions: self.row2_widget.show() self.setMinimumHeight(self.row2_widget.maximumHeight() + self.minimumHeight()) def sizeHint(self): """Return the size required to show the entire message.""" return QSize( super().sizeHint().width(), self.row2_widget.height() + self.message.sizeHint().height(), ) @classmethod def from_notification(cls, notification: Notification) -> NapariQtNotification: from ...utils.notifications import ErrorNotification actions = notification.actions if isinstance(notification, ErrorNotification): def show_tb(parent): tbdialog = QDialog(parent=parent.parent()) tbdialog.setModal(True) # this is about the minimum width to not get rewrap # and the minimum height to not have scrollbar tbdialog.resize(650, 270) tbdialog.setLayout(QVBoxLayout()) text = QTextEdit() text.setHtml(notification.as_html()) text.setReadOnly(True) tbdialog.layout().addWidget(text) tbdialog.show() actions = tuple(notification.actions) + ( (trans._('View Traceback'), show_tb), ) else: actions = notification.actions return cls( message=notification.message, severity=notification.severity, source=notification.source, actions=actions, ) @classmethod def show_notification(cls, notification: Notification): from ...utils.settings import SETTINGS # after https://github.com/napari/napari/issues/2370, # the os.getenv can be removed (and NAPARI_CATCH_ERRORS retired) if (os.getenv("NAPARI_CATCH_ERRORS") not in ('0', 'False') and notification.severity >= SETTINGS.application.gui_notification_level): cls.from_notification(notification).show()