class ParamLineEdit(ParamWidget): """ QLineEdit for typed user entry control. Parameter --------- parameter : str Name of parameter this widget controls. _type : type Type to convert the text to before sending it to the function. All values are initially `QString`s and then they are converted to the specified type. If this raises a ``ValueError`` due to an improperly entered value a ``np.nan`` is returned. default : bool, optional Default text for the QLineEdit. This if automatically populated into the QLineEdit field and it is also set as the ``placeHolderText``. parent : QWidget, optional """ def __init__(self, parameter, _type, default='', parent=None): super().__init__(parameter, default=default, parent=parent) # Store type information self._type = _type # Create our LineEdit # Set our default text self.param_edit = QLineEdit(parent=self) self.param_edit.setAlignment(Qt.AlignCenter) self.layout().addWidget(self.param_edit) # Configure default text of LineEdit # If there is no default, still give some placeholder text # to indicate the type of the command needed if default != inspect._empty: self.param_edit.setText(str(self.default)) self.param_edit.setPlaceholderText(str(self.default)) elif self._type in (int, float): self.param_edit.setPlaceholderText(str(self._type(0.0))) def get_param_value(self): """ Return the current value of the QLineEdit converted to :attr:`._type`. """ # Cast the current text into our type try: val = self._type(self.param_edit.text()) # If not possible, capture the exception and report `np.nan` except ValueError: logger.exception("Could not convert text to %r", self._type.__name__) val = np.nan return val
def _create_axis_label_widget(self): """Create the axis label widget which accompanies its slider.""" label = QLineEdit(self) label.setObjectName('axis_label') # needed for _update_label label.setText(self.dims.axis_labels[self.axis]) label.home(False) label.setToolTip('Edit to change axis label') label.setAcceptDrops(False) label.setEnabled(True) label.setAlignment(Qt.AlignRight) label.setContentsMargins(0, 0, 2, 0) label.textChanged.connect(self._update_label) label.editingFinished.connect(self._clear_label_focus) self.axis_label = label
def createEditor(self, parent, option, index): """Create editor widget""" model = index.model() # TODO: dtype should be taken from the model instead (or even from the actual value?) value = model.get_value(index) if self.dtype.name == "bool": # toggle value value = not value model.setData(index, to_qvariant(value)) return elif value is not np.ma.masked: minvalue, maxvalue = self.minvalue, self.maxvalue if minvalue is not None and maxvalue is not None: msg = "value must be between %s and %s" % (minvalue, maxvalue) elif minvalue is not None: msg = "value must be >= %s" % minvalue elif maxvalue is not None: msg = "value must be <= %s" % maxvalue else: msg = None # Not using a QSpinBox for integer inputs because I could not find # a way to prevent the spinbox/editor from closing if the value is # invalid. Using the builtin minimum/maximum of the spinbox works # but that provides no message so it is less clear. editor = QLineEdit(parent) if is_number(self.dtype): validator = QDoubleValidator(editor) if is_float(self.dtype) \ else QIntValidator(editor) if minvalue is not None: validator.setBottom(minvalue) if maxvalue is not None: validator.setTop(maxvalue) editor.setValidator(validator) def on_editor_text_edited(): if not editor.hasAcceptableInput(): QToolTip.showText(editor.mapToGlobal(QPoint()), msg) else: QToolTip.hideText() if msg is not None: editor.textEdited.connect(on_editor_text_edited) editor.setFont(self.font) editor.setAlignment(Qt.AlignRight) editor.destroyed.connect(self.on_editor_destroyed) self.editor_count += 1 return editor
def createEditor(self, parent, option, index): """Create editor widget""" model = index.model() value = model.get_value(index) if model._data.dtype.name == "bool": value = not value model.setData(index, to_qvariant(value)) return elif value is not np.ma.masked: editor = QLineEdit(parent) # editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) editor.setAlignment(Qt.AlignCenter) if is_number(self.dtype): editor.setValidator(QDoubleValidator(editor)) editor.returnPressed.connect(self.commitAndCloseEditor) return editor
def createEditor(self, parent, option, index): """Create editor widget""" model = index.model() value = model.get_value(index) if model._data.dtype.name == "bool": value = not value model.setData(index, to_qvariant(value)) return elif value is not np.ma.masked: editor = QLineEdit(parent) editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) editor.setAlignment(Qt.AlignCenter) if is_number(self.dtype): editor.setValidator(QDoubleValidator(editor)) editor.returnPressed.connect(self.commitAndCloseEditor) return editor
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setHighlightSections(True) self.setStretchLastSection(True) lineEdit = QLineEdit(self.viewport()) lineEdit.setAlignment(Qt.AlignCenter) lineEdit.setHidden(True) lineEdit.blockSignals(True) lineEdit.editingFinished.connect(self.finishEdit) self.lineEdit = lineEdit self.sectionEdit = 0 self.sectionDoubleClicked.connect(self.startEdit)
def startEdit(self, tabIndex: int) -> None: self.editingTabIndex = tabIndex rect: QRect = self.tabRect(tabIndex) topMargin = 3 leftMargin = 6 lineEdit = QLineEdit(self) lineEdit.setAlignment(Qt.AlignCenter) lineEdit.move(rect.left() + leftMargin, rect.top() + topMargin) lineEdit.resize(rect.width() - 2 * leftMargin, rect.height() - 2 * topMargin) lineEdit.setText(self.tabText(tabIndex)) lineEdit.selectAll() lineEdit.setFocus() lineEdit.show() lineEdit.editingFinished.connect(self.finishEdit) self.lineEdit = lineEdit
def createEditor(self, parent, option, index): """Create editor widget""" model = index.model() value = model.get_value(index) if type(value) == np.ndarray or model.readonly: # The editor currently cannot properly handle this case return elif model._data.dtype.name == "bool": value = not value model.setData(index, to_qvariant(value)) return elif value is not np.ma.masked: editor = QLineEdit(parent) editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) editor.setAlignment(Qt.AlignCenter) if is_number(self.dtype): validator = QDoubleValidator(editor) validator.setLocale(QLocale('C')) editor.setValidator(validator) editor.returnPressed.connect(self.commitAndCloseEditor) return editor
class BMWidget(QWidget): def __init__(self, points, *args, **kwargs): super(BMWidget, self).__init__() self.plot_nb = 0 self.curve_nb = 0 self.setup(points, *args, **kwargs) def params(self, *args, **kwargs): if kwargs.get("only_lines", False): return (("Lines", None), ) else: return ( ("Lines", None), ("Dots", None), ) def setup(self, points, *args, **kwargs): x = np.linspace(0.001, 20.0, int(points)) y = (np.sin(x) / x) * np.cos(20 * x) layout = QGridLayout() nbcol, col, row = 2, 0, 0 for style, symbol in self.params(*args, **kwargs): plot = BMPlot(style, x, y, getattr(QwtPlotCurve, style), symbol=symbol) layout.addWidget(plot, row, col) self.plot_nb += 1 self.curve_nb += plot.curve_nb col += 1 if col >= nbcol: row += 1 col = 0 self.text = QLineEdit() self.text.setReadOnly(True) self.text.setAlignment(Qt.AlignCenter) self.text.setText("Rendering plot...") layout.addWidget(self.text, row + 1, 0, 1, 2) self.setLayout(layout)
def createEditor(self, parent, option, index): # ToDo: set dec placed for IN and MM machines col = self._columns[index.column()] if col == 'R': editor = QLineEdit(parent) editor.setFrame(False) margins = editor.textMargins() padding = editor.fontMetrics().width(self._padding) + 1 margins.setLeft(margins.left() + padding) editor.setTextMargins(margins) return editor elif col in 'TPQ': editor = QSpinBox(parent) editor.setFrame(False) editor.setAlignment(Qt.AlignCenter) if col == 'Q': editor.setMaximum(9) else: editor.setMaximum(99999) return editor elif col in 'XYZABCUVWD': editor = QDoubleSpinBox(parent) editor.setFrame(False) editor.setAlignment(Qt.AlignCenter) editor.setDecimals(4) # editor.setStepType(QSpinBox.AdaptiveDecimalStepType) editor.setProperty('stepType', 1) # stepType was added in 5.12 editor.setRange(-1000, 1000) return editor elif col in 'IJ': editor = QDoubleSpinBox(parent) editor.setFrame(False) editor.setAlignment(Qt.AlignCenter) editor.setMaximum(360.0) editor.setMinimum(0.0) editor.setDecimals(4) # editor.setStepType(QSpinBox.AdaptiveDecimalStepType) editor.setProperty('stepType', 1) # stepType was added in 5.12 return editor return None
class FileChoose(QWidget): """ :type batch_manager: CalculationManager """ def __init__(self, settings: PartSettings, batch_manager, parent=None): QWidget.__init__(self, parent) self.files_to_proceed = set() self.settings = settings self.batch_manager = batch_manager self.files_widget = AddFiles(settings, self) self.progress = ProgressView(self, batch_manager) self.run_button = QPushButton("Process") self.run_button.setDisabled(True) self.calculation_choose = QComboBox() self.calculation_choose.addItem("<no workflow>") self.calculation_choose.currentIndexChanged[str].connect( self.change_situation) self.result_file = QLineEdit(self) self.result_file.setAlignment(Qt.AlignRight) self.result_file.setReadOnly(True) self.chose_result = QPushButton("Save result as", self) self.chose_result.clicked.connect(self.chose_result_file) self.run_button.clicked.connect(self.prepare_calculation) self.files_widget.file_list_changed.connect(self.change_situation) layout = QVBoxLayout() layout.addWidget(self.files_widget) calc_layout = QHBoxLayout() calc_layout.addWidget(QLabel("Batch workflow:")) calc_layout.addWidget(self.calculation_choose) calc_layout.addWidget(self.result_file) calc_layout.addWidget(self.chose_result) calc_layout.addStretch() calc_layout.addWidget(self.run_button) layout.addLayout(calc_layout) layout.addWidget(self.progress) self.setLayout(layout) def prepare_calculation(self): plan = self.settings.batch_plans[str( self.calculation_choose.currentText())] dial = CalculationPrepare(self.files_widget.get_paths(), plan, str(self.result_file.text()), self.settings, self.batch_manager) if dial.exec_(): self.batch_manager.add_calculation(dial.get_data()) self.progress.new_task() def showEvent(self, _): current_calc = str(self.calculation_choose.currentText()) new_list = ["<no calculation>"] + list( sorted(self.settings.batch_plans.keys())) try: index = new_list.index(current_calc) except ValueError: index = 0 self.calculation_choose.clear() self.calculation_choose.addItems(new_list) self.calculation_choose.setCurrentIndex(index) def change_situation(self): if (str(self.calculation_choose.currentText()) != "<no calculation>" and len(self.files_widget.files_to_proceed) != 0 and str(self.result_file.text()) != ""): self.run_button.setEnabled(True) else: self.run_button.setDisabled(True) def chose_result_file(self): dial = SaveDialog({SaveExcel.get_short_name(): SaveExcel}, system_widget=False, history=self.settings.get_path_history()) dial.setDirectory( self.settings.get( "io.save_directory", self.settings.get("io.open_directory", str(Path.home())))) if dial.exec_(): file_path = str(dial.selectedFiles()[0]) self.settings.add_path_history(os.path.dirname(file_path)) if os.path.splitext(file_path)[1] == "": file_path += ".xlsx" self.result_file.setText(file_path) self.change_situation()
class InteractionVolumeView(VolumeView): signalAxisChanged = Signal(int) def __init__(self, label=None, **kwargs): super(InteractionVolumeView, self).__init__(**kwargs) self.interactionFrame = QFrame(self) self.interactionFrame.setSizePolicy( QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)) self.interactionFrame.setFixedHeight(30) self.interactionLayout = QHBoxLayout(self.interactionFrame) self.interactionLayout.setContentsMargins(5, 5, 5, 5) self.sliceLabel = QLabel(self.interactionFrame) self.sliceLabel.setSizePolicy( QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)) self.updateSliceLabel() self.textLabel = QLineEdit(self.interactionFrame) self.textLabel.setReadOnly(True) self.textLabel.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)) self.textLabel.setAlignment(Qt.AlignCenter) stylesheet = \ "QLineEdit {\n" \ + "border: none;\n" \ + "background-color: rgba(255, 255, 255, 0);\n" \ + "}" self.textLabel.setStyleSheet(stylesheet) if label is not None: self.updateTextLabel(label) self.interactionLayout.addWidget(self.sliceLabel) self.interactionLayout.addWidget(self.textLabel) self.axisSelector = QComboBox(self.interactionFrame) self.axisSelector.setFixedWidth(40) self.axisSelector.insertItems(0, ["0", "1", "2"]) self.interactionLayout.addWidget(self.axisSelector) self.layout.addWidget(self.interactionFrame) self.signalSliceChanged.connect(self.updateSliceLabel) self.axisSelector.currentIndexChanged.connect(self.setAxisAndEmit) def setAxisAndEmit(self, axis): self.setAxis(axis) self.signalAxisChanged.emit(axis) def setAxis(self, axis): super(InteractionVolumeView, self).setAxis(axis) self.axisSelector.setCurrentIndex(axis) self.updateSliceLabel() def setSlice(self, slice_): super(InteractionVolumeView, self).setSlice(slice_) self.updateSliceLabel() def updateSliceLabel(self): try: length = str(len(str(self.numberOfSlices))) self.sliceLabel.setText( ("{:0" + length + "d}/{:0" + length + "}").format( self.currentSlice, self.numberOfSlices)) except AttributeError: pass def updateTextLabel(self, text): try: self.textLabel.setText(text) self.textLabel.setCursorPosition(0) except AttributeError: pass
class QtHighlightSizePreviewWidget(QDialog): """Creates custom widget to set highlight size. Parameters ---------- description : str Text to explain and display on widget. value : int Value of highlight size. min_value : int Minimum possible value of highlight size. max_value : int Maximum possible value of highlight size. unit : str Unit of highlight size. """ valueChanged = Signal(int) def __init__( self, parent: QWidget = None, description: str = "", value: int = 1, min_value: int = 1, max_value: int = 10, unit: str = "px", ): super().__init__(parent) self.setGeometry(300, 300, 125, 110) self._value = value if value else self.fontMetrics().height() self._min_value = min_value self._max_value = max_value # Widget self._lineedit = QLineEdit() self._description = QLabel(self) self._unit = QLabel(self) self._slider = QSlider(Qt.Horizontal) self._triangle = QtTriangle(self) self._slider_min_label = QLabel(self) self._slider_max_label = QLabel(self) self._preview = QtStar(self) self._preview_label = QLabel(self) self._validator = QIntValidator(min_value, max_value, self) # Widgets setup self._description.setText(description) self._description.setWordWrap(True) self._unit.setText(unit) self._unit.setAlignment(Qt.AlignBottom) self._lineedit.setValidator(self._validator) self._lineedit.setAlignment(Qt.AlignRight) self._lineedit.setAlignment(Qt.AlignBottom) self._slider_min_label.setText(str(min_value)) self._slider_min_label.setAlignment(Qt.AlignBottom) self._slider_max_label.setText(str(max_value)) self._slider_max_label.setAlignment(Qt.AlignBottom) self._slider.setMinimum(min_value) self._slider.setMaximum(max_value) self._preview.setValue(value) self._triangle.setValue(value) self._triangle.setMinimum(min_value) self._triangle.setMaximum(max_value) self._preview_label.setText(trans._("Preview")) self._preview_label.setAlignment(Qt.AlignHCenter) self._preview_label.setAlignment(Qt.AlignBottom) self._preview.setStyleSheet('border: 1px solid white;') # Signals self._slider.valueChanged.connect(self._update_value) self._lineedit.textChanged.connect(self._update_value) self._triangle.valueChanged.connect(self._update_value) # Layout triangle_layout = QHBoxLayout() triangle_layout.addWidget(self._triangle) triangle_layout.setContentsMargins(6, 35, 6, 0) triangle_slider_layout = QVBoxLayout() triangle_slider_layout.addLayout(triangle_layout) triangle_slider_layout.setContentsMargins(0, 0, 0, 0) triangle_slider_layout.addWidget(self._slider) triangle_slider_layout.setAlignment(Qt.AlignVCenter) # Bottom row layout lineedit_layout = QHBoxLayout() lineedit_layout.addWidget(self._lineedit) lineedit_layout.setAlignment(Qt.AlignBottom) bottom_left_layout = QHBoxLayout() bottom_left_layout.addLayout(lineedit_layout) bottom_left_layout.addWidget(self._unit) bottom_left_layout.addWidget(self._slider_min_label) bottom_left_layout.addLayout(triangle_slider_layout) bottom_left_layout.addWidget(self._slider_max_label) bottom_left_layout.setAlignment(Qt.AlignBottom) left_layout = QVBoxLayout() left_layout.addWidget(self._description) left_layout.addLayout(bottom_left_layout) left_layout.setAlignment(Qt.AlignLeft) preview_label_layout = QHBoxLayout() preview_label_layout.addWidget(self._preview_label) preview_label_layout.setAlignment(Qt.AlignHCenter) preview_layout = QVBoxLayout() preview_layout.addWidget(self._preview) preview_layout.addLayout(preview_label_layout) preview_layout.setAlignment(Qt.AlignCenter) layout = QHBoxLayout() layout.addLayout(left_layout) layout.addLayout(preview_layout) self.setLayout(layout) self._refresh() def _update_value(self, value): """Update highlight value. Parameters ---------- value: int Highlight value. """ if value == "": value = int(self._value) value = int(value) if value > self._max_value: value = self._max_value elif value < self._min_value: value = self._min_value if value != self._value: self.valueChanged.emit(value) self._value = value self._refresh() def _refresh(self): """Set every widget value to the new set value.""" self.blockSignals(True) self._lineedit.setText(str(self._value)) self._slider.setValue(self._value) self._triangle.setValue(self._value) self._preview.setValue(self._value) self.blockSignals(False) self.valueChanged.emit(self._value) def value(self): """Return current value. Returns ------- int Current value of highlight widget. """ return self._value def setValue(self, value): """Set new value and update widget. Parameters ---------- value: int Highlight value. """ self._update_value(value) self._refresh() def description(self): """Return the description text. Returns ------- str Current text in description. """ return self._description.text() def setDescription(self, text): """Set the description text. Parameters ---------- text: str Text to use in description box. """ self._description.setText(text) def unit(self): """Return highlight value unit text. Returns ------- str Current text in unit text. """ return self._unit.text() def setUnit(self, text): """Set highlight value unit. Parameters ---------- text: str Text used to describe units. """ self._unit.setText(text) def setMinimum(self, value): """Set minimum highlight value for star, triangle, text and slider. Parameters ---------- value: int Minimum highlight value. """ value = int(value) if value < self._max_value: self._min_value = value self._slider_min_label.setText(str(value)) self._slider.setMinimum(value) self._triangle.setMinimum(value) self._value = (self._min_value if self._value < self._min_value else self._value) self._refresh() else: raise ValueError( f"Minimum value must be smaller than {self._max_value}") def minimum(self): """Return minimum highlight value. Returns ------- int Minimum value of highlight widget. """ return self._min_value def setMaximum(self, value): """Set maximum highlight value. Parameters ---------- value: int Maximum highlight value. """ value = int(value) if value > self._min_value: self._max_value = value self._slider_max_label.setText(str(value)) self._slider.setMaximum(value) self._triangle.setMaximum(value) self._value = (self._max_value if self._value > self._max_value else self._value) self._refresh() else: raise ValueError( f"Maximum value must be larger than {self._min_value}") def maximum(self): """Return maximum highlight value. Returns ------- int Maximum value of highlight widget. """ return self._max_value
class FileChoose(QWidget): """ :type batch_manager: CalculationManager """ def __init__(self, settings: PartSettings, batch_manager, parent=None): super().__init__(parent) self.files_to_proceed = set() self.settings = settings self.batch_manager = batch_manager self.files_widget = AddFiles(settings, self) self.progress = ProgressView(self, batch_manager) self.run_button = QPushButton("Process") self.run_button.setDisabled(True) self.calculation_choose = SearchComboBox() self.calculation_choose.addItem("<no calculation>") self.calculation_choose.currentIndexChanged[str].connect( self.change_situation) self.result_file = QLineEdit(self) self.result_file.setAlignment(Qt.AlignRight) self.result_file.setReadOnly(True) self.chose_result = QPushButton("Save result as", self) self.chose_result.clicked.connect(self.chose_result_file) self.run_button.clicked.connect(self.prepare_calculation) self.files_widget.file_list_changed.connect(self.change_situation) self.settings.batch_plans_changed.connect(self._refresh_batch_list) layout = QVBoxLayout() layout.addWidget(self.files_widget) calc_layout = QHBoxLayout() calc_layout.addWidget(QLabel("Batch workflow:")) calc_layout.addWidget(self.calculation_choose) calc_layout.addWidget(self.result_file) calc_layout.addWidget(self.chose_result) calc_layout.addStretch() calc_layout.addWidget(self.run_button) layout.addLayout(calc_layout) layout.addWidget(self.progress) self.setLayout(layout) self._refresh_batch_list() def prepare_calculation(self): plan = self.settings.batch_plans[str( self.calculation_choose.currentText())] dial = CalculationPrepare(self.files_widget.get_paths(), plan, str(self.result_file.text()), self.settings, self.batch_manager) if dial.exec_(): try: self.batch_manager.add_calculation(dial.get_data()) self.progress.new_task() except PicklingError as e: if state_store.develop: QMessageBox.warning(self, "Pickle error", "Please restart PartSeg.") else: raise e def _refresh_batch_list(self): current_calc = str(self.calculation_choose.currentText()) new_list = ["<no calculation>"] + list( sorted(self.settings.batch_plans.keys())) try: index = new_list.index(current_calc) except ValueError: index = 0 self.calculation_choose.clear() self.calculation_choose.addItems(new_list) self.calculation_choose.setCurrentIndex(index) def change_situation(self): if (str(self.calculation_choose.currentText()) != "<no calculation>" and len(self.files_widget.files_to_proceed) != 0 and str(self.result_file.text()) != ""): self.run_button.setEnabled(True) else: self.run_button.setDisabled(True) if self.calculation_choose.currentText() in self.settings.batch_plans: plan = self.settings.batch_plans[str( self.calculation_choose.currentText())] self.files_widget.mask_list = plan.get_list_file_mask() else: self.files_widget.mask_list = [] def chose_result_file(self): dial = PSaveDialog(SaveExcel, system_widget=False, settings=self.settings, path=IO_SAVE_DIRECTORY) if dial.exec_(): file_path = str(dial.selectedFiles()[0]) if os.path.splitext(file_path)[1] == "": file_path += ".xlsx" self.result_file.setText(file_path) self.change_situation()
class Classifier(QWidget): def __init__(self, viewer, metadata_levels, initial_classes, *args, **kwargs): super(Classifier,self).__init__(*args,**kwargs) self.viewer = viewer # open image if not already open if len(self.viewer.layers)<1: msg = QMessageBox() msg.setIcon(QMessageBox.Information) msg.setText("No image open, please select an image in the following dialog.") msg.exec() self.viewer.window.qt_viewer._open_files_dialog() if len(self.viewer.layers)<1: # still no image open raise ValueError("Could not find image layers.") # get image shape self.shape = self.viewer.layers[0].shape # check metadata levels if not metadata_levels: self.metadata_levels = [f'dim_{dim}' for dim in range(len(self.shape[:-2]))] elif len(metadata_levels)!=len(self.shape[:-2]): metadata_levels_warning = NapariNotification((f'Number of metadata_levels ({len(metadata_levels)}) does not match ' f'number of leading image dimensions ({len(self.shape[:-2])}); will use default metadata_levels.'), severity='warning') metadata_levels_warning.show() self.metadata_levels = [f'dim_{dim}' for dim in range(len(self.shape[:-2]))] else: self.metadata_levels = metadata_levels # load metadata self.load_metadata() # initialize widget layout = QHBoxLayout() ## io panel save_button = QPushButton('Save...',self) save_button.clicked.connect(self.save_results) io_panel = QWidget() io_layout = QVBoxLayout() io_layout.addWidget(save_button) io_panel.setLayout(io_layout) layout.addWidget(io_panel) ## class panel classes_panel = QGroupBox('classes') classes_panel.setMinimumWidth(CLASS_PANEL_MINIMUM_WIDTH) ### layout for adding classes add_classes_layout = QHBoxLayout() add_class_button = QPushButton('Add class',self) add_class_button.clicked.connect(self.click_add_class) self.new_class_text = QLineEdit(self) self.new_class_text.setAlignment(Qt.AlignLeft) add_classes_layout.addWidget(add_class_button) add_classes_layout.addWidget(self.new_class_text) ### layout for class buttons self.class_button_layout = QGridLayout() ### add sub layouts to class panel classes_layout = QVBoxLayout() classes_layout.addLayout(add_classes_layout) classes_layout.addLayout(self.class_button_layout) classes_panel.setLayout(classes_layout) layout.addWidget(classes_panel) ## set widget layout layout.setAlignment(Qt.AlignTop) layout.setSpacing(4) self.setLayout(layout) self.setMaximumHeight(GUI_MAXIMUM_HEIGHT) self.setMaximumWidth(GUI_MAXIMUM_WIDTH) # initialize classes self.classes = [] if initial_classes is not None: for initial_class in initial_classes: self.add_class(initial_class) def load_metadata(self): # msg = QMessageBox() # msg.setIcon(QMessageBox.Information) # msg.setText(("Use the following dialog to choose a metadata table to open, " # "otherwise click 'cancel' to use an automatically generated table.")) # msg.exec() # filename = QFileDialog.getOpenFileName(self,'Open metadata table',DEFAULT_PATH,'Metadata table (*.csv *.hdf)') self.df_metadata = None # if filename[0]: # ext = filename[0].split('.')[-1] # if ext == 'csv': # self.df_metadata = pd.read_csv(filename[0]) # elif ext == 'hdf': # self.df_metadata = pd.read_hdf(filename[0]) # else: # print(f'filetype {ext} not recognized, creating default metadata table') if self.df_metadata is None: from itertools import product self.df_metadata = pd.DataFrame([{level:idx for level,idx in zip(self.metadata_levels,indices)} for indices in product(*(range(dim) for dim in self.shape[:-2]))] ) if 'annotated_class' not in self.df_metadata.columns: self.df_metadata = self.df_metadata.assign(annotated_class=None) self.df_metadata = self.df_metadata.set_index(self.metadata_levels) def click_add_class(self): self.add_class(self.new_class_text.text()) self.new_class_text.clear() def add_class(self,new_class): self.classes.append(new_class) if len(self.classes)<10: # shortcut key binding available self.viewer.bind_key(key=str(len(self.classes)), func=partial(self.classify_frame,chosen_class=self.classes[-1]), overwrite=True) new_class_button = QPushButton('{class_name} [{num}]'.format( class_name=self.classes[-1], num=len(self.classes)), self) else: # shortcut key binding not available many_classes_notification = NapariNotification( ('Shortcut key bindings not available with the 10th or further class'), severity='info') many_classes_notification.show() new_class_button = QPushButton(self.classes[-1],self) new_class_button.clicked.connect(partial(self.classify_frame,key_press=None,chosen_class=self.classes[-1])) self.class_button_layout.addWidget(new_class_button, ((len(self.classes)-1)%MAXIMUM_CLASS_BUTTONS_PER_COLUMN), int((len(self.classes)-1)/MAXIMUM_CLASS_BUTTONS_PER_COLUMN) ) def classify_frame(self,key_press,chosen_class): # TODO: create an annotation status bar that reports class of current slice coords = self.viewer.layers[0].coordinates[:-2] coords_string = ', '.join([f'{level}={val}' for level,val in zip(self.metadata_levels,coords)]) if self.df_metadata.loc[(coords),('annotated_class')] is not None: previous_class = self.df_metadata.loc[(coords),('annotated_class')] if previous_class != chosen_class: overwrite_notification = NapariNotification((f'{coords_string} previously annotated as ' f'`{previous_class}`, overwriting annotation as `{chosen_class}`'), severity='info') overwrite_notification.show() else: annotate_notification = NapariNotification((f'Annotating {coords_string} as `{chosen_class}`'), severity='info') annotate_notification.show() self.df_metadata.loc[(coords),('annotated_class')] = chosen_class if tuple(np.array(self.shape[:-2])-1)==coords: # last slice pass else: if coords[-1]<(self.shape[-3]-1): self.viewer.dims._increment_dims_right(axis=(-3)) else: self.viewer.dims.set_current_step(axis=(-3),value=0) self.viewer.dims._increment_dims_right(axis=(-4)) def save_results(self): filename = QFileDialog.getSaveFileName(self, 'Export classification data', os.path.join(DEFAULT_PATH, self.viewer.layers[0].name+'.classified.csv'), 'Classification files (*.csv *.hdf)') if filename[0]: ext = filename[0].split('.')[-1] if ext == 'csv': self.df_metadata.to_csv(filename[0]) elif ext == 'hdf': self.df_metadata.to_hdf(filename[0],'x',mode='w') else: print(f'filetype {ext} not recognized, cannot save') else: print('no file selected, did not save')
def _create_bump(self): def _add_entry(index): cbox = self.sender() text = cbox.itemText(index) if not text.startswith('other'): return win = LoadConfigDialog(self._config_type, self) confname, status = win.exec_() if not status: cbox.setCurrentIndex(0) return cbox.insertItem(index, confname) cbox.setCurrentIndex(index) wid = QDialog(self) wid.setObjectName(self._csorb.acc + 'App') lay = QGridLayout() wid.setLayout(lay) row = 0 lay.addWidget(QLabel('Base Orbit ', wid), row, 0) orbcombo = QComboBox(wid) orbcombo.addItems(['Register', 'ref_orb', 'bba_orb', 'other...']) orbcombo.setCurrentIndex(1) orbcombo.activated.connect(_add_entry) lay.addWidget(orbcombo, row, 1) row += 1 lay.addWidget(QLabel('Subsection', wid), row, 0) sscombo = QComboBox(wid) sub = ['SA', 'SB', 'SP', 'SB'] ssnames = [f'{d+1:02d}{sub[d%len(sub)]}' for d in range(20)] bcnames = [f'{d+1:02d}BC' for d in range(20)] names = [] for aaa, bbb in zip(ssnames, bcnames): names.extend([aaa, bbb]) sscombo.addItems(names) lay.addWidget(sscombo, row, 1) row += 1 lay.addWidget(QLabel('\u03B8<sub>x</sub> [urad]', wid), row, 0) angx = QLineEdit(wid) angx.setValidator(QDoubleValidator()) angx.setText('0.0') angx.setAlignment(Qt.AlignCenter) angx.setStyleSheet('max-width:5em;') lay.addWidget(angx, row, 1) row += 1 lay.addWidget(QLabel('X [um] ', wid), row, 0) posx = QLineEdit(wid) posx.setValidator(QDoubleValidator()) posx.setText('0.0') posx.setAlignment(Qt.AlignCenter) posx.setStyleSheet('max-width:5em;') lay.addWidget(posx, row, 1) row += 1 lay.addWidget(QLabel('\u03B8<sub>y</sub> [urad]', wid), row, 0) angy = QLineEdit(wid) angy.setValidator(QDoubleValidator()) angy.setText('0.0') angy.setAlignment(Qt.AlignCenter) angy.setStyleSheet('max-width:5em;') lay.addWidget(angy, row, 1) row += 1 lay.addWidget(QLabel('Y [um] ', wid), row, 0) posy = QLineEdit(wid) posy.setValidator(QDoubleValidator()) posy.setText('0.0') posy.setAlignment(Qt.AlignCenter) posy.setStyleSheet('max-width:5em;') lay.addWidget(posy, row, 1) row += 1 hlay = QHBoxLayout() cancel = QPushButton('Cancel', wid) confirm = QPushButton('Ok', wid) confirm.setDefault(True) cancel.clicked.connect(wid.reject) confirm.clicked.connect(wid.accept) hlay.addStretch() hlay.addWidget(cancel) hlay.addStretch() hlay.addWidget(confirm) hlay.addStretch() wid.layout().addItem(hlay, row, 0, 1, 2) res = wid.exec_() if res != QDialog.Accepted: return index = orbcombo.currentIndex() confname = orbcombo.itemText(index) if not index: orbx = _np.array(self.orbx) orby = _np.array(self.orby) elif index == orbcombo.count() - 1: return else: orbs = self._client.get_config_value(confname) orbx = _np.array(orbs['x']) orby = _np.array(orbs['y']) agx = float(angx.text()) agy = float(angy.text()) psx = float(posx.text()) psy = float(posy.text()) sub = sscombo.currentText() orbx, orby = _calculate_bump(orbx, orby, sub, agx, agy, psx, psy) txt = f'Bump@{sub}: ref={confname}\n' txt += f'ax={agx:.1f} ay={agy:.1f} dx={psx:.1f} dy={psy:.1f}' self._update_and_emit(txt, orbx, orby)
def createEditor(self, parent: QWidget, option: QStyleOptionViewItem, index: QModelIndex) -> QWidget: editor = QLineEdit(parent=parent) editor.setAlignment(Qt.AlignCenter) editor.selectAll() return editor
def _edit_orbit(self): orbx = self.orbx orby = self.orby wid = QDialog(self) wid.setObjectName(self._csorb.acc + 'App') wid.setLayout(QVBoxLayout()) hbl = QHBoxLayout() wid.layout().addItem(hbl) hbl.addWidget(QLabel('X = ', wid)) multx = QLineEdit(wid) multx.setValidator(QDoubleValidator()) multx.setText('1.0') # multx.setAlignment(Qt.AlignVCenter | Qt.AlignRight) multx.setAlignment(Qt.AlignCenter) multx.setStyleSheet('max-width:5em;') hbl.addWidget(multx) hbl.addWidget(QLabel('*X + ', wid)) addx = QLineEdit(wid) addx.setValidator(QDoubleValidator()) addx.setText('0.0') addx.setAlignment(Qt.AlignCenter) addx.setStyleSheet('max-width:5em;') hbl.addWidget(addx) hbl.addWidget(QLabel(' [um]', wid)) hbl = QHBoxLayout() wid.layout().addItem(hbl) hbl.addWidget(QLabel('Y = ', wid)) multy = QLineEdit(wid) multy.setValidator(QDoubleValidator()) multy.setText('1.0') # multy.setAlignment(Qt.AlignVCenter | Qt.AlignRight) multy.setAlignment(Qt.AlignCenter) multy.setStyleSheet('max-width:5em;') hbl.addWidget(multy) hbl.addWidget(QLabel('*Y + ', wid)) addy = QLineEdit(wid) addy.setValidator(QDoubleValidator()) addy.setText('0.0') addy.setAlignment(Qt.AlignCenter) addy.setStyleSheet('max-width:5em;') hbl.addWidget(addy) hbl.addWidget(QLabel(' [um]', wid)) hlay = QHBoxLayout() cancel = QPushButton('Cancel', wid) confirm = QPushButton('Ok', wid) confirm.setDefault(True) cancel.clicked.connect(wid.reject) confirm.clicked.connect(wid.accept) hlay.addStretch() hlay.addWidget(cancel) hlay.addStretch() hlay.addWidget(confirm) hlay.addStretch() wid.layout().addItem(hlay) res = wid.exec_() if res != QDialog.Accepted: return mltx = float(multx.text()) mlty = float(multy.text()) plusx = float(addx.text()) plusy = float(addy.text()) orbx = mltx * orbx + plusx orby = mlty * orby + plusy txt = '' txt += f'multx = {mltx:5.1f} offx = {plusx:7.1f}\n' txt += f'multy = {mlty:5.1f} offy = {plusy:7.1f}' self._update_and_emit(txt, orbx, orby)
class MainWindow(QMainWindow): max_recent_files = 10 def __init__(self): super().__init__() self.setWindowTitle("OkPlayer") icon = QIcon() icon.addPixmap(QPixmap("ok_64x64.ico"), QIcon.Normal, QIcon.Off) self.setWindowIcon(icon) self.recent_file_acts = [] self.init_menu() self.now = datetime.now() # Setting self.setting = {} self.load_setting() # Status bar self.learning_time_ms = 0 self.learning_time_ms_total = self.setting.get( "learning_time_ms_total", 0) self.status_bar = self.statusBar() self.label_learning_time = QLabel(self) self.label_learning_time.setAlignment(Qt.AlignRight) self.status_bar.addPermanentWidget(self.label_learning_time) self.label_learning_time.setText( f"Learning time: 00:00" f" / total {ms2min_sec(self.learning_time_ms_total)}") # Timer for learning time self.timer_learning_time = QTimer(self) self.timer_learning_time.timeout.connect(self.update_learning_time) self.timer_learning_time.setInterval(1000) # Player self.player = QMediaPlayer(self) self.player.mediaStatusChanged.connect(self.qmp_status_changed) self.player.positionChanged.connect(self.qmp_position_changed) self.player.setNotifyInterval(50) self.player.setVolume(50) self.player_buf = QBuffer() self.path_media = "" self.music_data = None self.duration_ms = 0 self.duration_str = "" # A/B Loop self.pos_loop_a = None self.pos_loop_b = None # Layout self.label_music = QLabel("No music", self) self.ico_play = qta.icon("fa.play") self.ico_pause = qta.icon("fa.pause") layout = QVBoxLayout() layout_volume = QHBoxLayout() layout_btn_progress = QVBoxLayout() layout_music_btns = QHBoxLayout() self.btn_rewind = QPushButton(qta.icon("fa.backward"), "", self) self.btn_rewind.clicked.connect(self.rewind) self.btn_play = QPushButton(self.ico_play, "", self) self.btn_play.clicked.connect(self.play) self.btn_fastforward = QPushButton(qta.icon("fa.forward"), "", self) self.btn_fastforward.clicked.connect(self.fastforward) self.btn_rewind.setFocusPolicy(Qt.NoFocus) self.btn_play.setFocusPolicy(Qt.NoFocus) self.btn_fastforward.setFocusPolicy(Qt.NoFocus) layout_music_btns.addWidget(self.btn_rewind) layout_music_btns.addWidget(self.btn_play) layout_music_btns.addWidget(self.btn_fastforward) layout_progress = QHBoxLayout() self.progressbar = MusicProgressBar(self) self.progressbar.sig_pb_pos.connect(self.set_media_position) self.elapsed_time = QLineEdit(f"00:00 / 00:00", self) self.elapsed_time.setReadOnly(True) self.elapsed_time.setAlignment(Qt.AlignHCenter) layout_progress.addWidget(self.progressbar) layout_progress.addWidget(self.elapsed_time) layout_btn_progress.addWidget(self.label_music) layout_btn_progress.addLayout(layout_music_btns) layout_btn_progress.addLayout(layout_progress) # Volume self.qdial_volume = QDial(self) self.qdial_volume.setMinimumWidth(110) self.qdial_volume.setWrapping(False) self.qdial_volume.setNotchesVisible(True) self.qdial_volume.setMinimum(0) self.qdial_volume.setMaximum(100) self.qdial_volume.setValue(self.player.volume()) self.qdial_volume.valueChanged.connect(self.qdial_changed) layout_volume.addLayout(layout_btn_progress) layout_volume.addWidget(self.qdial_volume) # Lyrics self.display_lyrics = LyricsDisplay(self) layout.addLayout(layout_volume) layout.addWidget(self.display_lyrics) central_widget = QWidget() central_widget.setLayout(layout) self.setCentralWidget(central_widget) # Auto Play self.update_recent_file_action() path = self.setting.get("LastPlayedPath", "") if osp.isfile(path): self.load_music_file(path) self.setFocus() def init_menu(self): """Init menu.""" color_icon = "#87939A" menu_bar = self.menuBar() menu_bar.setNativeMenuBar(False) # Don't use mac native menu bar # File file_menu = menu_bar.addMenu("&File") # Open open_action = QAction(qta.icon("ei.folder-open", color=color_icon), "&Open", self) open_action.setShortcut("Ctrl+O") open_action.setStatusTip("Open file") open_action.triggered.connect(self.open_music_file) file_menu.addAction(open_action) file_menu.addSeparator() # Recent Files for i in range(MainWindow.max_recent_files): self.recent_file_acts.append( QAction(self, visible=False, triggered=self.load_recent_music)) for i in range(MainWindow.max_recent_files): file_menu.addAction(self.recent_file_acts[i]) file_menu.addSeparator() # Exit exit_action = QAction(qta.icon("mdi.exit-run", color=color_icon), "&Exit", self) exit_action.setShortcut("Ctrl+Q") exit_action.setStatusTip("Exit App") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # Help help_menu = menu_bar.addMenu("&Help") about_action = QAction( "&About", self, statusTip="Show the application's About box", triggered=self.about, ) help_menu.addAction(about_action) def about(self): """Show messagebox for about.""" QMessageBox.about( self, "About music player a/b loop", "The music player a/b loop is made by <b>ok97465</b>", ) def update_recent_file_action(self): """Update recent file action.""" files = self.setting.get("recent_files", []) num_recent_files = min(len(files), MainWindow.max_recent_files) for i in range(num_recent_files): text = osp.splitext(osp.basename(files[i]))[0] self.recent_file_acts[i].setText(text) self.recent_file_acts[i].setData(files[i]) self.recent_file_acts[i].setVisible(True) for j in range(num_recent_files, MainWindow.max_recent_files): self.recent_file_acts[j].setVisible(False) def open_music_file(self): """Open music file.""" self.stop() fname = QFileDialog.getOpenFileName( self, "Open music file", "/home/ok97465", filter="Music Files (*.mp3, *.m4a)", ) self.load_music_file(fname[0]) def load_music_file(self, path: str): """Load music file""" if not osp.isfile(path): return self.path_media = path path_lyrics = path[:-3] + "vtt" self.display_lyrics.read_vtt(path_lyrics) fp = io.BytesIO() self.music_data = AudioSegment.from_file(path) self.music_data.export(fp, format="wav") self.player_buf.setData(fp.getvalue()) self.player_buf.open(QIODevice.ReadOnly) self.player.setMedia(QMediaContent(), self.player_buf) def load_recent_music(self): """Load recent music.""" action = self.sender() if action: self.stop() self.load_music_file(action.data()) def load_setting(self): """Load setting file.""" try: with open("setting.json", "r") as fp: self.setting = json.load(fp) except FileNotFoundError: pass def keyPressEvent(self, event): key = event.key() shift = event.modifiers() & Qt.ShiftModifier if shift: if key == Qt.Key_O: self.adjust_ab_loop(-100) else: if key in [Qt.Key_H, Qt.Key_Left, Qt.Key_A]: self.rewind(ms=5000) elif key in [Qt.Key_L, Qt.Key_Right, Qt.Key_D]: self.fastforward(ms=5000) elif key in [Qt.Key_J]: self.rewind(ms=1000 * 38) elif key in [Qt.Key_K, Qt.Key_F]: self.fastforward(ms=1000 * 38) elif key == Qt.Key_Up: self.control_volume(5) elif key == Qt.Key_Down: self.control_volume(-5) elif key in [Qt.Key_I, Qt.Key_W, Qt.Key_Menu]: self.set_ab_loop() elif key == Qt.Key_O: self.adjust_ab_loop(500) elif key in [Qt.Key_Space, Qt.Key_Hangul_Hanja]: self.play() elif key in [Qt.Key_S]: self.save_ab_loop() elif key in [Qt.Key_Q, Qt.Key_U, Qt.Key_Slash]: self.send_AB_loop_lyrics_to_papago() super().keyPressEvent(event) def set_ab_loop(self): """Set A/B loop.""" if self.pos_loop_b: self.pos_loop_b = None self.pos_loop_a = None elif self.pos_loop_a: self.pos_loop_b = self.player.position() self.player.setPosition(self.pos_loop_a) else: self.pos_loop_a = self.player.position() self.progressbar.pos_loop_a = self.pos_loop_a self.progressbar.pos_loop_b = self.pos_loop_b self.progressbar.repaint() def adjust_ab_loop(self, offset_ms): """Adjust A/B loop.""" if self.pos_loop_b: self.pos_loop_b += offset_ms self.pos_loop_a += offset_ms def save_ab_loop(self): """Save A/B loop""" if self.pos_loop_b is None: return is_playing = False if self.player.state() == QMediaPlayer.PlayingState: is_playing = True if is_playing: self.player.pause() path_new = (self.path_media[:-4] + f"{self.pos_loop_a}_{self.pos_loop_b}" + self.path_media[-4:]) seg = self.music_data[self.pos_loop_a:self.pos_loop_b] seg.export(path_new, format="mp3") if is_playing: self.player.play() def play(self): """Play music file.""" if self.player.state() == QMediaPlayer.PlayingState: self.player.pause() self.btn_play.setIcon(self.ico_play) self.timer_learning_time.stop() else: self.player.play() self.btn_play.setIcon(self.ico_pause) self.timer_learning_time.start() def stop(self): """Stop.""" self.save_current_media_info() self.player.stop() self.player_buf.close() self.path_media = "" self.pos_loop_b = None self.pos_loop_a = None self.timer_learning_time.stop() self.label_music.setText("No music") self.btn_play.setIcon(self.ico_play) def control_volume(self, step: int): """Control volume.""" volume = self.player.volume() if step < 0: new_volume = max([0, volume + step]) else: new_volume = min([100, volume + step]) self.qdial_volume.setValue(new_volume) def navigate_media(self, ms: int): """Navigate the position of media.""" position_ms = self.player.position() if ms < 0: new_position_ms = max([0, position_ms + ms]) else: new_position_ms = min([self.duration_ms, position_ms + ms]) self.player.setPosition(new_position_ms) def rewind(self, ms: int = 5000): """Re-wind media of QMediaPlayer.""" self.navigate_media(ms * -1) def fastforward(self, ms: int = 5000): """fastfoward media of QMediaPlayer.""" self.navigate_media(ms) def qmp_status_changed(self): """Handle status of QMediaPlayer if the status is changed.""" status = self.player.mediaStatus() if status == QMediaPlayer.LoadedMedia and self.path_media: duration_ms = self.player.duration() self.duration_ms = duration_ms self.duration_str = ms2min_sec(duration_ms) self.elapsed_time.setText(f"00:00 / {self.duration_str}") self.progressbar.setMaximum(duration_ms) music_basename = osp.splitext(osp.basename(self.path_media))[0] self.label_music.setText(music_basename) self.player.play() # read previous position path = self.path_media position = self.setting.get(path, 0) self.player.setPosition(position) # update recent files files = self.setting.get("recent_files", []) try: files.remove(path) except ValueError: pass files.insert(0, path) del files[MainWindow.max_recent_files:] self.setting["recent_files"] = files self.update_recent_file_action() # Player state state = self.player.state() if state in [QMediaPlayer.PausedState, QMediaPlayer.StoppedState]: self.btn_play.setIcon(self.ico_play) self.timer_learning_time.stop() elif state == QMediaPlayer.PlayingState: self.btn_play.setIcon(self.ico_pause) self.timer_learning_time.start() def qmp_position_changed(self, position_ms: int): """Handle position of qmedia if the position is changed.""" if self.pos_loop_b: if (position_ms == self.duration_ms) or (self.pos_loop_b < position_ms): self.player.setPosition(self.pos_loop_a) self.progressbar.setValue(position_ms) self.elapsed_time.setText( f"{ms2min_sec(position_ms)} / {self.duration_str}") self.display_lyrics.update_media_pos(position_ms) def qdial_changed(self, pos: int): """Handle Qdial position.""" self.player.setVolume(pos) def send_AB_loop_lyrics_to_papago(self): """Send AB loop lyrics to papago.""" if not self.pos_loop_b: return lyrics = self.display_lyrics.get_lyrics_in_range( self.pos_loop_a, self.pos_loop_b) lyrics = lyrics.replace("\n", "") webbrowser.open(f"https://papago.naver.com/?sk=en&tk=ko&st={lyrics}", autoraise=False) @Slot(int) def set_media_position(self, position_ms: int): """Set the position of Qmedia.""" self.player.setPosition(position_ms) def save_current_media_info(self): """Save current media info to setting file.""" if not osp.isfile(self.path_media): return if self.path_media: position = self.player.position() self.setting[self.path_media] = position self.setting["LastPlayedPath"] = self.path_media def update_learning_time(self): """Update learning time.""" self.learning_time_ms += 1000 self.learning_time_ms_total += 1000 self.label_learning_time.setText( f"Learning time : {ms2min_sec(self.learning_time_ms)}" f" / total : {ms2min_sec(self.learning_time_ms_total)}") def closeEvent(self, event): """Save setting.""" self.stop() self.setting["learning_time_ms_total"] = self.learning_time_ms_total with open("setting.json", "w") as fp: json.dump(self.setting, fp, indent=2) now = self.now cur = sqlite3.connect("history.db") cur.execute("CREATE TABLE IF NOT EXISTS LearningTimeData(" "DayOfWeek INTEGER, " "month INTEGER, " "day INTEGER, " "timestamp REAL, " "LearningTime_ms INTEGER)") cur.execute( "insert into LearningTimeData Values (?,?,?,?,?)", (now.weekday(), now.month, now.day, now.timestamp(), self.learning_time_ms), ) cur.commit() cur.close()
class QtSizeSliderPreviewWidget(QWidget): """ Widget displaying a description, textedit and slider to adjust font size with preview. Parameters ---------- parent : qtpy.QtWidgets.QWidget, optional Default is None. description : str, optional Default is "". preview_text : str, optional Default is "". value : int, optional Default is None. min_value : int, optional Default is 1. max_value : int, optional Default is 50. unit : str, optional Default is "px". """ valueChanged = Signal(int) def __init__( self, parent: QWidget = None, description: str = None, preview_text: str = None, value: int = None, min_value: int = 1, max_value: int = 50, unit: str = "px", ): super().__init__(parent) description = description or "" preview_text = preview_text or "" self._value = value if value else self.fontMetrics().height() self._min_value = min_value self._max_value = max_value # Widget self._lineedit = QLineEdit() self._description_label = QLabel(self) self._unit_label = QLabel(self) self._slider = QSlider(Qt.Horizontal, self) self._slider_min_label = QLabel(self) self._slider_max_label = QLabel(self) self._preview = QtFontSizePreview(self) self._preview_label = QLabel(self) self._validator = None # Widgets setup self._description_label.setText(description) self._description_label.setWordWrap(True) self._unit_label.setText(unit) self._lineedit.setAlignment(Qt.AlignRight) self._slider_min_label.setText(str(min_value)) self._slider_max_label.setText(str(max_value)) self._slider.setMinimum(min_value) self._slider.setMaximum(max_value) self._preview.setText(preview_text) self._preview_label.setText(trans._("preview")) self._preview_label.setAlignment(Qt.AlignHCenter) self.setFocusProxy(self._lineedit) # Layout left_bottom_layout = QHBoxLayout() left_bottom_layout.addWidget(self._lineedit) left_bottom_layout.addWidget(self._unit_label) left_bottom_layout.addWidget(self._slider_min_label) left_bottom_layout.addWidget(self._slider) left_bottom_layout.addWidget(self._slider_max_label) left_layout = QVBoxLayout() left_layout.addWidget(self._description_label) left_layout.addLayout(left_bottom_layout) right_layout = QVBoxLayout() right_layout.addWidget(self._preview) right_layout.addWidget(self._preview_label) layout = QHBoxLayout() layout.addLayout(left_layout, 2) layout.addLayout(right_layout, 1) self.setLayout(layout) # Signals self._slider.valueChanged.connect(self._update_value) self._lineedit.textChanged.connect(self._update_value) self._update_line_width() self._update_validator() self._update_value(self._value) def _update_validator(self): self._validator = QIntValidator(self._min_value, self._max_value, self) self._lineedit.setValidator(self._validator) def _update_line_width(self): """Update width ofg line text edit.""" size = self._lineedit.fontMetrics().horizontalAdvance( "m" * (1 + len(str(self._max_value)))) self._lineedit.setMaximumWidth(size) self._lineedit.setMinimumWidth(size) def _update_value(self, value: int): """Update internal value and emit if changed.""" if value == "": value = int(self._value) value = int(value) if value > self._max_value: value = self._max_value elif value < self._min_value: value = self._min_value if value != self._value: self.valueChanged.emit(value) self._value = value self._refresh(self._value) def _refresh(self, value: int = None): """Refresh the value on all subwidgets.""" value = value if value else self._value self.blockSignals(True) self._lineedit.setText(str(value)) self._slider.setValue(value) font = QFont() font.setPixelSize(value) self._preview.setFont(font) font = QFont() font.setPixelSize(self.fontMetrics().height() - 4) self._preview_label.setFont(font) self.blockSignals(False) def description(self) -> str: """Return the current widget description. Returns ------- str The description text. """ return self._description_label.text() def setDescription(self, text: str): """Set the current widget description. Parameters ---------- text : str The description text. """ self._description_label.setText(text) def previewText(self) -> str: """Return the current preview text. Returns ------- str The current preview text. """ return self._preview.text() def setPreviewText(self, text: str): """Set the current preview text. Parameters ---------- text : str The current preview text. """ self._preview.setText(text) def unit(self) -> str: """Return the current unit text. Returns ------- str The current unit text. """ return self._unit_label.text() def setUnit(self, text: str): """Set the current unit text. Parameters ---------- text : str The current preview text. """ self._unit_label.setText(text) def minimum(self) -> int: """Return the current minimum value for the slider and value in textbox. Returns ------- int The minimum value for the slider. """ return self._min_value def setMinimum(self, value: int): """Set the current minimum value for the slider and value in textbox. Parameters ---------- value : int The minimum value for the slider. """ value = int(value) if value < self._max_value: self._min_value = value self._value = (self._min_value if self._value < self._min_value else self._value) self._slider_min_label.setText(str(value)) self._slider.setMinimum(value) self._update_validator() self._refresh() else: raise ValueError( trans._("Minimum value must be smaller than {}".format( self._max_value))) def maximum(self) -> int: """Return the maximum value for the slider and value in textbox. Returns ------- int The maximum value for the slider. """ return self._max_value def setMaximum(self, value: int): """Set the maximum value for the slider and value in textbox. Parameters ---------- value : int The maximum value for the slider. """ value = int(value) if value > self._min_value: self._max_value = value self._value = (self._max_value if self._value > self._max_value else self._value) self._slider_max_label.setText(str(value)) self._slider.setMaximum(value) self._update_validator() self._update_line_width() self._refresh() else: raise ValueError( trans._("Maximum value must be larger than {}".format( self._min_value))) def value(self) -> int: """Return the current widget value. Returns ------- int The current value. """ return self._value def setValue(self, value: int): """Set the current widget value. Parameters ---------- value : int The current value. """ self._update_value(value)
class SelectArithmetic(QDialog): def __init__(self, data, data_collection, parent=None): super(SelectArithmetic, self).__init__(parent) # Get the data_components (e.g., FLUX, DQ, ERROR etc) # Using list comprehension to keep the order of the component_ids self.data_components = [ str(x).strip() for x in data.component_ids() if not x in data.coordinate_components ] self.setWindowFlags(self.windowFlags() | Qt.Tool) self.title = "Arithmetic Calculation" self.data = data self.data_collection = data_collection self.parent = parent self.currentAxes = None self.currentKernel = None self.createUI() def createUI(self): """ Create the popup box with the calculation input area and buttons. :return: """ boldFont = QtGui.QFont() boldFont.setBold(True) # Create calculation label and input box self.calculation_label = QLabel("Calculation:") self.calculation_label.setFixedWidth(100) self.calculation_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.calculation_label.setFont(boldFont) self.calculation_text = QLineEdit() self.calculation_text.setMinimumWidth(200) self.calculation_text.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hbl1 = QHBoxLayout() hbl1.addWidget(self.calculation_label) hbl1.addWidget(self.calculation_text) # Create calculation label and input box self.error_label = QLabel("") self.error_label.setFixedWidth(100) self.error_label_text = QLabel("") self.error_label_text.setMinimumWidth(200) self.error_label_text.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hbl_error = QHBoxLayout() hbl_error.addWidget(self.error_label) hbl_error.addWidget(self.error_label_text) # Show the available data self.data_available_text_label = QLabel("Data available: ") self.data_available_text_label.setFixedWidth(100) self.data_available_text_label.setAlignment( (Qt.AlignRight | Qt.AlignTop)) self.data_available_text_label.setFont(boldFont) self.data_available_label = QLabel(', '.join(self.data_components)) self.data_available_label.setMinimumWidth(200) self.data_available_label.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hbl2 = QHBoxLayout() hbl2.addWidget(self.data_available_text_label) hbl2.addWidget(self.data_available_label) # Show the examples self.example_text_label = QLabel("Examples: ") self.example_text_label.setFixedWidth(100) self.example_text_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.example_text_label.setFont(boldFont) examples = """Assuming we have data available called FLUX and ERROR: - Subtract 1000 from {0}: {0}new = {0} - 1000 - Double the FLUX: {0}new = {0} * 2 - Scale FLUX between 0 and 1: {0}norm = ({0} - min({0})) - (max({0})-min({0})) - Signal to noise: SNR = {0} / {1} - Masking: {0}new = {0} * ({1} < 0.1*mean({1})) """.format(self.data_components[0], self.data_components[1]) self.examples_label = QLabel(examples) self.examples_label.setMinimumWidth(200) self.examples_label.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hbl_examples = QHBoxLayout() hbl_examples.addWidget(self.example_text_label) hbl_examples.addWidget(self.examples_label) # Create Calculate and Cancel buttons self.calculateButton = QPushButton("Calculate") self.calculateButton.clicked.connect(self.calculate_callback) self.calculateButton.setDefault(True) self.cancelButton = QPushButton("Cancel") self.cancelButton.clicked.connect(self.cancel_callback) hbl5 = QHBoxLayout() hbl5.addStretch(1) hbl5.addWidget(self.cancelButton) hbl5.addWidget(self.calculateButton) # Add calculation and buttons to popup box vbl = QVBoxLayout() vbl.addLayout(hbl1) vbl.addLayout(hbl_error) vbl.addLayout(hbl2) vbl.addLayout(hbl_examples) vbl.addLayout(hbl5) self.setLayout(vbl) self.setMaximumWidth(700) self.show() def calculate_callback(self): """ Callback for when they hit calculate :return: """ # Create the interpreter from asteval import Interpreter aeval = Interpreter() # Grab the calculation from the text box which the user wants to do calculation = str(self.calculation_text.text()) lhs = calculation.split('=')[0].strip() # Use the package asteval to do the calculation, we are going to # assume here that the lhs of the equals sign is going to be the output named variable try: if lhs in self.data_components: raise KeyError( '{} is already in the data components, use a different variable on the left hand side.' .format(lhs)) # Pull in the required data and run the calculation for dc in self.data_components: if dc in calculation: aeval.symtable[dc] = self.data[dc] aeval(calculation) # Pull out the output data and add to the proper drop-downs out_data = aeval.symtable[lhs] self.data.add_component(out_data, lhs) # Add the new data to the list of available data for arithemitic operations self.data_components.append(lhs) self.close() except KeyError as e: self.calculation_text.setStyleSheet( "background-color: rgba(255, 0, 0, 128);") # Display the error in the Qt popup if aeval.error_msg: self.error_label_text.setText('{}'.format(aeval.error_msg)) else: self.error_label_text.setText('{}'.format(e)) self.error_label_text.setStyleSheet("color: rgba(255, 0, 0, 128)") def cancel_callback(self, caller=0): """ Cancel callback when the person hits the cancel button :param caller: :return: """ self.close() def keyPressEvent(self, e): if e.key() == Qt.Key_Escape: self.cancel_callback()
class QtDimSliderWidget(QWidget): """Compound widget to hold the label, slider and play button for an axis. These will usually be instantiated in the QtDims._create_sliders method. This widget *must* be instantiated with a parent QtDims. """ axis_label_changed = Signal(int, str) # axis, label fps_changed = Signal(float) mode_changed = Signal(str) range_changed = Signal(tuple) play_started = Signal() play_stopped = Signal() def __init__(self, parent: QWidget, axis: int): super().__init__(parent=parent) self.axis = axis self.qt_dims = parent self.dims = parent.dims self.axis_label = None self.slider = None self.play_button = None self.curslice_label = QLineEdit(self) self.curslice_label.setToolTip(f'Current slice for axis {axis}') # if we set the QIntValidator to actually reflect the range of the data # then an invalid (i.e. too large) index doesn't actually trigger the # editingFinished event (the user is expected to change the value)... # which is confusing to the user, so instead we use an IntValidator # that makes sure the user can only enter integers, but we do our own # value validation in self.change_slice self.curslice_label.setValidator(QIntValidator(0, 999999)) self.curslice_label.editingFinished.connect(self._set_slice_from_label) self.totslice_label = QLabel(self) self.totslice_label.setToolTip(f'Total slices for axis {axis}') self.curslice_label.setObjectName('slice_label') self.totslice_label.setObjectName('slice_label') sep = QFrame(self) sep.setFixedSize(1, 14) sep.setObjectName('slice_label_sep') self._fps = 10 self._minframe = None self._maxframe = None self._loop_mode = LoopMode.LOOP layout = QHBoxLayout() self._create_axis_label_widget() self._create_range_slider_widget() self._create_play_button_widget() layout.addWidget(self.axis_label) layout.addWidget(self.play_button) layout.addWidget(self.slider, stretch=1) layout.addWidget(self.curslice_label) layout.addWidget(sep) layout.addWidget(self.totslice_label) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(2) self.setLayout(layout) self.dims.events.axis_labels.connect(self._pull_label) def _set_slice_from_label(self): """Update the dims point based on the curslice_label.""" val = int(self.curslice_label.text()) max_allowed = self.dims.max_indices[self.axis] if val > max_allowed: val = max_allowed self.curslice_label.setText(str(val)) self.curslice_label.clearFocus() self.qt_dims.setFocus() self.dims.set_point(self.axis, val) def _create_axis_label_widget(self): """Create the axis label widget which accompanies its slider.""" label = QLineEdit(self) label.setObjectName('axis_label') # needed for _update_label label.setText(self.dims.axis_labels[self.axis]) label.home(False) label.setToolTip('Edit to change axis label') label.setAcceptDrops(False) label.setEnabled(True) label.setAlignment(Qt.AlignRight) label.setContentsMargins(0, 0, 2, 0) label.textChanged.connect(self._update_label) label.editingFinished.connect(self._clear_label_focus) self.axis_label = label def _create_range_slider_widget(self): """Creates a range slider widget for a given axis.""" _range = self.dims.range[self.axis] # Set the maximum values of the range slider to be one step less than # the range of the layer as otherwise the slider can move beyond the # shape of the layer as the endpoint is included _range = (_range[0], _range[1] - _range[2], _range[2]) point = self.dims.point[self.axis] slider = ModifiedScrollBar(Qt.Horizontal) slider.setFocusPolicy(Qt.NoFocus) slider.setMinimum(int(_range[0])) slider.setMaximum(int(_range[1])) slider.setSingleStep(int(_range[2])) slider.setPageStep(int(_range[2])) slider.setValue(point) # Listener to be used for sending events back to model: slider.valueChanged.connect( lambda value: self.dims.set_point(self.axis, value)) def slider_focused_listener(): self.qt_dims.last_used = self.axis # linking focus listener to the last used: slider.sliderPressed.connect(slider_focused_listener) self.slider = slider def _create_play_button_widget(self): """Creates the actual play button, which has the modal popup.""" self.play_button = QtPlayButton(self.qt_dims, self.axis) self.play_button.mode_combo.activated[str].connect( lambda x: self.__class__.loop_mode.fset( self, LoopMode(x.replace(' ', '_')))) def fps_listener(*args): fps = self.play_button.fpsspin.value() fps *= -1 if self.play_button.reverse_check.isChecked() else 1 self.__class__.fps.fset(self, fps) self.play_button.fpsspin.editingFinished.connect(fps_listener) self.play_button.reverse_check.stateChanged.connect(fps_listener) self.play_stopped.connect(self.play_button._handle_stop) self.play_started.connect(self.play_button._handle_start) def _pull_label(self, event): """Updates the label LineEdit from the dims model.""" if event.axis == self.axis: label = self.dims.axis_labels[self.axis] self.axis_label.setText(label) self.axis_label_changed.emit(self.axis, label) def _update_label(self): """Update dimension slider label.""" with self.dims.events.axis_labels.blocker(): self.dims.set_axis_label(self.axis, self.axis_label.text()) self.axis_label_changed.emit(self.axis, self.axis_label.text()) def _clear_label_focus(self): """Clear focus from dimension slider label.""" self.axis_label.clearFocus() self.qt_dims.setFocus() def _update_range(self): """Updates range for slider.""" displayed_sliders = self.qt_dims._displayed_sliders _range = self.dims.range[self.axis] _range = (_range[0], _range[1] - _range[2], _range[2]) if _range not in (None, (None, None, None)): if _range[1] == 0: displayed_sliders[self.axis] = False self.qt_dims.last_used = None self.hide() else: if (not displayed_sliders[self.axis] and self.axis not in self.dims.displayed): displayed_sliders[self.axis] = True self.last_used = self.axis self.show() self.slider.setMinimum(int(_range[0])) self.slider.setMaximum(int(_range[1])) self.slider.setSingleStep(int(_range[2])) self.slider.setPageStep(int(_range[2])) maxi = self.dims.max_indices[self.axis] self.totslice_label.setText(str(int(maxi))) self.totslice_label.setAlignment(Qt.AlignLeft) self._update_slice_labels() else: displayed_sliders[self.axis] = False self.hide() def _update_slider(self): """Update dimension slider.""" mode = self.dims.mode[self.axis] if mode == DimsMode.POINT: self.slider.setValue(int(self.dims.point[self.axis])) self._update_slice_labels() def _update_slice_labels(self): """Update slice labels to match current dimension slider position.""" step = self.dims.range[self.axis][2] self.curslice_label.setText( str(int(self.dims.point[self.axis] // step))) self.curslice_label.setAlignment(Qt.AlignRight) @property def fps(self): """Frames per second for animation.""" return self._fps @fps.setter def fps(self, value): """Frames per second for animation. Paramters --------- value : float Frames per second for animation. """ self._fps = value self.play_button.fpsspin.setValue(abs(value)) self.play_button.reverse_check.setChecked(value < 0) self.fps_changed.emit(value) @property def loop_mode(self): """Loop mode for animation. Loop mode enumeration napari._qt._constants.LoopMode Available options for the loop mode string enumeration are: - LoopMode.ONCE Animation will stop once movie reaches the max frame (if fps > 0) or the first frame (if fps < 0). - LoopMode.LOOP Movie will return to the first frame after reaching the last frame, looping continuously until stopped. - LoopMode.BACK_AND_FORTH Movie will loop continuously until stopped, reversing direction when the maximum or minimum frame has been reached. """ return self._loop_mode @loop_mode.setter def loop_mode(self, value): """Loop mode for animation. Paramters --------- value : napari._qt._constants.LoopMode Loop mode for animation. Available options for the loop mode string enumeration are: - LoopMode.ONCE Animation will stop once movie reaches the max frame (if fps > 0) or the first frame (if fps < 0). - LoopMode.LOOP Movie will return to the first frame after reaching the last frame, looping continuously until stopped. - LoopMode.BACK_AND_FORTH Movie will loop continuously until stopped, reversing direction when the maximum or minimum frame has been reached. """ self._loop_mode = value self.play_button.mode_combo.setCurrentText(str(value)) self.mode_changed.emit(str(value)) @property def frame_range(self): """Frame range for animation, as (minimum_frame, maximum_frame).""" frame_range = (self._minframe, self._maxframe) frame_range = frame_range if any(frame_range) else None return frame_range @frame_range.setter def frame_range(self, value): """Frame range for animation, as (minimum_frame, maximum_frame). Paramters --------- value : tuple(int, int) Frame range as tuple/list with range (minimum_frame, maximum_frame) """ if not isinstance(value, (tuple, list, type(None))): raise TypeError('frame_range value must be a list or tuple') if value and not len(value) == 2: raise ValueError('frame_range must have a length of 2') if value is None: value = (None, None) self._minframe, self._maxframe = value self.range_changed.emit(tuple(value)) def _update_play_settings(self, fps, loop_mode, frame_range): """Update settings for animation. Parameters ---------- fps : float Frames per second to play the animation. loop_mode : napari._qt._constants.LoopMode Loop mode for animation. Available options for the loop mode string enumeration are: - LoopMode.ONCE Animation will stop once movie reaches the max frame (if fps > 0) or the first frame (if fps < 0). - LoopMode.LOOP Movie will return to the first frame after reaching the last frame, looping continuously until stopped. - LoopMode.BACK_AND_FORTH Movie will loop continuously until stopped, reversing direction when the maximum or minimum frame has been reached. frame_range : tuple(int, int) Frame range as tuple/list with range (minimum_frame, maximum_frame) """ if fps is not None: self.fps = fps if loop_mode is not None: self.loop_mode = loop_mode if frame_range is not None: self.frame_range = frame_range def _play( self, fps: Optional[float] = None, loop_mode: Optional[str] = None, frame_range: Optional[Tuple[int, int]] = None, ): """Animate (play) axis. Same API as QtDims.play() Putting the AnimationWorker logic here makes it easier to call QtDims.play(axis), or hit the keybinding, and have each axis remember it's own settings (fps, mode, etc...). Parameters ---------- fps : float Frames per second for animation. loop_mode : napari._qt._constants.LoopMode Loop mode for animation. Available options for the loop mode string enumeration are: - LoopMode.ONCE Animation will stop once movie reaches the max frame (if fps > 0) or the first frame (if fps < 0). - LoopMode.LOOP Movie will return to the first frame after reaching the last frame, looping continuously until stopped. - LoopMode.BACK_AND_FORTH Movie will loop continuously until stopped, reversing direction when the maximum or minimum frame has been reached. frame_range : tuple(int, int) Frame range as tuple/list with range (minimum_frame, maximum_frame) """ # having this here makes sure that using the QtDims.play() API # keeps the play preferences synchronized with the play_button.popup self._update_play_settings(fps, loop_mode, frame_range) # setting fps to 0 just stops the animation if fps == 0: return worker, thread = _new_worker_qthread( AnimationWorker, self, _start_thread=True, _connect={'frame_requested': self.qt_dims._set_frame}, ) worker.finished.connect(self.qt_dims.stop) thread.finished.connect(self.play_stopped.emit) self.play_started.emit() self.thread = thread return worker, thread
class CollapseCube(QDialog): def __init__(self, data, data_collection=[], allow_preview=False, parent=None): super(CollapseCube, self).__init__(parent) self.setWindowTitle("Collapse Cube Along Spectral Axis") # Get the data_components (e.g., FLUX, DQ, ERROR etc) # Using list comprehension to keep the order of the component_ids self.data_components = [ str(x).strip() for x in data.component_ids() if not x in data.coordinate_components ] self.setWindowFlags(self.windowFlags() | Qt.Tool) self.title = "Cube Collapse" self.data = data self.data_collection = data_collection self.parent = parent self._general_description = "Collapse the data cube over the spectral range based on the mathematical operation. The nearest index or wavelength will be chosen if a specified number is out of bounds" self._custom_description = "To use the spectral viewer to define a region to collapse over, cancel this, create an ROI and then select this Collapse Cube again." self.currentAxes = None self.currentKernel = None self.createUI() def createUI(self): """ Create the popup box with the calculation input area and buttons. :return: """ boldFont = QtGui.QFont() boldFont.setBold(True) # Create data component label and input box self.widget_desc = QLabel(self._general_description) self.widget_desc.setWordWrap(True) self.widget_desc.setFixedWidth(350) self.widget_desc.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hb_desc = QHBoxLayout() hb_desc.addWidget(self.widget_desc) # Create data component label and input box self.data_label = QLabel("Data:") self.data_label.setFixedWidth(100) self.data_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.data_label.setFont(boldFont) self.data_combobox = QComboBox() self.data_combobox.addItems([ str(x).strip() for x in self.data.component_ids() if not x in self.data.coordinate_components ]) self.data_combobox.setMinimumWidth(200) hb_data = QHBoxLayout() hb_data.addWidget(self.data_label) hb_data.addWidget(self.data_combobox) # Create operation label and input box self.operation_label = QLabel("Operation:") self.operation_label.setFixedWidth(100) self.operation_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.operation_label.setFont(boldFont) self.operation_combobox = QComboBox() self.operation_combobox.addItems(operations.keys()) self.operation_combobox.setMinimumWidth(200) hb_operation = QHBoxLayout() hb_operation.addWidget(self.operation_label) hb_operation.addWidget(self.operation_combobox) # Create region label and input box self.region_label = QLabel("region:") self.region_label.setFixedWidth(100) self.region_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.region_label.setFont(boldFont) self.region_combobox = QComboBox() # Get the Specviz regions and add them in to the Combo box for roi in self.parent.specviz._widget.roi_bounds: self.region_combobox.addItem("Specviz ROI ({:.3}, {:.3})".format( roi[0], roi[1])) self.region_combobox.addItems( ["Custom (Wavelengths)", "Custom (Indices)"]) self.region_combobox.setMinimumWidth(200) self.region_combobox.currentIndexChanged.connect( self._region_selection_change) hb_region = QHBoxLayout() hb_region.addWidget(self.region_label) hb_region.addWidget(self.region_combobox) # Create error label self.error_label = QLabel("") self.error_label.setFixedWidth(100) self.error_label_text = QLabel("") self.error_label_text.setMinimumWidth(200) self.error_label_text.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hbl_error = QHBoxLayout() hbl_error.addWidget(self.error_label) hbl_error.addWidget(self.error_label_text) # Create start label and input box self.start_label = QLabel("Start:") self.start_label.setFixedWidth(100) self.start_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.start_label.setFont(boldFont) self.start_text = QLineEdit() self.start_text.setMinimumWidth(200) self.start_text.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hb_start = QHBoxLayout() hb_start.addWidget(self.start_label) hb_start.addWidget(self.start_text) # Create end label and input box self.end_label = QLabel("End:") self.end_label.setFixedWidth(100) self.end_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.end_label.setFont(boldFont) self.end_text = QLineEdit() self.end_text.setMinimumWidth(200) self.end_text.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hb_end = QHBoxLayout() hb_end.addWidget(self.end_label) hb_end.addWidget(self.end_text) # Create Calculate and Cancel buttons self.calculateButton = QPushButton("Calculate") self.calculateButton.clicked.connect(self.calculate_callback) self.calculateButton.setDefault(True) self.cancelButton = QPushButton("Cancel") self.cancelButton.clicked.connect(self.cancel_callback) hb_buttons = QHBoxLayout() hb_buttons.addStretch(1) hb_buttons.addWidget(self.cancelButton) hb_buttons.addWidget(self.calculateButton) # # Sigma clipping # vbox_sigma_clipping = QVBoxLayout() self.sigma_description = QLabel( "Sigma clipping is implemented using <a href='http://docs.astropy.org/en/stable/api/astropy.stats.sigma_clip.html'>astropy.stats.sigma_clip</a>. Empty values will use defaults listed on the webpage, <b>but</b> if the first sigma is empty, then no clipping will be done." ) self.sigma_description.setWordWrap(True) hb_sigma = QHBoxLayout() hb_sigma.addWidget(self.sigma_description) vbox_sigma_clipping.addLayout(hb_sigma) # Create sigma self.sigma_label = QLabel("Sigma:") self.sigma_label.setFixedWidth(100) self.sigma_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.sigma_label.setFont(boldFont) self.sigma_text = QLineEdit() self.sigma_text.setMinimumWidth(200) self.sigma_text.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hb_sigma = QHBoxLayout() hb_sigma.addWidget(self.sigma_label) hb_sigma.addWidget(self.sigma_text) vbox_sigma_clipping.addLayout(hb_sigma) # Create sigma_lower self.sigma_lower_label = QLabel("Sigma Lower:") self.sigma_lower_label.setFixedWidth(100) self.sigma_lower_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.sigma_lower_label.setFont(boldFont) self.sigma_lower_text = QLineEdit() self.sigma_lower_text.setMinimumWidth(200) self.sigma_lower_text.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hb_sigma_lower = QHBoxLayout() hb_sigma_lower.addWidget(self.sigma_lower_label) hb_sigma_lower.addWidget(self.sigma_lower_text) vbox_sigma_clipping.addLayout(hb_sigma_lower) # Create sigma_upper self.sigma_upper_label = QLabel("Sigma Upper:") self.sigma_upper_label.setFixedWidth(100) self.sigma_upper_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.sigma_upper_label.setFont(boldFont) self.sigma_upper_text = QLineEdit() self.sigma_upper_text.setMinimumWidth(200) self.sigma_upper_text.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hb_sigma_upper = QHBoxLayout() hb_sigma_upper.addWidget(self.sigma_upper_label) hb_sigma_upper.addWidget(self.sigma_upper_text) vbox_sigma_clipping.addLayout(hb_sigma_upper) # Create sigma_iters self.sigma_iters_label = QLabel("Sigma Iterations:") self.sigma_iters_label.setFixedWidth(100) self.sigma_iters_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.sigma_iters_label.setFont(boldFont) self.sigma_iters_text = QLineEdit() self.sigma_iters_text.setMinimumWidth(200) self.sigma_iters_text.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hb_sigma_iters = QHBoxLayout() hb_sigma_iters.addWidget(self.sigma_iters_label) hb_sigma_iters.addWidget(self.sigma_iters_text) vbox_sigma_clipping.addLayout(hb_sigma_iters) # Add calculation and buttons to popup box vbl = QVBoxLayout() vbl.addLayout(hb_desc) vbl.addLayout(hb_data) vbl.addLayout(hb_operation) vbl.addLayout(hb_region) vbl.addLayout(hb_start) vbl.addLayout(hb_end) vbl.addLayout(vbox_sigma_clipping) vbl.addLayout(hbl_error) vbl.addLayout(hb_buttons) self.setLayout(vbl) self.setMaximumWidth(700) self.show() # Fire the callback to set the default values for everything self._region_selection_change(0) def _region_selection_change(self, index): """ Callback for a change on the region selection combo box. :param newvalue: :return: """ newvalue = self.region_combobox.currentText() # First, let's see if this is one of the custom options if 'Custom' in newvalue and 'Wavelength' in newvalue: # Custom Wavelengths self.hide_start_end(False) self.start_label.setText("Start Wavelength:") self.end_label.setText("End Wavelength:") elif 'Custom' in newvalue and 'Indices' in newvalue: # Custom indices self.hide_start_end(False) self.start_label.setText("Start Index:") self.end_label.setText("End Index:") else: # Region defined in specviz self.hide_start_end(True) # We are going to store the start and end wavelengths in the text boxes even though # they are hidden. This way we can use the text boxes later as a hidden storage container. # TODO: Should probably save the ROIs so the start and end values are more accurate. regex = r"-?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?" floating = re.findall(regex, newvalue) self.start_text.setText(floating[0]) self.end_text.setText(floating[1]) # Let's update the text on the widget if 'Custom' in newvalue: self.widget_desc.setText(self._general_description + "\n\n" + self._custom_description) else: self.widget_desc.setText(self._general_description) def hide_start_end(self, dohide): """ Show or hide the start and end indices depending if the region is defined from the specviz plot OR if we are using custom limits. :param dohide: :return: """ if dohide: self.start_label.hide() self.start_text.hide() self.end_label.hide() self.end_text.hide() else: self.start_label.show() self.start_text.show() self.end_label.show() self.end_text.show() def calculate_callback(self): """ Callback for when they hit calculate :return: """ # Grab the values of interest data_name = self.data_combobox.currentText() start_value = self.start_text.text().strip() end_value = self.end_text.text().strip() self.error_label_text.setText(' ') self.error_label_text.setStyleSheet("color: rgba(255, 0, 0, 128)") # Sanity checks first if not start_value and not end_value: self.start_label.setStyleSheet("color: rgba(255, 0, 0, 128)") self.end_label.setStyleSheet("color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'Must set at least one of start or end value') return wavelengths = np.array(self.parent._wavelengths) # If indicies, get them and check to see if the inputs are good. if 'Indices' in self.region_combobox.currentText(): if len(start_value) == 0: start_index = 0 else: try: start_index = int(start_value) except ValueError as e: self.start_label.setStyleSheet( "color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'Start value must be an integer') return if start_index < 0: start_index = 0 if len(end_value) == 0: end_index = len(wavelengths) - 1 else: try: end_index = int(end_value) except ValueError as e: self.end_label.setStyleSheet("color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'End value must be an integer') return if end_index > len(wavelengths) - 1: end_index = len(wavelengths) - 1 else: # Wavelength inputs if len(start_value) == 0: start_index = 0 else: # convert wavelength to float value try: start_value = float(start_value) except ValueError as e: self.start_label.setStyleSheet( "color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'Start value must be a floating point number') return # Look up index start_index = np.argsort(np.abs(wavelengths - start_value))[0] if len(end_value) == 0: end_index = len(wavelengths) - 1 else: # convert wavelength to float value try: end_value = float(end_value) except ValueError as e: self.end_label.setStyleSheet("color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'End value must be a floating point number') return # Look up index end_index = np.argsort(np.abs(wavelengths - end_value))[0] # Check to make sure at least one of start or end is within the range of the wavelengths. if (start_index < 0 and end_index < 0) or (start_index > len(wavelengths) and end_index > len(wavelengths)): self.start_label.setStyleSheet("color: rgba(255, 0, 0, 128)") self.end_label.setStyleSheet("color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'Can not have both start and end outside of the wavelength range.' ) return if start_index > end_index: self.start_label.setStyleSheet("color: rgba(255, 0, 0, 128)") self.end_label.setStyleSheet("color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'Start value must be less than end value') return # Check to see if the wavelength (indices) are the same. if start_index == end_index: self.start_label.setStyleSheet("color: rgba(255, 0, 0, 128)") self.end_label.setStyleSheet("color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'Can not have both start and end wavelengths be the same.') return # Set the start and end values in the text boxes -- in case they enter one way out of range then # we'll fix it. ts = start_index if 'Indices' in self.region_combobox.currentText( ) else wavelengths[start_index] self.start_text.setText('{}'.format(ts)) te = end_index if 'Indices' in self.region_combobox.currentText( ) else wavelengths[end_index] self.end_text.setText('{}'.format(te)) data_name = self.data_combobox.currentText() operation = self.operation_combobox.currentText() # Do calculation if we got this far wavelengths, new_component = collapse_cube(self.data[data_name], data_name, self.data.coords.wcs, operation, start_index, end_index) # Get the start and end wavelengths from the newly created spectral cube and use for labeling the cube. # Convert to the current units. start_wavelength = wavelengths[0].to( self.parent._units_controller._new_units) end_wavelength = wavelengths[-1].to( self.parent._units_controller._new_units) label = '{}-collapse-{} ({:0.3}, {:0.3})'.format( data_name, operation, start_wavelength, end_wavelength) # Apply sigma clipping sigma = self.sigma_text.text().strip() if len(sigma) > 0: try: sigma = float(sigma) except ValueError as e: self.sigma_label.setStyleSheet("color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'If sigma set, it must be a floating point number') return sigma_lower = self.sigma_lower_text.text().strip() if len(sigma_lower) > 0: try: sigma_lower = float(sigma_lower) except ValueError as e: self.sigma_lower_label.setStyleSheet( "color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'If sigma lower set, it must be a floating point number' ) return else: sigma_lower = None sigma_upper = self.sigma_upper_text.text().strip() if len(sigma_upper) > 0: try: sigma_upper = float(sigma_upper) except ValueError as e: self.sigma_upper_label.setStyleSheet( "color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'If sigma upper set, it must be a floating point number' ) return else: sigma_upper = None sigma_iters = self.sigma_iters_text.text().strip() if len(sigma_iters) > 0: try: sigma_iters = float(sigma_iters) except ValueError as e: self.sigma_iters_label.setStyleSheet( "color: rgba(255, 0, 0, 128)") self.error_label_text.setText( 'If sigma iters set, it must be a floating point number' ) return else: sigma_iters = None new_component = sigma_clip(new_component, sigma=sigma, sigma_lower=sigma_lower, sigma_upper=sigma_upper, iters=sigma_iters) # Add to label so it is clear which overlay/component is which if sigma: label += ' sigma={}'.format(sigma) if sigma_lower: label += ' sigma_lower={}'.format(sigma_lower) if sigma_upper: label += ' sigma_upper={}'.format(sigma_upper) if sigma_iters: label += ' sigma_iters={}'.format(sigma_iters) # Add new overlay/component to cubeviz. We add this both to the 2D # container Data object and also as an overlay. In future we might be # able to use the 2D container Data object for the overlays directly. add_to_2d_container(self.parent, self.data, new_component, label) self.parent.add_overlay(new_component, label) self.close() # Show new dialog self.final_dialog(label) def final_dialog(self, label): """ Final dialog that to show where the calculated collapsed cube was put. :param label: :return: """ final_dialog = QDialog() # Create data component label and input box widget_desc = QLabel( 'The collapsed cube was added as an overlay with label "{}"'. format(label)) widget_desc.setWordWrap(True) widget_desc.setFixedWidth(350) widget_desc.setAlignment((Qt.AlignLeft | Qt.AlignTop)) hb_desc = QHBoxLayout() hb_desc.addWidget(widget_desc) # Create Ok button okButton = QPushButton("Ok") okButton.clicked.connect(lambda: final_dialog.close()) okButton.setDefault(True) hb_buttons = QHBoxLayout() hb_buttons.addStretch(1) hb_buttons.addWidget(okButton) # Add description and buttons to popup box vbl = QVBoxLayout() vbl.addLayout(hb_desc) vbl.addLayout(hb_buttons) final_dialog.setLayout(vbl) final_dialog.setMaximumWidth(400) final_dialog.show() def cancel_callback(self, caller=0): """ Cancel callback when the person hits the cancel button :param caller: :return: """ self.close() def keyPressEvent(self, e): if e.key() == Qt.Key_Escape: self.cancel_callback()