class AcceptFiles(QDialog): def __init__(self, files): super().__init__() self.ok = QPushButton("Add", self) self.ok.clicked.connect(self.accept) discard = QPushButton("Discard", self) discard.clicked.connect(self.close) self.files = QListWidget(self) self.files.setSelectionMode(QAbstractItemView.ExtendedSelection) for file_name in files: self.files.addItem(file_name) for i in range(self.files.count()): self.files.item(i).setSelected(True) self.ok.setDefault(True) self.ok.setAutoDefault(True) layout = QVBoxLayout() layout.addWidget(QLabel("Found {} files".format(len(files)))) layout.addWidget(self.files) butt_layout = QHBoxLayout() butt_layout.addWidget(discard) butt_layout.addStretch() butt_layout.addWidget(self.ok) layout.addLayout(butt_layout) self.setLayout(layout) def selection_changed(self): if self.files.selectedItems().count() == 0: self.ok.setDisabled(True) else: self.ok.setEnabled(True) def get_files(self): return [str(item.text()) for item in self.files.selectedItems()]
class DuplicateNormConfig(SiriusDialog): """Auxiliary window to duplicate a normalized config.""" insertConfig = Signal(float, str, dict) def __init__(self, parent, psname2strength): """Initialize object.""" super().__init__(parent) self.setObjectName('BOApp') self.setWindowTitle('Duplicate Normalized Configuration') self.psname2strength = psname2strength self._setupUi() def _setupUi(self): self.le_label = QLineEdit(self) self.sb_time = QDoubleSpinBoxPlus(self) self.sb_time.setMaximum(490) self.sb_time.setDecimals(3) self.bt_duplic = QPushButton('Duplicate', self) self.bt_duplic.setAutoDefault(False) self.bt_duplic.setDefault(False) self.bt_duplic.clicked.connect(self._emitConfigData) self.bt_cancel = QPushButton('Cancel', self) self.bt_cancel.setAutoDefault(False) self.bt_cancel.setDefault(False) self.bt_cancel.clicked.connect(self.close) # layout lay = QGridLayout() lay.setVerticalSpacing(15) lay.addWidget(QLabel('<h4>Duplicate Normalized Configuration</h4>', self), 0, 0, 1, 2, alignment=Qt.AlignCenter) lay.addWidget( QLabel( 'Choose a label and a time to insert\n' 'the new configuration:', self), 1, 0, 1, 2) lay.addWidget(QLabel('Label: ', self), 2, 0) lay.addWidget(self.le_label, 2, 1) lay.addWidget(QLabel('Time [ms]: ', self), 3, 0) lay.addWidget(self.sb_time, 3, 1) lay.addWidget(self.bt_cancel, 4, 0) lay.addWidget(self.bt_duplic, 4, 1) self.setLayout(lay) def _emitConfigData(self): time = self.sb_time.value() label = self.le_label.text() psname2strength = self.psname2strength self.insertConfig.emit(time, label, psname2strength) self.close()
class Ui_LoginWidget: def setupUi(self, widget: QWidget): self.addressLineEdit = QLineEdit('localhost') self.portLineEdit = QLineEdit('8080') self.nicknameLineEdit = QLineEdit() formLayout = QFormLayout() formLayout.addRow('&Nickname', self.nicknameLineEdit) formLayout.addRow('Host &address', self.addressLineEdit) formLayout.addRow('Host &port', self.portLineEdit) self.pushButton = QPushButton('&Chat!') self.pushButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.pushButton.setDefault(True) layout = QVBoxLayout(widget) layout.addLayout(formLayout) layout.addWidget(self.pushButton, 1, Qt.AlignTop | Qt.AlignRight)
def createTopRightGroupBox(self): self.topRightGroupBox = QGroupBox("Group 2") defaultPushButton = QPushButton("Default Push Button") defaultPushButton.setDefault(True) togglePushButton = QPushButton("Toggle Push Button") togglePushButton.setCheckable(True) togglePushButton.setChecked(True) flatPushButton = QPushButton("Flat Push Button") flatPushButton.setFlat(True) layout = QVBoxLayout() layout.addWidget(defaultPushButton) layout.addWidget(togglePushButton) layout.addWidget(flatPushButton) layout.addStretch(1) self.topRightGroupBox.setLayout(layout)
def __init__(self, parent=None, current_key='', selectable_keys=[]): QWidget.__init__(self, parent) self.setMinimumWidth(450) self.setMinimumHeight(200) self._dynamic = False self.setWindowTitle("Copy the style of {0} to other keys".format(current_key)) self.activateWindow() layout = QFormLayout(self) self._ert = ERT.ert """:type: res.enkf.enkf_main.EnKFMain""" self.model = self._ert self._filter_popup = FilterPopup(self) self._filter_popup.filterSettingsChanged.connect(self.filterSettingsChanged) filter_popup_button = QToolButton() filter_popup_button.setIcon(resourceIcon("ide/cog_edit.png")) filter_popup_button.clicked.connect(self._filter_popup.show) self._list_model = FilterableKwListModel(self._ert, selectable_keys) self._list_model.unselectAll() self._cl = CheckList(self._list_model, custom_filter_button=filter_popup_button) layout.addWidget(self._cl) apply_button = QPushButton("Apply") apply_button.clicked.connect(self.accept) apply_button.setDefault(True) close_button = QPushButton("Close") close_button.setToolTip("Hide this dialog") close_button.clicked.connect(self.reject) button_layout = QHBoxLayout() button_layout.addStretch() button_layout.addWidget(apply_button) button_layout.addWidget(close_button) layout.addRow(button_layout)
def _open_matrix_sel(self): wid = QDialog(self) wid.setObjectName(self._csorb.acc + 'App') wid.setLayout(QVBoxLayout()) cbbox = QComboBox(wid) cbbox.setEditable(True) cbbox.setMaxVisibleItems(10) corrnames = self._csorb.ch_names + self._csorb.cv_names if self._csorb.acc in {'SI', 'BO'}: corrnames.append('RF') cbbox.addItems(corrnames) wid.layout().addWidget(QLabel('Choose the corrector:', wid)) wid.layout().addWidget(cbbox) ledit = QDoubleSpinBoxPlus(wid) ledit.setMinimum(float('-inf')) ledit.setMaximum(float('inf')) ledit.setValue(1.0) wid.layout().addWidget(QLabel('Choose the Kick [urad]:', wid)) wid.layout().addWidget(ledit) ledit.valueChanged.connect(_part(self._accept_mat_sel, ledit, cbbox)) cbbox.currentIndexChanged.connect( _part(self._accept_mat_sel, ledit, cbbox)) hlay = QHBoxLayout() cancel = QPushButton('Cancel', wid) confirm = QPushButton('Ok', wid) cancel.clicked.connect(wid.reject) confirm.clicked.connect(wid.accept) confirm.clicked.connect(_part(self._accept_mat_sel, ledit, cbbox)) confirm.setDefault(True) hlay.addStretch() hlay.addWidget(cancel) hlay.addStretch() hlay.addWidget(confirm) hlay.addStretch() wid.layout().addItem(hlay) res = wid.exec_() if res != QDialog.Accepted: self._reset_orbit()
def __init__(self, parent, current_key, key_defs): QWidget.__init__(self, parent) self.setMinimumWidth(450) self.setMinimumHeight(200) self._dynamic = False self.setWindowTitle(f"Copy the style of {current_key} to other keys") self.activateWindow() layout = QFormLayout(self) self._filter_popup = FilterPopup(self, key_defs) self._filter_popup.filterSettingsChanged.connect( self.filterSettingsChanged) filter_popup_button = QToolButton() filter_popup_button.setIcon(resourceIcon("filter_list.svg")) filter_popup_button.clicked.connect(self._filter_popup.show) self._list_model = FilterableKwListModel(key_defs) self._list_model.unselectAll() self._cl = CheckList(self._list_model, custom_filter_button=filter_popup_button) layout.addWidget(self._cl) apply_button = QPushButton("Apply") apply_button.clicked.connect(self.accept) apply_button.setDefault(True) close_button = QPushButton("Close") close_button.setToolTip("Hide this dialog") close_button.clicked.connect(self.reject) button_layout = QHBoxLayout() button_layout.addStretch() button_layout.addWidget(apply_button) button_layout.addWidget(close_button) layout.addRow(button_layout)
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 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, self._extra_message)) 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()
class MomentMapsGUI(QDialog): def __init__(self, data, data_collection, parent=None): super(MomentMapsGUI, 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.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) hbl1 = QHBoxLayout() hbl1.addWidget(self.data_label) hbl1.addWidget(self.data_combobox) # Create calculation label and input box self.order_label = QLabel("Order:") self.order_label.setFixedWidth(100) self.order_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.order_label.setFont(boldFont) self.order_combobox = QComboBox() self.order_combobox.addItems(["1", "2", "3", "4", "5", "6", "7", "8"]) self.order_combobox.setMinimumWidth(200) hbl2 = QHBoxLayout() hbl2.addWidget(self.order_label) hbl2.addWidget(self.order_combobox) # 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(hbl2) vbl.addLayout(hbl5) self.setLayout(vbl) self.setMaximumWidth(700) self.show() def calculate_callback(self): """ Callback for when they hit calculate :return: """ # Determine the data component and order order = int(self.order_combobox.currentText()) data_name = self.data_combobox.currentText() # Grab spectral-cube import spectral_cube cube = spectral_cube.SpectralCube(self.data[data_name], wcs=self.data.coords.wcs) # 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: cube_moment = cube.moment(order=order, axis=0) label = '{}-moment-{}'.format(data_name, order) self.parent.add_overlay(cube_moment.value, label) except Exception as e: print('Error {}'.format(e)) self.close() 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 TextEditor(BaseDialog): """Array Editor Dialog""" def __init__(self, text, title='', font=None, parent=None, readonly=False): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.text = None self.btn_save_and_close = None # Display text as unicode if it comes as bytes, so users see # its right representation if is_binary_string(text): self.is_binary = True text = to_text_string(text, 'utf8') else: self.is_binary = False self.layout = QVBoxLayout() self.setLayout(self.layout) # Text edit self.edit = QTextEdit(parent) self.edit.setReadOnly(readonly) self.edit.textChanged.connect(self.text_changed) self.edit.setPlainText(text) if font is None: font = get_font() self.edit.setFont(font) self.layout.addWidget(self.edit) # Buttons configuration btn_layout = QHBoxLayout() btn_layout.addStretch() if not readonly: self.btn_save_and_close = QPushButton(_('Save and Close')) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_('Close')) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) self.layout.addLayout(btn_layout) # Make the dialog act as a window self.setWindowFlags(Qt.Window) self.setWindowIcon(ima.icon('edit')) if title: try: unicode_title = to_text_string(title) except UnicodeEncodeError: unicode_title = u'' else: unicode_title = u'' self.setWindowTitle(_("Text editor") + \ u"%s" % (u" - " + unicode_title if unicode_title else u"")) @Slot() def text_changed(self): """Text has changed""" # Save text as bytes, if it was initially bytes if self.is_binary: self.text = to_binary_string(self.edit.toPlainText(), 'utf8') else: self.text = to_text_string(self.edit.toPlainText()) if self.btn_save_and_close: self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) def get_value(self): """Return modified text""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.text def setup_and_check(self, value): """Verify if TextEditor is able to display strings passed to it.""" try: to_text_string(value, 'utf8') return True except: return False
class MomentMapsGUI(QDialog): def __init__(self, data, data_collection, parent=None): super(MomentMapsGUI, 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.data = data self.data_collection = data_collection self.parent = parent self.label = '' self.calculateButton = None self.cancelButton = None def display(self): """ Create the popup box with the calculation input area and buttons. :return: """ self.setWindowFlags(self.windowFlags() | Qt.Tool) self.setWindowTitle("Create Moment Map") boldFont = QtGui.QFont() boldFont.setBold(True) # Create calculation 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) hbl1 = QHBoxLayout() hbl1.addWidget(self.data_label) hbl1.addWidget(self.data_combobox) # Create calculation label and input box self.order_label = QLabel("Order:") self.order_label.setFixedWidth(100) self.order_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.order_label.setFont(boldFont) self.order_combobox = QComboBox() self.order_combobox.addItems(["1", "2", "3", "4", "5", "6", "7", "8"]) self.order_combobox.setMinimumWidth(200) hbl2 = QHBoxLayout() hbl2.addWidget(self.order_label) hbl2.addWidget(self.order_combobox) # 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(hbl2) vbl.addLayout(hbl5) self.setLayout(vbl) self.setMaximumWidth(700) self.show() def do_calculation(self, order, data_name): # Grab spectral-cube import spectral_cube cube = spectral_cube.SpectralCube(self.data[data_name], wcs=self.data.coords.wcs) cube_moment = cube.moment(order=order, axis=0) self.label = '{}-moment-{}'.format(data_name, order) # 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, cube_moment.value, cube_moment.unit, self.label) # Going to pass in just the value into the overlay as the units aren't # currently used for the overlay area. BUT, this is probably not the # best way to do this. self.parent.add_overlay(cube_moment.value, self.label, display_now=False) def calculate_callback(self): """ Callback for when they hit calculate :return: """ # Determine the data component and order order = int(self.order_combobox.currentText()) data_name = self.data_combobox.currentText() try: self.do_calculation(order, data_name) except Exception as e: show_error_message(str(e), 'Moment Map Error', parent=self) self.close() 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 ProjectDialog(QDialog): """Project creation dialog.""" sig_project_creation_requested = Signal(str, str, object) """ This signal is emitted to request the Projects plugin the creation of a project. Parameters ---------- project_path: str Location of project. project_type: str Type of project as defined by project types. project_packages: object Package to install. Currently not in use. """ def __init__(self, parent, project_types): """Project creation dialog.""" super(ProjectDialog, self).__init__(parent=parent) self.plugin = parent self._project_types = project_types self.project_data = {} # Variables current_python_version = '.'.join([to_text_string(sys.version_info[0]), to_text_string(sys.version_info[1])]) python_versions = ['2.7', '3.4', '3.5'] if current_python_version not in python_versions: python_versions.append(current_python_version) python_versions = sorted(python_versions) self.project_name = None self.location = get_home_dir() # Widgets self.groupbox = QGroupBox() self.radio_new_dir = QRadioButton(_("New directory")) self.radio_from_dir = QRadioButton(_("Existing directory")) self.label_project_name = QLabel(_('Project name')) self.label_location = QLabel(_('Location')) self.label_project_type = QLabel(_('Project type')) self.label_python_version = QLabel(_('Python version')) self.text_project_name = QLineEdit() self.text_location = QLineEdit(get_home_dir()) self.combo_project_type = QComboBox() self.combo_python_version = QComboBox() self.label_information = QLabel("") self.button_select_location = QToolButton() self.button_cancel = QPushButton(_('Cancel')) self.button_create = QPushButton(_('Create')) self.bbox = QDialogButtonBox(Qt.Horizontal) self.bbox.addButton(self.button_cancel, QDialogButtonBox.ActionRole) self.bbox.addButton(self.button_create, QDialogButtonBox.ActionRole) # Widget setup self.combo_python_version.addItems(python_versions) self.radio_new_dir.setChecked(True) self.text_location.setEnabled(True) self.text_location.setReadOnly(True) self.button_select_location.setIcon(get_std_icon('DirOpenIcon')) self.button_cancel.setDefault(True) self.button_cancel.setAutoDefault(True) self.button_create.setEnabled(False) for (id_, name) in [(pt_id, pt.get_name()) for pt_id, pt in project_types.items()]: self.combo_project_type.addItem(name, id_) self.combo_python_version.setCurrentIndex( python_versions.index(current_python_version)) self.setWindowTitle(_('Create new project')) self.setFixedWidth(500) self.label_python_version.setVisible(False) self.combo_python_version.setVisible(False) # Layouts layout_top = QHBoxLayout() layout_top.addWidget(self.radio_new_dir) layout_top.addWidget(self.radio_from_dir) layout_top.addStretch(1) self.groupbox.setLayout(layout_top) layout_grid = QGridLayout() layout_grid.addWidget(self.label_project_name, 0, 0) layout_grid.addWidget(self.text_project_name, 0, 1, 1, 2) layout_grid.addWidget(self.label_location, 1, 0) layout_grid.addWidget(self.text_location, 1, 1) layout_grid.addWidget(self.button_select_location, 1, 2) layout_grid.addWidget(self.label_project_type, 2, 0) layout_grid.addWidget(self.combo_project_type, 2, 1, 1, 2) layout_grid.addWidget(self.label_python_version, 3, 0) layout_grid.addWidget(self.combo_python_version, 3, 1, 1, 2) layout_grid.addWidget(self.label_information, 4, 0, 1, 3) layout = QVBoxLayout() layout.addWidget(self.groupbox) layout.addSpacing(10) layout.addLayout(layout_grid) layout.addStretch() layout.addSpacing(20) layout.addWidget(self.bbox) self.setLayout(layout) # Signals and slots self.button_select_location.clicked.connect(self.select_location) self.button_create.clicked.connect(self.create_project) self.button_cancel.clicked.connect(self.close) self.radio_from_dir.clicked.connect(self.update_location) self.radio_new_dir.clicked.connect(self.update_location) self.text_project_name.textChanged.connect(self.update_location) def select_location(self): """Select directory.""" location = osp.normpath( getexistingdirectory( self, _("Select directory"), self.location, ) ) if location: if is_writable(location): self.location = location self.update_location() def update_location(self, text=''): """Update text of location.""" self.text_project_name.setEnabled(self.radio_new_dir.isChecked()) name = self.text_project_name.text().strip() if name and self.radio_new_dir.isChecked(): path = osp.join(self.location, name) self.button_create.setDisabled(os.path.isdir(path)) elif self.radio_from_dir.isChecked(): self.button_create.setEnabled(True) path = self.location else: self.button_create.setEnabled(False) path = self.location self.text_location.setText(path) # Validate name with the method from the currently selected project project_type_id = self.combo_project_type.currentData() validate_func = self._project_types[project_type_id].validate_name validated, msg = validate_func(path, name) msg = "" if validated else msg self.label_information.setText(msg) self.button_create.setEnabled(validated) def create_project(self): """Create project.""" self.project_data = { "root_path": self.text_location.text(), "project_type": self.combo_project_type.currentData(), } self.sig_project_creation_requested.emit( self.text_location.text(), self.combo_project_type.currentData(), [], ) self.accept()
class CustomizePlotDialog(QDialog): applySettings = Signal() undoSettings = Signal() redoSettings = Signal() resetSettings = Signal() copySettings = Signal(str) copySettingsToOthers = Signal(list) def __init__(self, title, parent=None, key=''): QDialog.__init__(self, parent) self.setWindowTitle(title) self._ert = ERT.ert """:type: res.enkf.enkf_main.EnKFMain""" self.key_manager = self._ert.getKeyManager() """:type: res.enkf.key_manager.KeyManager """ self.current_key = key self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowCloseButtonHint) self._tab_map = {} self._tab_order = [] layout = QVBoxLayout() self._tabs = QTabWidget() layout.addWidget(self._tabs) layout.setSizeConstraint(QLayout.SetFixedSize) # not resizable!!! self._button_layout = QHBoxLayout() self._reset_button = QToolButton() self._reset_button.setIcon(resourceIcon("update.png")) self._reset_button.setToolTip("Reset all settings back to default") self._reset_button.clicked.connect(self.resetSettings) self._undo_button = QToolButton() self._undo_button.setIcon(resourceIcon("undo.png")) self._undo_button.setToolTip("Undo") self._undo_button.clicked.connect(self.undoSettings) self._redo_button = QToolButton() self._redo_button.setIcon(resourceIcon("redo.png")) self._redo_button.setToolTip("Redo") self._redo_button.clicked.connect(self.redoSettings) self._redo_button.setEnabled(False) self._copy_from_button = QToolButton() self._copy_from_button.setIcon(resourceIcon("copy_from.png")) self._copy_from_button.setToolTip("Copy settings from another key") self._copy_from_button.setPopupMode(QToolButton.InstantPopup) self._copy_from_button.setEnabled(False) self._copy_to_button = QToolButton() self._copy_to_button.setIcon(resourceIcon("copy_to.png")) self._copy_to_button.setToolTip( "Copy current plot settings to other keys") self._copy_to_button.setPopupMode(QToolButton.InstantPopup) self._copy_to_button.clicked.connect(self.initiateCopyStyleToDialog) self._copy_to_button.setEnabled(True) tool_menu = QMenu(self._copy_from_button) self._popup_list = QListWidget(tool_menu) self._popup_list.setSortingEnabled(True) self._popup_list.itemClicked.connect(self.keySelected) action = QWidgetAction(tool_menu) action.setDefaultWidget(self._popup_list) tool_menu.addAction(action) self._copy_from_button.setMenu(tool_menu) self._apply_button = QPushButton("Apply") self._apply_button.setToolTip("Apply the new settings") self._apply_button.clicked.connect(self.applySettings) self._apply_button.setDefault(True) self._close_button = QPushButton("Close") self._close_button.setToolTip("Hide this dialog") self._close_button.clicked.connect(self.hide) self._button_layout.addWidget(self._reset_button) self._button_layout.addStretch() self._button_layout.addWidget(self._undo_button) self._button_layout.addWidget(self._redo_button) self._button_layout.addWidget(self._copy_from_button) self._button_layout.addWidget(self._copy_to_button) self._button_layout.addStretch() self._button_layout.addWidget(self._apply_button) self._button_layout.addWidget(self._close_button) layout.addStretch() layout.addLayout(self._button_layout) self.setLayout(layout) def initiateCopyStyleToDialog(self): all_other_keys = [ k for k in self.key_manager.allDataTypeKeys() if k != self.current_key ] dialog = CopyStyleToDialog(self, self.current_key, all_other_keys) if dialog.exec_(): self.copySettingsToOthers.emit(dialog.getSelectedKeys()) def addCopyableKey(self, key): self._popup_list.addItem(key) def keySelected(self, list_widget_item): self.copySettings.emit(str(list_widget_item.text())) def currentPlotKeyChanged(self, new_key): self.current_key = new_key def keyPressEvent(self, q_key_event): if q_key_event.key() == Qt.Key_Escape: self.hide() else: QDialog.keyPressEvent(self, q_key_event) def addTab(self, attribute_name, title, widget): self._tabs.addTab(widget, title) self._tab_map[attribute_name] = widget self._tab_order.append(attribute_name) def __getitem__(self, item): """ @rtype: ert_gui.tools.plot.customize.customization_view.CustomizationView """ return self._tab_map[item] def __iter__(self): for attribute_name in self._tab_order: yield self._tab_map[attribute_name] def setUndoRedoCopyState(self, undo, redo, copy=False): self._undo_button.setEnabled(undo) self._redo_button.setEnabled(redo) self._copy_from_button.setEnabled(copy)
class ContourOptionsDialog(QDialog): """ Dialog box for selecting contour options """ def __init__(self, contour_settings): super(ContourOptionsDialog, self).__init__(contour_settings.cubeviz_layout) self.setWindowFlags(self.windowFlags() | Qt.Tool) self.setWindowTitle("Contour Options") self.is_preview_active = False # preview mode? self.contour_settings = contour_settings # ref to caller ContourSettings self.image_viewer = self.contour_settings.image_viewer # ref to image viewer self.options = self.contour_settings.options # ref to ContourSettings options self._colormap_members = self.contour_settings.colormap_members # Colormap options self._colormap_index = DEFAULT_GLUE_COLORMAP_INDEX # Currently selected colormap if "cmap" in self.options: if self.options["cmap"] in self._colormap_members: self._colormap_index = self._colormap_members.index( self.options["cmap"]) # Is there a user spacing? if self.contour_settings.spacing is None: self.is_custom_spacing = False else: self.is_custom_spacing = True # Is there a user min? if self.contour_settings.vmin is None: self.is_vmin = False else: self.is_vmin = True # Is there a user max? if self.contour_settings.vmax is None: self.is_vmax = False else: self.is_vmax = True self.add_contour_label = self.contour_settings.add_contour_label # bool self._init_ui() def _init_ui(self): # Line 1: Color map self.colormap_label = QLabel("Color Scheme: ") self.colormap_combo = QColormapCombo() self.colormap_combo.addItem("", userData=cm.viridis) self.colormap_combo._update_icons() self.colormap_combo.setCurrentIndex(self._colormap_index) self.colormap_combo.setMaximumWidth(150) self.colormap_combo.currentIndexChanged.connect( self._on_colormap_change) # hbl is short for Horizontal Box Layout hbl1 = QHBoxLayout() hbl1.addWidget(self.colormap_label) hbl1.addWidget(self.colormap_combo) # Line 2: Display contour labels self.contour_label_checkBox = QCheckBox("Contour labels (font size):") if self.contour_settings.add_contour_label: self.contour_label_checkBox.setChecked(True) self.contour_label_checkBox.toggled.connect(self.toggle_labels) font_string = str(self.contour_settings.font_size) self.font_size_input = QLineEdit(font_string) self.font_size_input.setFixedWidth(150) self.font_size_input.setDisabled( not self.contour_settings.add_contour_label) hbl2 = QHBoxLayout() hbl2.addWidget(self.contour_label_checkBox) hbl2.addWidget(self.font_size_input) # Line 3: Contour Spacing self.custom_spacing_checkBox = QCheckBox("Contour spacing (interval):") if self.is_custom_spacing: self.custom_spacing_checkBox.setChecked(True) self.custom_spacing_checkBox.toggled.connect(self.custom_spacing) self.spacing_input = QLineEdit() self.spacing_input.setFixedWidth(150) self.spacing_input.setDisabled(not self.is_custom_spacing) spacing = "" if self.is_custom_spacing: spacing = str(self.contour_settings.spacing) elif self.contour_settings.data_spacing is not None: spacing = self.contour_settings.data_spacing spacing = "{0:1.4f}".format(spacing) self.spacing_default_text = spacing self.spacing_input.setText(spacing) hbl3 = QHBoxLayout() hbl3.addWidget(self.custom_spacing_checkBox) hbl3.addWidget(self.spacing_input) # Line 4: Vmax self.vmax_checkBox = QCheckBox("Set max:") self.vmax_input = QLineEdit() self.vmax_input.setFixedWidth(150) self.vmax_input.setDisabled(not self.is_vmax) vmax = "" if self.is_vmax: self.vmax_checkBox.setChecked(True) vmax = str(self.contour_settings.vmax) elif self.contour_settings.data_max is not None: vmax = self.contour_settings.data_max vmax = "{0:1.4f}".format(vmax) self.vmax_input.setText(vmax) self.vmax_default_text = vmax self.vmax_checkBox.toggled.connect(self.toggle_vmax) hbl4 = QHBoxLayout() hbl4.addWidget(self.vmax_checkBox) hbl4.addWidget(self.vmax_input) # Line 5: Vmin self.vmin_checkBox = QCheckBox("Set min:") self.vmin_input = QLineEdit() self.vmin_input.setFixedWidth(150) self.vmin_input.setDisabled(not self.is_vmin) vmin = "" if self.is_vmin: self.vmin_checkBox.setChecked(True) vmin = str(self.contour_settings.vmin) elif self.contour_settings.data_min is not None: vmin = self.contour_settings.data_min vmin = "{0:1.4f}".format(vmin) self.vmin_input.setText(vmin) self.vmin_default_text = vmin self.vmin_checkBox.toggled.connect(self.toggle_vmin) hbl5 = QHBoxLayout() hbl5.addWidget(self.vmin_checkBox) hbl5.addWidget(self.vmin_input) # Line f: self.previewButton = QPushButton("Preview") self.previewButton.clicked.connect(self.preview) self.defaultButton = QPushButton("Reset") self.defaultButton.clicked.connect(self.default) self.okButton = QPushButton("OK") self.okButton.clicked.connect(self.finish) self.okButton.setDefault(True) self.cancelButton = QPushButton("Cancel") self.cancelButton.clicked.connect(self.cancel) hblf = QHBoxLayout() hblf.addStretch(1) hblf.addWidget(self.previewButton) hblf.addWidget(self.defaultButton) hblf.addWidget(self.cancelButton) hblf.addWidget(self.okButton) vbl = QVBoxLayout() vbl.addLayout(hbl1) vbl.addLayout(hbl2) vbl.addLayout(hbl3) vbl.addLayout(hbl4) vbl.addLayout(hbl5) vbl.addLayout(hblf) self.setLayout(vbl) self.show() def update_data_vals(self, vmin="", vmax="", spacing="1"): self.vmin_default_text = vmin if not self.is_vmin: self.vmin_input.setText(vmin) self.vmax_default_text = vmax if not self.is_vmax: self.vmax_input.setText(vmax) self.spacing_default_text = spacing if not self.is_custom_spacing: self.spacing_input.setText(spacing) def _on_colormap_change(self, index): """Combo index changed handler""" self._colormap_index = index def custom_spacing(self): """Checkbox toggled handler""" if self.is_custom_spacing: self.is_custom_spacing = False self.spacing_input.setDisabled(True) self.spacing_input.setText(self.spacing_default_text) self.spacing_input.setStyleSheet("") else: self.is_custom_spacing = True self.spacing_input.setDisabled(False) def toggle_labels(self): """Checkbox toggled handler""" if self.add_contour_label: self.add_contour_label = False self.font_size_input.setDisabled(True) font_string = str(self.contour_settings.font_size) self.font_size_input.setText(font_string) self.font_size_input.setStyleSheet("") else: self.add_contour_label = True self.font_size_input.setDisabled(False) def toggle_vmax(self): """Checkbox toggled handler""" if self.is_vmax: self.is_vmax = False self.vmax_input.setDisabled(True) self.vmax_input.setText(self.vmax_default_text) self.vmax_input.setStyleSheet("") else: self.is_vmax = True self.vmax_input.setDisabled(False) def toggle_vmin(self): """Checkbox toggled handler""" if self.is_vmin: self.is_vmin = False self.vmin_input.setDisabled(True) self.vmin_input.setText(self.vmin_default_text) self.vmin_input.setStyleSheet("") else: self.is_vmin = True self.vmin_input.setDisabled(False) def input_validation(self): red = "background-color: rgba(255, 0, 0, 128);" def float_check(min_val=None): if user_input.text() == "": user_input.setStyleSheet(red) return False else: try: value = float(user_input.text()) if min_val is not None: if value <= min_val: user_input.setStyleSheet(red) return False else: user_input.setStyleSheet("") except ValueError: user_input.setStyleSheet(red) return False return True def int_check(min_val=None): if user_input.text() == "": user_input.setStyleSheet(red) return False else: try: value = int(user_input.text()) if min_val is not None: if value <= min_val: user_input.setStyleSheet(red) return False else: user_input.setStyleSheet("") except ValueError: user_input.setStyleSheet(red) return False return True success = True # Check 1: spacing_input if self.is_custom_spacing: user_input = self.spacing_input float_check(0) success = success and float_check() # Check 2: font_size_input if self.add_contour_label: user_input = self.font_size_input int_check(0) success = success and int_check() # Check 3: vmax if self.is_vmax: user_input = self.vmax_input float_check() success = success and float_check() # Check 4: vmax if self.is_vmin: user_input = self.vmin_input float_check() success = success and float_check() # Check 5: vmax and vmin if self.is_vmax and self.is_vmin and success: vmax = float(self.vmax_input.text()) vmin = float(self.vmin_input.text()) if vmax <= vmin: self.vmax_input.setStyleSheet(red) self.vmin_input.setStyleSheet(red) success = False return success def finish(self): """ Ok button pressed. Finalize options and send to image viewer """ success = self.input_validation() if not success: return # Change Color Map self._colormap_index = self.colormap_combo.currentIndex() colormap = self._colormap_members[self._colormap_index] self.contour_settings.options["cmap"] = colormap # labels self.contour_settings.add_contour_label = self.add_contour_label # font size if self.add_contour_label: font_size = int(self.font_size_input.text()) self.contour_settings.font_size = font_size else: self.contour_settings.font_size = DEFAULT_CONTOUR_FONT_SIZE # Spacing if self.is_custom_spacing: self.contour_settings.spacing = float(self.spacing_input.text()) else: self.contour_settings.spacing = None # vmax if self.is_vmax: vmax = float(self.vmax_input.text()) self.contour_settings.vmax = vmax self.contour_settings.options["vmax"] = vmax else: self.contour_settings.vmax = None self.contour_settings.options["vmax"] = None # vmin if self.is_vmin: vmin = float(self.vmin_input.text()) self.contour_settings.vmin = vmin self.contour_settings.options["vmin"] = vmin else: self.contour_settings.vmin = None self.contour_settings.options["vmin"] = None # Redraw contour if self.contour_settings.image_viewer.is_contour_active: self.contour_settings.draw_function() self.close() def preview(self): """ Prepare preview contour settings and send to image viewer """ success = self.input_validation() if not success: return image_viewer = self.contour_settings.image_viewer preview_settings = ContourSettings(image_viewer) preview_settings.dialog = self preview_settings.options = self.contour_settings.options.copy() preview_settings.spacing = self.contour_settings.spacing # Change Color Map self._colormap_index = self.colormap_combo.currentIndex() colormap = self._colormap_members[self._colormap_index] preview_settings.options["cmap"] = colormap # labels add_contour_label = self.contour_label_checkBox.isChecked() preview_settings.add_contour_label = add_contour_label # font size if add_contour_label: font_size = int(self.font_size_input.text()) preview_settings.font_size = font_size # Spacing if self.is_custom_spacing: preview_settings.spacing = float(self.spacing_input.text()) else: preview_settings.spacing = None # vmax if self.is_vmax: vmax = float(self.vmax_input.text()) preview_settings.vmax = vmax preview_settings.options["vmax"] = vmax else: preview_settings.vmax = None preview_settings.options["vmax"] = None # vmin if self.is_vmin: vmin = float(self.vmin_input.text()) preview_settings.vmin = vmin preview_settings.options["vmin"] = vmin else: preview_settings.vmin = None preview_settings.options["vmin"] = None # Redraw contour if image_viewer.is_contour_active: self.is_preview_active = True image_viewer.set_contour_preview(preview_settings) else: message = "Contour map is currently switched off. " \ "Please turn on the contour map by selecting " \ "a component from the contour map drop-down menu." info = QMessageBox.critical(self, "Error", message) def default(self): """ Set options back to default and send to image viewer """ self.contour_settings.options = self.contour_settings.default_options() self.contour_settings.spacing = None self.contour_settings.font_size = DEFAULT_CONTOUR_FONT_SIZE self.contour_settings.vmax = None self.contour_settings.vmin = None self.contour_settings.add_contour_label = False if self.contour_settings.image_viewer.is_contour_active: self.contour_settings.draw_function() self.contour_settings.options_dialog() def cancel(self): if self.contour_settings.image_viewer.is_contour_active: self.contour_settings.draw_function() self.close() def closeEvent(self, event): """closeEvent handler""" if self.is_preview_active: self.contour_settings.image_viewer.end_contour_preview() def keyPressEvent(self, e): if e.key() == Qt.Key_Escape: self.cancel()
class CondaPackagesWidget(QWidget): """ Conda Packages Widget. """ # Location of updated repo.json files from continuum/binstar CONDA_CONF_PATH = get_conf_path('repo') # Location of continuum/anaconda default repos shipped with conda-manager DATA_PATH = get_module_data_path() # file inside DATA_PATH with metadata for conda packages DATABASE_FILE = 'packages.ini' sig_worker_ready = Signal() sig_packages_ready = Signal() sig_environment_created = Signal(object, object) sig_environment_removed = Signal(object, object) sig_environment_cloned = Signal(object, object) sig_channels_updated = Signal(tuple, tuple) # channels, active_channels sig_process_cancelled = Signal() sig_next_focus = Signal() sig_packages_busy = Signal() def __init__(self, parent, name=None, prefix=None, channels=(), active_channels=(), conda_url='https://conda.anaconda.org', conda_api_url='https://api.anaconda.org', setup=True, data_directory=None, extra_metadata={}): super(CondaPackagesWidget, self).__init__(parent) # Check arguments: active channels, must be witbhin channels for ch in active_channels: if ch not in channels: raise Exception("'active_channels' must be also within " "'channels'") if data_directory is None: data_directory = self.CONDA_CONF_PATH self._parent = parent self._current_action_name = '' self._hide_widgets = False self._metadata = extra_metadata # From repo.continuum self._metadata_links = {} # Bundled metadata self.api = ManagerAPI() self.busy = False self.data_directory = data_directory self.conda_url = conda_url self.conda_api_url = conda_api_url self.name = name self.package_blacklist = [] self.prefix = prefix self.root_prefix = self.api.ROOT_PREFIX self.style_sheet = None self.message = '' self.apply_actions_dialog = None self.conda_errors = [] self.message_box_error = None self.token = None if channels: self._channels = channels self._active_channels = active_channels else: self._channels = self.api.conda_get_condarc_channels() self._active_channels = self._channels[:] try: import spyderlib.utils.icon_manager as ima icon_options = ima.icon('tooloptions') except Exception: import qtawesome as qta icon_options = qta.icon('fa.cog') # Widgets self.cancel_dialog = ClosePackageManagerDialog self.bbox = QDialogButtonBox(Qt.Horizontal) self.button_cancel = QPushButton('Cancel') self.button_channels = QPushButton(_('Channels')) self.button_ok = QPushButton(_('Ok')) self.button_update = QPushButton(_('Update index...')) self.button_apply = QPushButton(_('Apply')) self.button_clear = QPushButton(_('Clear')) self.button_options = QToolButton() self.combobox_filter = DropdownPackageFilter(self) self.frame_top = FramePackageTop() self.frame_bottom = FramePackageTop() self.progress_bar = ProgressBarPackage(self) self.status_bar = LabelPackageStatus(self) self.table = TableCondaPackages(self) self.textbox_search = LineEditSearch(self) self.widgets = [self.button_update, self.button_channels, self.combobox_filter, self.textbox_search, self.table, self.button_ok, self.button_apply, self.button_clear, self.button_options] self.table_first_row = FirstRowWidget( widget_before=self.textbox_search) self.table_last_row = LastRowWidget( widgets_after=[self.button_apply, self.button_clear, self.button_cancel, self.combobox_filter]) # Widget setup self.button_options.setPopupMode(QToolButton.InstantPopup) self.button_options.setIcon(icon_options) self.button_options.setAutoRaise(True) max_height = self.status_bar.fontMetrics().height() max_width = self.textbox_search.fontMetrics().width('M'*23) self.bbox.addButton(self.button_ok, QDialogButtonBox.ActionRole) self.button_ok.setAutoDefault(True) self.button_ok.setDefault(True) self.button_ok.setMaximumSize(QSize(0, 0)) self.button_ok.setVisible(False) self.combobox_filter.addItems([k for k in C.COMBOBOX_VALUES_ORDERED]) self.combobox_filter.setMinimumWidth(120) self.progress_bar.setMaximumHeight(max_height*1.2) self.progress_bar.setMaximumWidth(max_height*12) self.progress_bar.setTextVisible(False) self.progress_bar.setVisible(False) self.setMinimumSize(QSize(480, 300)) self.setWindowTitle(_("Conda Package Manager")) self.status_bar.setFixedHeight(max_height*1.5) self.textbox_search.setMaximumWidth(max_width) self.textbox_search.setPlaceholderText('Search Packages') self.table_first_row.setMaximumHeight(0) self.table_last_row.setMaximumHeight(0) self.table_last_row.setVisible(False) self.table_first_row.setVisible(False) # Layout top_layout = QHBoxLayout() top_layout.addWidget(self.combobox_filter) top_layout.addWidget(self.button_channels) top_layout.addWidget(self.button_update) top_layout.addWidget(self.textbox_search) top_layout.addStretch() top_layout.addWidget(self.button_options) middle_layout = QVBoxLayout() middle_layout.addWidget(self.table_first_row) middle_layout.addWidget(self.table) middle_layout.addWidget(self.table_last_row) bottom_layout = QHBoxLayout() bottom_layout.addWidget(self.status_bar) bottom_layout.addStretch() bottom_layout.addWidget(self.progress_bar) bottom_layout.addWidget(self.button_cancel) bottom_layout.addWidget(self.button_apply) bottom_layout.addWidget(self.button_clear) layout = QVBoxLayout(self) layout.addLayout(top_layout) layout.addLayout(middle_layout) layout.addLayout(bottom_layout) layout.addSpacing(6) self.setLayout(layout) self.setTabOrder(self.combobox_filter, self.button_channels) self.setTabOrder(self.button_channels, self.button_update) self.setTabOrder(self.button_update, self.textbox_search) self.setTabOrder(self.textbox_search, self.table_first_row) self.setTabOrder(self.table, self.table_last_row) self.setTabOrder(self.table_last_row, self.button_apply) self.setTabOrder(self.button_apply, self.button_clear) self.setTabOrder(self.button_clear, self.button_cancel) # Signals and slots self.api.sig_repodata_updated.connect(self._repodata_updated) self.combobox_filter.currentIndexChanged.connect(self.filter_package) self.button_apply.clicked.connect(self.apply_multiple_actions) self.button_clear.clicked.connect(self.clear_actions) self.button_cancel.clicked.connect(self.cancel_process) self.button_channels.clicked.connect(self.show_channels_dialog) self.button_update.clicked.connect(self.update_package_index) self.textbox_search.textChanged.connect(self.search_package) self.table.sig_conda_action_requested.connect(self._run_conda_action) self.table.sig_actions_updated.connect(self.update_actions) self.table.sig_pip_action_requested.connect(self._run_pip_action) self.table.sig_status_updated.connect(self.update_status) self.table.sig_next_focus.connect(self.table_last_row.handle_tab) self.table.sig_previous_focus.connect( lambda: self.table_first_row.widget_before.setFocus()) self.table_first_row.sig_enter_first.connect(self._handle_tab_focus) self.table_last_row.sig_enter_last.connect(self._handle_backtab_focus) # Setup self.api.client_set_domain(conda_api_url) self.api.set_data_directory(self.data_directory) self._load_bundled_metadata() self.update_actions(0) if setup: self.set_environment(name=name, prefix=prefix) self.setup() # --- Helpers # ------------------------------------------------------------------------- def _handle_tab_focus(self): self.table.setFocus() if self.table.proxy_model: index = self.table.proxy_model.index(0, 0) self.table.setCurrentIndex(index) def _handle_backtab_focus(self): self.table.setFocus() if self.table.proxy_model: row = self.table.proxy_model.rowCount() - 1 index = self.table.proxy_model.index(row, 0) self.table.setCurrentIndex(index) # --- Callbacks # ------------------------------------------------------------------------- def _load_bundled_metadata(self): """ """ logger.debug('') parser = cp.ConfigParser() db_file = CondaPackagesWidget.DATABASE_FILE with open(osp.join(self.DATA_PATH, db_file)) as f: parser.readfp(f) for name in parser.sections(): metadata = {} for key, data in parser.items(name): metadata[key] = data self._metadata_links[name] = metadata def _setup_packages(self, worker, data, error): """ """ if error: logger.error(error) else: logger.debug('') combobox_index = self.combobox_filter.currentIndex() status = C.PACKAGE_STATUS[combobox_index] packages = worker.packages # Remove blacklisted packages for package in self.package_blacklist: if package in packages: packages.pop(package) for i, row in enumerate(data): if package == data[i][C.COL_NAME]: data.pop(i) self.table.setup_model(packages, data, self._metadata_links) self.combobox_filter.setCurrentIndex(combobox_index) self.filter_package(status) if self._current_model_index: self.table.setCurrentIndex(self._current_model_index) self.table.verticalScrollBar().setValue(self._current_table_scroll) if error: self.update_status(error, False) self.sig_packages_ready.emit() self.table.setFocus() def get_logged_user_list_channels(self): channels = [] for ch in self._active_channels: if self.conda_url in ch and 'repo.continuum' not in ch: channel = ch.split('/')[-1] channels.append(channel) return channels def _prepare_model_data(self, worker=None, output=None, error=None): """ """ if error: logger.error(error) else: logger.debug('') packages, apps = output # worker = self.api.pip_list(prefix=self.prefix) # worker.sig_finished.connect(self._pip_list_ready) logins = self.get_logged_user_list_channels() worker = self.api.client_multi_packages(logins=logins, access='private') worker.sig_finished.connect(self._user_private_packages_ready) worker.packages = packages worker.apps = apps def _user_private_packages_ready(self, worker, output, error): if error: logger.error(error) else: logger.debug('') packages = worker.packages apps = worker.apps worker = self.api.pip_list(prefix=self.prefix) worker.sig_finished.connect(self._pip_list_ready) worker.packages = packages worker.apps = apps # private_packages = {} # if output: # all_private_packages = output # for item in all_private_packages: # name = item.get('name', '') # public = item.get('public', True) # package_types = item.get('package_types', []) # latest_version = item.get('latest_version', '') # if name and not public and 'conda' in package_types: # private_packages[name] = {'versions': item.get('versions', []), # 'app_entry': {}, # 'type': {}, # 'size': {}, # 'latest_version': latest_version, # } worker.private_packages = output def _pip_list_ready(self, worker, pip_packages, error): """ """ if error: logger.error(error) else: logger.debug('') packages = worker.packages private_packages = worker.private_packages linked_packages = self.api.conda_linked(prefix=self.prefix) data = self.api.client_prepare_packages_data(packages, linked_packages, pip_packages, private_packages) combobox_index = self.combobox_filter.currentIndex() status = C.PACKAGE_STATUS[combobox_index] # Remove blacklisted packages for package in self.package_blacklist: if package in packages: packages.pop(package) for i, row in enumerate(data): if package == data[i][C.COL_NAME]: data.pop(i) self.table.setup_model(packages, data, self._metadata_links) self.combobox_filter.setCurrentIndex(combobox_index) self.filter_package(status) if self._current_model_index: self.table.setCurrentIndex(self._current_model_index) self.table.verticalScrollBar().setValue(self._current_table_scroll) if error: self.update_status(str(error), False) self.sig_packages_ready.emit() self.table.setFocus() def _repodata_updated(self, paths): """ """ worker = self.api.client_load_repodata(paths, extra_data={}, metadata=self._metadata) worker.paths = paths worker.sig_finished.connect(self._prepare_model_data) def _metadata_updated(self, worker, path, error): """ """ if error: logger.error(error) else: logger.debug('') if path and osp.isfile(path): with open(path, 'r') as f: data = f.read() try: self._metadata = json.loads(data) except Exception: self._metadata = {} else: self._metadata = {} self.api.update_repodata(self._channels) # --- # ------------------------------------------------------------------------- def _run_multiple_actions(self, worker=None, output=None, error=None): """ """ logger.error(str(error)) if output and isinstance(output, dict): conda_error_type = output.get('error_type', None) conda_error = output.get('error', None) if conda_error_type or conda_error: self.conda_errors.append((conda_error_type, conda_error)) logger.error((conda_error_type, conda_error)) if self._multiple_process: status, func = self._multiple_process.popleft() self.update_status(status) worker = func() worker.sig_finished.connect(self._run_multiple_actions) worker.sig_partial.connect(self._partial_output_ready) else: if self.conda_errors and self.message_box_error: text = "The following errors occured:" error = '' for conda_error in self.conda_errors: error += str(conda_error[0]) + ':\n' error += str(conda_error[1]) + '\n\n' dlg = self.message_box_error(text=text, error=error, title='Conda process error') dlg.setMinimumWidth(400) dlg.exec_() self.update_status('', hide=False) self.setup() def _pip_process_ready(self, worker, output, error): """ """ if error is not None: status = _('there was an error') self.update_status(hide=False, message=status) else: self.update_status(hide=True) self.setup() def _conda_process_ready(self, worker, output, error): """ """ if error is not None: status = _('there was an error') self.update_status(hide=False, message=status) else: self.update_status(hide=True) conda_error = None conda_error_type = None if output and isinstance(output, dict): conda_error_type = output.get('error_type') conda_error = output.get('error') if conda_error_type or conda_error: logger.error((conda_error_type, conda_error)) dic = self._temporal_action_dic if dic['action'] == C.ACTION_CREATE: self.sig_environment_created.emit(conda_error, conda_error_type) elif dic['action'] == C.ACTION_CLONE: self.sig_environment_cloned.emit(conda_error, conda_error_type) elif dic['action'] == C.ACTION_REMOVE_ENV: self.sig_environment_removed.emit(conda_error, conda_error_type) self.setup() def _partial_output_ready(self, worker, output, error): """ """ message = None progress = (0, 0) if isinstance(output, dict): progress = (output.get('progress', None), output.get('maxval', None)) name = output.get('name', None) fetch = output.get('fetch', None) if fetch: message = "Downloading <b>{0}</b>...".format(fetch) if name: self._current_action_name = name message = "Linking <b>{0}</b>...".format(name) logger.debug(message) self.update_status(message, progress=progress) def _run_pip_action(self, package_name, action): """ DEPRECATED """ prefix = self.prefix if prefix == self.root_prefix: name = 'root' elif self.api.conda_environment_exists(prefix=prefix): name = osp.basename(prefix) else: name = prefix if action == C.ACTION_REMOVE: msgbox = QMessageBox.question(self, "Remove pip package: " "{0}".format(package_name), "Do you want to proceed?", QMessageBox.Yes | QMessageBox.No) if msgbox == QMessageBox.Yes: self.update_status() worker = self.api.pip_remove(prefix=self.prefix, pkgs=[package_name]) worker.sig_finished.connect(self._pip_process_ready) status = (_('Removing pip package <b>') + package_name + '</b>' + _(' from <i>') + name + '</i>') self.update_status(hide=True, message=status, progress=[0, 0]) def _run_conda_action(self, package_name, action, version, versions, packages_sizes): """ DEPRECATED """ prefix = self.prefix dlg = CondaPackageActionDialog(self, prefix, package_name, action, version, versions, packages_sizes, self._active_channels) if dlg.exec_(): dic = {} self.status = 'Processing' self.update_status(hide=True) self.repaint() ver1 = dlg.label_version.text() ver2 = dlg.combobox_version.currentText() pkg = u'{0}={1}{2}'.format(package_name, ver1, ver2) dep = dlg.checkbox.checkState() state = dlg.checkbox.isEnabled() dlg.close() dic['pkg'] = pkg dic['dep'] = not (dep == 0 and state) dic['action'] = None self._run_conda_process(action, dic) def _run_conda_process(self, action, dic): """ DEPRECTAED """ self._temporal_action_dic = dic # prefix = self.prefix # # if prefix == self.root_prefix: # name = 'root' # elif self.api.conda_environment_exists(prefix=prefix): # name = osp.basename(prefix) # else: # name = prefix if 'pkgs' in dic and 'dep' in dic: dep = dic['dep'] pkgs = dic['pkgs'] if not isinstance(pkgs, list): pkgs = [pkgs] # if (action == C.ACTION_INSTALL or action == C.ACTION_UPGRADE or # action == C.ACTION_DOWNGRADE): # status = _('Installing <b>') + dic['pkg'] + '</b>' # status = status + _(' into <i>') + name + '</i>' # worker = self.api.conda_install(prefix=prefix, pkgs=pkgs, dep=dep, # channels=self._active_channels) # elif action == C.ACTION_REMOVE: # status = (_('Removing <b>') + dic['pkg'] + '</b>' + # _(' from <i>') + name + '</i>') # worker = self.api.conda_remove(pkgs[0], prefix=prefix) # --- Environment management actions name = dic['name'] if action == C.ACTION_CREATE: status = _('Creating environment <b>') + name + '</b>' worker = self.api.conda_create(name=name, pkgs=pkgs, channels=self._active_channels) elif action == C.ACTION_CLONE: clone = dic['clone'] status = (_('Cloning ') + '<i>' + clone + _('</i> into <b>') + name + '</b>') worker = self.api.conda_clone(clone, name=name) elif action == C.ACTION_REMOVE_ENV: status = _('Removing environment <b>') + name + '</b>' worker = self.api.conda_remove(name=name, all_=True) worker.sig_finished.connect(self._conda_process_ready) worker.sig_partial.connect(self._partial_output_ready) self.update_status(hide=True, message=status, progress=None) self._temporal_action_dic = dic return worker # Public API # ------------------------------------------------------------------------- def prepare_model_data(self, packages, apps): """ """ logger.debug('') self._prepare_model_data(output=(packages, apps)) # These should be private methods.... def enable_widgets(self): """ """ self.table.hide_columns() def disable_widgets(self): """ """ self.table.hide_action_columns() def accept_channels_dialog(self): self.button_channels.setFocus() self.button_channels.toggle() def update_actions(self, number_of_actions): """ """ self.button_apply.setVisible(bool(number_of_actions)) self.button_clear.setVisible(bool(number_of_actions)) # --- Non UI API # ------------------------------------------------------------------------- def setup(self, check_updates=False, blacklist=[], metadata={}): """ Setup packages. Main triger method to download repodata, load repodata, prepare and updating the data model. Parameters ---------- check_updates : bool If `True`, checks that the latest repodata is available on the listed channels. If `False`, the data will be loaded from the downloaded files without checking for newer versions. blacklist: list of str List of conda package names to be excluded from the actual package manager view. """ self.sig_packages_busy.emit() if self.busy: logger.debug('Busy...') return else: logger.debug('') if blacklist: self.package_blacklist = [p.lower() for p in blacklist] if metadata: self._metadata = metadata self._current_model_index = self.table.currentIndex() self._current_table_scroll = self.table.verticalScrollBar().value() self.update_status('Updating package index', True) if check_updates: worker = self.api.update_metadata() worker.sig_finished.connect(self._metadata_updated) else: paths = self.api.repodata_files(channels=self._active_channels) self._repodata_updated(paths) def update_domains(self, anaconda_api_url=None, conda_url=None): """ """ logger.debug(str((anaconda_api_url, conda_url))) update = False if anaconda_api_url: if self.conda_api_url != anaconda_api_url: update = True self.conda_api_url = anaconda_api_url self.api.client_set_domain(anaconda_api_url) if conda_url: if self.conda_url != conda_url: update = True self.conda_url = conda_url if update: pass def set_environment(self, name=None, prefix=None): """ This does not update the package manager! """ logger.debug(str((name, prefix))) if prefix and self.api.conda_environment_exists(prefix=prefix): self.prefix = prefix elif name and self.api.conda_environment_exists(name=name): self.prefix = self.get_prefix_envname(name) else: self.prefix = self.root_prefix def set_token(self, token): self.token = token def update_channels(self, channels, active_channels): """ """ logger.debug(str((channels, active_channels))) if sorted(self._active_channels) != sorted(active_channels) or \ sorted(self._channels) != sorted(channels): self._channels = channels self._active_channels = active_channels self.sig_channels_updated.emit(tuple(channels), tuple(active_channels)) self.setup(check_updates=True) def update_style_sheet(self, style_sheet=None, extra_dialogs={}, palette={}): if style_sheet: self.style_sheet = style_sheet self.table.update_style_palette(palette=palette) self.textbox_search.update_style_sheet(style_sheet) self.setStyleSheet(style_sheet) if extra_dialogs: cancel_dialog = extra_dialogs.get('cancel_dialog', None) apply_actions_dialog = extra_dialogs.get('apply_actions_dialog', None) message_box_error = extra_dialogs.get('message_box_error', None) if cancel_dialog: self.cancel_dialog = cancel_dialog if apply_actions_dialog: self.apply_actions_dialog = apply_actions_dialog if message_box_error: self.message_box_error = message_box_error # --- UI API # ------------------------------------------------------------------------- def filter_package(self, value): """ """ self.table.filter_status_changed(value) def show_channels_dialog(self): """ Show the channels dialog. """ button_channels = self.button_channels self.dlg = DialogChannels(self, channels=self._channels, active_channels=self._active_channels, conda_url=self.conda_url) self.dlg.update_style_sheet(style_sheet=self.style_sheet) button_channels.setDisabled(True) self.dlg.sig_channels_updated.connect(self.update_channels) self.dlg.rejected.connect(lambda: button_channels.setEnabled(True)) self.dlg.rejected.connect(button_channels.toggle) self.dlg.rejected.connect(button_channels.setFocus) self.dlg.accepted.connect(self.accept_channels_dialog) geo_tl = button_channels.geometry().topLeft() tl = button_channels.parentWidget().mapToGlobal(geo_tl) x = tl.x() + 2 y = tl.y() + button_channels.height() self.dlg.move(x, y) self.dlg.show() self.dlg.button_add.setFocus() def update_package_index(self): """ """ self.setup(check_updates=True) def search_package(self, text): """ """ self.table.search_string_changed(text) def apply_multiple_actions(self): """ """ logger.debug('') self.conda_errors = [] prefix = self.prefix if prefix == self.root_prefix: name = 'root' elif self.api.conda_environment_exists(prefix=prefix): name = osp.basename(prefix) else: name = prefix actions = self.table.get_actions() if actions is None: return self._multiple_process = deque() pip_actions = actions[C.PIP_PACKAGE] conda_actions = actions[C.CONDA_PACKAGE] pip_remove = pip_actions.get(C.ACTION_REMOVE, []) conda_remove = conda_actions.get(C.ACTION_REMOVE, []) conda_install = conda_actions.get(C.ACTION_INSTALL, []) conda_upgrade = conda_actions.get(C.ACTION_UPGRADE, []) conda_downgrade = conda_actions.get(C.ACTION_DOWNGRADE, []) message = '' template_1 = '<li><b>{0}={1}</b></li>' template_2 = '<li><b>{0}: {1} -> {2}</b></li>' if pip_remove: temp = [template_1.format(i['name'], i['version_to']) for i in pip_remove] message += ('The following pip packages will be removed: ' '<ul>' + ''.join(temp) + '</ul>') if conda_remove: temp = [template_1.format(i['name'], i['version_to']) for i in conda_remove] message += ('<br>The following conda packages will be removed: ' '<ul>' + ''.join(temp) + '</ul>') if conda_install: temp = [template_1.format(i['name'], i['version_to']) for i in conda_install] message += ('<br>The following conda packages will be installed: ' '<ul>' + ''.join(temp) + '</ul>') if conda_downgrade: temp = [template_2.format( i['name'], i['version_from'], i['version_to']) for i in conda_downgrade] message += ('<br>The following conda packages will be downgraded: ' '<ul>' + ''.join(temp) + '</ul>') if conda_upgrade: temp = [template_2.format( i['name'], i['version_from'], i['version_to']) for i in conda_upgrade] message += ('<br>The following conda packages will be upgraded: ' '<ul>' + ''.join(temp) + '</ul>') message += '<br>' if self.apply_actions_dialog: dlg = self.apply_actions_dialog(message, parent=self) dlg.update_style_sheet(style_sheet=self.style_sheet) reply = dlg.exec_() else: reply = QMessageBox.question(self, 'Proceed with the following actions?', message, buttons=QMessageBox.Ok | QMessageBox.Cancel) if reply: # Pip remove for pkg in pip_remove: status = (_('Removing pip package <b>') + pkg['name'] + '</b>' + _(' from <i>') + name + '</i>') pkgs = [pkg['name']] def trigger(prefix=prefix, pkgs=pkgs): return lambda: self.api.pip_remove(prefix=prefix, pkgs=pkgs) self._multiple_process.append([status, trigger()]) # Process conda actions if conda_remove: status = (_('Removing conda packages <b>') + '</b>' + _(' from <i>') + name + '</i>') pkgs = [i['name'] for i in conda_remove] def trigger(prefix=prefix, pkgs=pkgs): return lambda: self.api.conda_remove(pkgs=pkgs, prefix=prefix) self._multiple_process.append([status, trigger()]) if conda_install: pkgs = ['{0}={1}'.format(i['name'], i['version_to']) for i in conda_install] status = (_('Installing conda packages <b>') + '</b>' + _(' on <i>') + name + '</i>') def trigger(prefix=prefix, pkgs=pkgs): return lambda: self.api.conda_install( prefix=prefix, pkgs=pkgs, channels=self._active_channels, token=self.token) self._multiple_process.append([status, trigger()]) # Conda downgrade if conda_downgrade: status = (_('Downgrading conda packages <b>') + '</b>' + _(' on <i>') + name + '</i>') pkgs = ['{0}={1}'.format(i['name'], i['version_to']) for i in conda_downgrade] def trigger(prefix=prefix, pkgs=pkgs): return lambda: self.api.conda_install( prefix=prefix, pkgs=pkgs, channels=self._active_channels, token=self.token) self._multiple_process.append([status, trigger()]) # Conda update if conda_upgrade: status = (_('Upgrading conda packages <b>') + '</b>' + _(' on <i>') + name + '</i>') pkgs = ['{0}={1}'.format(i['name'], i['version_to']) for i in conda_upgrade] def trigger(prefix=prefix, pkgs=pkgs): return lambda: self.api.conda_install( prefix=prefix, pkgs=pkgs, channels=self._active_channels, token=self.token) self._multiple_process.append([status, trigger()]) self._run_multiple_actions() def clear_actions(self): """ """ self.table.clear_actions() def cancel_process(self): """ Allow user to cancel an ongoing process. """ logger.debug(str('process canceled by user.')) if self.busy: dlg = self.cancel_dialog() reply = dlg.exec_() if reply: self.update_status(hide=False, message='Process cancelled') self.api.conda_terminate() self.api.download_requests_terminate() self.api.conda_clear_lock() self.table.clear_actions() self.sig_process_cancelled.emit() else: QDialog.reject(self) def update_status(self, message=None, hide=True, progress=None, env=False): """ Update status bar, progress bar display and widget visibility message : str Message to display in status bar. hide : bool Enable/Disable widgets. progress : [int, int] Show status bar progress. [0, 0] means spinning statusbar. """ self.busy = hide for widget in self.widgets: widget.setDisabled(hide) self.table.verticalScrollBar().setValue(self._current_table_scroll) self.button_apply.setVisible(False) self.button_clear.setVisible(False) self.progress_bar.setVisible(hide) self.button_cancel.setVisible(hide) if message is not None: self.message = message if self.prefix == self.root_prefix: short_env = 'root' # elif self.api.environment_exists(prefix=self.prefix): # short_env = osp.basename(self.prefix) else: short_env = self.prefix if env: self.message = '{0} (<b>{1}</b>)'.format( self.message, short_env, ) self.status_bar.setText(self.message) if progress is not None: current_progress, max_progress = 0, 0 if progress[1]: max_progress = progress[1] if progress[0]: current_progress = progress[0] self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(max_progress) self.progress_bar.setValue(current_progress) else: self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(0) # --- Conda helpers # ------------------------------------------------------------------------- def get_environment_prefix(self): """ Returns the active environment prefix. """ return self.prefix def get_environment_name(self): """ Returns the active environment name if it is located in the default conda environments directory, otherwise it returns the prefix. """ name = osp.basename(self.prefix) if not (name and self.api.environment_exists(name=name)): name = self.prefix return name def get_environments(self): """ Get a list of conda environments located in the default conda environments directory. """ return self.api.conda_get_envs() def get_prefix_envname(self, name): """ Returns the prefix for a given environment by name. """ return self.api.conda_get_prefix_envname(name) def get_package_versions(self, name): """ """ return self.table.source_model.get_package_versions(name) # --- Conda actions # ------------------------------------------------------------------------- def create_environment(self, name=None, prefix=None, packages=['python']): """ """ # If environment exists already? GUI should take care of this # BUT the api call should simply set that env as the env dic = {} dic['name'] = name dic['prefix'] = prefix dic['pkgs'] = packages dic['dep'] = True # Not really needed but for the moment! dic['action'] = C.ACTION_CREATE return self._run_conda_process(dic['action'], dic) def clone_environment(self, name=None, prefix=None, clone=None): dic = {} dic['name'] = name dic['prefix'] = prefix dic['clone'] = clone dic['pkgs'] = None dic['dep'] = True # Not really needed but for the moment! dic['action'] = C.ACTION_CLONE return self._run_conda_process(dic['action'], dic) def remove_environment(self, name=None, prefix=None): dic = {} dic['name'] = name dic['pkgs'] = None dic['dep'] = True # Not really needed but for the moment! dic['action'] = C.ACTION_REMOVE_ENV return self._run_conda_process(dic['action'], dic) # New api def show_login_dialog(self): pass def show_options_menu(self): pass
class DataFrameEditor(QDialog): """ Dialog for displaying and editing DataFrame and related objects. Signals ------- sig_option_changed(str, object): Raised if an option is changed. Arguments are name of option and its new value. """ sig_option_changed = Signal(str, object) def __init__(self, parent=None): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.is_series = False self.layout = None def setup_and_check(self, data, title=''): """ Setup DataFrameEditor: return False if data is not supported, True otherwise. Supported types for data are DataFrame, Series and DatetimeIndex. """ self.layout = QGridLayout() self.setLayout(self.layout) self.setWindowIcon(ima.icon('arredit')) if title: title = to_text_string(title) + " - %s" % data.__class__.__name__ else: title = _("%s editor") % data.__class__.__name__ if isinstance(data, Series): self.is_series = True data = data.to_frame() elif isinstance(data, DatetimeIndex): data = DataFrame(data) self.setWindowTitle(title) self.resize(600, 500) self.dataModel = DataFrameModel(data, parent=self) self.dataModel.dataChanged.connect(self.save_and_close_enable) self.dataTable = DataFrameView(self, self.dataModel) self.layout.addWidget(self.dataTable) self.setLayout(self.layout) self.setMinimumSize(400, 300) # Make the dialog act as a window self.setWindowFlags(Qt.Window) btn_layout = QHBoxLayout() btn = QPushButton(_("Format")) # disable format button for int type btn_layout.addWidget(btn) btn.clicked.connect(self.change_format) btn = QPushButton(_('Resize')) btn_layout.addWidget(btn) btn.clicked.connect(self.resize_to_contents) bgcolor = QCheckBox(_('Background color')) bgcolor.setChecked(self.dataModel.bgcolor_enabled) bgcolor.setEnabled(self.dataModel.bgcolor_enabled) bgcolor.stateChanged.connect(self.change_bgcolor_enable) btn_layout.addWidget(bgcolor) self.bgcolor_global = QCheckBox(_('Column min/max')) self.bgcolor_global.setChecked(self.dataModel.colum_avg_enabled) self.bgcolor_global.setEnabled(not self.is_series and self.dataModel.bgcolor_enabled) self.bgcolor_global.stateChanged.connect(self.dataModel.colum_avg) btn_layout.addWidget(self.bgcolor_global) btn_layout.addStretch() self.btn_save_and_close = QPushButton(_('Save and Close')) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_('Close')) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) self.layout.addLayout(btn_layout, 2, 0) return True @Slot(QModelIndex, QModelIndex) def save_and_close_enable(self, top_left, bottom_right): """Handle the data change event to enable the save and close button.""" self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) def change_bgcolor_enable(self, state): """ This is implementet so column min/max is only active when bgcolor is """ self.dataModel.bgcolor(state) self.bgcolor_global.setEnabled(not self.is_series and state > 0) def change_format(self): """ Ask user for display format for floats and use it. This function also checks whether the format is valid and emits `sig_option_changed`. """ format, valid = QInputDialog.getText(self, _('Format'), _("Float formatting"), QLineEdit.Normal, self.dataModel.get_format()) if valid: format = str(format) try: format % 1.1 except: msg = _("Format ({}) is incorrect").format(format) QMessageBox.critical(self, _("Error"), msg) return if not format.startswith('%'): msg = _("Format ({}) should start with '%'").format(format) QMessageBox.critical(self, _("Error"), msg) return self.dataModel.set_format(format) self.sig_option_changed.emit('dataframe_format', format) def get_value(self): """Return modified Dataframe -- this is *not* a copy""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute df = self.dataModel.get_data() if self.is_series: return df.iloc[:, 0] else: return df def resize_to_contents(self): QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) self.dataTable.resizeColumnsToContents() self.dataModel.fetch_more(columns=True) self.dataTable.resizeColumnsToContents() QApplication.restoreOverrideCursor()
class SlitSelectionUI(QDialog): """ Custom slit selection UI and editor. Right now it only applies slits temporarly, ie. if the current target is changed, slit settings will be lost. """ def __init__(self, mosviz_viewer, parent=None): super(SlitSelectionUI, self).__init__(parent=parent) self.mosviz_viewer = mosviz_viewer self._slit_dict = {} self._mosviz_table_option_text = 'Slit from MOSViz Table' self._init_ui() def _init_ui(self): self.slit_type_label = QLabel('Slit Type') self.slit_type_combo = QComboBox() self.slit_type_combo.currentIndexChanged.connect(self.update_info) hbl1 = QHBoxLayout() hbl1.addWidget(self.slit_type_label) hbl1.addWidget(self.slit_type_combo) self.slit_width_label = QLabel('Slit Width') self.slit_width_input = QLineEdit() self.slit_width_combo = QComboBox() self.slit_width_units = QLabel('arcsec') hbl2 = QHBoxLayout() hbl2.addWidget(self.slit_width_label) hbl2.addWidget(self.slit_width_input) hbl2.addWidget(self.slit_width_combo) hbl2.addWidget(self.slit_width_units) self.slit_length_label = QLabel('Slit Length') self.slit_length_input = QLineEdit() self.slit_length_combo = QComboBox() self.slit_length_units = QLabel('arcsec') hbl3 = QHBoxLayout() hbl3.addWidget(self.slit_length_label) hbl3.addWidget(self.slit_length_input) hbl3.addWidget(self.slit_length_combo) hbl3.addWidget(self.slit_length_units) self.okButton = QPushButton('Apply') self.okButton.clicked.connect(self.apply) self.okButton.setDefault(True) self.cancelButton = QPushButton('Cancel') self.cancelButton.clicked.connect(self.cancel) hbl4 = QHBoxLayout() hbl4.addWidget(self.cancelButton) hbl4.addWidget(self.okButton) vbl = QVBoxLayout() vbl.addLayout(hbl1) vbl.addLayout(hbl2) vbl.addLayout(hbl3) vbl.addLayout(hbl4) self.setLayout(vbl) self.vbl = vbl self._load_selections() self._populate_combo() self.update_info(0) self.show() def _load_selections(self): """Load preconfigured slit shapes from yaml file""" file_path = os.path.join(os.path.dirname(__file__), 'saved_slits.yaml') with open(file_path) as f: self.slit_dict = yaml.load(f) def _populate_combo(self, default_index=0): """Populate combo box with slit types""" name_list = [self._mosviz_table_option_text] + \ [self.slit_dict[s]['name'] for s in sorted(self.slit_dict)] + \ ['Custom'] key_list = ['default'] + [s for s in sorted(self.slit_dict)] + ['custom'] combo_input = [(name, key) for name, key in zip(name_list, key_list)] update_combobox(self.slit_type_combo, combo_input, default_index=default_index) @property def width(self): if self.slit_width_combo.isVisible(): width = self.slit_width_combo.currentData() else: width = self.slit_width_input.text() return u.Quantity(width) @property def length(self): if self.slit_length_combo.isVisible(): length = self.slit_length_combo.currentData() else: length = self.slit_length_input.text() return u.Quantity(length) @property def width_units(self): return u.Unit(self.slit_width_units.text()) @property def length_units(self): return u.Unit(self.slit_length_units.text()) def update_info(self, index): """ Update width and hight based on combo index. Callback for combo box. """ key = self.slit_type_combo.currentData() length = width = None width_units = length_units = '' if key == 'default': slit_info = self.mosviz_viewer.get_slit_dimensions_from_file() width_units, length_units = self.mosviz_viewer.get_slit_units_from_file() if slit_info is None: length, width = ['N/A', 'N/A'] else: length, width = slit_info elif key != 'custom': if 'length' in self.slit_dict[key]: length = self.slit_dict[key]['length'] if 'width' in self.slit_dict[key]: width = self.slit_dict[key]['width'] else: width_units = length_units = 'arcsec' for input_widget in [self.slit_width_input, self.slit_length_input]: input_widget.setStyleSheet("") if isinstance(width, list): self.slit_width_input.hide() self.slit_width_combo.show() combo_input = [(str(i), str(i)) for i in width] update_combobox(self.slit_width_combo, combo_input) elif width is None: self.slit_width_combo.hide() self.slit_width_input.show() self.slit_width_input.setText('') self.slit_width_input.setDisabled(False) else: self.slit_width_combo.hide() self.slit_width_input.show() self.slit_width_input.setText(str(width)) self.slit_width_input.setDisabled(True) self.slit_width_units.setText(width_units) if isinstance(length, list): self.slit_length_input.hide() self.slit_length_combo.show() combo_input = [(str(i), str(i)) for i in length] update_combobox(self.slit_length_combo, combo_input) elif length is None: self.slit_length_combo.hide() self.slit_length_input.show() self.slit_length_input.setText('') self.slit_length_input.setDisabled(False) else: self.slit_length_combo.hide() self.slit_length_input.show() self.slit_length_input.setText(str(length)) self.slit_length_input.setDisabled(True) self.slit_length_units.setText(length_units) def input_validation(self): red = "background-color: rgba(255, 0, 0, 128);" success = True for input_widget in [self.slit_width_input, self.slit_length_input]: if not input_widget.isVisible(): continue if input_widget.text() == "": input_widget.setStyleSheet(red) success = False else: try: num = u.Quantity(input_widget.text()).value if num <= 0: input_widget.setStyleSheet(red) success = False else: input_widget.setStyleSheet("") except ValueError: input_widget.setStyleSheet(red) success = False return success def apply(self): """Validate and replace current slit""" key = self.slit_type_combo.currentData() if not self.input_validation() and key != "default": return if key == "default": slit_info = self.mosviz_viewer.get_slit_dimensions_from_file() if slit_info is None: self.mosviz_viewer.slit_controller.clear_slits() else: self.mosviz_viewer.add_slit() else: width = (self.width * self.width_units).to(u.arcsec) length = (self.length * self.length_units).to(u.arcsec) self.mosviz_viewer.slit_controller.clear_slits() self.mosviz_viewer.add_slit(width=width, length=length) if self.mosviz_viewer.slit_controller.has_slits: self.mosviz_viewer.image_widget.draw_slit() self.mosviz_viewer.image_widget.set_slit_limits() self.mosviz_viewer.image_widget._redraw() self.cancel() def cancel(self): self.close()
class MOSVizViewer(DataViewer): LABEL = "MOSViz Viewer" window_closed = Signal() _toolbar_cls = MOSViewerToolbar tools = [] subtools = [] def __init__(self, session, parent=None): super(MOSVizViewer, self).__init__(session, parent=parent) self.slit_controller = SlitController(self) self.load_ui() # Define some data containers self.filepath = None self.savepath = None self.data_idx = None self.comments = False self.textChangedAt = None self.mask = None self.cutout_wcs = None self.level2_data = None self.spec2d_data = None self.catalog = None self.current_row = None self._specviz_instance = None self._loaded_data = {} self._primary_data = None self._layer_view = SimpleLayerWidget(parent=self) self._layer_view.layer_combo.currentIndexChanged.connect(self._selection_changed) self.resize(800, 600) self.image_viewer_hidden = False def load_ui(self): """ Setup the MOSView viewer interface. """ self.central_widget = QWidget(self) path = os.path.join(UI_DIR, 'mos_widget.ui') loadUi(path, self.central_widget) self.image_widget = DrawableImageWidget(slit_controller=self.slit_controller) self.spectrum2d_widget = Spectrum2DWidget() self._specviz_viewer = Workspace() self._specviz_viewer.add_plot_window() self.spectrum1d_widget = self._specviz_viewer.current_plot_window self.spectrum1d_widget.plot_widget.getPlotItem().layout.setContentsMargins(45, 0, 25, 0) # Set up helper for sharing axes. SharedAxisHelper defaults to no sharing # and we control the sharing later by setting .sharex and .sharey on the # helper self.spectrum2d_image_share = SharedAxisHelper(self.spectrum2d_widget._axes, self.image_widget._axes) # We only need to set the image widget to keep the same aspect ratio # since the two other viewers don't require square pixels, so the axes # should not change shape. self.image_widget._axes.set_adjustable('datalim') self.meta_form_layout = self.central_widget.meta_form_layout self.meta_form_layout.setFieldGrowthPolicy(self.meta_form_layout.ExpandingFieldsGrow) self.central_widget.left_vertical_splitter.insertWidget(0, self.image_widget) self.central_widget.right_vertical_splitter.addWidget(self.spectrum2d_widget) self.central_widget.right_vertical_splitter.addWidget(self.spectrum1d_widget.widget()) # Set the splitter stretch factors self.central_widget.left_vertical_splitter.setStretchFactor(0, 1) self.central_widget.left_vertical_splitter.setStretchFactor(1, 8) self.central_widget.right_vertical_splitter.setStretchFactor(0, 1) self.central_widget.right_vertical_splitter.setStretchFactor(1, 2) self.central_widget.horizontal_splitter.setStretchFactor(0, 1) self.central_widget.horizontal_splitter.setStretchFactor(1, 2) # Keep the left and right splitters in sync otherwise the axes don't line up self.central_widget.left_vertical_splitter.splitterMoved.connect(self._left_splitter_moved) self.central_widget.right_vertical_splitter.splitterMoved.connect(self._right_splitter_moved) # Set the central widget self.setCentralWidget(self.central_widget) self.central_widget.show() # Define the options widget self._options_widget = OptionsWidget() def show(self, *args, **kwargs): super(MOSVizViewer, self).show(*args, **kwargs) # Trigger a sync between the splitters self._left_splitter_moved() if self.image_viewer_hidden: self.image_widget.hide() else: self.image_widget.show() @avoid_circular def _right_splitter_moved(self, *args, **kwargs): if self.image_widget.isHidden(): return sizes = self.central_widget.right_vertical_splitter.sizes() if sizes == [0, 0]: sizes = [230, 230] self.central_widget.left_vertical_splitter.setSizes(sizes) @avoid_circular def _left_splitter_moved(self, *args, **kwargs): if self.image_widget.isHidden(): return sizes = self.central_widget.left_vertical_splitter.sizes() if sizes == [0, 0]: sizes = [230, 230] self.central_widget.right_vertical_splitter.setSizes(sizes) def setup_connections(self): """ Connects gui elements to event calls. """ # Connect the selection event for the combo box to what's displayed self.toolbar.source_select.currentIndexChanged[int].connect( lambda ind: self.load_selection(self.catalog[ind])) self.toolbar.source_select.currentIndexChanged[int].connect( lambda ind: self._set_navigation(ind)) # Connect the exposure selection event self.toolbar.exposure_select.currentIndexChanged[int].connect( lambda ind: self.load_exposure(ind)) self.toolbar.exposure_select.currentIndexChanged[int].connect( lambda ind: self._set_exposure_navigation(ind)) # Connect the specviz button if SpecvizDataViewer is not None: self.toolbar.open_specviz.triggered.connect( lambda: self._open_in_specviz()) else: self.toolbar.open_specviz.setDisabled(True) # Connect slit previous and next buttons self.toolbar.cycle_next_action.triggered.connect( lambda: self._set_navigation( self.toolbar.source_select.currentIndex() + 1)) self.toolbar.cycle_previous_action.triggered.connect( lambda: self._set_navigation( self.toolbar.source_select.currentIndex() - 1)) # Connect exposure previous and next buttons self.toolbar.exposure_next_action.triggered.connect( lambda: self._set_exposure_navigation( self.toolbar.exposure_select.currentIndex() + 1)) self.toolbar.exposure_previous_action.triggered.connect( lambda: self._set_exposure_navigation( self.toolbar.exposure_select.currentIndex() - 1)) # Connect the toolbar axes setting actions self.toolbar.lock_x_action.triggered.connect( lambda state: self.set_locked_axes(x=state)) self.toolbar.lock_y_action.triggered.connect( lambda state: self.set_locked_axes(y=state)) def options_widget(self): return self._options_widget def initialize_toolbar(self): """ Initialize the custom toolbar for the MOSViz viewer. """ from glue.config import viewer_tool self.toolbar = self._toolbar_cls(self) for tool_id in self.tools: mode_cls = viewer_tool.members[tool_id] mode = mode_cls(self) self.toolbar.add_tool(mode) self.addToolBar(self.toolbar) self.setup_connections() def register_to_hub(self, hub): super(MOSVizViewer, self).register_to_hub(hub) def has_data_or_subset(x): if x.sender is self._primary_data: return True elif isinstance(x.sender, Subset) and x.sender.data is self._primary_data: return True else: return False hub.subscribe(self, msg.SubsetCreateMessage, handler=self._add_subset, filter=has_data_or_subset) hub.subscribe(self, msg.SubsetUpdateMessage, handler=self._update_subset, filter=has_data_or_subset) hub.subscribe(self, msg.SubsetDeleteMessage, handler=self._remove_subset, filter=has_data_or_subset) hub.subscribe(self, msg.DataUpdateMessage, handler=self._update_data, filter=has_data_or_subset) def add_data(self, data): """ Processes data message from the central communication hub. Parameters ---------- data : :class:`glue.core.data.Data` Data object. """ # Check whether the data is suitable for the MOSViz viewer - basically # we expect a table of 1D columns with at least three string and four # floating-point columns. if data.ndim != 1: QMessageBox.critical(self, "Error", "MOSViz viewer can only be used " "for data with 1-dimensional components", buttons=QMessageBox.Ok) return False components = [data.get_component(cid) for cid in data.main_components] categorical = [c for c in components if c.categorical] if len(categorical) < 3: QMessageBox.critical(self, "Error", "MOSViz viewer expected at least " "three string components/columns, representing " "the filenames of the 1D and 2D spectra and " "cutouts", buttons=QMessageBox.Ok) return False # We can relax the following requirement if we make the slit parameters # optional numerical = [c for c in components if c.numeric] if len(numerical) < 4: QMessageBox.critical(self, "Error", "MOSViz viewer expected at least " "four numerical components/columns, representing " "the slit position, length, and position angle", buttons=QMessageBox.Ok) return False # Make sure the loaders and column names are correct result = confirm_loaders_and_column_names(data) if not result: return False self._primary_data = data self._layer_view.data = data self._unpack_selection(data) return True def add_data_for_testing(self, data): """ Processes data message from the central communication hub. Parameters ---------- data : :class:`glue.core.data.Data` Data object. """ # Check whether the data is suitable for the MOSViz viewer - basically # we expect a table of 1D columns with at least three string and four # floating-point columns. if data.ndim != 1: QMessageBox.critical(self, "Error", "MOSViz viewer can only be used " "for data with 1-dimensional components", buttons=QMessageBox.Ok) return False components = [data.get_component(cid) for cid in data.main_components] categorical = [c for c in components if c.categorical] if len(categorical) < 3: QMessageBox.critical(self, "Error", "MOSViz viewer expected at least " "three string components/columns, representing " "the filenames of the 1D and 2D spectra and " "cutouts", buttons=QMessageBox.Ok) return False # We can relax the following requirement if we make the slit parameters # optional numerical = [c for c in components if c.numeric] if len(numerical) < 4: QMessageBox.critical(self, "Error", "MOSViz viewer expected at least " "four numerical components/columns, representing " "the slit position, length, and position angle", buttons=QMessageBox.Ok) return False # Block of code to bypass the loader_selection gui ######################################################### if 'loaders' not in data.meta: data.meta['loaders'] = {} # Deimos data data.meta['loaders']['spectrum1d'] = "DEIMOS 1D Spectrum" data.meta['loaders']['spectrum2d'] = "DEIMOS 2D Spectrum" data.meta['loaders']['cutout'] = "ACS Cutout Image" if 'special_columns' not in data.meta: data.meta['special_columns'] = {} data.meta['special_columns']['spectrum1d'] = 'spectrum1d' data.meta['special_columns']['spectrum2d'] = 'spectrum2d' data.meta['special_columns']['source_id'] = 'id' data.meta['special_columns']['cutout'] = 'cutout' data.meta['special_columns']['slit_ra'] = 'ra' data.meta['special_columns']['slit_dec'] = 'dec' data.meta['special_columns']['slit_width'] = 'slit_width' data.meta['special_columns']['slit_length'] = 'slit_length' data.meta['loaders_confirmed'] = True ######################################################### self._primary_data = data self._layer_view.data = data self._unpack_selection(data) return True def add_subset(self, subset): """ Processes subset messages from the central communication hub. Parameters ---------- subset : Subset object. """ self._layer_view.refresh() index = self._layer_view.layer_combo.findData(subset) self._layer_view.layer_combo.setCurrentIndex(index) return True def _update_data(self, message): """ Update data message. Parameters ---------- message : :class:`glue.core.message.Message` Data message object. """ self._layer_view.refresh() def _add_subset(self, message): """ Add subset message. Parameters ---------- message : :class:`glue.core.message.Message` Subset message object. """ self._layer_view.refresh() def _update_subset(self, message): """ Update subset message. Parameters ---------- message : :class:`glue.core.message.Message` Update message object. """ self._layer_view.refresh() self._unpack_selection(message.subset) def _remove_subset(self, message): """ Remove subset message. Parameters ---------- message : :class:`glue.core.message.Message` Subset message object. """ self._layer_view.refresh() self._unpack_selection(message.subset.data) def _selection_changed(self): self._unpack_selection(self._layer_view.layer_combo.currentData()) def _unpack_selection(self, data): """ Interprets the :class:`glue.core.data.Data` object by decomposing the data elements, extracting relevant data, and recomposing a package-agnostic dictionary object containing the relevant data. Parameters ---------- data : :class:`glue.core.data.Data` Glue data object to decompose. """ mask = None if isinstance(data, Subset): try: mask = data.to_mask() except IncompatibleAttribute: return if not np.any(mask): return data = data.data self.mask = mask # Clear the table self.catalog = Table() self.catalog.meta = data.meta self.comments = False col_names = data.components for att in col_names: cid = data.id[att] component = data.get_component(cid) if component.categorical: comp_labels = component.labels[mask] if comp_labels.ndim > 1: comp_labels = comp_labels[0] if str(att) in ["comments", "flag"]: self.comments = True elif str(att) in ['level2', 'spectrum1d', 'spectrum2d', 'cutout']: self.filepath = component._load_log.path p = Path(self.filepath) path = os.path.sep.join(p.parts[:-1]) self.catalog[str(att)] = [os.path.join(path, x) for x in comp_labels] else: self.catalog[str(att)] = comp_labels else: comp_data = component.data[mask] if comp_data.ndim > 1: comp_data = comp_data[0] self.catalog[str(att)] = comp_data if len(self.catalog) > 0: if not self.comments: self.comments = self._load_comments(data.label) #Returns bool else: self._data_collection_index(data.label) self._get_save_path() # Update gui elements self._update_navigation(select=0) def _update_navigation(self, select=0): """ Updates the :class:`qtpy.QtWidgets.QComboBox` widget with the appropriate source `id`s from the MOS catalog. """ if self.toolbar is None: return self.toolbar.source_select.blockSignals(True) self.toolbar.source_select.clear() if len(self.catalog) > 0 and self.catalog.meta["special_columns"]["source_id"] in self.catalog.colnames: self.toolbar.source_select.addItems(self.catalog[self.catalog.meta["special_columns"]["source_id"]][:]) self.toolbar.source_select.setCurrentIndex(select) self.toolbar.source_select.blockSignals(False) self.toolbar.source_select.currentIndexChanged.emit(select) def _set_navigation(self, index): if len(self.catalog) < index: return if 0 <= index < self.toolbar.source_select.count(): self.toolbar.source_select.setCurrentIndex(index) if index <= 0: self.toolbar.cycle_previous_action.setDisabled(True) else: self.toolbar.cycle_previous_action.setDisabled(False) if index >= self.toolbar.source_select.count() - 1: self.toolbar.cycle_next_action.setDisabled(True) else: self.toolbar.cycle_next_action.setDisabled(False) def _set_exposure_navigation(self, index): # For level 3-only data. if index == None: # for some unknown reason (related to layout # managers perhaps?), the combo box does not # disappear from screen even when forced to # hide. Next best solution is to disable it. self.toolbar.exposure_select.setEnabled(False) self.toolbar.exposure_next_action.setEnabled(False) self.toolbar.exposure_previous_action.setEnabled(False) return if index > self.toolbar.exposure_select.count(): return if 0 <= index < self.toolbar.exposure_select.count(): self.toolbar.exposure_select.setCurrentIndex(index) if index < 1: self.toolbar.exposure_previous_action.setEnabled(False) else: self.toolbar.exposure_previous_action.setEnabled(True) if index >= self.toolbar.exposure_select.count() - 1: self.toolbar.exposure_next_action.setEnabled(False) else: self.toolbar.exposure_next_action.setEnabled(True) def _open_in_specviz(self): if self._specviz_instance is None: # Store a reference to the currently opened data viewer. This means # new "open in specviz" events will be added to the current viewer # as opposed to opening a new viewer. self._specviz_instance = self.session.application.new_data_viewer( SpecvizDataViewer) # Clear the reference to ensure no qt dangling pointers def _clear_instance_reference(): self._specviz_instance = None self._specviz_instance.window_closed.connect( _clear_instance_reference) # Create a new Spectrum1D object from the flux data attribute of # the incoming data spec = glue_data_to_spectrum1d(self._loaded_data['spectrum1d'], 'Flux') # Create a DataItem from the Spectrum1D object, which adds the data # to the internel specviz model data_item = self._specviz_instance.current_workspace.model.add_data( spec, 'Spectrum1D') self._specviz_instance.current_workspace.force_plot(data_item) def load_selection(self, row): """ Processes a row in the MOS catalog by first loading the data set, updating the stored data components, and then rendering the data on the visible MOSViz viewer plots. Parameters ---------- row : `astropy.table.Row` A row object representing a row in the MOS catalog. Each key should be a column name. """ self.current_row = row # Get loaders loader_spectrum1d = SPECTRUM1D_LOADERS[self.catalog.meta["loaders"]["spectrum1d"]] loader_spectrum2d = SPECTRUM2D_LOADERS[self.catalog.meta["loaders"]["spectrum2d"]] loader_cutout = CUTOUT_LOADERS[self.catalog.meta["loaders"]["cutout"]] # Get column names colname_spectrum1d = self.catalog.meta["special_columns"]["spectrum1d"] colname_spectrum2d = self.catalog.meta["special_columns"]["spectrum2d"] colname_cutout = self.catalog.meta["special_columns"]["cutout"] level2_data = None if "level2" in self.catalog.meta["loaders"]: loader_level2 = LEVEL2_LOADERS[self.catalog.meta["loaders"]["level2"]] colname_level2 = self.catalog.meta["special_columns"]["level2"] level2_basename = os.path.basename(row[colname_level2]) if level2_basename != "None": level2_data = loader_level2(row[colname_level2]) spec1d_basename = os.path.basename(row[colname_spectrum1d]) if spec1d_basename == "None": spec1d_data = None else: spec1d_data = loader_spectrum1d(row[colname_spectrum1d]) spec2d_basename = os.path.basename(row[colname_spectrum2d]) if spec2d_basename == "None": spec2d_data = None else: spec2d_data = loader_spectrum2d(row[colname_spectrum2d]) image_basename = os.path.basename(row[colname_cutout]) if image_basename == "None": image_data = None else: image_data = loader_cutout(row[colname_cutout]) self._update_data_components(spec1d_data, key='spectrum1d') self._update_data_components(spec2d_data, key='spectrum2d') self._update_data_components(image_data, key='cutout') self.level2_data = level2_data self.spec2d_data = spec2d_data self.render_data(row, spec1d_data, spec2d_data, image_data, level2_data) def load_exposure(self, index): ''' Loads the level 2 exposure into the 2D spectrum plot widget. It can also load back the level 3 spectrum. ''' name = self.toolbar.exposure_select.currentText() if 'Level 3' in name: self.spectrum2d_widget.set_image( image = self.spec2d_data.get_component(self.spec2d_data.id['Flux']).data, interpolation = 'none', aspect = 'auto', extent = self.extent, origin='lower') else: if name in [component.label for component in self.level2_data.components]: self.spectrum2d_widget.set_image( image = self.level2_data.get_component(self.level2_data.id[name]).data, interpolation = 'none', aspect = 'auto', extent = self.extent, origin='lower') def _update_data_components(self, data, key): """ Update the data components that act as containers for the displayed data in the MOSViz viewer. This obviates the need to keep creating new data components. Parameters ---------- data : :class:`glue.core.data.Data` Data object to replace within the component. key : str References the particular data set type. """ cur_data = self._loaded_data.get(key, None) if cur_data is not None and data is None: self._loaded_data[key] = None self.session.data_collection.remove(cur_data) elif cur_data is None and data is not None: self._loaded_data[key] = data self.session.data_collection.append(data) elif data is not None: cur_data.update_values_from_data(data) else: return def add_slit(self, row=None, width=None, length=None): if row is None: row = self.current_row wcs = self.cutout_wcs if wcs is None: raise Exception("Image viewer has no WCS information") ra = row[self.catalog.meta["special_columns"]["slit_ra"]] dec = row[self.catalog.meta["special_columns"]["slit_dec"]] if width is None: width = row[self.catalog.meta["special_columns"]["slit_width"]] if length is None: length = row[self.catalog.meta["special_columns"]["slit_length"]] self.slit_controller.add_rectangle_sky_slit(wcs, ra, dec, width, length) def render_data(self, row, spec1d_data=None, spec2d_data=None, image_data=None, level2_data=None): """ Render the updated data sets in the individual plot widgets within the MOSViz viewer. """ self._check_unsaved_comments() self.image_viewer_hidden = image_data is None if spec1d_data is not None: # TODO: This should not be needed. Must explore why the core model # is out of sync with the proxy model. self.spectrum1d_widget.plot_widget.clear_plots() # Clear the specviz model of any rendered plot items self._specviz_viewer.model.clear() # Create a new Spectrum1D object from the flux data attribute of # the incoming data spec = glue_data_to_spectrum1d(spec1d_data, 'Flux') # Create a DataItem from the Spectrum1D object, which adds the data # to the internel specviz model data_item = self._specviz_viewer.model.add_data(spec, 'Spectrum1D') # Get the PlotDataItem rendered via the plot's proxy model and # ensure that it is visible in the plot plot_data_item = self.spectrum1d_widget.proxy_model.item_from_id(data_item.identifier) plot_data_item.visible = True plot_data_item.color = "#000000" # Explicitly let the plot widget know that data items have changed self.spectrum1d_widget.plot_widget.on_item_changed(data_item) if not self.image_viewer_hidden: if not self.image_widget.isVisible(): self.image_widget.setVisible(True) wcs = image_data.coords.wcs self.cutout_wcs = wcs array = image_data.get_component(image_data.id['Flux']).data # Add the slit patch to the plot self.slit_controller.clear_slits() if "slit_width" in self.catalog.meta["special_columns"] and \ "slit_length" in self.catalog.meta["special_columns"] and \ wcs is not None: self.add_slit(row) self.image_widget.draw_slit() else: self.image_widget.reset_limits() self.image_widget.set_image(array, wcs=wcs, interpolation='none', origin='lower') self.image_widget.axes.set_xlabel("Spatial X") self.image_widget.axes.set_ylabel("Spatial Y") if self.slit_controller.has_slits: self.image_widget.set_slit_limits() self.image_widget._redraw() else: self.cutout_wcs = None # Plot the 2D spectrum data last because by then we can make sure that # we set up the extent of the image appropriately if the cutout and the # 1D spectrum are present so that the axes can be locked. # We are repurposing the spectrum 2d widget to handle the display of both # the level 3 and level 2 spectra. if spec2d_data is not None or level2_data is not None: self._load_spectrum2d_widget(spec2d_data, level2_data) else: self.spectrum2d_widget.no_data() # Clear the meta information widget # NOTE: this process is inefficient for i in range(self.meta_form_layout.count()): wid = self.meta_form_layout.itemAt(i).widget() label = self.meta_form_layout.labelForField(wid) if label is not None: label.deleteLater() wid.deleteLater() # Repopulate the form layout # NOTE: this process is inefficient for col in row.colnames: if col.lower() not in ["comments", "flag"]: line_edit = QLineEdit(str(row[col]), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow(col, line_edit) # Set up comment and flag input/display boxes if self.comments: if self.savepath is not None: if self.savepath == -1: line_edit = QLineEdit(os.path.basename("Not Saving to File."), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow("Save File", line_edit) else: line_edit = QLineEdit(os.path.basename(self.savepath), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow("Save File", line_edit) self.input_flag = QLineEdit(self.get_flag(), self.central_widget.meta_form_widget) self.input_flag.textChanged.connect(self._text_changed) self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") self.meta_form_layout.addRow("Flag", self.input_flag) self.input_comments = QPlainTextEdit(self.get_comment(), self.central_widget.meta_form_widget) self.input_comments.textChanged.connect(self._text_changed) self.input_comments.setStyleSheet("background-color: rgba(255, 255, 255);") self.meta_form_layout.addRow("Comments", self.input_comments) self.input_save = QPushButton('Save', self.central_widget.meta_form_widget) self.input_save.clicked.connect(self.update_comments) self.input_save.setDefault(True) self.input_refresh = QPushButton('Reload', self.central_widget.meta_form_widget) self.input_refresh.clicked.connect(self.refresh_comments) self.meta_form_layout.addRow(self.input_save, self.input_refresh) if not self.isHidden() and self.image_viewer_hidden: self.image_widget.setVisible(False) def _load_spectrum2d_widget(self, spec2d_data, level2_data): if not spec2d_data: return xp2d = np.arange(spec2d_data.shape[1]) yp2d = np.repeat(0, spec2d_data.shape[1]) spectrum2d_disp, spectrum2d_offset = spec2d_data.coords.pixel2world(xp2d, yp2d) x_min = spectrum2d_disp.min() x_max = spectrum2d_disp.max() if self.slit_controller.has_slits and \ None not in self.slit_controller.y_bounds: y_min, y_max = self.slit_controller.y_bounds else: y_min = -0.5 y_max = spec2d_data.shape[0] - 0.5 self.extent = [x_min, x_max, y_min, y_max] # By default, displays the level 3 spectrum. The level 2 # data is plotted elsewhere, driven by the exposure_select # combo box signals. self.spectrum2d_widget.set_image( image=spec2d_data.get_component(spec2d_data.id['Flux']).data, interpolation='none', aspect='auto', extent=self.extent, origin='lower') self.spectrum2d_widget.axes.set_xlabel("Wavelength") self.spectrum2d_widget.axes.set_ylabel("Spatial Y") self.spectrum2d_widget._redraw() # If the axis are linked between the 1d and 2d views, setting the data # often ignores the initial bounds and instead uses the bounds of the # 2d data until the 1d view is moved. Force the 2d viewer to honor # the 1d view bounds. self.spectrum1d_widget.plot_widget.getPlotItem().sigXRangeChanged.emit( None, self.spectrum1d_widget.plot_widget.viewRange()[0]) # Populates the level 2 exposures combo box if level2_data: self.toolbar.exposure_select.clear() self.toolbar.exposure_select.addItems(['Level 3']) components = level2_data.main_components + level2_data.derived_components self.toolbar.exposure_select.addItems([component.label for component in components]) self._set_exposure_navigation(0) else: self._set_exposure_navigation(None) @defer_draw def set_locked_axes(self, x=None, y=None): # Here we only change the setting if x or y are not None # since if set_locked_axes is called with eg. x=True, then # we shouldn't change the y setting. if x is not None: if x: # Lock the x axis if x is True def on_x_range_changed(xlim): self.spectrum2d_widget.axes.set_xlim(*xlim) self.spectrum2d_widget._redraw() self.spectrum1d_widget.plot_widget.getPlotItem().sigXRangeChanged.connect( lambda a, b: on_x_range_changed(b)) # Call the slot to update the axis linking initially # FIXME: Currently, this does not work for some reason. on_x_range_changed(self.spectrum1d_widget.plot_widget.viewRange()[0]) else: # Unlock the x axis if x is False self.spectrum1d_widget.plot_widget.getPlotItem().sigXRangeChanged.disconnect() if y is not None: self.spectrum2d_image_share.sharey = y self.spectrum2d_widget._redraw() self.image_widget._redraw() def layer_view(self): return self._layer_view def _text_changed(self): if self.textChangedAt is None: i = self.toolbar.source_select.currentIndex() self.textChangedAt = self._index_hash(i) def _check_unsaved_comments(self): if self.textChangedAt is None: return #Nothing to be changed i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) if self.textChangedAt == i: self.textChangedAt = None return #This is a refresh info = "Comments or flags changed but were not saved. Would you like to save them?" reply = QMessageBox.question(self, '', info, QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.update_comments(True) self.textChangedAt = None def _data_collection_index(self, label): idx = -1 for i, l in enumerate(self.session.data_collection): if l.label == label: idx = i break if idx == -1: return -1 self.data_idx = idx return idx def _index_hash(self, i): """Local selection index -> Table index""" if self.mask is not None: size = self.mask.size temp = np.arange(size) return temp[self.mask][i] else: return i def _id_to_index_hash(self, ID, l): """Object Name -> Table index""" for i, name in enumerate(l): if name == ID: return i return None def get_slit_dimensions_from_file(self): if self.catalog is None: return None if "slit_width" in self.catalog.meta["special_columns"] and \ "slit_length" in self.catalog.meta["special_columns"]: width = self.current_row[self.catalog.meta["special_columns"]["slit_width"]] length = self.current_row[self.catalog.meta["special_columns"]["slit_length"]] return [length, width] return None def get_slit_units_from_file(self): # TODO: Update once units infrastructure is in place return ["arcsec", "arcsec"] def get_comment(self): idx = self.data_idx i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) comp = self.session.data_collection[idx].get_component("comments") return comp.labels[i] def get_flag(self): idx = self.data_idx i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) comp = self.session.data_collection[idx].get_component("flag") return comp.labels[i] def send_NumericalDataChangedMessage(self): idx = self.data_idx data = self.session.data_collection[idx] data.hub.broadcast(msg.NumericalDataChangedMessage(data, "comments")) def refresh_comments(self): self.input_flag.setText(self.get_flag()) self.input_comments.setPlainText(self.get_comment()) self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") self.textChangedAt = None def _get_save_path(self): """ Try to get save path from other MOSVizViewer instances """ for v in self.session.application.viewers[0]: if isinstance(v, MOSVizViewer): if v.savepath is not None: if v.data_idx == self.data_idx: self.savepath = v.savepath break def _setup_save_path(self): """ Prompt the user for a file to save comments and flags into. """ fail = True success = False info = "Where would you like to save comments and flags?" option = pick_item([0, 1], [os.path.basename(self.filepath), "New MOSViz Table file"], label=info, title="Comment Setup") if option == 0: self.savepath = self.filepath elif option == 1: dirname = os.path.dirname(self.filepath) path = compat.getsavefilename(caption="New MOSViz Table File", basedir=dirname, filters="*.txt")[0] if path == "": return fail self.savepath = path else: return fail for v in self.session.application.viewers[0]: if isinstance(v, MOSVizViewer): if v.data_idx == self.data_idx: v.savepath = self.savepath self._layer_view.refresh() return success def update_comments(self, pastSelection = False): """ Process comment and flag changes and save to file. Parameters ---------- pastSelection : bool True when updating past selections. Used when user forgets to save. """ if self.input_flag.text() == "": self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") return i = None try: i = int(self.input_flag.text()) except ValueError: self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") info = QMessageBox.information(self, "Status:", "Flag must be an int!") return self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") idx = self.data_idx if pastSelection: i = self.textChangedAt self.textChangedAt = None else: i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) data = self.session.data_collection[idx] comp = data.get_component("comments") comp.labels.flags.writeable = True comp.labels[i] = self.input_comments.toPlainText() comp = data.get_component("flag") comp.labels.flags.writeable = True comp.labels[i] = self.input_flag.text() self.send_NumericalDataChangedMessage() self.write_comments() self.textChangedAt = None def _load_comments(self, label): """ Populate the comments and flag columns. Attempt to load comments from file. Parameters ---------- label : str The label of the data in session.data_collection. """ #Make sure its the right data #(beacuse subset data is masked) idx = self._data_collection_index(label) if idx == -1: return False data = self.session.data_collection[idx] #Fill in default comments: length = data.shape[0] new_comments = np.array(["" for i in range(length)], dtype=object) new_flags = np.array(["0" for i in range(length)], dtype=object) #Fill in any saved comments: meta = data.meta obj_names = data.get_component(self.catalog.meta["special_columns"]["source_id"]).labels if "MOSViz_comments" in meta.keys(): try: comments = meta["MOSViz_comments"] for key in comments.keys(): index = self._id_to_index_hash(key, obj_names) if index is not None: line = comments[key] new_comments[index] = line except Exception as e: print("MOSViz Comment Load Failed: ", e) if "MOSViz_flags" in meta.keys(): try: flags = meta["MOSViz_flags"] for key in flags.keys(): index = self._id_to_index_hash(key, obj_names) if index is not None: line = flags[key] new_flags[index] = line except Exception as e: print("MOSViz Flag Load Failed: ", e) #Send to DC data.add_component(CategoricalComponent(new_flags, "flag"), "flag") data.add_component(CategoricalComponent(new_comments, "comments"), "comments") return True def write_comments(self): """ Setup save file. Write comments and flags to file """ if self.savepath is None: fail = self._setup_save_path() if fail: return if self.savepath == -1: return #Do not save to file option idx = self.data_idx data = self.session.data_collection[idx] save_comments = data.get_component("comments").labels save_flag = data.get_component("flag").labels obj_names = data.get_component(self.catalog.meta["special_columns"]["source_id"]).labels fn = self.savepath folder = os.path.dirname(fn) t = astropy_table.data_to_astropy_table(data) #Check if load and save dir paths match temp = os.path.dirname(self.filepath) if not os.path.samefile(folder, temp): t['spectrum1d'].flags.writeable = True t['spectrum2d'].flags.writeable = True t['cutout'].flags.writeable = True for i in range(len(t)): t['spectrum1d'][i] = os.path.abspath(t['spectrum1d'][i]) t['spectrum2d'][i] = os.path.abspath(t['spectrum2d'][i]) t['cutout'][i] = os.path.abspath(t['cutout'][i]) try: t.remove_column("comments") t.remove_column("flag") keys = t.meta.keys() if "MOSViz_comments" in keys: t.meta.pop("MOSViz_comments") if "MOSViz_flags" in keys: t.meta.pop("MOSViz_flags") comments = OrderedDict() flags = OrderedDict() for i, line in enumerate(save_comments): if line != "": line = line.replace("\n", " ") key = str(obj_names[i]) comments[key] = line for i, line in enumerate(save_flag): if line != "0" and line != "": line = comments.replace("\n", " ") key = str(obj_names[i]) flags[key] = line if len(comments) > 0: t.meta["MOSViz_comments"] = comments if len(flags) > 0: t.meta["MOSViz_flags"] = flags t.write(fn, format="ascii.ecsv", overwrite=True) except Exception as e: print("Comment write failed:", e) def closeEvent(self, event): """ Clean up the extraneous data components created when opening the MOSViz viewer by overriding the parent class's close event. """ super(MOSVizViewer, self).closeEvent(event) for data in self._loaded_data.values(): self.session.data_collection.remove(data)
class ProjectDialog(QDialog): """Project creation dialog.""" # path, type, packages sig_project_creation_requested = Signal(object, object, object) def __init__(self, parent): """Project creation dialog.""" super(ProjectDialog, self).__init__(parent=parent) # Variables current_python_version = '.'.join([to_text_string(sys.version_info[0]), to_text_string(sys.version_info[1])]) python_versions = ['2.7', '3.4', '3.5'] if current_python_version not in python_versions: python_versions.append(current_python_version) python_versions = sorted(python_versions) self.project_name = None self.location = get_home_dir() # Widgets self.groupbox = QGroupBox() self.radio_new_dir = QRadioButton(_("New directory")) self.radio_from_dir = QRadioButton(_("Existing directory")) self.label_project_name = QLabel(_('Project name')) self.label_location = QLabel(_('Location')) self.label_project_type = QLabel(_('Project type')) self.label_python_version = QLabel(_('Python version')) self.text_project_name = QLineEdit() self.text_location = QLineEdit(get_home_dir()) self.combo_project_type = QComboBox() self.combo_python_version = QComboBox() self.button_select_location = QToolButton() self.button_cancel = QPushButton(_('Cancel')) self.button_create = QPushButton(_('Create')) self.bbox = QDialogButtonBox(Qt.Horizontal) self.bbox.addButton(self.button_cancel, QDialogButtonBox.ActionRole) self.bbox.addButton(self.button_create, QDialogButtonBox.ActionRole) # Widget setup self.combo_python_version.addItems(python_versions) self.radio_new_dir.setChecked(True) self.text_location.setEnabled(True) self.text_location.setReadOnly(True) self.button_select_location.setIcon(get_std_icon('DirOpenIcon')) self.button_cancel.setDefault(True) self.button_cancel.setAutoDefault(True) self.button_create.setEnabled(False) self.combo_project_type.addItems(self._get_project_types()) self.combo_python_version.setCurrentIndex( python_versions.index(current_python_version)) self.setWindowTitle(_('Create new project')) self.setFixedWidth(500) self.label_python_version.setVisible(False) self.combo_python_version.setVisible(False) # Layouts layout_top = QHBoxLayout() layout_top.addWidget(self.radio_new_dir) layout_top.addWidget(self.radio_from_dir) layout_top.addStretch(1) self.groupbox.setLayout(layout_top) layout_grid = QGridLayout() layout_grid.addWidget(self.label_project_name, 0, 0) layout_grid.addWidget(self.text_project_name, 0, 1, 1, 2) layout_grid.addWidget(self.label_location, 1, 0) layout_grid.addWidget(self.text_location, 1, 1) layout_grid.addWidget(self.button_select_location, 1, 2) layout_grid.addWidget(self.label_project_type, 2, 0) layout_grid.addWidget(self.combo_project_type, 2, 1, 1, 2) layout_grid.addWidget(self.label_python_version, 3, 0) layout_grid.addWidget(self.combo_python_version, 3, 1, 1, 2) layout = QVBoxLayout() layout.addWidget(self.groupbox) layout.addSpacing(10) layout.addLayout(layout_grid) layout.addStretch() layout.addSpacing(20) layout.addWidget(self.bbox) self.setLayout(layout) # Signals and slots self.button_select_location.clicked.connect(self.select_location) self.button_create.clicked.connect(self.create_project) self.button_cancel.clicked.connect(self.close) self.radio_from_dir.clicked.connect(self.update_location) self.radio_new_dir.clicked.connect(self.update_location) self.text_project_name.textChanged.connect(self.update_location) def _get_project_types(self): """Get all available project types.""" project_types = get_available_project_types() projects = [] for project in project_types: projects.append(project.PROJECT_TYPE_NAME) return projects def select_location(self): """Select directory.""" location = osp.normpath(getexistingdirectory(self, _("Select directory"), self.location)) if location: if is_writable(location): self.location = location self.update_location() def update_location(self, text=''): """Update text of location.""" self.text_project_name.setEnabled(self.radio_new_dir.isChecked()) name = self.text_project_name.text().strip() if name and self.radio_new_dir.isChecked(): path = osp.join(self.location, name) self.button_create.setDisabled(os.path.isdir(path)) elif self.radio_from_dir.isChecked(): self.button_create.setEnabled(True) path = self.location else: self.button_create.setEnabled(False) path = self.location self.text_location.setText(path) def create_project(self): """Create project.""" packages = ['python={0}'.format(self.combo_python_version.currentText())] self.sig_project_creation_requested.emit( self.text_location.text(), self.combo_project_type.currentText(), packages) self.accept()
class ArrayEditor(QDialog): """Array Editor Dialog""" def __init__(self, parent=None): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.data = None self.arraywidget = None self.stack = None self.layout = None self.btn_save_and_close = None self.btn_close = None # Values for 3d array editor self.dim_indexes = [{}, {}, {}] self.last_dim = 0 # Adjust this for changing the startup dimension def setup_and_check(self, data, title='', readonly=False, xlabels=None, ylabels=None): """ Setup ArrayEditor: return False if data is not supported, True otherwise """ self.data = data readonly = readonly or not self.data.flags.writeable is_record_array = data.dtype.names is not None is_masked_array = isinstance(data, np.ma.MaskedArray) if data.ndim > 3: self.error(_("Arrays with more than 3 dimensions are not " "supported")) return False if xlabels is not None and len(xlabels) != self.data.shape[1]: self.error(_("The 'xlabels' argument length do no match array " "column number")) return False if ylabels is not None and len(ylabels) != self.data.shape[0]: self.error(_("The 'ylabels' argument length do no match array row " "number")) return False if not is_record_array: dtn = data.dtype.name if dtn not in SUPPORTED_FORMATS and not dtn.startswith('str') \ and not dtn.startswith('unicode'): arr = _("%s arrays") % data.dtype.name self.error(_("%s are currently not supported") % arr) return False self.layout = QGridLayout() self.setLayout(self.layout) self.setWindowIcon(ima.icon('arredit')) if title: title = to_text_string(title) + " - " + _("NumPy array") else: title = _("Array editor") if readonly: title += ' (' + _('read only') + ')' self.setWindowTitle(title) self.resize(600, 500) # Stack widget self.stack = QStackedWidget(self) if is_record_array: for name in data.dtype.names: self.stack.addWidget(ArrayEditorWidget(self, data[name], readonly, xlabels, ylabels)) elif is_masked_array: self.stack.addWidget(ArrayEditorWidget(self, data, readonly, xlabels, ylabels)) self.stack.addWidget(ArrayEditorWidget(self, data.data, readonly, xlabels, ylabels)) self.stack.addWidget(ArrayEditorWidget(self, data.mask, readonly, xlabels, ylabels)) elif data.ndim == 3: pass else: self.stack.addWidget(ArrayEditorWidget(self, data, readonly, xlabels, ylabels)) self.arraywidget = self.stack.currentWidget() if self.arraywidget: self.arraywidget.model.dataChanged.connect( self.save_and_close_enable) self.stack.currentChanged.connect(self.current_widget_changed) self.layout.addWidget(self.stack, 1, 0) # Buttons configuration btn_layout = QHBoxLayout() if is_record_array or is_masked_array or data.ndim == 3: if is_record_array: btn_layout.addWidget(QLabel(_("Record array fields:"))) names = [] for name in data.dtype.names: field = data.dtype.fields[name] text = name if len(field) >= 3: title = field[2] if not is_text_string(title): title = repr(title) text += ' - '+title names.append(text) else: names = [_('Masked data'), _('Data'), _('Mask')] if data.ndim == 3: # QSpinBox self.index_spin = QSpinBox(self, keyboardTracking=False) self.index_spin.valueChanged.connect(self.change_active_widget) # QComboBox names = [str(i) for i in range(3)] ra_combo = QComboBox(self) ra_combo.addItems(names) ra_combo.currentIndexChanged.connect(self.current_dim_changed) # Adding the widgets to layout label = QLabel(_("Axis:")) btn_layout.addWidget(label) btn_layout.addWidget(ra_combo) self.shape_label = QLabel() btn_layout.addWidget(self.shape_label) label = QLabel(_("Index:")) btn_layout.addWidget(label) btn_layout.addWidget(self.index_spin) self.slicing_label = QLabel() btn_layout.addWidget(self.slicing_label) # set the widget to display when launched self.current_dim_changed(self.last_dim) else: ra_combo = QComboBox(self) ra_combo.currentIndexChanged.connect(self.stack.setCurrentIndex) ra_combo.addItems(names) btn_layout.addWidget(ra_combo) if is_masked_array: label = QLabel(_("<u>Warning</u>: changes are applied separately")) label.setToolTip(_("For performance reasons, changes applied "\ "to masked array won't be reflected in "\ "array's data (and vice-versa).")) btn_layout.addWidget(label) btn_layout.addStretch() if not readonly: self.btn_save_and_close = QPushButton(_('Save and Close')) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_('Close')) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) self.layout.addLayout(btn_layout, 2, 0) self.setMinimumSize(400, 300) # Make the dialog act as a window self.setWindowFlags(Qt.Window) return True @Slot(QModelIndex, QModelIndex) def save_and_close_enable(self, left_top, bottom_right): """Handle the data change event to enable the save and close button.""" if self.btn_save_and_close: self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) def current_widget_changed(self, index): self.arraywidget = self.stack.widget(index) self.arraywidget.model.dataChanged.connect(self.save_and_close_enable) def change_active_widget(self, index): """ This is implemented for handling negative values in index for 3d arrays, to give the same behavior as slicing """ string_index = [':']*3 string_index[self.last_dim] = '<font color=red>%i</font>' self.slicing_label.setText((r"Slicing: [" + ", ".join(string_index) + "]") % index) if index < 0: data_index = self.data.shape[self.last_dim] + index else: data_index = index slice_index = [slice(None)]*3 slice_index[self.last_dim] = data_index stack_index = self.dim_indexes[self.last_dim].get(data_index) if stack_index is None: stack_index = self.stack.count() try: self.stack.addWidget(ArrayEditorWidget( self, self.data[tuple(slice_index)])) except IndexError: # Handle arrays of size 0 in one axis self.stack.addWidget(ArrayEditorWidget(self, self.data)) self.dim_indexes[self.last_dim][data_index] = stack_index self.stack.update() self.stack.setCurrentIndex(stack_index) def current_dim_changed(self, index): """ This change the active axis the array editor is plotting over in 3D """ self.last_dim = index string_size = ['%i']*3 string_size[index] = '<font color=red>%i</font>' self.shape_label.setText(('Shape: (' + ', '.join(string_size) + ') ') % self.data.shape) if self.index_spin.value() != 0: self.index_spin.setValue(0) else: # this is done since if the value is currently 0 it does not emit # currentIndexChanged(int) self.change_active_widget(0) self.index_spin.setRange(-self.data.shape[index], self.data.shape[index]-1) @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.stack.count()): self.stack.widget(index).accept_changes() QDialog.accept(self) def get_value(self): """Return modified array -- this is *not* a copy""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.data def error(self, message): """An error occured, closing the dialog box""" QMessageBox.critical(self, _("Array editor"), message) self.setAttribute(Qt.WA_DeleteOnClose) self.reject() @Slot() def reject(self): """Reimplement Qt method""" if self.arraywidget is not None: for index in range(self.stack.count()): self.stack.widget(index).reject_changes() QDialog.reject(self)
class ImageRotationDialog(ExToolWindow): def __init__(self, signal, axes, parent, plugin): super(ImageRotationDialog, self).__init__(parent) self.ui = parent self.create_controls() self.accepted.connect(self.ok) self.rejected.connect(self.close_new) self.signal = signal self.plugin = plugin self.new_out = None self._connected_updates = False if isinstance(axes, str): axm = signal.signal.axes_manager if axes.startswith("nav"): axes = (axm._axes.index(axm.navigation_axes[0]), axm._axes.index(axm.navigation_axes[1])) elif axes.startswith("sig"): axes = (axm._axes.index(axm.signal_axes[0]), axm._axes.index(axm.signal_axes[1])) self.axes = axes self.setWindowTitle(tr("Rotate")) # TODO: TAG: Functionality check if not hasattr(signal.signal, 'events'): self.gbo_preview.setVisible(False) # TODO: Add dynamic rotation, e.g. one that rotates when source # signal's data_changed event triggers def connect(self): # TODO: Don't have to con/dis those in gbo self.opt_new.toggled.connect(self.close_new) self.num_angle.valueChanged.connect(self.update) self.chk_grid.toggled.connect(self.update) self.num_grid.valueChanged.connect(self.update) self.chk_reshape.toggled.connect(self.update) self.opt_new.toggled.connect(self.update) self.opt_replace.toggled.connect(self.update) def disconnect(self): self.num_angle.valueChanged.disconnect(self.update) self.chk_grid.toggled.disconnect(self.update) self.num_grid.valueChanged.disconnect(self.update) self.chk_reshape.toggled.disconnect(self.update) self.opt_new.toggled.disconnect(self.update) self.opt_replace.toggled.disconnect(self.update) def ok(self): # Draw figure if not already done # TODO: TAG: Functionality check if not hasattr(self.signal.signal, 'events') or \ not self.gbo_preview.isChecked(): self.update() angle = self.num_angle.value() reshape = self.chk_reshape.isChecked() self.plugin.record_code( r"<p>.rotate_signal({0}, reshape={1}, axes={2})".format( angle, reshape, self.axes)) # Clean up event connections if self.new_out is not None: self.connect_update_plot(self.new_out.signal, disconnect=True) def close_new(self, value=False): if self.new_out is not None and not value: self.new_out.close() self.new_out = None self._connected_updates = False def set_preview(self, value): if not hasattr(self.signal.signal, 'events'): return if value: self.connect() self.update() else: self.disconnect() self.close_new() def _axes_in_nav(self): axm = self.signal.signal.axes_manager navidx = [axm._axes.index(ax) for ax in axm.navigation_axes] if self.axes[0] in navidx: return True return False def connect_update_plot(self, signal, disconnect=False): if self._connected_updates != disconnect: return # Nothing to do, prevent double connections if self._axes_in_nav(): f = signal._plot.navigator_plot.update else: f = signal._plot.signal_plot.update # TODO: TAG: Functionality check if hasattr(signal, 'events') and hasattr( signal.events, 'data_changed'): if disconnect: signal.events.data_changed.disconnect(f) else: signal.events.data_changed.connect(f, []) self._connected_updates = not disconnect def update(self): angle = self.num_angle.value() reshape = self.chk_reshape.isChecked() if self.opt_new.isChecked(): if self.new_out is None: out = None else: out = self.new_out.signal elif self.opt_replace.isChecked(): out = self.signal.signal else: return # Indeterminate state, do nothing s = self.plugin.rotate_signal(angle, self.signal.signal, record=False, reshape=reshape, out=out, axes=self.axes) if out is None: s.metadata.General.title = self.signal.name + "[Rotated]" s.plot() self.connect_update_plot(s) if (self.gbo_preview.isChecked() and self.opt_new.isChecked() and self.new_out is None): self.new_out = self.ui.lut_signalwrapper[s] else: s = out if self.chk_grid.isChecked() is True: pass # TODO: Draw grid def create_controls(self): """ Create UI controls. """ vbox = QVBoxLayout() form = QFormLayout() self.num_angle = QDoubleSpinBox() self.num_angle.setValue(0.0) self.num_angle.setMinimum(-360) self.num_angle.setMaximum(360) form.addRow(tr("Angle:"), self.num_angle) vbox.addLayout(form) self.gbo_preview = QGroupBox(tr("Preview")) self.gbo_preview.setCheckable(True) self.gbo_preview.setChecked(False) gbo_vbox = QVBoxLayout() self.chk_grid = QCheckBox(tr("Grid")) self.chk_grid.setChecked(False) self.num_grid = QSpinBox() self.num_grid.setValue(4) self.num_grid.setMinimum(1) self.num_grid.setEnabled(False) self.chk_grid.toggled[bool].connect(self.num_grid.setEnabled) gbo_vbox.addWidget(self.chk_grid) gbo_vbox.addWidget(self.num_grid) self.gbo_preview.setLayout(gbo_vbox) vbox.addWidget(self.gbo_preview) self.gbo_preview.toggled[bool].connect(self.set_preview) self.gbo_output = QGroupBox(tr("Output")) self.opt_new = QRadioButton(tr("New signal")) self.opt_replace = QRadioButton(tr("In place")) self.opt_new.setChecked(True) gbo_vbox2 = QVBoxLayout() gbo_vbox2.addWidget(self.opt_new) gbo_vbox2.addWidget(self.opt_replace) self.gbo_output.setLayout(gbo_vbox2) vbox.addWidget(self.gbo_output) self.chk_reshape = QCheckBox(tr("Resize to fit")) self.chk_reshape.setChecked(False) vbox.addWidget(self.chk_reshape) self.btn_ok = QPushButton(tr("&OK")) self.btn_ok.setDefault(True) self.btn_ok.clicked.connect(self.accept) self.btn_cancel = QPushButton(tr("&Cancel")) self.btn_cancel.clicked.connect(self.reject) hbox = QHBoxLayout() hbox.addWidget(self.btn_ok) hbox.addWidget(self.btn_cancel) vbox.addLayout(hbox) vbox.addStretch(1) self.setLayout(vbox)
def _build_waverange_dialog(self, wave_range, line_list): dialog = QDialog(parent=self.centralWidget) dialog.setWindowTitle("Wavelength range") dialog.setWindowModality(Qt.ApplicationModal) dialog.resize(370, 250) button_ok = QPushButton("OK") button_ok.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) button_cancel = QPushButton("Cancel") button_cancel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) button_ok.clicked.connect(dialog.accept) button_cancel.clicked.connect(dialog.reject) min_text = QLineEdit("%.2f" % wave_range[0].value) max_text = QLineEdit("%.2f" % wave_range[1].value) validator = QDoubleValidator() validator.setBottom(0.0) validator.setDecimals(2) min_text.setValidator(validator) max_text.setValidator(validator) min_text.setFixedWidth(150) max_text.setFixedWidth(150) min_text.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) max_text.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) min_text.setToolTip("Minimum wavelength to read from list.") max_text.setToolTip("Maximum wavelength to read from list.") nlines_label = self._compute_nlines_in_waverange( line_list, min_text, max_text) min_text.editingFinished.connect( lambda: self._compute_nlines_in_waverange( line_list, min_text, max_text, label=nlines_label)) max_text.editingFinished.connect( lambda: self._compute_nlines_in_waverange( line_list, min_text, max_text, label=nlines_label)) # set up layouts and widgets for the dialog. text_pane = QWidget() text_layout = QGridLayout() text_layout.addWidget(min_text, 1, 0) text_layout.addWidget(QLabel("Minimum wavelength"), 0, 0) text_layout.addWidget(max_text, 1, 1) text_layout.addWidget(QLabel("Maximum wavelength"), 0, 1) spacerItem = QSpacerItem(40, 10, QSizePolicy.Expanding, QSizePolicy.Minimum) text_layout.addItem(spacerItem, 1, 2) text_pane.setLayout(text_layout) label_pane = QWidget() label_layout = QHBoxLayout() label_layout.addWidget(nlines_label) label_layout.addWidget(QLabel(" lines included in range.")) label_layout.addStretch() label_pane.setLayout(label_layout) button_pane = QWidget() button_layout = QHBoxLayout() button_layout.addStretch() button_layout.addWidget(button_cancel) button_layout.addWidget(button_ok) button_pane.setLayout(button_layout) dialog_layout = QVBoxLayout() dialog_layout.setSizeConstraint(QLayout.SetMaximumSize) dialog_layout.addWidget(text_pane) dialog_layout.addWidget(label_pane) dialog_layout.addStretch() dialog_layout.addWidget(button_pane) dialog.setLayout(dialog_layout) button_ok.setDefault(True) button_cancel.setDefault(False) accepted = dialog.exec_() > 0 amin = amax = None if accepted: return self._get_range_from_textfields(min_text, max_text) return (amin, amax)
class TextEditor(QDialog): """Array Editor Dialog""" def __init__(self, text, title='', font=None, parent=None, readonly=False, size=(400, 300)): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.text = None self.btn_save_and_close = None # Display text as unicode if it comes as bytes, so users see # its right representation if is_binary_string(text): self.is_binary = True text = to_text_string(text, 'utf8') else: self.is_binary = False self.layout = QVBoxLayout() self.setLayout(self.layout) # Text edit self.edit = QTextEdit(parent) self.edit.setReadOnly(readonly) self.edit.textChanged.connect(self.text_changed) self.edit.setPlainText(text) if font is None: font = get_font() self.edit.setFont(font) self.layout.addWidget(self.edit) # Buttons configuration btn_layout = QHBoxLayout() btn_layout.addStretch() if not readonly: self.btn_save_and_close = QPushButton(_('Save and Close')) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_('Close')) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) self.layout.addLayout(btn_layout) # Make the dialog act as a window self.setWindowFlags(Qt.Window) self.setWindowIcon(ima.icon('edit')) self.setWindowTitle(_("Text editor") + \ "%s" % (" - "+str(title) if str(title) else "")) self.resize(size[0], size[1]) @Slot() def text_changed(self): """Text has changed""" # Save text as bytes, if it was initially bytes if self.is_binary: self.text = to_binary_string(self.edit.toPlainText(), 'utf8') else: self.text = to_text_string(self.edit.toPlainText()) if self.btn_save_and_close: self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) def get_value(self): """Return modified text""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.text def setup_and_check(self, value): """Verify if TextEditor is able to display strings passed to it.""" try: to_text_string(value, 'utf8') return True except: return False
class ContourOptionsDialog(QDialog): """ Dialog box for selecting contour options """ def __init__(self, contour_settings): super(ContourOptionsDialog, self).__init__(contour_settings.cubeviz_layout) self.setWindowFlags(self.windowFlags() | Qt.Tool) self.setWindowTitle("Contour Options") self.is_preview_active = False # preview mode? self.contour_settings = contour_settings # ref to caller ContourSettings self.image_viewer = self.contour_settings.image_viewer # ref to image viewer self.options = self.contour_settings.options # ref to ContourSettings options self._colormap_members = self.contour_settings.colormap_members # Colormap options self._colormap_index = DEFAULT_GLUE_COLORMAP_INDEX # Currently selected colormap if "cmap" in self.options: if self.options["cmap"] in self._colormap_members: self._colormap_index = self._colormap_members.index(self.options["cmap"]) # Is there a user spacing? if self.contour_settings.spacing is None: self.is_custom_spacing = False else: self.is_custom_spacing = True # Is there a user min? if self.contour_settings.vmin is None: self.is_vmin = False else: self.is_vmin = True # Is there a user max? if self.contour_settings.vmax is None: self.is_vmax = False else: self.is_vmax = True self.add_contour_label = self.contour_settings.add_contour_label # bool self._init_ui() def _init_ui(self): # Line 1: Color map self.colormap_label = QLabel("Color Scheme: ") self.colormap_combo = QColormapCombo() self.colormap_combo.addItem("", userData=UserDataWrapper(cm.viridis)) self.colormap_combo._update_icons() self.colormap_combo.setCurrentIndex(self._colormap_index) self.colormap_combo.setMaximumWidth(150) self.colormap_combo.currentIndexChanged.connect( self._on_colormap_change) # hbl is short for Horizontal Box Layout hbl1 = QHBoxLayout() hbl1.addWidget(self.colormap_label) hbl1.addWidget(self.colormap_combo) # Line 2: Display contour labels self.contour_label_checkBox = QCheckBox("Contour labels (font size):") if self.contour_settings.add_contour_label: self.contour_label_checkBox.setChecked(True) self.contour_label_checkBox.toggled.connect(self.toggle_labels) font_string = str(self.contour_settings.font_size) self.font_size_input = QLineEdit(font_string) self.font_size_input.setFixedWidth(150) self.font_size_input.setDisabled( not self.contour_settings.add_contour_label) hbl2 = QHBoxLayout() hbl2.addWidget(self.contour_label_checkBox) hbl2.addWidget(self.font_size_input) # Line 3: Contour Spacing self.custom_spacing_checkBox = QCheckBox("Contour spacing (interval):") if self.is_custom_spacing: self.custom_spacing_checkBox.setChecked(True) self.custom_spacing_checkBox.toggled.connect(self.custom_spacing) self.spacing_input = QLineEdit() self.spacing_input.setFixedWidth(150) self.spacing_input.setDisabled(not self.is_custom_spacing) spacing = "" if self.is_custom_spacing: spacing = str(self.contour_settings.spacing) elif self.contour_settings.data_spacing is not None: spacing = self.contour_settings.data_spacing spacing = "{0:1.4f}".format(spacing) self.spacing_default_text = spacing self.spacing_input.setText(spacing) hbl3 = QHBoxLayout() hbl3.addWidget(self.custom_spacing_checkBox) hbl3.addWidget(self.spacing_input) # Line 4: Vmax self.vmax_checkBox = QCheckBox("Set max:") self.vmax_input = QLineEdit() self.vmax_input.setFixedWidth(150) self.vmax_input.setDisabled(not self.is_vmax) vmax = "" if self.is_vmax: self.vmax_checkBox.setChecked(True) vmax = str(self.contour_settings.vmax) elif self.contour_settings.data_max is not None: vmax = self.contour_settings.data_max vmax = "{0:1.4f}".format(vmax) self.vmax_input.setText(vmax) self.vmax_default_text = vmax self.vmax_checkBox.toggled.connect(self.toggle_vmax) hbl4 = QHBoxLayout() hbl4.addWidget(self.vmax_checkBox) hbl4.addWidget(self.vmax_input) # Line 5: Vmin self.vmin_checkBox = QCheckBox("Set min:") self.vmin_input = QLineEdit() self.vmin_input.setFixedWidth(150) self.vmin_input.setDisabled(not self.is_vmin) vmin = "" if self.is_vmin: self.vmin_checkBox.setChecked(True) vmin = str(self.contour_settings.vmin) elif self.contour_settings.data_min is not None: vmin = self.contour_settings.data_min vmin = "{0:1.4f}".format(vmin) self.vmin_input.setText(vmin) self.vmin_default_text = vmin self.vmin_checkBox.toggled.connect(self.toggle_vmin) hbl5 = QHBoxLayout() hbl5.addWidget(self.vmin_checkBox) hbl5.addWidget(self.vmin_input) # Line f: self.previewButton = QPushButton("Preview") self.previewButton.clicked.connect(self.preview) self.defaultButton = QPushButton("Reset") self.defaultButton.clicked.connect(self.default) self.okButton = QPushButton("OK") self.okButton.clicked.connect(self.finish) self.okButton.setDefault(True) self.cancelButton = QPushButton("Cancel") self.cancelButton.clicked.connect(self.cancel) hblf = QHBoxLayout() hblf.addStretch(1) hblf.addWidget(self.previewButton) hblf.addWidget(self.defaultButton) hblf.addWidget(self.cancelButton) hblf.addWidget(self.okButton) vbl = QVBoxLayout() vbl.addLayout(hbl1) vbl.addLayout(hbl2) vbl.addLayout(hbl3) vbl.addLayout(hbl4) vbl.addLayout(hbl5) vbl.addLayout(hblf) self.setLayout(vbl) self.show() def update_data_vals(self, vmin="", vmax="", spacing="1"): self.vmin_default_text = vmin if not self.is_vmin: self.vmin_input.setText(vmin) self.vmax_default_text = vmax if not self.is_vmax: self.vmax_input.setText(vmax) self.spacing_default_text = spacing if not self.is_custom_spacing: self.spacing_input.setText(spacing) def _on_colormap_change(self, index): """Combo index changed handler""" self._colormap_index = index def custom_spacing(self): """Checkbox toggled handler""" if self.is_custom_spacing: self.is_custom_spacing = False self.spacing_input.setDisabled(True) spacing = "" if self.contour_settings.data_spacing: spacing = self.contour_settings.data_spacing spacing = "{0:1.4f}".format(spacing) self.spacing_input.setText(spacing) self.spacing_input.setStyleSheet("") else: self.is_custom_spacing = True self.spacing_input.setDisabled(False) def toggle_labels(self): """Checkbox toggled handler""" if self.add_contour_label: self.add_contour_label = False self.font_size_input.setDisabled(True) font_string = str(self.contour_settings.font_size) self.font_size_input.setText(font_string) self.font_size_input.setStyleSheet("") else: self.add_contour_label = True self.font_size_input.setDisabled(False) def toggle_vmax(self): """Checkbox toggled handler""" if self.is_vmax: self.is_vmax = False self.vmax_input.setDisabled(True) vmax = "" if self.contour_settings.data_max: vmax = self.contour_settings.data_max vmax = "{0:1.4f}".format(vmax) self.vmax_input.setText(vmax) self.vmax_input.setStyleSheet("") else: self.is_vmax = True self.vmax_input.setDisabled(False) def toggle_vmin(self): """Checkbox toggled handler""" if self.is_vmin: self.is_vmin = False self.vmin_input.setDisabled(True) vmin = "" if self.contour_settings.data_min: vmin = self.contour_settings.data_min vmin = "{0:1.4f}".format(vmin) self.vmin_input.setText(vmin) self.vmin_input.setStyleSheet("") else: self.is_vmin = True self.vmin_input.setDisabled(False) def input_validation(self): red = "background-color: rgba(255, 0, 0, 128);" def float_check(min_val=None): if user_input.text() == "": user_input.setStyleSheet(red) return False else: try: value = float(user_input.text()) if min_val is not None: if value <= min_val: user_input.setStyleSheet(red) return False else: user_input.setStyleSheet("") except ValueError: user_input.setStyleSheet(red) return False return True def int_check(min_val=None): if user_input.text() == "": user_input.setStyleSheet(red) return False else: try: value = int(user_input.text()) if min_val is not None: if value <= min_val: user_input.setStyleSheet(red) return False else: user_input.setStyleSheet("") except ValueError: user_input.setStyleSheet(red) return False return True success = True # Check 1: spacing_input if self.is_custom_spacing: user_input = self.spacing_input float_check(0) success = success and float_check() # Check 2: font_size_input if self.add_contour_label: user_input = self.font_size_input int_check(0) success = success and int_check() # Check 3: vmax if self.is_vmax: user_input = self.vmax_input float_check() success = success and float_check() # Check 4: vmax if self.is_vmin: user_input = self.vmin_input float_check() success = success and float_check() # Check 5: vmax and vmin if self.is_vmax and self.is_vmin and success: vmax = float(self.vmax_input.text()) vmin = float(self.vmin_input.text()) if vmax <= vmin: self.vmax_input.setStyleSheet(red) self.vmin_input.setStyleSheet(red) success = False return success def finish(self): """ Ok button pressed. Finalize options and send to image viewer """ success = self.input_validation() if not success: return # Change Color Map self._colormap_index = self.colormap_combo.currentIndex() colormap = self._colormap_members[self._colormap_index] self.contour_settings.options["cmap"] = colormap # labels self.contour_settings.add_contour_label = self.add_contour_label # font size if self.add_contour_label: font_size = int(self.font_size_input.text()) self.contour_settings.font_size = font_size else: self.contour_settings.font_size = DEFAULT_CONTOUR_FONT_SIZE # Spacing if self.is_custom_spacing: self.contour_settings.spacing = float(self.spacing_input.text()) else: self.contour_settings.spacing = None # vmax if self.is_vmax: vmax = float(self.vmax_input.text()) self.contour_settings.vmax = vmax self.contour_settings.options["vmax"] = vmax else: self.contour_settings.vmax = None self.contour_settings.options["vmax"] = None # vmin if self.is_vmin: vmin = float(self.vmin_input.text()) self.contour_settings.vmin = vmin self.contour_settings.options["vmin"] = vmin else: self.contour_settings.vmin = None self.contour_settings.options["vmin"] = None # Redraw contour if self.contour_settings.image_viewer.is_contour_active: self.contour_settings.draw_function() self.close() def preview(self): """ Prepare preview contour settings and send to image viewer """ success = self.input_validation() if not success: return image_viewer = self.contour_settings.image_viewer preview_settings = ContourSettings(image_viewer) preview_settings.dialog = self preview_settings.options = self.contour_settings.options.copy() preview_settings.spacing = self.contour_settings.spacing # Change Color Map self._colormap_index = self.colormap_combo.currentIndex() colormap = self._colormap_members[self._colormap_index] preview_settings.options["cmap"] = colormap # labels add_contour_label = self.contour_label_checkBox.isChecked() preview_settings.add_contour_label = add_contour_label # font size if add_contour_label: font_size = int(self.font_size_input.text()) preview_settings.font_size = font_size # Spacing if self.is_custom_spacing: preview_settings.spacing = float(self.spacing_input.text()) else: preview_settings.spacing = None # vmax if self.is_vmax: vmax = float(self.vmax_input.text()) preview_settings.vmax = vmax preview_settings.options["vmax"] = vmax else: preview_settings.vmax = None preview_settings.options["vmax"] = None # vmin if self.is_vmin: vmin = float(self.vmin_input.text()) preview_settings.vmin = vmin preview_settings.options["vmin"] = vmin else: preview_settings.vmin = None preview_settings.options["vmin"] = None # Redraw contour if image_viewer.is_contour_active: self.is_preview_active = True image_viewer.set_contour_preview(preview_settings) else: message = "Contour map is currently switched off. " \ "Please turn on the contour map by selecting " \ "a component from the contour map drop-down menu." info = QMessageBox.critical(self, "Error", message) def default(self): """ Set options back to default and send to image viewer """ self.contour_settings.options = self.contour_settings.default_options() self.contour_settings.spacing = None self.contour_settings.font_size = DEFAULT_CONTOUR_FONT_SIZE self.contour_settings.vmax = None self.contour_settings.vmin = None self.contour_settings.add_contour_label = False if self.contour_settings.image_viewer.is_contour_active: self.contour_settings.draw_function() self.contour_settings.options_dialog() def cancel(self): if self.contour_settings.image_viewer.is_contour_active: self.contour_settings.draw_function() self.close() def closeEvent(self, event): """closeEvent handler""" if self.is_preview_active: self.contour_settings.image_viewer.end_contour_preview() def keyPressEvent(self, e): if e.key() == Qt.Key_Escape: self.cancel()
class ImportWizard(QDialog): """Text data import wizard""" def __init__(self, parent, text, title=None, icon=None, contents_title=None, varname=None): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) if title is None: title = _("Import wizard") self.setWindowTitle(title) if icon is None: self.setWindowIcon(ima.icon('fileimport')) if contents_title is None: contents_title = _("Raw text") if varname is None: varname = _("variable_name") self.var_name, self.clip_data = None, None # Setting GUI self.tab_widget = QTabWidget(self) self.text_widget = ContentsWidget(self, text) self.table_widget = PreviewWidget(self) self.tab_widget.addTab(self.text_widget, _("text")) self.tab_widget.setTabText(0, contents_title) self.tab_widget.addTab(self.table_widget, _("table")) self.tab_widget.setTabText(1, _("Preview")) self.tab_widget.setTabEnabled(1, False) name_layout = QHBoxLayout() name_label = QLabel(_("Variable Name")) name_layout.addWidget(name_label) self.name_edt = QLineEdit() self.name_edt.setText(varname) name_layout.addWidget(self.name_edt) btns_layout = QHBoxLayout() cancel_btn = QPushButton(_("Cancel")) btns_layout.addWidget(cancel_btn) cancel_btn.clicked.connect(self.reject) h_spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) btns_layout.addItem(h_spacer) self.back_btn = QPushButton(_("Previous")) self.back_btn.setEnabled(False) btns_layout.addWidget(self.back_btn) self.back_btn.clicked.connect(ft_partial(self._set_step, step=-1)) self.fwd_btn = QPushButton(_("Next")) btns_layout.addWidget(self.fwd_btn) self.fwd_btn.clicked.connect(ft_partial(self._set_step, step=1)) self.done_btn = QPushButton(_("Done")) self.done_btn.setEnabled(False) btns_layout.addWidget(self.done_btn) self.done_btn.clicked.connect(self.process) self.text_widget.asDataChanged.connect(self.fwd_btn.setEnabled) self.text_widget.asDataChanged.connect(self.done_btn.setDisabled) layout = QVBoxLayout() layout.addLayout(name_layout) layout.addWidget(self.tab_widget) layout.addLayout(btns_layout) self.setLayout(layout) def _focus_tab(self, tab_idx): """Change tab focus""" for i in range(self.tab_widget.count()): self.tab_widget.setTabEnabled(i, False) self.tab_widget.setTabEnabled(tab_idx, True) self.tab_widget.setCurrentIndex(tab_idx) def _set_step(self, step): """Proceed to a given step""" new_tab = self.tab_widget.currentIndex() + step assert new_tab < self.tab_widget.count() and new_tab >= 0 if new_tab == self.tab_widget.count()-1: try: self.table_widget.open_data(self._get_plain_text(), self.text_widget.get_col_sep(), self.text_widget.get_row_sep(), self.text_widget.trnsp_box.isChecked(), self.text_widget.get_skiprows(), self.text_widget.get_comments()) self.done_btn.setEnabled(True) self.done_btn.setDefault(True) self.fwd_btn.setEnabled(False) self.back_btn.setEnabled(True) except (SyntaxError, AssertionError) as error: QMessageBox.critical(self, _("Import wizard"), _("<b>Unable to proceed to next step</b>" "<br><br>Please check your entries." "<br><br>Error message:<br>%s") % str(error)) return elif new_tab == 0: self.done_btn.setEnabled(False) self.fwd_btn.setEnabled(True) self.back_btn.setEnabled(False) self._focus_tab(new_tab) def get_data(self): """Return processed data""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.var_name, self.clip_data def _simplify_shape(self, alist, rec=0): """Reduce the alist dimension if needed""" if rec != 0: if len(alist) == 1: return alist[-1] return alist if len(alist) == 1: return self._simplify_shape(alist[-1], 1) return [self._simplify_shape(al, 1) for al in alist] def _get_table_data(self): """Return clipboard processed as data""" data = self._simplify_shape( self.table_widget.get_data()) if self.table_widget.array_btn.isChecked(): return array(data) elif pd and self.table_widget.df_btn.isChecked(): info = self.table_widget.pd_info buf = io.StringIO(self.table_widget.pd_text) return pd.read_csv(buf, **info) return data def _get_plain_text(self): """Return clipboard as text""" return self.text_widget.text_editor.toPlainText() @Slot() def process(self): """Process the data from clipboard""" var_name = self.name_edt.text() try: self.var_name = str(var_name) except UnicodeEncodeError: self.var_name = to_text_string(var_name) if self.text_widget.get_as_data(): self.clip_data = self._get_table_data() elif self.text_widget.get_as_code(): self.clip_data = try_to_eval( to_text_string(self._get_plain_text())) else: self.clip_data = to_text_string(self._get_plain_text()) self.accept()
class ConvertFluxUnitGUI(QDialog): """ GUI for unit conversions """ def __init__(self, controller, parent=None): super(ConvertFluxUnitGUI, self).__init__(parent=parent) self.setWindowFlags(self.windowFlags() | Qt.Tool) self.title = "Unit Conversion" self.setMinimumSize(400, 270) self.cubeviz_layout = controller.cubeviz_layout self._hub = self.cubeviz_layout.session.hub self.controller = controller self.data = controller.data self.controller_components = controller._components self.current_unit = None self._init_ui() def _init_ui(self): # LINE 1: Data component drop down self.component_prompt = QLabel("Data Component:") self.component_prompt.setWordWrap(True) # Add the data component labels to the drop down, with the ComponentID # set as the userData: if self.parent is not None and hasattr(self.parent, 'data_components'): self.label_data = [(str(cid), cid) for cid in self.parent.data_components] else: self.label_data = [(str(cid), cid) for cid in self.data.visible_components] default_index = 0 self.component_combo = QComboBox() self.component_combo.setFixedWidth(200) update_combobox(self.component_combo, self.label_data, default_index=default_index) self.component_combo.currentIndexChanged.connect(self.update_unit_layout) # hbl is short for Horizontal Box Layout hbl1 = QHBoxLayout() hbl1.addWidget(self.component_prompt) hbl1.addWidget(self.component_combo) hbl1.addStretch(1) # LINE 2: Unit conversion layout # This layout is filled by CubeVizUnit self.unit_layout = QHBoxLayout() # this is hbl2 # LINE 3: Message box self.message_box = QLabel("") hbl3 = QHBoxLayout() hbl3.addWidget(self.message_box) hbl3.addStretch(1) # Line 4: Buttons self.okButton = QPushButton("Convert Units") self.okButton.clicked.connect(self.call_main) self.okButton.setDefault(True) self.cancelButton = QPushButton("Cancel") self.cancelButton.clicked.connect(self.cancel) hbl4 = QHBoxLayout() hbl4.addStretch(1) hbl4.addWidget(self.cancelButton) hbl4.addWidget(self.okButton) vbl = QVBoxLayout() vbl.addLayout(hbl1) vbl.addLayout(self.unit_layout) vbl.addLayout(hbl3) vbl.addLayout(hbl4) self.setLayout(vbl) self.vbl = vbl self.update_unit_layout(default_index) self.show() def update_unit_layout(self, index): """ Call back for component selection drop down. """ component_id = str(self.component_combo.currentData()) # STEP1: Clean up widgets from last component widgets = (self.unit_layout.itemAt(i) for i in range(self.unit_layout.count())) for w in widgets: if isinstance(w, QSpacerItem): self.unit_layout.removeItem(w) continue elif isinstance(w, QWidgetItem): w = w.widget() if hasattr(w, "deleteLater"): w.deleteLater() self.message_box.setText("") if self.current_unit: self.current_unit.reset_widgets() # STEP2: Add now component and connect to CubeVizUnit # so that new widgets are populated. if component_id in self.controller_components: cubeviz_unit = self.controller_components[component_id] self.current_unit = cubeviz_unit cubeviz_unit.set_message_box(self.message_box) cubeviz_unit.populate_unit_layout(self.unit_layout, self) if cubeviz_unit.is_convertible: self.okButton.setEnabled(True) else: self.okButton.setEnabled(False) else: self.current_unit = None default_message = "CubeViz can not convert this unit." default_label = QLabel(default_message) self.unit_layout.addWidget(default_label) self.okButton.setEnabled(False) self.unit_layout.update() self.vbl.update() def call_main(self): """ Calls CubeVizUnit.change_units to finalize conversions. Updates plots with new units. :return: """ success = self.current_unit.change_units() if not success: # Todo: Warning should pop up return component_id = self.component_combo.currentData() self.data.get_component(component_id).units = self.current_unit.unit_string msg = FluxUnitsUpdateMessage(self, self.current_unit.unit, component_id) self._hub.broadcast(msg) self.close() def cancel(self): self.close()
class SelectSmoothing(QDialog): """ SelectSmoothing launches a GUI and executes smoothing. Any output is added to the input data as a new component. """ def __init__(self, data, parent=None, smooth_cube=None, allow_preview=False, allow_spectral_axes=False): super(SelectSmoothing, self).__init__(parent) self.setWindowFlags(self.windowFlags() | Qt.Tool) self.parent = parent self.title = "Smoothing Selection" self.data = data # Glue data to be smoothed # Check if smooth object is the caller if smooth_cube is None: self.smooth_cube = SmoothCube(data=self.data) else: self.smooth_cube = smooth_cube self.allow_spectral_axes = allow_spectral_axes self.allow_preview = allow_preview self.is_preview_active = False # Flag to show if smoothing preview is active self.abort_window = None # Small window pop up when smoothing. self.component_id = None # Glue data component to smooth over self.current_axis = None # Selected smoothing_axis self.current_kernel_type = None # Selected kernel type, a key in SmoothCube.kernel_registry self.current_kernel_name = None # Name of selected kernel self._init_selection_ui() # Format and show gui def _init_selection_ui(self): # LINE 1: Radio box spatial vs spectral axis self.axes_prompt = QLabel("Smoothing Axis:") self.axes_prompt.setMinimumWidth(150) self.spatial_radio = QRadioButton("Spatial") self.spatial_radio.setChecked(True) self.current_axis = "spatial" self.spatial_radio.toggled.connect(self.spatial_radio_checked) self.spectral_radio = QRadioButton("Spectral") self.spectral_radio.toggled.connect(self.spectral_radio_checked) # hbl is short for Horizontal Box Layout hbl1 = QHBoxLayout() hbl1.addWidget(self.axes_prompt) hbl1.addWidget(self.spatial_radio) hbl1.addWidget(self.spectral_radio) # LINE 2: Kernel Type prompt self.k_type_prompt = QLabel("Kernel Type:") self.k_type_prompt.setMinimumWidth(150) # Load kernel types + names and add to drop down self._load_options() self.combo = QComboBox() self.combo.setMinimumWidth(150) self.combo.addItems(self.options[self.current_axis]) hbl2 = QHBoxLayout() hbl2.addWidget(self.k_type_prompt) hbl2.addWidget(self.combo) # LINE 3: Kernel size self.size_prompt = QLabel( self.smooth_cube.get_kernel_size_prompt(self.current_kernel_type)) self.size_prompt.setWordWrap(True) self.size_prompt.setMinimumWidth(150) self.unit_label = QLabel( self.smooth_cube.get_kernel_unit(self.current_kernel_type)) self.k_size = QLineEdit("1") # Default Kernel size set here hbl3 = QHBoxLayout() hbl3.addWidget(self.size_prompt) hbl3.addWidget(self.k_size) hbl3.addWidget(self.unit_label) # LINE 4: Data component drop down self.component_prompt = QLabel("Data Component:") self.component_prompt.setWordWrap(True) self.component_prompt.setMinimumWidth(150) # Load component_ids and add to drop down # Add the data component labels to the drop down, with the ComponentID # set as the userData: if self.parent is not None and hasattr(self.parent, 'data_components'): labeldata = [(str(cid), cid) for cid in self.parent.data_components] else: labeldata = [(str(cid), cid) for cid in self.data.main_components()] self.component_combo = QComboBox() update_combobox(self.component_combo, labeldata) self.component_combo.setMaximumWidth(150) self.component_combo.setCurrentIndex(0) if self.allow_preview: self.component_combo.currentIndexChanged.connect( self.update_preview_button) hbl4 = QHBoxLayout() hbl4.addWidget(self.component_prompt) hbl4.addWidget(self.component_combo) # Line 5: Preview Message message = "Info: Smoothing previews are displayed on " \ "CubeViz's left and single image viewers." self.preview_message = QLabel(message) self.preview_message.setWordWrap(True) self.preview_message.hide() hbl5 = QHBoxLayout() hbl5.addWidget(self.preview_message) # LINE 6: preview ok cancel buttons self.previewButton = QPushButton("Preview") self.previewButton.clicked.connect(self.call_preview) self.okButton = QPushButton("OK") self.okButton.clicked.connect(self.call_main) self.okButton.setDefault(True) self.cancelButton = QPushButton("Cancel") self.cancelButton.clicked.connect(self.cancel) hbl6 = QHBoxLayout() hbl6.addStretch(1) if self.allow_preview: hbl6.addWidget(self.previewButton) hbl6.addWidget(self.cancelButton) hbl6.addWidget(self.okButton) # Add Lines to Vertical Layout # vbl is short for Vertical Box Layout vbl = QVBoxLayout() if self.allow_spectral_axes: vbl.addLayout(hbl1) vbl.addLayout(hbl2) vbl.addLayout(hbl3) vbl.addLayout(hbl4) vbl.addLayout(hbl5) vbl.addLayout(hbl6) self.setLayout(vbl) self.setMaximumWidth(330) # Connect kernel combo box to event handler self.combo.currentIndexChanged.connect(self.selection_changed) self.selection_changed(0) self.show() def _load_options(self): """Extract names + types of kernels from SmoothCube.kernel_registry""" kernel_registry = self.smooth_cube.get_kernel_registry() self.options = {"spatial": [], "spectral": []} for k in kernel_registry: axis = kernel_registry[k]["axis"] for a in axis: if "spatial" == a: self.options["spatial"].append(kernel_registry[k]["name"]) elif "spectral" == a: self.options["spectral"].append(kernel_registry[k]["name"]) self.options["spectral"].sort() self.options["spatial"].sort() self.current_kernel_name = self.options[self.current_axis][0] self.current_kernel_type = self.smooth_cube.name_to_kernel_type( self.options[self.current_axis][0]) def selection_changed(self, i): """ Update kernel type, units, etc... when kernel name changes in combo box. """ keys = self.options[self.current_axis] name = keys[i] self.current_kernel_name = name self.current_kernel_type = self.smooth_cube.name_to_kernel_type(name) self.unit_label.setText( self.smooth_cube.get_kernel_unit(self.current_kernel_type)) self.size_prompt.setText( self.smooth_cube.get_kernel_size_prompt(self.current_kernel_type)) def spatial_radio_checked(self): self.current_axis = "spatial" self.update_preview_button() self.combo.clear() self.combo.addItems(self.options[self.current_axis]) def spectral_radio_checked(self): self.current_axis = "spectral" self.update_preview_button() self.combo.clear() self.combo.addItems(self.options[self.current_axis]) def input_validation(self): """ Check if input will break Smoothing :return: bool: True if no errors """ red = "background-color: rgba(255, 0, 0, 128);" success = True # Check 1: k_size if self.k_size == "": self.k_size.setStyleSheet(red) success = False else: try: if self.current_kernel_type == "median": k_size = int(self.k_size.text()) else: k_size = float(self.k_size.text()) if k_size <= 0: self.k_size.setStyleSheet(red) success = False else: self.k_size.setStyleSheet("") except ValueError: if self.current_kernel_type == "median": info = QMessageBox.critical( self, "Error", "Kernel size must be integer for median") self.k_size.setStyleSheet(red) success = False return success def call_main(self): try: self.main() except Exception as e: info = QMessageBox.critical(self, "Error", str(e)) self.cancel() raise def main(self): """ Main function to process input and call smoothing function """ success = self.input_validation() if not success: return self.hide() self.abort_window = AbortWindow(self) QApplication.processEvents() # Add smoothing parameters self.smooth_cube.abort_window = self.abort_window if self.smooth_cube.parent is None and self.parent is not self.smooth_cube: self.smooth_cube.parent = self.parent if self.parent is not self.smooth_cube: self.smooth_cube.data = self.data self.smooth_cube.smoothing_axis = self.current_axis self.smooth_cube.kernel_type = self.current_kernel_type if self.current_kernel_type == "median": self.smooth_cube.kernel_size = int(self.k_size.text()) else: self.smooth_cube.kernel_size = float(self.k_size.text()) self.smooth_cube.component_id = str(self.component_combo.currentText()) self.smooth_cube.output_as_component = True if self.is_preview_active: self.parent.end_smoothing_preview() self.is_preview_active = False self.smooth_cube.multi_threading_smooth() return def update_preview_button(self): if self.parent is None or "spatial" != self.current_axis: self.previewButton.setDisabled(True) return self.previewButton.setDisabled(False) return def call_preview(self): try: self.preview() except Exception as e: info = QMessageBox.critical(self, "Error", str(e)) self.cancel() raise def preview(self): """Preview current options""" success = self.input_validation() if not success: return if self.smooth_cube.parent is None and self.parent is not self.smooth_cube: self.smooth_cube.parent = self.parent self.smooth_cube.smoothing_axis = self.current_axis self.smooth_cube.kernel_type = self.current_kernel_type if self.current_kernel_type == "median": self.smooth_cube.kernel_size = int(self.k_size.text()) else: self.smooth_cube.kernel_size = float(self.k_size.text()) preview_function = self.smooth_cube.preview_smoothing preview_title = self.smooth_cube.get_preview_title() component_id = self.component_combo.currentData() self.parent.start_smoothing_preview(preview_function, component_id, preview_title) self.is_preview_active = True self.preview_message.show() def cancel(self): self.clean_up() def clean_up(self): self.close() if self.abort_window is not None: self.abort_window.close() if self.is_preview_active: self.parent.end_smoothing_preview() self.is_preview_active = False def closeEvent(self, event): if self.is_preview_active: self.parent.end_smoothing_preview() self.is_preview_active = False def keyPressEvent(self, e): if e.key() == Qt.Key_Escape: self.clean_up()
class MOSVizViewer(DataViewer): LABEL = "MOSViz Viewer" window_closed = Signal() _toolbar_cls = MOSViewerToolbar def __init__(self, session, parent=None): super(MOSVizViewer, self).__init__(session, parent=parent) self.load_ui() # Define some data containers self.filepath = None self.savepath = None self.data_idx = None self.comments = False self.textChangedAt = None self.mask = None self.catalog = None self.current_row = None self._specviz_instance = None self._loaded_data = {} self._primary_data = None self._layer_view = SimpleLayerWidget(parent=self) self._layer_view.layer_combo.currentIndexChanged.connect( self._selection_changed) def load_ui(self): """ Setup the MOSView viewer interface. """ self.central_widget = QWidget(self) path = os.path.join(UI_DIR, 'mos_widget.ui') loadUi(path, self.central_widget) self.image_widget = DrawableImageWidget() self.spectrum2d_widget = MOSImageWidget() self.spectrum1d_widget = Line1DWidget() # Set up helper for sharing axes. SharedAxisHelper defaults to no sharing # and we control the sharing later by setting .sharex and .sharey on the # helper self.spectrum2d_spectrum1d_share = SharedAxisHelper( self.spectrum2d_widget._axes, self.spectrum1d_widget._axes) self.spectrum2d_image_share = SharedAxisHelper( self.spectrum2d_widget._axes, self.image_widget._axes) # We only need to set the image widget to keep the same aspect ratio # since the two other viewers don't require square pixels, so the axes # should not change shape. self.image_widget._axes.set_adjustable('datalim') self.meta_form_layout = self.central_widget.meta_form_layout self.meta_form_layout.setFieldGrowthPolicy( self.meta_form_layout.ExpandingFieldsGrow) self.central_widget.left_vertical_splitter.insertWidget( 0, self.image_widget) self.central_widget.right_vertical_splitter.addWidget( self.spectrum2d_widget) self.central_widget.right_vertical_splitter.addWidget( self.spectrum1d_widget) # Set the splitter stretch factors self.central_widget.left_vertical_splitter.setStretchFactor(0, 1) self.central_widget.left_vertical_splitter.setStretchFactor(1, 8) self.central_widget.right_vertical_splitter.setStretchFactor(0, 1) self.central_widget.right_vertical_splitter.setStretchFactor(1, 2) self.central_widget.horizontal_splitter.setStretchFactor(0, 1) self.central_widget.horizontal_splitter.setStretchFactor(1, 2) # Keep the left and right splitters in sync otherwise the axes don't line up self.central_widget.left_vertical_splitter.splitterMoved.connect( self._left_splitter_moved) self.central_widget.right_vertical_splitter.splitterMoved.connect( self._right_splitter_moved) # Set the central widget self.setCentralWidget(self.central_widget) # Define the options widget self._options_widget = OptionsWidget() @avoid_circular def _right_splitter_moved(self, *args, **kwargs): sizes = self.central_widget.right_vertical_splitter.sizes() self.central_widget.left_vertical_splitter.setSizes(sizes) @avoid_circular def _left_splitter_moved(self, *args, **kwargs): sizes = self.central_widget.left_vertical_splitter.sizes() self.central_widget.right_vertical_splitter.setSizes(sizes) def setup_connections(self): """ Connects gui elements to event calls. """ # Connect the selection event for the combo box to what's displayed self.toolbar.source_select.currentIndexChanged[int].connect( lambda ind: self.load_selection(self.catalog[ind])) self.toolbar.source_select.currentIndexChanged[int].connect( lambda ind: self._set_navigation(ind)) # Connect the specviz button if SpecVizViewer is not None: self.toolbar.open_specviz.triggered.connect( lambda: self._open_in_specviz()) else: self.toolbar.open_specviz.setDisabled(True) # Connect previous and forward buttons self.toolbar.cycle_next_action.triggered.connect( lambda: self._set_navigation(self.toolbar.source_select. currentIndex() + 1)) # Connect previous and previous buttons self.toolbar.cycle_previous_action.triggered.connect( lambda: self._set_navigation(self.toolbar.source_select. currentIndex() - 1)) # Connect the toolbar axes setting actions self.toolbar.lock_x_action.triggered.connect( lambda state: self.set_locked_axes(x=state)) self.toolbar.lock_y_action.triggered.connect( lambda state: self.set_locked_axes(y=state)) def options_widget(self): return self._options_widget def initialize_toolbar(self): """ Initialize the custom toolbar for the MOSViz viewer. """ from glue.config import viewer_tool self.toolbar = self._toolbar_cls(self) for tool_id in self.tools: mode_cls = viewer_tool.members[tool_id] mode = mode_cls(self) self.toolbar.add_tool(mode) self.addToolBar(self.toolbar) self.setup_connections() def register_to_hub(self, hub): super(MOSVizViewer, self).register_to_hub(hub) def has_data_or_subset(x): if x.sender is self._primary_data: return True elif isinstance(x.sender, Subset) and x.sender.data is self._primary_data: return True else: return False hub.subscribe(self, msg.SubsetCreateMessage, handler=self._add_subset, filter=has_data_or_subset) hub.subscribe(self, msg.SubsetUpdateMessage, handler=self._update_subset, filter=has_data_or_subset) hub.subscribe(self, msg.SubsetDeleteMessage, handler=self._remove_subset, filter=has_data_or_subset) hub.subscribe(self, msg.DataUpdateMessage, handler=self._update_data, filter=has_data_or_subset) def add_data(self, data): """ Processes data message from the central communication hub. Parameters ---------- data : :class:`glue.core.data.Data` Data object. """ # Check whether the data is suitable for the MOSViz viewer - basically # we expect a table of 1D columns with at least three string and four # floating-point columns. if data.ndim != 1: QMessageBox.critical(self, "Error", "MOSViz viewer can only be used " "for data with 1-dimensional components", buttons=QMessageBox.Ok) return False components = [ data.get_component(cid) for cid in data.visible_components ] categorical = [c for c in components if c.categorical] if len(categorical) < 3: QMessageBox.critical( self, "Error", "MOSViz viewer expected at least " "three string components/columns, representing " "the filenames of the 1D and 2D spectra and " "cutouts", buttons=QMessageBox.Ok) return False # We can relax the following requirement if we make the slit parameters # optional numerical = [c for c in components if c.numeric] if len(numerical) < 4: QMessageBox.critical( self, "Error", "MOSViz viewer expected at least " "four numerical components/columns, representing " "the slit position, length, and position angle", buttons=QMessageBox.Ok) return False # Make sure the loaders and column names are correct result = confirm_loaders_and_column_names(data) if not result: return False self._primary_data = data self._layer_view.data = data self._unpack_selection(data) return True def add_subset(self, subset): """ Processes subset messages from the central communication hub. Parameters ---------- subset : Subset object. """ self._layer_view.refresh() index = self._layer_view.layer_combo.findData(subset) self._layer_view.layer_combo.setCurrentIndex(index) return True def _update_data(self, message): """ Update data message. Parameters ---------- message : :class:`glue.core.message.Message` Data message object. """ self._layer_view.refresh() def _add_subset(self, message): """ Add subset message. Parameters ---------- message : :class:`glue.core.message.Message` Subset message object. """ self._layer_view.refresh() def _update_subset(self, message): """ Update subset message. Parameters ---------- message : :class:`glue.core.message.Message` Update message object. """ self._layer_view.refresh() self._unpack_selection(message.subset) def _remove_subset(self, message): """ Remove subset message. Parameters ---------- message : :class:`glue.core.message.Message` Subset message object. """ self._layer_view.refresh() self._unpack_selection(message.subset.data) def _selection_changed(self): self._unpack_selection(self._layer_view.layer_combo.currentData()) def _unpack_selection(self, data): """ Interprets the :class:`glue.core.data.Data` object by decomposing the data elements, extracting relevant data, and recomposing a package-agnostic dictionary object containing the relevant data. Parameters ---------- data : :class:`glue.core.data.Data` Glue data object to decompose. """ mask = None if isinstance(data, Subset): try: mask = data.to_mask() except IncompatibleAttribute: return if not np.any(mask): return data = data.data self.mask = mask # Clear the table self.catalog = Table() self.catalog.meta = data.meta self.comments = False col_names = data.components for att in col_names: cid = data.id[att] component = data.get_component(cid) if component.categorical: comp_labels = component.labels[mask] if comp_labels.ndim > 1: comp_labels = comp_labels[0] if str(att) in ["comments", "flag"]: self.comments = True elif str(att) in ['spectrum1d', 'spectrum2d', 'cutout']: self.filepath = component._load_log.path path = '/'.join(component._load_log.path.split('/')[:-1]) self.catalog[str(att)] = [ os.path.join(path, x) for x in comp_labels ] else: self.catalog[str(att)] = comp_labels else: comp_data = component.data[mask] if comp_data.ndim > 1: comp_data = comp_data[0] self.catalog[str(att)] = comp_data if len(self.catalog) > 0: if not self.comments: self.comments = self._load_comments(data.label) #Returns bool else: self._data_collection_index(data.label) self._get_save_path() # Update gui elements self._update_navigation(select=0) def _update_navigation(self, select=0): """ Updates the :class:`qtpy.QtWidgets.QComboBox` widget with the appropriate source `id`s from the MOS catalog. """ if self.toolbar is None: return self.toolbar.source_select.blockSignals(True) self.toolbar.source_select.clear() if len(self.catalog) > 0 and 'id' in self.catalog.colnames: self.toolbar.source_select.addItems(self.catalog['id'][:]) self.toolbar.source_select.setCurrentIndex(select) self.toolbar.source_select.blockSignals(False) self.toolbar.source_select.currentIndexChanged.emit(select) def _set_navigation(self, index): if len(self.catalog) < index: return if 0 <= index < self.toolbar.source_select.count(): self.toolbar.source_select.setCurrentIndex(index) if index <= 0: self.toolbar.cycle_previous_action.setDisabled(True) else: self.toolbar.cycle_previous_action.setDisabled(False) if index >= self.toolbar.source_select.count() - 1: self.toolbar.cycle_next_action.setDisabled(True) else: self.toolbar.cycle_next_action.setDisabled(False) def _open_in_specviz(self): _specviz_instance = self.session.application.new_data_viewer( SpecVizViewer) spec1d_data = self._loaded_data['spectrum1d'] spec_data = Spectrum1DRef( data=spec1d_data.get_component(spec1d_data.id['Flux']).data, dispersion=spec1d_data.get_component( spec1d_data.id['Wavelength']).data, uncertainty=StdDevUncertainty( spec1d_data.get_component(spec1d_data.id['Uncertainty']).data), unit="", name=self.current_row['id'], wcs=WCS(spec1d_data.header)) _specviz_instance.open_data(spec_data) def load_selection(self, row): """ Processes a row in the MOS catalog by first loading the data set, updating the stored data components, and then rendering the data on the visible MOSViz viewer plots. Parameters ---------- row : :class:`astropy.table.Row` A row object representing a row in the MOS catalog. Each key should be a column name. """ self.current_row = row # Get loaders loader_spectrum1d = SPECTRUM1D_LOADERS[self.catalog.meta["loaders"] ["spectrum1d"]] loader_spectrum2d = SPECTRUM2D_LOADERS[self.catalog.meta["loaders"] ["spectrum2d"]] loader_cutout = CUTOUT_LOADERS[self.catalog.meta["loaders"]["cutout"]] # Get column names colname_spectrum1d = self.catalog.meta["special_columns"]["spectrum1d"] colname_spectrum2d = self.catalog.meta["special_columns"]["spectrum2d"] colname_cutout = self.catalog.meta["special_columns"]["cutout"] spec1d_data = loader_spectrum1d(row[colname_spectrum1d]) spec2d_data = loader_spectrum2d(row[colname_spectrum2d]) self._update_data_components(spec1d_data, key='spectrum1d') self._update_data_components(spec2d_data, key='spectrum2d') basename = os.path.basename(row[colname_cutout]) if basename == "None": self.render_data(row, spec1d_data, spec2d_data, None) else: image_data = loader_cutout(row[colname_cutout]) self._update_data_components(image_data, key='cutout') self.render_data(row, spec1d_data, spec2d_data, image_data) def _update_data_components(self, data, key): """ Update the data components that act as containers for the displayed data in the MOSViz viewer. This obviates the need to keep creating new data components. Parameters ---------- data : :class:`glue.core.data.Data` Data object to replace within the component. key : str References the particular data set type. """ cur_data = self._loaded_data.get(key, None) if cur_data is None: self._loaded_data[key] = data self.session.data_collection.append(data) else: cur_data.update_values_from_data(data) def render_data(self, row, spec1d_data=None, spec2d_data=None, image_data=None): """ Render the updated data sets in the individual plot widgets within the MOSViz viewer. """ self._check_unsaved_comments() if spec1d_data is not None: spectrum1d_x = spec1d_data[spec1d_data.id['Wavelength']] spectrum1d_y = spec1d_data[spec1d_data.id['Flux']] spectrum1d_yerr = spec1d_data[spec1d_data.id['Uncertainty']] self.spectrum1d_widget.set_data(x=spectrum1d_x, y=spectrum1d_y, yerr=spectrum1d_yerr) # Try to retrieve the wcs information try: flux_unit = spec1d_data.header.get('BUNIT', 'Jy').lower() flux_unit = flux_unit.replace('counts', 'count') flux_unit = u.Unit(flux_unit) except ValueError: flux_unit = u.Unit("Jy") try: disp_unit = spec1d_data.header.get('CUNIT1', 'Angstrom').lower() disp_unit = u.Unit(disp_unit) except ValueError: disp_unit = u.Unit("Angstrom") self.spectrum1d_widget.axes.set_xlabel( "Wavelength [{}]".format(disp_unit)) self.spectrum1d_widget.axes.set_ylabel( "Flux [{}]".format(flux_unit)) if image_data is not None: wcs = image_data.coords.wcs self.image_widget.set_image(image_data.get_component( image_data.id['Flux']).data, wcs=wcs, interpolation='none', origin='lower') self.image_widget.axes.set_xlabel("Spatial X") self.image_widget.axes.set_ylabel("Spatial Y") # Add the slit patch to the plot ra = row[self.catalog.meta["special_columns"] ["slit_ra"]] * u.degree dec = row[self.catalog.meta["special_columns"] ["slit_dec"]] * u.degree slit_width = row[self.catalog.meta["special_columns"] ["slit_width"]] slit_length = row[self.catalog.meta["special_columns"] ["slit_length"]] skycoord = SkyCoord(ra, dec, frame='fk5') xp, yp = skycoord.to_pixel(wcs) scale = np.sqrt(proj_plane_pixel_area(wcs)) * 3600. dx = slit_width / scale dy = slit_length / scale self.image_widget.draw_rectangle(x=xp, y=yp, width=dx, height=dy) self.image_widget._redraw() else: self.image_widget.setVisible(False) # Plot the 2D spectrum data last because by then we can make sure that # we set up the extent of the image appropriately if the cutout and the # 1D spectrum are present so that the axes can be locked. if spec2d_data is not None: wcs = spec2d_data.coords.wcs xp2d = np.arange(spec2d_data.shape[1]) yp2d = np.repeat(0, spec2d_data.shape[1]) spectrum2d_disp, spectrum2d_offset = spec2d_data.coords.pixel2world( xp2d, yp2d) x_min = spectrum2d_disp.min() x_max = spectrum2d_disp.max() if image_data is None: y_min = -0.5 y_max = spec2d_data.shape[0] - 0.5 else: y_min = yp - dy / 2. y_max = yp + dy / 2. extent = [x_min, x_max, y_min, y_max] self.spectrum2d_widget.set_image(image=spec2d_data.get_component( spec2d_data.id['Flux']).data, interpolation='none', aspect='auto', extent=extent, origin='lower') self.spectrum2d_widget.axes.set_xlabel("Wavelength") self.spectrum2d_widget.axes.set_ylabel("Spatial Y") self.spectrum2d_widget._redraw() # Clear the meta information widget # NOTE: this process is inefficient for i in range(self.meta_form_layout.count()): wid = self.meta_form_layout.itemAt(i).widget() label = self.meta_form_layout.labelForField(wid) if label is not None: label.deleteLater() wid.deleteLater() # Repopulate the form layout # NOTE: this process is inefficient for col in row.colnames: if col.lower() not in ["comments", "flag"]: line_edit = QLineEdit(str(row[col]), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow(col, line_edit) # Set up comment and flag input/display boxes if self.comments: if self.savepath is not None: if self.savepath == -1: line_edit = QLineEdit( os.path.basename("Not Saving to File."), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow("Save File", line_edit) else: line_edit = QLineEdit(os.path.basename(self.savepath), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow("Save File", line_edit) self.input_flag = QLineEdit(self.get_flag(), self.central_widget.meta_form_widget) self.input_flag.textChanged.connect(self._text_changed) self.input_flag.setStyleSheet( "background-color: rgba(255, 255, 255);") self.meta_form_layout.addRow("Flag", self.input_flag) self.input_comments = QPlainTextEdit( self.get_comment(), self.central_widget.meta_form_widget) self.input_comments.textChanged.connect(self._text_changed) self.input_comments.setStyleSheet( "background-color: rgba(255, 255, 255);") self.meta_form_layout.addRow("Comments", self.input_comments) self.input_save = QPushButton('Save', self.central_widget.meta_form_widget) self.input_save.clicked.connect(self.update_comments) self.input_save.setDefault(True) self.input_refresh = QPushButton( 'Reload', self.central_widget.meta_form_widget) self.input_refresh.clicked.connect(self.refresh_comments) self.meta_form_layout.addRow(self.input_save, self.input_refresh) @defer_draw def set_locked_axes(self, x=None, y=None): # Here we only change the setting if x or y are not None # since if set_locked_axes is called with eg. x=True, then # we shouldn't change the y setting. if x is not None: self.spectrum2d_spectrum1d_share.sharex = x if y is not None: self.spectrum2d_image_share.sharey = y self.spectrum1d_widget._redraw() self.spectrum2d_widget._redraw() self.image_widget._redraw() def layer_view(self): return self._layer_view def _text_changed(self): if self.textChangedAt is None: i = self.toolbar.source_select.currentIndex() self.textChangedAt = self._index_hash(i) def _check_unsaved_comments(self): if self.textChangedAt is None: return #Nothing to be changed i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) if self.textChangedAt == i: self.textChangedAt = None return #This is a refresh info = "Comments or flags changed but were not saved. Would you like to save them?" reply = QMessageBox.question(self, '', info, QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.update_comments(True) self.textChangedAt = None def _data_collection_index(self, label): idx = -1 for i, l in enumerate(self.session.data_collection): if l.label == label: idx = i break if idx == -1: return -1 self.data_idx = idx return idx def _index_hash(self, i): """Local selection index -> Table index""" if self.mask is not None: size = self.mask.size temp = np.arange(size) return temp[self.mask][i] else: return i def _id_to_index_hash(self, ID, l): """Object Name -> Table index""" for i, name in enumerate(l): if name == ID: return i return None def get_comment(self): idx = self.data_idx i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) comp = self.session.data_collection[idx].get_component("comments") return comp._categorical_data[i] def get_flag(self): idx = self.data_idx i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) comp = self.session.data_collection[idx].get_component("flag") return comp._categorical_data[i] def send_NumericalDataChangedMessage(self): idx = self.data_idx data = self.session.data_collection[idx] data.hub.broadcast(msg.NumericalDataChangedMessage(data, "comments")) def refresh_comments(self): self.input_flag.setText(self.get_flag()) self.input_comments.setPlainText(self.get_comment()) self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") self.textChangedAt = None def _get_save_path(self): """ Try to get save path from other MOSVizViewer instances """ for v in self.session.application.viewers[0]: if isinstance(v, MOSVizViewer): if v.savepath is not None: if v.data_idx == self.data_idx: self.savepath = v.savepath break def _setup_save_path(self): """ Prompt the user for a file to save comments and flags into. """ fail = True success = False info = "Where would you like to save comments and flags?" option = pick_item( [0, 1], [os.path.basename(self.filepath), "New MOSViz Table file"], label=info, title="Comment Setup") if option == 0: self.savepath = self.filepath elif option == 1: dirname = os.path.dirname(self.filepath) path = compat.getsavefilename(caption="New MOSViz Table File", basedir=dirname, filters="*.txt")[0] if path == "": return fail self.savepath = path else: return fail for v in self.session.application.viewers[0]: if isinstance(v, MOSVizViewer): if v.data_idx == self.data_idx: v.savepath = self.savepath self._layer_view.refresh() return success def update_comments(self, pastSelection=False): """ Process comment and flag changes and save to file. Parameters ---------- pastSelection : bool True when updating past selections. Used when user forgets to save. """ if self.input_flag.text() == "": self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") return i = None try: i = int(self.input_flag.text()) except ValueError: self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") info = QMessageBox.information(self, "Status:", "Flag must be an int!") return self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") idx = self.data_idx if pastSelection: i = self.textChangedAt self.textChangedAt = None else: i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) data = self.session.data_collection[idx] comp = data.get_component("comments") comp._categorical_data.flags.writeable = True comp._categorical_data[i] = self.input_comments.toPlainText() comp = data.get_component("flag") comp._categorical_data.flags.writeable = True comp._categorical_data[i] = self.input_flag.text() self.send_NumericalDataChangedMessage() self.write_comments() self.textChangedAt = None def _load_comments(self, label): """ Populate the comments and flag columns. Attempt to load comments from file. Parameters ---------- label : str The label of the data in session.data_collection. """ #Make sure its the right data #(beacuse subset data is masked) idx = self._data_collection_index(label) if idx == -1: return False data = self.session.data_collection[idx] #Fill in default comments: length = data.shape[0] new_comments = np.array(["" for i in range(length)], dtype=object) new_flags = np.array(["0" for i in range(length)], dtype=object) #Fill in any saved comments: meta = data.meta obj_names = data.get_component("id")._categorical_data if "MOSViz_comments" in meta.keys(): try: comments = meta["MOSViz_comments"] for key in comments.keys(): index = self._id_to_index_hash(key, obj_names) if index is not None: line = comments[key] new_comments[index] = line except Exception as e: print("MOSViz Comment Load Failed: ", e) if "MOSViz_flags" in meta.keys(): try: flags = meta["MOSViz_flags"] for key in flags.keys(): index = self._id_to_index_hash(key, obj_names) if index is not None: line = flags[key] new_flags[index] = line except Exception as e: print("MOSViz Flag Load Failed: ", e) #Send to DC data.add_component(CategoricalComponent(new_flags, "flag"), "flag") data.add_component(CategoricalComponent(new_comments, "comments"), "comments") return True def write_comments(self): """ Setup save file. Write comments and flags to file """ if self.savepath is None: fail = self._setup_save_path() if fail: return if self.savepath == -1: return #Do not save to file option idx = self.data_idx data = self.session.data_collection[idx] save_comments = data.get_component("comments")._categorical_data save_flag = data.get_component("flag")._categorical_data obj_names = data.get_component("id")._categorical_data fn = self.savepath folder = os.path.dirname(fn) t = astropy_table.data_to_astropy_table(data) #Check if load and save dir paths match temp = os.path.dirname(self.filepath) if not os.path.samefile(folder, temp): t['spectrum1d'].flags.writeable = True t['spectrum2d'].flags.writeable = True t['cutout'].flags.writeable = True for i in range(len(t)): t['spectrum1d'][i] = os.path.abspath(t['spectrum1d'][i]) t['spectrum2d'][i] = os.path.abspath(t['spectrum2d'][i]) t['cutout'][i] = os.path.abspath(t['cutout'][i]) try: t.remove_column("comments") t.remove_column("flag") keys = t.meta.keys() if "MOSViz_comments" in keys: t.meta.pop("MOSViz_comments") if "MOSViz_flags" in keys: t.meta.pop("MOSViz_flags") comments = OrderedDict() flags = OrderedDict() for i, line in enumerate(save_comments): if line != "": line = line.replace("\n", " ") key = str(obj_names[i]) comments[key] = line for i, line in enumerate(save_flag): if line != "0" and line != "": line = com.replace("\n", " ") key = str(obj_names[i]) flags[key] = line if len(comments) > 0: t.meta["MOSViz_comments"] = comments if len(flags) > 0: t.meta["MOSViz_flags"] = flags t.write(fn, format="ascii.ecsv", overwrite=True) except Exception as e: print("Comment write failed:", e) def closeEvent(self, event): """ Clean up the extraneous data components created when opening the MOSViz viewer by overriding the parent class's close event. """ super(MOSVizViewer, self).closeEvent(event) for data in self._loaded_data.values(): self.session.data_collection.remove(data)
class ConvertFluxUnitGUI(QDialog): """ GUI for unit conversions """ def __init__(self, controller, parent=None, convert_data=False): super(ConvertFluxUnitGUI, self).__init__(parent=parent) self.setWindowFlags(self.windowFlags() | Qt.Tool) self.title = "Unit Conversion" self.setMinimumSize(400, 270) self.convert_data = convert_data self.cubeviz_layout = controller.cubeviz_layout self._hub = self.cubeviz_layout.session.hub self.controller = controller self.data = controller.data self.controller_components = controller._components self.current_unit = None self.current_layout = None self._init_ui() def _init_ui(self): # LINE 1: Data component drop down self.component_prompt = QLabel("Data Component:") self.component_prompt.setWordWrap(True) # Add the data component labels to the drop down, with the ComponentID # set as the userData: if self.parent is not None and hasattr(self.parent, 'data_components'): self.label_data = [(str(cid), cid) for cid in self.parent.data_components] else: self.label_data = [(str(cid), cid) for cid in self.data.visible_components] default_index = 0 self.component_combo = QComboBox() self.component_combo.setFixedWidth(200) update_combobox(self.component_combo, self.label_data, default_index=default_index) self.component_combo.currentIndexChanged.connect(self.update_unit_layout) # hbl is short for Horizontal Box Layout hbl1 = QHBoxLayout() hbl1.addWidget(self.component_prompt) hbl1.addWidget(self.component_combo) hbl1.addStretch(1) # LINE 2: Unit conversion layout # This layout is filled by CubeVizUnit self.unit_layout = QHBoxLayout() # this is hbl2 # LINE 3: Message box self.message_box = QLabel("") hbl3 = QHBoxLayout() hbl3.addWidget(self.message_box) hbl3.addStretch(1) # Line 4: Buttons ok_text = "Convert Data" if self.convert_data else "Convert Displayed Units" ok_function = self.convert_data_units if self.convert_data else self.convert_displayed_units self.okButton = QPushButton(ok_text) self.okButton.clicked.connect(ok_function) self.okButton.setDefault(True) self.cancelButton = QPushButton("Cancel") self.cancelButton.clicked.connect(self.cancel) hbl4 = QHBoxLayout() hbl4.addStretch(1) hbl4.addWidget(self.cancelButton) hbl4.addWidget(self.okButton) vbl = QVBoxLayout() vbl.addLayout(hbl1) vbl.addLayout(self.unit_layout) vbl.addLayout(hbl3) vbl.addLayout(hbl4) self.setLayout(vbl) self.vbl = vbl self.update_unit_layout(default_index) self.show() def update_unit_layout(self, index): """ Call back for component selection drop down. """ component_id = self.component_combo.currentData() # STEP1: Clean up widgets from last component widgets = (self.unit_layout.itemAt(i) for i in range(self.unit_layout.count())) for w in widgets: if isinstance(w, QSpacerItem): self.unit_layout.removeItem(w) continue elif isinstance(w, QWidgetItem): w = w.widget() if hasattr(w, "deleteLater"): w.deleteLater() self.message_box.setText("") if self.current_layout: self.current_layout.reset_widgets() # STEP2: Add now component and connect to CubeVizUnit # so that new widgets are populated. if component_id in self.controller_components: cubeviz_unit = self.controller_components[component_id] self.current_unit = cubeviz_unit wave = self.controller.wave pixel_area = self.controller.pixel_area layout = assign_cubeviz_unit_layout(cubeviz_unit, wave=wave, pixel_area=pixel_area) layout.set_message_box(self.message_box) layout.populate_unit_layout(self.unit_layout, self) self.current_layout = layout if ASTROPY_CubeVizUnit == cubeviz_unit.type: self.okButton.setEnabled(True) else: self.okButton.setEnabled(False) else: self.current_unit = None default_message = "CubeViz can not convert this unit." default_label = QLabel(default_message) self.unit_layout.addWidget(default_label) self.okButton.setEnabled(False) self.unit_layout.update() self.vbl.update() def convert_displayed_units(self): """ Calls CubeVizUnit.change_units to finalize conversions. Updates plots with new units. :return: """ success = self.current_layout.change_units() if not success: info = QMessageBox.critical(self, "Error", "Conversion failed.") return new_unit = self.current_layout.new_unit self.current_unit.unit = new_unit self.current_unit.unit_string = str(new_unit) component_id = self.component_combo.currentData() self.data.get_component(component_id).units = self.current_unit.unit_string msg = FluxUnitsUpdateMessage(self, self.current_unit, component_id) self._hub.broadcast(msg) self.close() def convert_data_units(self): """ Calls CubeVizUnit.change_units to finalize conversions. Updates plots with new units. :return: """ success = self.current_layout.change_units() if not success: info = QMessageBox.critical(self, "Error", "Conversion failed.") return new_unit = self.current_layout.new_unit self.current_unit.unit = new_unit self.current_unit.unit_string = str(new_unit) component_id = self.component_combo.currentData() component = component_id.parent.get_component(component_id) old_array = component._data.copy() old_array.flags.writeable = True wavelengths = self.controller.construct_3d_wavelengths(old_array) new_array = self.current_unit.convert_value(old_array, wave=wavelengths) component._data = new_array self.current_unit = self.controller.add_component_unit(component_id, new_unit) component.units = self.current_unit.unit_string msg = FluxUnitsUpdateMessage(self, self.current_unit, component_id) self._hub.broadcast(msg) self.close() def cancel(self): self.close()
class PreferencesDialog(QDialog): """Preferences Dialog for Napari user settings.""" valueChanged = Signal() ui_schema = { "call_order": { "ui:widget": "plugins" }, "highlight_thickness": { "ui:widget": "highlight" }, "shortcuts": { "ui:widget": "shortcuts" }, } resized = Signal(QSize) closed = Signal() def __init__(self, parent=None): super().__init__(parent) self._list = QListWidget(self) self._stack = QStackedWidget(self) self._list.setObjectName("Preferences") # Set up buttons self._button_cancel = QPushButton(trans._("Cancel")) self._button_ok = QPushButton(trans._("OK")) self._default_restore = QPushButton(trans._("Restore defaults")) # Setup self.setWindowTitle(trans._("Preferences")) self._button_ok.setDefault(True) # Layout left_layout = QVBoxLayout() left_layout.addWidget(self._list) left_layout.addStretch() left_layout.addWidget(self._default_restore) left_layout.addWidget(self._button_cancel) left_layout.addWidget(self._button_ok) main_layout = QHBoxLayout() main_layout.addLayout(left_layout, 1) main_layout.addWidget(self._stack, 3) self.setLayout(main_layout) # Signals self._list.currentRowChanged.connect( lambda index: self._stack.setCurrentIndex(index)) self._button_cancel.clicked.connect(self.on_click_cancel) self._button_ok.clicked.connect(self.on_click_ok) self._default_restore.clicked.connect(self.restore_defaults) self.rejected.connect(self.on_click_cancel) # Make widget self.make_dialog() self._list.setCurrentRow(0) def _restart_dialog(self, event=None, extra_str=""): """Displays the dialog informing user a restart is required. Paramters --------- event : Event extra_str : str Extra information to add to the message about needing a restart. """ text_str = trans._( "napari requires a restart for image rendering changes to apply.") widget = ResetNapariInfoDialog( parent=self, text=text_str, ) widget.exec_() def closeEvent(self, event): """Override to emit signal.""" self.closed.emit() super().closeEvent(event) def reject(self): """Override to handle Escape.""" super().reject() self.close() def resizeEvent(self, event): """Override to emit signal.""" self.resized.emit(event.size()) super().resizeEvent(event) def make_dialog(self): """Removes settings not to be exposed to user and creates dialog pages.""" settings = get_settings() # Because there are multiple pages, need to keep a dictionary of values dicts. # One set of keywords are for each page, then in each entry for a page, there are dicts # of setting and its value. self._values_orig_dict = {} self._values_dict = {} self._setting_changed_dict = {} for page, setting in settings.schemas().items(): schema, values, properties = self.get_page_dict(setting) self._setting_changed_dict[page] = {} self._values_orig_dict[page] = values self._values_dict[page] = values # Only add pages if there are any properties to add. if properties: self.add_page(schema, values) def get_page_dict(self, setting): """Provides the schema, set of values for each setting, and the properties for each setting. Parameters ---------- setting : dict Dictionary of settings for a page within the settings manager. Returns ------- schema : dict Json schema of the setting page. values : dict Dictionary of values currently set for each parameter in the settings. properties : dict Dictionary of properties within the json schema. """ schema = json.loads(setting['json_schema']) # Resolve allOf references definitions = schema.get("definitions", {}) if definitions: for key, data in schema["properties"].items(): if "allOf" in data: allof = data["allOf"] allof = [d["$ref"].rsplit("/")[-1] for d in allof] for definition in allof: local_def = definitions[definition] schema["properties"][key]["enum"] = local_def["enum"] schema["properties"][key]["type"] = "string" # Need to remove certain properties that will not be displayed on the GUI properties = schema.pop('properties') model = setting['model'] values = model.dict() napari_config = getattr(model, "NapariConfig", None) if napari_config is not None: for val in napari_config.preferences_exclude: properties.pop(val) values.pop(val) schema['properties'] = properties return schema, values, properties def restore_defaults(self): """Launches dialog to confirm restore settings choice.""" self._reset_dialog = ConfirmDialog( parent=self, text=trans._("Are you sure you want to restore default settings?"), ) self._reset_dialog.valueChanged.connect(self._reset_widgets) self._reset_dialog.exec_() def _reset_widgets(self, event=None): """Deletes the widgets and rebuilds with defaults. Parameter --------- event: bool Indicates whether to restore the defaults. When a user clicks "Restore", the signal event emitted will be True. If "Cancel" is selected, event will be False and nothing is done. """ if event is True: get_settings().reset() self.close() self.valueChanged.emit() self._list.clear() for n in range(self._stack.count()): widget = self._stack.removeWidget( # noqa: F841 self._stack.currentWidget()) del widget self.make_dialog() self._list.setCurrentRow(0) self.show() def on_click_ok(self): """Keeps the selected preferences saved to settings.""" self.close() def on_click_cancel(self): """Restores the settings in place when dialog was launched.""" # Need to check differences for each page. settings = get_settings() for n in range(self._stack.count()): # Must set the current row so that the proper list is updated # in check differences. self._list.setCurrentRow(n) page = self._list.currentItem().text().split(" ")[0].lower() # get new values for settings. If they were changed from values at beginning # of preference dialog session, change them back. # Using the settings value seems to be the best way to get the checkboxes right # on the plugin call order widget. setting = settings.schemas()[page] schema, new_values, properties = self.get_page_dict(setting) self.check_differences(self._values_orig_dict[page], new_values) # need to reset plugin_manager to defaults and change keybindings in action_manager. # Emit signal to do this in main window. self.valueChanged.emit() self._list.setCurrentRow(0) self.close() def add_page(self, schema, values): """Creates a new page for each section in dialog. Parameters ---------- schema : dict Json schema including all information to build each page in the preferences dialog. values : dict Dictionary of current values set in preferences. """ widget = self.build_page_dialog(schema, values) self._list.addItem(schema["title"]) self._stack.addWidget(widget) def build_page_dialog(self, schema, values): """Builds the preferences widget using the json schema builder. Parameters ---------- schema : dict Json schema including all information to build each page in the preferences dialog. values : dict Dictionary of current values set in preferences. """ settings = get_settings() builder = WidgetBuilder() form = builder.create_form(schema, self.ui_schema) # Disable widgets that loaded settings from environment variables section = schema["section"] form_layout = form.widget.layout() for row in range(form.widget.layout().rowCount()): widget = form_layout.itemAt(row, form_layout.FieldRole).widget() name = widget._name disable = bool( settings._env_settings.get(section, {}).get(name, None)) widget.setDisabled(disable) try: widget.opacity.setOpacity(0.3 if disable else 1) except AttributeError: # some widgets may not have opacity (such as the QtPluginSorter) pass # set state values for widget form.widget.state = values if section == 'experimental': # need to disable async if octree is enabled. if values['octree'] is True: form = self._disable_async(form, values) form.widget.on_changed.connect(lambda d: self.check_differences( d, self._values_dict[schema["title"].lower()], )) return form def _disable_async(self, form, values, disable=True, state=True): """Disable async if octree is True.""" settings = get_settings() # need to make sure that if async_ is an environment setting, that we don't # enable it here. if (settings._env_settings['experimental'].get('async_', None) is not None): disable = True idx = list(values.keys()).index('async_') form_layout = form.widget.layout() widget = form_layout.itemAt(idx, form_layout.FieldRole).widget() widget.opacity.setOpacity(0.3 if disable else 1) widget.setDisabled(disable) return form def _values_changed(self, page, new_dict, old_dict): """Loops through each setting in a page to determine if it changed. Parameters ---------- new_dict : dict Dict that has the most recent changes by user. Each key is a setting value and each item is the value. old_dict : dict Dict wtih values set at the begining of preferences dialog session. """ for setting_name, value in new_dict.items(): if value != old_dict[setting_name]: self._setting_changed_dict[page][setting_name] = value elif (value == old_dict[setting_name] and setting_name in self._setting_changed_dict[page]): self._setting_changed_dict[page].pop(setting_name) def set_current_index(self, index: int): """ Set the current page on the preferences by index. Parameters ---------- index : int Index of page to set as current one. """ self._list.setCurrentRow(index) def check_differences(self, new_dict, old_dict): """Changes settings in settings manager with changes from dialog. Parameters ---------- new_dict : dict Dict that has the most recent changes by user. Each key is a setting parameter and each item is the value. old_dict : dict Dict wtih values set at the beginning of the preferences dialog session. """ settings = get_settings() page = self._list.currentItem().text().split(" ")[0].lower() self._values_changed(page, new_dict, old_dict) different_values = self._setting_changed_dict[page] if len(different_values) > 0: # change the values in settings for setting_name, value in different_values.items(): try: setattr(settings._settings[page], setting_name, value) self._values_dict[page] = new_dict if page == 'experimental': if setting_name == 'octree': # disable/enable async checkbox widget = self._stack.currentWidget() cstate = True if value is True else False self._disable_async(widget, new_dict, disable=cstate) # need to inform user that napari restart needed. self._restart_dialog() elif setting_name == 'async_': # need to inform user that napari restart needed. self._restart_dialog() except: # noqa: E722 continue
class SelectSmoothing(QDialog): """ SelectSmoothing launches a GUI and executes smoothing. Any output is added to the input data as a new component. """ def __init__(self, data, parent=None, smooth_cube=None, allow_preview=False, allow_spectral_axes=False): super(SelectSmoothing, self).__init__(parent) self.setWindowFlags(self.windowFlags() | Qt.Tool) self.parent = parent self.title = "Smoothing Selection" self.data = data # Glue data to be smoothed # Check if smooth object is the caller if smooth_cube is None: self.smooth_cube = SmoothCube(data=self.data) else: self.smooth_cube = smooth_cube self.allow_spectral_axes = allow_spectral_axes self.allow_preview = allow_preview self.is_preview_active = False # Flag to show if smoothing preview is active self.abort_window = None # Small window pop up when smoothing. self.component_id = None # Glue data component to smooth over self.current_axis = None # Selected smoothing_axis self.current_kernel_type = None # Selected kernel type, a key in SmoothCube.kernel_registry self.current_kernel_name = None # Name of selected kernel self._init_selection_ui() # Format and show gui def _init_selection_ui(self): # LINE 1: Radio box spatial vs spectral axis self.axes_prompt = QLabel("Smoothing Axis:") self.axes_prompt.setMinimumWidth(150) self.spatial_radio = QRadioButton("Spatial") self.spatial_radio.setChecked(True) self.current_axis = "spatial" self.spatial_radio.toggled.connect(self.spatial_radio_checked) self.spectral_radio = QRadioButton("Spectral") self.spectral_radio.toggled.connect(self.spectral_radio_checked) # hbl is short for Horizontal Box Layout hbl1 = QHBoxLayout() hbl1.addWidget(self.axes_prompt) hbl1.addWidget(self.spatial_radio) hbl1.addWidget(self.spectral_radio) # LINE 2: Kernel Type prompt self.k_type_prompt = QLabel("Kernel Type:") self.k_type_prompt.setMinimumWidth(150) # Load kernel types + names and add to drop down self._load_options() self.combo = QComboBox() self.combo.setMinimumWidth(150) self.combo.addItems(self.options[self.current_axis]) hbl2 = QHBoxLayout() hbl2.addWidget(self.k_type_prompt) hbl2.addWidget(self.combo) # LINE 3: Kernel size self.size_prompt = QLabel(self.smooth_cube.get_kernel_size_prompt(self.current_kernel_type)) self.size_prompt.setWordWrap(True) self.size_prompt.setMinimumWidth(150) self.unit_label = QLabel(self.smooth_cube.get_kernel_unit(self.current_kernel_type)) self.k_size = QLineEdit("1") # Default Kernel size set here hbl3 = QHBoxLayout() hbl3.addWidget(self.size_prompt) hbl3.addWidget(self.k_size) hbl3.addWidget(self.unit_label) # LINE 4: Data component drop down self.component_prompt = QLabel("Data Component:") self.component_prompt.setWordWrap(True) self.component_prompt.setMinimumWidth(150) # Load component_ids and add to drop down # Add the data component labels to the drop down, with the ComponentID # set as the userData: if self.parent is not None and hasattr(self.parent, 'data_components'): labeldata = [(str(cid), cid) for cid in self.parent.data_components] else: labeldata = [(str(cid), cid) for cid in self.data.main_components()] self.component_combo = QComboBox() update_combobox(self.component_combo, labeldata) self.component_combo.setMaximumWidth(150) self.component_combo.setCurrentIndex(0) if self.allow_preview: self.component_combo.currentIndexChanged.connect(self.update_preview_button) hbl4 = QHBoxLayout() hbl4.addWidget(self.component_prompt) hbl4.addWidget(self.component_combo) # Line 5: Preview Message message = "Info: Smoothing previews are displayed on " \ "CubeViz's left and single image viewers." self.preview_message = QLabel(message) self.preview_message.setWordWrap(True) self.preview_message.hide() hbl5 = QHBoxLayout() hbl5.addWidget(self.preview_message) # LINE 6: preview ok cancel buttons self.previewButton = QPushButton("Preview Slice") self.previewButton.clicked.connect(self.call_preview) self.okButton = QPushButton("Smooth Cube") self.okButton.clicked.connect(self.call_main) self.okButton.setDefault(True) self.cancelButton = QPushButton("Cancel") self.cancelButton.clicked.connect(self.cancel) hbl6 = QHBoxLayout() hbl6.addStretch(1) if self.allow_preview: hbl6.addWidget(self.previewButton) hbl6.addWidget(self.cancelButton) hbl6.addWidget(self.okButton) # Add Lines to Vertical Layout # vbl is short for Vertical Box Layout vbl = QVBoxLayout() if self.allow_spectral_axes: vbl.addLayout(hbl1) vbl.addLayout(hbl2) vbl.addLayout(hbl3) vbl.addLayout(hbl4) vbl.addLayout(hbl5) vbl.addLayout(hbl6) self.setLayout(vbl) self.setMaximumWidth(330) # Connect kernel combo box to event handler self.combo.currentIndexChanged.connect(self.selection_changed) self.selection_changed(0) self.show() def _load_options(self): """Extract names + types of kernels from SmoothCube.kernel_registry""" kernel_registry = self.smooth_cube.get_kernel_registry() self.options = {"spatial": [], "spectral": []} for k in kernel_registry: axis = kernel_registry[k]["axis"] for a in axis: if "spatial" == a: self.options["spatial"].append(kernel_registry[k]["name"]) elif "spectral" == a: self.options["spectral"].append(kernel_registry[k]["name"]) self.options["spectral"].sort() self.options["spatial"].sort() self.current_kernel_name = self.options[self.current_axis][0] self.current_kernel_type = self.smooth_cube.name_to_kernel_type(self.options[self.current_axis][0]) def selection_changed(self, i): """ Update kernel type, units, etc... when kernel name changes in combo box. """ keys = self.options[self.current_axis] name = keys[i] self.current_kernel_name = name self.current_kernel_type = self.smooth_cube.name_to_kernel_type(name) self.unit_label.setText(self.smooth_cube.get_kernel_unit(self.current_kernel_type)) self.size_prompt.setText(self.smooth_cube.get_kernel_size_prompt(self.current_kernel_type)) def spatial_radio_checked(self): self.current_axis = "spatial" self.update_preview_button() self.combo.clear() self.combo.addItems(self.options[self.current_axis]) def spectral_radio_checked(self): self.current_axis = "spectral" self.update_preview_button() self.combo.clear() self.combo.addItems(self.options[self.current_axis]) def input_validation(self): """ Check if input will break Smoothing :return: bool: True if no errors """ red = "background-color: rgba(255, 0, 0, 128);" success = True # Check 1: k_size if self.k_size == "": self.k_size.setStyleSheet(red) success = False else: try: if self.current_kernel_type == "median": k_size = int(self.k_size.text()) else: k_size = float(self.k_size.text()) if k_size <= 0: self.k_size.setStyleSheet(red) success = False else: self.k_size.setStyleSheet("") except ValueError: if self.current_kernel_type == "median": info = QMessageBox.critical(self, "Error", "Kernel size must be integer for median") self.k_size.setStyleSheet(red) success = False return success def call_main(self): try: self.main() except Exception as e: info = QMessageBox.critical(self, "Error", str(e)) self.cancel() raise def main(self): """ Main function to process input and call smoothing function """ success = self.input_validation() if not success: return self.hide() self.abort_window = AbortWindow(self) QApplication.processEvents() # Add smoothing parameters self.smooth_cube.abort_window = self.abort_window if self.smooth_cube.parent is None and self.parent is not self.smooth_cube: self.smooth_cube.parent = self.parent if self.parent is not self.smooth_cube: self.smooth_cube.data = self.data self.smooth_cube.smoothing_axis = self.current_axis self.smooth_cube.kernel_type = self.current_kernel_type if self.current_kernel_type == "median": self.smooth_cube.kernel_size = int(self.k_size.text()) else: self.smooth_cube.kernel_size = float(self.k_size.text()) self.smooth_cube.component_id = str(self.component_combo.currentText()) self.smooth_cube.output_as_component = True if self.is_preview_active: self.parent.end_smoothing_preview() self.is_preview_active = False self.smooth_cube.multi_threading_smooth() return def update_preview_button(self): if self.parent is None or "spatial" != self.current_axis: self.previewButton.setDisabled(True) return self.previewButton.setDisabled(False) return def call_preview(self): try: self.preview() except Exception as e: info = QMessageBox.critical(self, "Error", str(e)) self.cancel() raise def preview(self): """Preview current options""" success = self.input_validation() if not success: return if self.smooth_cube.parent is None and self.parent is not self.smooth_cube: self.smooth_cube.parent = self.parent self.smooth_cube.smoothing_axis = self.current_axis self.smooth_cube.kernel_type = self.current_kernel_type if self.current_kernel_type == "median": self.smooth_cube.kernel_size = int(self.k_size.text()) else: self.smooth_cube.kernel_size = float(self.k_size.text()) preview_function = self.smooth_cube.preview_smoothing preview_title = self.smooth_cube.get_preview_title() component_id = self.component_combo.currentData() self.parent.start_smoothing_preview(preview_function, component_id, preview_title) self.is_preview_active = True self.preview_message.show() def cancel(self): self.clean_up() def clean_up(self): self.close() if self.abort_window is not None: self.abort_window.close() if self.is_preview_active: self.parent.end_smoothing_preview() self.is_preview_active = False def closeEvent(self, event): if self.is_preview_active: self.parent.end_smoothing_preview() self.is_preview_active = False def keyPressEvent(self, e): if e.key() == Qt.Key_Escape: self.clean_up()
class ImageRotationDialog(ExToolWindow): def __init__(self, signal, axes, parent, plugin): super(ImageRotationDialog, self).__init__(parent) self.ui = parent self.create_controls() self.accepted.connect(self.ok) self.rejected.connect(self.close_new) self.signal = signal self.plugin = plugin self.new_out = None self._connected_updates = False if isinstance(axes, str): axm = signal.signal.axes_manager if axes.startswith("nav"): axes = (axm._axes.index(axm.navigation_axes[0]), axm._axes.index(axm.navigation_axes[1])) elif axes.startswith("sig"): axes = (axm._axes.index(axm.signal_axes[0]), axm._axes.index(axm.signal_axes[1])) self.axes = axes self.setWindowTitle(tr("Rotate")) # TODO: TAG: Functionality check if not hasattr(signal.signal, 'events'): self.gbo_preview.setVisible(False) # TODO: Add dynamic rotation, e.g. one that rotates when source # signal's data_changed event triggers def connect(self): # TODO: Don't have to con/dis those in gbo self.opt_new.toggled.connect(self.close_new) self.num_angle.valueChanged.connect(self.update) self.chk_grid.toggled.connect(self.update) self.num_grid.valueChanged.connect(self.update) self.chk_reshape.toggled.connect(self.update) self.opt_new.toggled.connect(self.update) self.opt_replace.toggled.connect(self.update) def disconnect(self): self.num_angle.valueChanged.disconnect(self.update) self.chk_grid.toggled.disconnect(self.update) self.num_grid.valueChanged.disconnect(self.update) self.chk_reshape.toggled.disconnect(self.update) self.opt_new.toggled.disconnect(self.update) self.opt_replace.toggled.disconnect(self.update) def ok(self): # Draw figure if not already done # TODO: TAG: Functionality check if not hasattr(self.signal.signal, 'events') or \ not self.gbo_preview.isChecked(): self.update() angle = self.num_angle.value() reshape = self.chk_reshape.isChecked() self.plugin.record_code( r"<p>.rotate_signal({0}, reshape={1}, axes={2})".format( angle, reshape, self.axes)) # Clean up event connections if self.new_out is not None: self.connect_update_plot(self.new_out.signal, disconnect=True) def close_new(self, value=False): if self.new_out is not None and not value: self.new_out.close() self.new_out = None self._connected_updates = False def set_preview(self, value): if not hasattr(self.signal.signal, 'events'): return if value: self.connect() self.update() else: self.disconnect() self.close_new() def _axes_in_nav(self): axm = self.signal.signal.axes_manager navidx = [axm._axes.index(ax) for ax in axm.navigation_axes] if self.axes[0] in navidx: return True return False def connect_update_plot(self, signal, disconnect=False): if self._connected_updates != disconnect: return # Nothing to do, prevent double connections if self._axes_in_nav(): f = signal._plot.navigator_plot.update else: f = signal._plot.signal_plot.update # TODO: TAG: Functionality check if hasattr(signal, 'events') and hasattr(signal.events, 'data_changed'): if disconnect: signal.events.data_changed.disconnect(f) else: signal.events.data_changed.connect(f, []) self._connected_updates = not disconnect def update(self): angle = self.num_angle.value() reshape = self.chk_reshape.isChecked() if self.opt_new.isChecked(): if self.new_out is None: out = None else: out = self.new_out.signal elif self.opt_replace.isChecked(): out = self.signal.signal else: return # Indeterminate state, do nothing s = self.plugin.rotate_signal(angle, self.signal.signal, record=False, reshape=reshape, out=out, axes=self.axes) if out is None: s.metadata.General.title = self.signal.name + "[Rotated]" s.plot() self.connect_update_plot(s) if (self.gbo_preview.isChecked() and self.opt_new.isChecked() and self.new_out is None): self.new_out = self.ui.lut_signalwrapper[s] else: s = out if self.chk_grid.isChecked() is True: pass # TODO: Draw grid def create_controls(self): """ Create UI controls. """ vbox = QVBoxLayout() form = QFormLayout() self.num_angle = QDoubleSpinBox() self.num_angle.setValue(0.0) self.num_angle.setMinimum(-360) self.num_angle.setMaximum(360) form.addRow(tr("Angle:"), self.num_angle) vbox.addLayout(form) self.gbo_preview = QGroupBox(tr("Preview")) self.gbo_preview.setCheckable(True) self.gbo_preview.setChecked(False) gbo_vbox = QVBoxLayout() self.chk_grid = QCheckBox(tr("Grid")) self.chk_grid.setChecked(False) self.num_grid = QSpinBox() self.num_grid.setValue(4) self.num_grid.setMinimum(1) self.num_grid.setEnabled(False) self.chk_grid.toggled[bool].connect(self.num_grid.setEnabled) gbo_vbox.addWidget(self.chk_grid) gbo_vbox.addWidget(self.num_grid) self.gbo_preview.setLayout(gbo_vbox) vbox.addWidget(self.gbo_preview) self.gbo_preview.toggled[bool].connect(self.set_preview) self.gbo_output = QGroupBox(tr("Output")) self.opt_new = QRadioButton(tr("New signal")) self.opt_replace = QRadioButton(tr("In place")) self.opt_new.setChecked(True) gbo_vbox2 = QVBoxLayout() gbo_vbox2.addWidget(self.opt_new) gbo_vbox2.addWidget(self.opt_replace) self.gbo_output.setLayout(gbo_vbox2) vbox.addWidget(self.gbo_output) self.chk_reshape = QCheckBox(tr("Resize to fit")) self.chk_reshape.setChecked(False) vbox.addWidget(self.chk_reshape) self.btn_ok = QPushButton(tr("&OK")) self.btn_ok.setDefault(True) self.btn_ok.clicked.connect(self.accept) self.btn_cancel = QPushButton(tr("&Cancel")) self.btn_cancel.clicked.connect(self.reject) hbox = QHBoxLayout() hbox.addWidget(self.btn_ok) hbox.addWidget(self.btn_cancel) vbox.addLayout(hbox) vbox.addStretch(1) self.setLayout(vbox)
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 ObjectExplorer(BaseDialog, SpyderConfigurationAccessor): """Object explorer main widget window.""" CONF_SECTION = 'variable_explorer' def __init__(self, obj, name='', expanded=False, resize_to_contents=True, parent=None, attribute_columns=DEFAULT_ATTR_COLS, attribute_details=DEFAULT_ATTR_DETAILS, readonly=None, reset=False): """ Constructor :param name: name of the object as it will appear in the root node :param expanded: show the first visible root element expanded :param resize_to_contents: resize columns to contents ignoring width of the attributes :param obj: any Python object or variable :param attribute_columns: list of AttributeColumn objects that define which columns are present in the table and their defaults :param attribute_details: list of AttributeDetails objects that define which attributes can be selected in the details pane. :param reset: If true the persistent settings, such as column widths, are reset. """ QDialog.__init__(self, parent=parent) self.setAttribute(Qt.WA_DeleteOnClose) # Options show_callable_attributes = self.get_conf('show_callable_attributes') show_special_attributes = self.get_conf('show_special_attributes') # Model self._attr_cols = attribute_columns self._attr_details = attribute_details self.readonly = readonly self.btn_save_and_close = None self.btn_close = None self._tree_model = TreeModel(obj, obj_name=name, attr_cols=self._attr_cols) self._proxy_tree_model = TreeProxyModel( show_callable_attributes=show_callable_attributes, show_special_attributes=show_special_attributes) self._proxy_tree_model.setSourceModel(self._tree_model) # self._proxy_tree_model.setSortRole(RegistryTableModel.SORT_ROLE) self._proxy_tree_model.setDynamicSortFilter(True) # self._proxy_tree_model.setSortCaseSensitivity(Qt.CaseInsensitive) # Tree widget self.obj_tree = ToggleColumnTreeView() self.obj_tree.setAlternatingRowColors(True) self.obj_tree.setModel(self._proxy_tree_model) self.obj_tree.setSelectionBehavior(QAbstractItemView.SelectRows) self.obj_tree.setUniformRowHeights(True) self.obj_tree.add_header_context_menu() # Views self._setup_actions() self._setup_menu(show_callable_attributes=show_callable_attributes, show_special_attributes=show_special_attributes) self._setup_views() if name: name = "{} -".format(name) self.setWindowTitle("{} {}".format(name, EDITOR_NAME)) self.setWindowFlags(Qt.Window) self._resize_to_contents = resize_to_contents self._readViewSettings(reset=reset) # Update views with model self.toggle_show_special_attribute_action.setChecked( show_special_attributes) self.toggle_show_callable_action.setChecked(show_callable_attributes) # Select first row so that a hidden root node will not be selected. first_row_index = self._proxy_tree_model.firstItemIndex() self.obj_tree.setCurrentIndex(first_row_index) if self._tree_model.inspectedNodeIsVisible or expanded: self.obj_tree.expand(first_row_index) def get_value(self): """Get editor current object state.""" return self._tree_model.inspectedItem.obj def _make_show_column_function(self, column_idx): """Creates a function that shows or hides a column.""" show_column = lambda checked: self.obj_tree.setColumnHidden( column_idx, not checked) return show_column def _setup_actions(self): """Creates the main window actions.""" # Show/hide callable objects self.toggle_show_callable_action = QAction( _("Show callable attributes"), self, checkable=True, shortcut=QKeySequence("Alt+C"), statusTip=_("Shows/hides attributes that are callable " "(functions, methods, etc)")) self.toggle_show_callable_action.toggled.connect( self._proxy_tree_model.setShowCallables) self.toggle_show_callable_action.toggled.connect( self.obj_tree.resize_columns_to_contents) # Show/hide special attributes self.toggle_show_special_attribute_action = QAction( _("Show __special__ attributes"), self, checkable=True, shortcut=QKeySequence("Alt+S"), statusTip=_("Shows or hides __special__ attributes")) self.toggle_show_special_attribute_action.toggled.connect( self._proxy_tree_model.setShowSpecialAttributes) self.toggle_show_special_attribute_action.toggled.connect( self.obj_tree.resize_columns_to_contents) def _setup_menu(self, show_callable_attributes=False, show_special_attributes=False): """Sets up the main menu.""" self.tools_layout = QHBoxLayout() callable_attributes = create_toolbutton( self, text=_("Show callable attributes"), icon=ima.icon("class"), toggled=self._toggle_show_callable_attributes_action) callable_attributes.setCheckable(True) callable_attributes.setChecked(show_callable_attributes) self.tools_layout.addWidget(callable_attributes) special_attributes = create_toolbutton( self, text=_("Show __special__ attributes"), icon=ima.icon("private2"), toggled=self._toggle_show_special_attributes_action) special_attributes.setCheckable(True) special_attributes.setChecked(show_special_attributes) self.tools_layout.addWidget(special_attributes) self.tools_layout.addStretch() self.options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) self.options_button.setPopupMode(QToolButton.InstantPopup) self.show_cols_submenu = QMenu(self) self.options_button.setMenu(self.show_cols_submenu) # Don't show menu arrow and remove padding if is_dark_interface(): self.options_button.setStyleSheet( ("QToolButton::menu-indicator{image: none;}\n" "QToolButton{padding: 3px;}")) else: self.options_button.setStyleSheet( "QToolButton::menu-indicator{image: none;}") self.tools_layout.addWidget(self.options_button) @Slot() def _toggle_show_callable_attributes_action(self): """Toggle show callable atributes action.""" action_checked = not self.toggle_show_callable_action.isChecked() self.toggle_show_callable_action.setChecked(action_checked) self.set_conf('show_callable_attributes', action_checked) @Slot() def _toggle_show_special_attributes_action(self): """Toggle show special attributes action.""" action_checked = ( not self.toggle_show_special_attribute_action.isChecked()) self.toggle_show_special_attribute_action.setChecked(action_checked) self.set_conf('show_special_attributes', action_checked) def _setup_views(self): """Creates the UI widgets.""" self.central_splitter = QSplitter(self, orientation=Qt.Vertical) layout = create_plugin_layout(self.tools_layout, self.central_splitter) self.setLayout(layout) # Stretch last column? # It doesn't play nice when columns are hidden and then shown again. obj_tree_header = self.obj_tree.header() obj_tree_header.setSectionsMovable(True) obj_tree_header.setStretchLastSection(False) add_actions(self.show_cols_submenu, self.obj_tree.toggle_column_actions_group.actions()) self.central_splitter.addWidget(self.obj_tree) # Bottom pane bottom_pane_widget = QWidget() bottom_layout = QHBoxLayout() bottom_layout.setSpacing(0) bottom_layout.setContentsMargins(5, 5, 5, 5) # left top right bottom bottom_pane_widget.setLayout(bottom_layout) self.central_splitter.addWidget(bottom_pane_widget) group_box = QGroupBox(_("Details")) bottom_layout.addWidget(group_box) v_group_layout = QVBoxLayout() h_group_layout = QHBoxLayout() h_group_layout.setContentsMargins(2, 2, 2, 2) # left top right bottom group_box.setLayout(v_group_layout) v_group_layout.addLayout(h_group_layout) # Radio buttons radio_widget = QWidget() radio_layout = QVBoxLayout() radio_layout.setContentsMargins(0, 0, 0, 0) # left top right bottom radio_widget.setLayout(radio_layout) self.button_group = QButtonGroup(self) for button_id, attr_detail in enumerate(self._attr_details): radio_button = QRadioButton(attr_detail.name) radio_layout.addWidget(radio_button) self.button_group.addButton(radio_button, button_id) self.button_group.buttonClicked[int].connect( self._change_details_field) self.button_group.button(0).setChecked(True) radio_layout.addStretch(1) h_group_layout.addWidget(radio_widget) # Editor widget self.editor = SimpleCodeEditor(self) self.editor.setReadOnly(True) h_group_layout.addWidget(self.editor) # Save and close buttons btn_layout = QHBoxLayout() btn_layout.addStretch() if not self.readonly: self.btn_save_and_close = QPushButton(_('Save and Close')) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_('Close')) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) v_group_layout.addLayout(btn_layout) # Splitter parameters self.central_splitter.setCollapsible(0, False) self.central_splitter.setCollapsible(1, True) self.central_splitter.setSizes([500, 320]) # Connect signals # Keep a temporary reference of the selection_model to prevent # segfault in PySide. # See http://permalink.gmane.org/gmane.comp.lib.qt.pyside.devel/222 selection_model = self.obj_tree.selectionModel() selection_model.currentChanged.connect(self._update_details) # Check if the values of the model have been changed self._proxy_tree_model.sig_setting_data.connect( self.save_and_close_enable) self._proxy_tree_model.sig_update_details.connect( self._update_details_for_item) # End of setup_methods def _readViewSettings(self, reset=False): """ Reads the persistent program settings. :param reset: If True, the program resets to its default settings. """ pos = QPoint(20, 20) window_size = QSize(825, 650) details_button_idx = 0 header = self.obj_tree.header() header_restored = False if reset: logger.debug("Resetting persistent view settings") else: pos = pos window_size = window_size details_button_idx = details_button_idx # splitter_state = settings.value("central_splitter/state") splitter_state = None if splitter_state: self.central_splitter.restoreState(splitter_state) # header_restored = self.obj_tree.read_view_settings( # 'table/header_state', # settings, reset) header_restored = False if not header_restored: column_sizes = [col.width for col in self._attr_cols] column_visible = [col.col_visible for col in self._attr_cols] for idx, size in enumerate(column_sizes): if not self._resize_to_contents and size > 0: # Just in case header.resizeSection(idx, size) else: header.resizeSections(QHeaderView.ResizeToContents) break for idx, visible in enumerate(column_visible): elem = self.obj_tree.toggle_column_actions_group.actions()[idx] elem.setChecked(visible) self.resize(window_size) button = self.button_group.button(details_button_idx) if button is not None: button.setChecked(True) @Slot() def save_and_close_enable(self): """Handle the data change event to enable the save and close button.""" if self.btn_save_and_close: self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) @Slot(QModelIndex, QModelIndex) def _update_details(self, current_index, _previous_index): """Shows the object details in the editor given an index.""" tree_item = self._proxy_tree_model.treeItem(current_index) self._update_details_for_item(tree_item) def _change_details_field(self, _button_id=None): """Changes the field that is displayed in the details pane.""" # logger.debug("_change_details_field: {}".format(_button_id)) current_index = self.obj_tree.selectionModel().currentIndex() tree_item = self._proxy_tree_model.treeItem(current_index) self._update_details_for_item(tree_item) @Slot(TreeItem) def _update_details_for_item(self, tree_item): """Shows the object details in the editor given an tree_item.""" try: # obj = tree_item.obj button_id = self.button_group.checkedId() assert button_id >= 0, ("No radio button selected. " "Please report this bug.") attr_details = self._attr_details[button_id] data = attr_details.data_fn(tree_item) self.editor.setPlainText(data) self.editor.setWordWrapMode(attr_details.line_wrap) self.editor.setup_editor( font=get_font(font_size_delta=DEFAULT_SMALL_DELTA), show_blanks=False, color_scheme=CONF.get('appearance', 'selected'), scroll_past_end=False, ) self.editor.set_text(data) if attr_details.name == 'Source code': self.editor.set_language('Python') else: self.editor.set_language('Rst') except Exception as ex: self.editor.setStyleSheet("color: red;") stack_trace = traceback.format_exc() self.editor.setPlainText("{}\n\n{}".format(ex, stack_trace)) self.editor.setWordWrapMode( QTextOption.WrapAtWordBoundaryOrAnywhere) @classmethod def create_explorer(cls, *args, **kwargs): """ Creates and shows and ObjectExplorer window. The *args and **kwargs will be passed to the ObjectExplorer constructor A (class attribute) reference to the browser window is kept to prevent it from being garbage-collected. """ object_explorer = cls(*args, **kwargs) object_explorer.exec_() return object_explorer
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)
class ImportWizard(BaseDialog): """Text data import wizard""" def __init__(self, parent, text, title=None, icon=None, contents_title=None, varname=None): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) if title is None: title = _("Import wizard") self.setWindowTitle(title) if icon is None: self.setWindowIcon(ima.icon('fileimport')) if contents_title is None: contents_title = _("Raw text") if varname is None: varname = _("variable_name") self.var_name, self.clip_data = None, None # Setting GUI self.tab_widget = QTabWidget(self) self.text_widget = ContentsWidget(self, text) self.table_widget = PreviewWidget(self) self.tab_widget.addTab(self.text_widget, _("text")) self.tab_widget.setTabText(0, contents_title) self.tab_widget.addTab(self.table_widget, _("table")) self.tab_widget.setTabText(1, _("Preview")) self.tab_widget.setTabEnabled(1, False) name_layout = QHBoxLayout() name_label = QLabel(_("Variable Name")) name_layout.addWidget(name_label) self.name_edt = QLineEdit() self.name_edt.setText(varname) name_layout.addWidget(self.name_edt) btns_layout = QHBoxLayout() cancel_btn = QPushButton(_("Cancel")) btns_layout.addWidget(cancel_btn) cancel_btn.clicked.connect(self.reject) h_spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) btns_layout.addItem(h_spacer) self.back_btn = QPushButton(_("Previous")) self.back_btn.setEnabled(False) btns_layout.addWidget(self.back_btn) self.back_btn.clicked.connect(ft_partial(self._set_step, step=-1)) self.fwd_btn = QPushButton(_("Next")) if not text: self.fwd_btn.setEnabled(False) btns_layout.addWidget(self.fwd_btn) self.fwd_btn.clicked.connect(ft_partial(self._set_step, step=1)) self.done_btn = QPushButton(_("Done")) self.done_btn.setEnabled(False) btns_layout.addWidget(self.done_btn) self.done_btn.clicked.connect(self.process) self.text_widget.asDataChanged.connect(self.fwd_btn.setEnabled) self.text_widget.asDataChanged.connect(self.done_btn.setDisabled) layout = QVBoxLayout() layout.addLayout(name_layout) layout.addWidget(self.tab_widget) layout.addLayout(btns_layout) self.setLayout(layout) def _focus_tab(self, tab_idx): """Change tab focus""" for i in range(self.tab_widget.count()): self.tab_widget.setTabEnabled(i, False) self.tab_widget.setTabEnabled(tab_idx, True) self.tab_widget.setCurrentIndex(tab_idx) def _set_step(self, step): """Proceed to a given step""" new_tab = self.tab_widget.currentIndex() + step assert new_tab < self.tab_widget.count() and new_tab >= 0 if new_tab == self.tab_widget.count() - 1: try: self.table_widget.open_data( self._get_plain_text(), self.text_widget.get_col_sep(), self.text_widget.get_row_sep(), self.text_widget.trnsp_box.isChecked(), self.text_widget.get_skiprows(), self.text_widget.get_comments()) self.done_btn.setEnabled(True) self.done_btn.setDefault(True) self.fwd_btn.setEnabled(False) self.back_btn.setEnabled(True) except (SyntaxError, AssertionError) as error: QMessageBox.critical( self, _("Import wizard"), _("<b>Unable to proceed to next step</b>" "<br><br>Please check your entries." "<br><br>Error message:<br>%s") % str(error)) return elif new_tab == 0: self.done_btn.setEnabled(False) self.fwd_btn.setEnabled(True) self.back_btn.setEnabled(False) self._focus_tab(new_tab) def get_data(self): """Return processed data""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.var_name, self.clip_data def _simplify_shape(self, alist, rec=0): """Reduce the alist dimension if needed""" if rec != 0: if len(alist) == 1: return alist[-1] return alist if len(alist) == 1: return self._simplify_shape(alist[-1], 1) return [self._simplify_shape(al, 1) for al in alist] def _get_table_data(self): """Return clipboard processed as data""" data = self._simplify_shape(self.table_widget.get_data()) if self.table_widget.array_btn.isChecked(): return array(data) elif pd and self.table_widget.df_btn.isChecked(): info = self.table_widget.pd_info buf = io.StringIO(self.table_widget.pd_text) return pd.read_csv(buf, **info) return data def _get_plain_text(self): """Return clipboard as text""" return self.text_widget.text_editor.toPlainText() @Slot() def process(self): """Process the data from clipboard""" var_name = self.name_edt.text() try: self.var_name = str(var_name) except UnicodeEncodeError: self.var_name = to_text_string(var_name) if self.text_widget.get_as_data(): self.clip_data = self._get_table_data() elif self.text_widget.get_as_code(): self.clip_data = try_to_eval(to_text_string( self._get_plain_text())) else: self.clip_data = to_text_string(self._get_plain_text()) self.accept()
class ProjectDialog(QDialog): """Project creation dialog.""" # path, type, packages sig_project_creation_requested = Signal(object, object, object) def __init__(self, parent): """Project creation dialog.""" super(ProjectDialog, self).__init__(parent=parent) # Variables current_python_version = '.'.join([ to_text_string(sys.version_info[0]), to_text_string(sys.version_info[1]) ]) python_versions = ['2.7', '3.4', '3.5'] if current_python_version not in python_versions: python_versions.append(current_python_version) python_versions = sorted(python_versions) self.project_name = None self.location = get_home_dir() # Widgets self.groupbox = QGroupBox() self.radio_new_dir = QRadioButton(_("New directory")) self.radio_from_dir = QRadioButton(_("Existing directory")) self.label_project_name = QLabel(_('Project name')) self.label_location = QLabel(_('Location')) self.label_project_type = QLabel(_('Project type')) self.label_python_version = QLabel(_('Python version')) self.text_project_name = QLineEdit() self.text_location = QLineEdit(get_home_dir()) self.combo_project_type = QComboBox() self.combo_python_version = QComboBox() self.button_select_location = QToolButton() self.button_cancel = QPushButton(_('Cancel')) self.button_create = QPushButton(_('Create')) self.bbox = QDialogButtonBox(Qt.Horizontal) self.bbox.addButton(self.button_cancel, QDialogButtonBox.ActionRole) self.bbox.addButton(self.button_create, QDialogButtonBox.ActionRole) # Widget setup self.combo_python_version.addItems(python_versions) self.radio_new_dir.setChecked(True) self.text_location.setEnabled(True) self.text_location.setReadOnly(True) self.button_select_location.setIcon(get_std_icon('DirOpenIcon')) self.button_cancel.setDefault(True) self.button_cancel.setAutoDefault(True) self.button_create.setEnabled(False) self.combo_project_type.addItems(self._get_project_types()) self.combo_python_version.setCurrentIndex( python_versions.index(current_python_version)) self.setWindowTitle(_('Create new project')) self.setFixedWidth(500) self.label_python_version.setVisible(False) self.combo_python_version.setVisible(False) # Layouts layout_top = QHBoxLayout() layout_top.addWidget(self.radio_new_dir) layout_top.addWidget(self.radio_from_dir) layout_top.addStretch(1) self.groupbox.setLayout(layout_top) layout_grid = QGridLayout() layout_grid.addWidget(self.label_project_name, 0, 0) layout_grid.addWidget(self.text_project_name, 0, 1, 1, 2) layout_grid.addWidget(self.label_location, 1, 0) layout_grid.addWidget(self.text_location, 1, 1) layout_grid.addWidget(self.button_select_location, 1, 2) layout_grid.addWidget(self.label_project_type, 2, 0) layout_grid.addWidget(self.combo_project_type, 2, 1, 1, 2) layout_grid.addWidget(self.label_python_version, 3, 0) layout_grid.addWidget(self.combo_python_version, 3, 1, 1, 2) layout = QVBoxLayout() layout.addWidget(self.groupbox) layout.addSpacing(10) layout.addLayout(layout_grid) layout.addStretch() layout.addSpacing(20) layout.addWidget(self.bbox) self.setLayout(layout) # Signals and slots self.button_select_location.clicked.connect(self.select_location) self.button_create.clicked.connect(self.create_project) self.button_cancel.clicked.connect(self.close) self.radio_from_dir.clicked.connect(self.update_location) self.radio_new_dir.clicked.connect(self.update_location) self.text_project_name.textChanged.connect(self.update_location) def _get_project_types(self): """Get all available project types.""" project_types = get_available_project_types() projects = [] for project in project_types: projects.append(project.PROJECT_TYPE_NAME) return projects def select_location(self): """Select directory.""" location = getexistingdirectory(self, _("Select directory"), self.location) if location: if is_writable(location): self.location = location self.update_location() def update_location(self, text=''): """Update text of location.""" self.text_project_name.setEnabled(self.radio_new_dir.isChecked()) name = self.text_project_name.text().strip() if name and self.radio_new_dir.isChecked(): path = osp.join(self.location, name) self.button_create.setDisabled(os.path.isdir(path)) elif self.radio_from_dir.isChecked(): self.button_create.setEnabled(True) path = self.location else: self.button_create.setEnabled(False) path = self.location self.text_location.setText(path) def create_project(self): """Create project.""" packages = [ 'python={0}'.format(self.combo_python_version.currentText()) ] self.sig_project_creation_requested.emit( self.text_location.text(), self.combo_project_type.currentText(), packages) self.accept()
class ArrayEditor(QDialog): """Array Editor Dialog""" def __init__(self, parent=None): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.data = None self.arraywidget = None self.stack = None self.layout = None self.btn_save_and_close = None self.btn_close = None # Values for 3d array editor self.dim_indexes = [{}, {}, {}] self.last_dim = 0 # Adjust this for changing the startup dimension def setup_and_check(self, data, title='', readonly=False, xlabels=None, ylabels=None): """ Setup ArrayEditor: return False if data is not supported, True otherwise """ self.data = data self.data.flags.writeable = True is_record_array = data.dtype.names is not None is_masked_array = isinstance(data, np.ma.MaskedArray) if data.ndim > 3: self.error( _("Arrays with more than 3 dimensions are not " "supported")) return False if xlabels is not None and len(xlabels) != self.data.shape[1]: self.error( _("The 'xlabels' argument length do no match array " "column number")) return False if ylabels is not None and len(ylabels) != self.data.shape[0]: self.error( _("The 'ylabels' argument length do no match array row " "number")) return False if not is_record_array: dtn = data.dtype.name if dtn not in SUPPORTED_FORMATS and not dtn.startswith('str') \ and not dtn.startswith('unicode'): arr = _("%s arrays") % data.dtype.name self.error(_("%s are currently not supported") % arr) return False self.layout = QGridLayout() self.setLayout(self.layout) self.setWindowIcon(ima.icon('arredit')) if title: title = to_text_string(title) + " - " + _("NumPy array") else: title = _("Array editor") if readonly: title += ' (' + _('read only') + ')' self.setWindowTitle(title) self.resize(600, 500) # Stack widget self.stack = QStackedWidget(self) if is_record_array: for name in data.dtype.names: self.stack.addWidget( ArrayEditorWidget(self, data[name], readonly, xlabels, ylabels)) elif is_masked_array: self.stack.addWidget( ArrayEditorWidget(self, data, readonly, xlabels, ylabels)) self.stack.addWidget( ArrayEditorWidget(self, data.data, readonly, xlabels, ylabels)) self.stack.addWidget( ArrayEditorWidget(self, data.mask, readonly, xlabels, ylabels)) elif data.ndim == 3: pass else: self.stack.addWidget( ArrayEditorWidget(self, data, readonly, xlabels, ylabels)) self.arraywidget = self.stack.currentWidget() if self.arraywidget: self.arraywidget.model.dataChanged.connect( self.save_and_close_enable) self.stack.currentChanged.connect(self.current_widget_changed) self.layout.addWidget(self.stack, 1, 0) # Buttons configuration btn_layout = QHBoxLayout() if is_record_array or is_masked_array or data.ndim == 3: if is_record_array: btn_layout.addWidget(QLabel(_("Record array fields:"))) names = [] for name in data.dtype.names: field = data.dtype.fields[name] text = name if len(field) >= 3: title = field[2] if not is_text_string(title): title = repr(title) text += ' - ' + title names.append(text) else: names = [_('Masked data'), _('Data'), _('Mask')] if data.ndim == 3: # QSpinBox self.index_spin = QSpinBox(self, keyboardTracking=False) self.index_spin.valueChanged.connect(self.change_active_widget) # QComboBox names = [str(i) for i in range(3)] ra_combo = QComboBox(self) ra_combo.addItems(names) ra_combo.currentIndexChanged.connect(self.current_dim_changed) # Adding the widgets to layout label = QLabel(_("Axis:")) btn_layout.addWidget(label) btn_layout.addWidget(ra_combo) self.shape_label = QLabel() btn_layout.addWidget(self.shape_label) label = QLabel(_("Index:")) btn_layout.addWidget(label) btn_layout.addWidget(self.index_spin) self.slicing_label = QLabel() btn_layout.addWidget(self.slicing_label) # set the widget to display when launched self.current_dim_changed(self.last_dim) else: ra_combo = QComboBox(self) ra_combo.currentIndexChanged.connect( self.stack.setCurrentIndex) ra_combo.addItems(names) btn_layout.addWidget(ra_combo) if is_masked_array: label = QLabel( _("<u>Warning</u>: changes are applied separately")) label.setToolTip(_("For performance reasons, changes applied "\ "to masked array won't be reflected in "\ "array's data (and vice-versa).")) btn_layout.addWidget(label) btn_layout.addStretch() if not readonly: self.btn_save_and_close = QPushButton(_('Save and Close')) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_('Close')) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) self.layout.addLayout(btn_layout, 2, 0) self.setMinimumSize(400, 300) # Make the dialog act as a window self.setWindowFlags(Qt.Window) return True @Slot(QModelIndex, QModelIndex) def save_and_close_enable(self, left_top, bottom_right): """Handle the data change event to enable the save and close button.""" if self.btn_save_and_close: self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) def current_widget_changed(self, index): self.arraywidget = self.stack.widget(index) self.arraywidget.model.dataChanged.connect(self.save_and_close_enable) def change_active_widget(self, index): """ This is implemented for handling negative values in index for 3d arrays, to give the same behavior as slicing """ string_index = [':'] * 3 string_index[self.last_dim] = '<font color=red>%i</font>' self.slicing_label.setText( (r"Slicing: [" + ", ".join(string_index) + "]") % index) if index < 0: data_index = self.data.shape[self.last_dim] + index else: data_index = index slice_index = [slice(None)] * 3 slice_index[self.last_dim] = data_index stack_index = self.dim_indexes[self.last_dim].get(data_index) if stack_index == None: stack_index = self.stack.count() try: self.stack.addWidget( ArrayEditorWidget(self, self.data[slice_index])) except IndexError: # Handle arrays of size 0 in one axis self.stack.addWidget(ArrayEditorWidget(self, self.data)) self.dim_indexes[self.last_dim][data_index] = stack_index self.stack.update() self.stack.setCurrentIndex(stack_index) def current_dim_changed(self, index): """ This change the active axis the array editor is plotting over in 3D """ self.last_dim = index string_size = ['%i'] * 3 string_size[index] = '<font color=red>%i</font>' self.shape_label.setText( ('Shape: (' + ', '.join(string_size) + ') ') % self.data.shape) if self.index_spin.value() != 0: self.index_spin.setValue(0) else: # this is done since if the value is currently 0 it does not emit # currentIndexChanged(int) self.change_active_widget(0) self.index_spin.setRange(-self.data.shape[index], self.data.shape[index] - 1) @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.stack.count()): self.stack.widget(index).accept_changes() QDialog.accept(self) def get_value(self): """Return modified array -- this is *not* a copy""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.data def error(self, message): """An error occured, closing the dialog box""" QMessageBox.critical(self, _("Array editor"), message) self.setAttribute(Qt.WA_DeleteOnClose) self.reject() @Slot() def reject(self): """Reimplement Qt method""" if self.arraywidget is not None: for index in range(self.stack.count()): self.stack.widget(index).reject_changes() QDialog.reject(self)
class CondaPackagesWidget(QWidget): """ Conda Packages Widget. """ # Location of updated repo.json files from continuum/binstar CONDA_CONF_PATH = get_conf_path('repo') # Location of continuum/anaconda default repos shipped with conda-manager DATA_PATH = get_module_data_path() # file inside DATA_PATH with metadata for conda packages DATABASE_FILE = 'packages.ini' sig_worker_ready = Signal() sig_packages_ready = Signal() sig_environment_created = Signal(object, object) sig_environment_removed = Signal(object, object) sig_environment_cloned = Signal(object, object) sig_channels_updated = Signal(tuple, tuple) # channels, active_channels sig_process_cancelled = Signal() sig_next_focus = Signal() sig_packages_busy = Signal() def __init__(self, parent, name=None, prefix=None, channels=(), active_channels=(), conda_url='https://conda.anaconda.org', conda_api_url='https://api.anaconda.org', setup=True, data_directory=None, extra_metadata={}): super(CondaPackagesWidget, self).__init__(parent) # Check arguments: active channels, must be witbhin channels for ch in active_channels: if ch not in channels: raise Exception("'active_channels' must be also within " "'channels'") if data_directory is None: data_directory = self.CONDA_CONF_PATH self._parent = parent self._current_action_name = '' self._hide_widgets = False self._metadata = extra_metadata # From repo.continuum self._metadata_links = {} # Bundled metadata self.api = ManagerAPI() self.busy = False self.data_directory = data_directory self.conda_url = conda_url self.conda_api_url = conda_api_url self.name = name self.package_blacklist = [] self.prefix = prefix self.root_prefix = self.api.ROOT_PREFIX self.style_sheet = None self.message = '' self.apply_actions_dialog = None self.conda_errors = [] self.message_box_error = None self.token = None if channels: self._channels = channels self._active_channels = active_channels else: self._channels = self.api.conda_get_condarc_channels() self._active_channels = self._channels[:] try: import spyderlib.utils.icon_manager as ima icon_options = ima.icon('tooloptions') except Exception: import qtawesome as qta icon_options = qta.icon('fa.cog') # Widgets self.cancel_dialog = ClosePackageManagerDialog self.bbox = QDialogButtonBox(Qt.Horizontal) self.button_cancel = QPushButton('Cancel') self.button_channels = QPushButton(_('Channels')) self.button_ok = QPushButton(_('Ok')) self.button_update = QPushButton(_('Update index...')) self.button_apply = QPushButton(_('Apply')) self.button_clear = QPushButton(_('Clear')) self.button_options = QToolButton() self.combobox_filter = DropdownPackageFilter(self) self.frame_top = FramePackageTop() self.frame_bottom = FramePackageTop() self.progress_bar = ProgressBarPackage(self) self.status_bar = LabelPackageStatus(self) self.table = TableCondaPackages(self) self.textbox_search = LineEditSearch(self) self.widgets = [ self.button_update, self.button_channels, self.combobox_filter, self.textbox_search, self.table, self.button_ok, self.button_apply, self.button_clear, self.button_options ] self.table_first_row = FirstRowWidget( widget_before=self.textbox_search) self.table_last_row = LastRowWidget(widgets_after=[ self.button_apply, self.button_clear, self.button_cancel, self.combobox_filter ]) # Widget setup self.button_options.setPopupMode(QToolButton.InstantPopup) self.button_options.setIcon(icon_options) self.button_options.setAutoRaise(True) max_height = self.status_bar.fontMetrics().height() max_width = self.textbox_search.fontMetrics().width('M' * 23) self.bbox.addButton(self.button_ok, QDialogButtonBox.ActionRole) self.button_ok.setAutoDefault(True) self.button_ok.setDefault(True) self.button_ok.setMaximumSize(QSize(0, 0)) self.button_ok.setVisible(False) self.combobox_filter.addItems([k for k in C.COMBOBOX_VALUES_ORDERED]) self.combobox_filter.setMinimumWidth(120) self.progress_bar.setMaximumHeight(max_height * 1.2) self.progress_bar.setMaximumWidth(max_height * 12) self.progress_bar.setTextVisible(False) self.progress_bar.setVisible(False) self.setMinimumSize(QSize(480, 300)) self.setWindowTitle(_("Conda Package Manager")) self.status_bar.setFixedHeight(max_height * 1.5) self.textbox_search.setMaximumWidth(max_width) self.textbox_search.setPlaceholderText('Search Packages') self.table_first_row.setMaximumHeight(0) self.table_last_row.setMaximumHeight(0) self.table_last_row.setVisible(False) self.table_first_row.setVisible(False) # Layout top_layout = QHBoxLayout() top_layout.addWidget(self.combobox_filter) top_layout.addWidget(self.button_channels) top_layout.addWidget(self.button_update) top_layout.addWidget(self.textbox_search) top_layout.addStretch() top_layout.addWidget(self.button_options) middle_layout = QVBoxLayout() middle_layout.addWidget(self.table_first_row) middle_layout.addWidget(self.table) middle_layout.addWidget(self.table_last_row) bottom_layout = QHBoxLayout() bottom_layout.addWidget(self.status_bar) bottom_layout.addStretch() bottom_layout.addWidget(self.progress_bar) bottom_layout.addWidget(self.button_cancel) bottom_layout.addWidget(self.button_apply) bottom_layout.addWidget(self.button_clear) layout = QVBoxLayout(self) layout.addLayout(top_layout) layout.addLayout(middle_layout) layout.addLayout(bottom_layout) layout.addSpacing(6) self.setLayout(layout) self.setTabOrder(self.combobox_filter, self.button_channels) self.setTabOrder(self.button_channels, self.button_update) self.setTabOrder(self.button_update, self.textbox_search) self.setTabOrder(self.textbox_search, self.table_first_row) self.setTabOrder(self.table, self.table_last_row) self.setTabOrder(self.table_last_row, self.button_apply) self.setTabOrder(self.button_apply, self.button_clear) self.setTabOrder(self.button_clear, self.button_cancel) # Signals and slots self.api.sig_repodata_updated.connect(self._repodata_updated) self.combobox_filter.currentIndexChanged.connect(self.filter_package) self.button_apply.clicked.connect(self.apply_multiple_actions) self.button_clear.clicked.connect(self.clear_actions) self.button_cancel.clicked.connect(self.cancel_process) self.button_channels.clicked.connect(self.show_channels_dialog) self.button_update.clicked.connect(self.update_package_index) self.textbox_search.textChanged.connect(self.search_package) self.table.sig_conda_action_requested.connect(self._run_conda_action) self.table.sig_actions_updated.connect(self.update_actions) self.table.sig_pip_action_requested.connect(self._run_pip_action) self.table.sig_status_updated.connect(self.update_status) self.table.sig_next_focus.connect(self.table_last_row.handle_tab) self.table.sig_previous_focus.connect( lambda: self.table_first_row.widget_before.setFocus()) self.table_first_row.sig_enter_first.connect(self._handle_tab_focus) self.table_last_row.sig_enter_last.connect(self._handle_backtab_focus) # Setup self.api.client_set_domain(conda_api_url) self.api.set_data_directory(self.data_directory) self._load_bundled_metadata() self.update_actions(0) if setup: self.set_environment(name=name, prefix=prefix) self.setup() # --- Helpers # ------------------------------------------------------------------------- def _handle_tab_focus(self): self.table.setFocus() if self.table.proxy_model: index = self.table.proxy_model.index(0, 0) self.table.setCurrentIndex(index) def _handle_backtab_focus(self): self.table.setFocus() if self.table.proxy_model: row = self.table.proxy_model.rowCount() - 1 index = self.table.proxy_model.index(row, 0) self.table.setCurrentIndex(index) # --- Callbacks # ------------------------------------------------------------------------- def _load_bundled_metadata(self): """ """ logger.debug('') parser = cp.ConfigParser() db_file = CondaPackagesWidget.DATABASE_FILE with open(osp.join(self.DATA_PATH, db_file)) as f: parser.readfp(f) for name in parser.sections(): metadata = {} for key, data in parser.items(name): metadata[key] = data self._metadata_links[name] = metadata def _setup_packages(self, worker, data, error): """ """ if error: logger.error(error) else: logger.debug('') combobox_index = self.combobox_filter.currentIndex() status = C.PACKAGE_STATUS[combobox_index] packages = worker.packages # Remove blacklisted packages for package in self.package_blacklist: if package in packages: packages.pop(package) for i, row in enumerate(data): if package == data[i][C.COL_NAME]: data.pop(i) self.table.setup_model(packages, data, self._metadata_links) self.combobox_filter.setCurrentIndex(combobox_index) self.filter_package(status) if self._current_model_index: self.table.setCurrentIndex(self._current_model_index) self.table.verticalScrollBar().setValue(self._current_table_scroll) if error: self.update_status(error, False) self.sig_packages_ready.emit() self.table.setFocus() def get_logged_user_list_channels(self): channels = [] for ch in self._active_channels: if self.conda_url in ch and 'repo.continuum' not in ch: channel = ch.split('/')[-1] channels.append(channel) return channels def _prepare_model_data(self, worker=None, output=None, error=None): """ """ if error: logger.error(error) else: logger.debug('') packages, apps = output # worker = self.api.pip_list(prefix=self.prefix) # worker.sig_finished.connect(self._pip_list_ready) logins = self.get_logged_user_list_channels() worker = self.api.client_multi_packages(logins=logins, access='private') worker.sig_finished.connect(self._user_private_packages_ready) worker.packages = packages worker.apps = apps def _user_private_packages_ready(self, worker, output, error): if error: logger.error(error) else: logger.debug('') packages = worker.packages apps = worker.apps worker = self.api.pip_list(prefix=self.prefix) worker.sig_finished.connect(self._pip_list_ready) worker.packages = packages worker.apps = apps # private_packages = {} # if output: # all_private_packages = output # for item in all_private_packages: # name = item.get('name', '') # public = item.get('public', True) # package_types = item.get('package_types', []) # latest_version = item.get('latest_version', '') # if name and not public and 'conda' in package_types: # private_packages[name] = {'versions': item.get('versions', []), # 'app_entry': {}, # 'type': {}, # 'size': {}, # 'latest_version': latest_version, # } worker.private_packages = output def _pip_list_ready(self, worker, pip_packages, error): """ """ if error: logger.error(error) else: logger.debug('') packages = worker.packages private_packages = worker.private_packages linked_packages = self.api.conda_linked(prefix=self.prefix) data = self.api.client_prepare_packages_data(packages, linked_packages, pip_packages, private_packages) combobox_index = self.combobox_filter.currentIndex() status = C.PACKAGE_STATUS[combobox_index] # Remove blacklisted packages for package in self.package_blacklist: if package in packages: packages.pop(package) for i, row in enumerate(data): if package == data[i][C.COL_NAME]: data.pop(i) self.table.setup_model(packages, data, self._metadata_links) self.combobox_filter.setCurrentIndex(combobox_index) self.filter_package(status) if self._current_model_index: self.table.setCurrentIndex(self._current_model_index) self.table.verticalScrollBar().setValue(self._current_table_scroll) if error: self.update_status(str(error), False) self.sig_packages_ready.emit() self.table.setFocus() def _repodata_updated(self, paths): """ """ worker = self.api.client_load_repodata(paths, extra_data={}, metadata=self._metadata) worker.paths = paths worker.sig_finished.connect(self._prepare_model_data) def _metadata_updated(self, worker, path, error): """ """ if error: logger.error(error) else: logger.debug('') if path and osp.isfile(path): with open(path, 'r') as f: data = f.read() try: self._metadata = json.loads(data) except Exception: self._metadata = {} else: self._metadata = {} self.api.update_repodata(self._channels) # --- # ------------------------------------------------------------------------- def _run_multiple_actions(self, worker=None, output=None, error=None): """ """ logger.error(str(error)) if output and isinstance(output, dict): conda_error_type = output.get('error_type', None) conda_error = output.get('error', None) if conda_error_type or conda_error: self.conda_errors.append((conda_error_type, conda_error)) logger.error((conda_error_type, conda_error)) if self._multiple_process: status, func = self._multiple_process.popleft() self.update_status(status) worker = func() worker.sig_finished.connect(self._run_multiple_actions) worker.sig_partial.connect(self._partial_output_ready) else: if self.conda_errors and self.message_box_error: text = "The following errors occured:" error = '' for conda_error in self.conda_errors: error += str(conda_error[0]) + ':\n' error += str(conda_error[1]) + '\n\n' dlg = self.message_box_error(text=text, error=error, title='Conda process error') dlg.setMinimumWidth(400) dlg.exec_() self.update_status('', hide=False) self.setup() def _pip_process_ready(self, worker, output, error): """ """ if error is not None: status = _('there was an error') self.update_status(hide=False, message=status) else: self.update_status(hide=True) self.setup() def _conda_process_ready(self, worker, output, error): """ """ if error is not None: status = _('there was an error') self.update_status(hide=False, message=status) else: self.update_status(hide=True) conda_error = None conda_error_type = None if output and isinstance(output, dict): conda_error_type = output.get('error_type') conda_error = output.get('error') if conda_error_type or conda_error: logger.error((conda_error_type, conda_error)) dic = self._temporal_action_dic if dic['action'] == C.ACTION_CREATE: self.sig_environment_created.emit(conda_error, conda_error_type) elif dic['action'] == C.ACTION_CLONE: self.sig_environment_cloned.emit(conda_error, conda_error_type) elif dic['action'] == C.ACTION_REMOVE_ENV: self.sig_environment_removed.emit(conda_error, conda_error_type) self.setup() def _partial_output_ready(self, worker, output, error): """ """ message = None progress = (0, 0) if isinstance(output, dict): progress = (output.get('progress', None), output.get('maxval', None)) name = output.get('name', None) fetch = output.get('fetch', None) if fetch: message = "Downloading <b>{0}</b>...".format(fetch) if name: self._current_action_name = name message = "Linking <b>{0}</b>...".format(name) logger.debug(message) self.update_status(message, progress=progress) def _run_pip_action(self, package_name, action): """ DEPRECATED """ prefix = self.prefix if prefix == self.root_prefix: name = 'root' elif self.api.conda_environment_exists(prefix=prefix): name = osp.basename(prefix) else: name = prefix if action == C.ACTION_REMOVE: msgbox = QMessageBox.question( self, "Remove pip package: " "{0}".format(package_name), "Do you want to proceed?", QMessageBox.Yes | QMessageBox.No) if msgbox == QMessageBox.Yes: self.update_status() worker = self.api.pip_remove(prefix=self.prefix, pkgs=[package_name]) worker.sig_finished.connect(self._pip_process_ready) status = (_('Removing pip package <b>') + package_name + '</b>' + _(' from <i>') + name + '</i>') self.update_status(hide=True, message=status, progress=[0, 0]) def _run_conda_action(self, package_name, action, version, versions, packages_sizes): """ DEPRECATED """ prefix = self.prefix dlg = CondaPackageActionDialog(self, prefix, package_name, action, version, versions, packages_sizes, self._active_channels) if dlg.exec_(): dic = {} self.status = 'Processing' self.update_status(hide=True) self.repaint() ver1 = dlg.label_version.text() ver2 = dlg.combobox_version.currentText() pkg = u'{0}={1}{2}'.format(package_name, ver1, ver2) dep = dlg.checkbox.checkState() state = dlg.checkbox.isEnabled() dlg.close() dic['pkg'] = pkg dic['dep'] = not (dep == 0 and state) dic['action'] = None self._run_conda_process(action, dic) def _run_conda_process(self, action, dic): """ DEPRECTAED """ self._temporal_action_dic = dic # prefix = self.prefix # # if prefix == self.root_prefix: # name = 'root' # elif self.api.conda_environment_exists(prefix=prefix): # name = osp.basename(prefix) # else: # name = prefix if 'pkgs' in dic and 'dep' in dic: dep = dic['dep'] pkgs = dic['pkgs'] if not isinstance(pkgs, list): pkgs = [pkgs] # if (action == C.ACTION_INSTALL or action == C.ACTION_UPGRADE or # action == C.ACTION_DOWNGRADE): # status = _('Installing <b>') + dic['pkg'] + '</b>' # status = status + _(' into <i>') + name + '</i>' # worker = self.api.conda_install(prefix=prefix, pkgs=pkgs, dep=dep, # channels=self._active_channels) # elif action == C.ACTION_REMOVE: # status = (_('Removing <b>') + dic['pkg'] + '</b>' + # _(' from <i>') + name + '</i>') # worker = self.api.conda_remove(pkgs[0], prefix=prefix) # --- Environment management actions name = dic['name'] if action == C.ACTION_CREATE: status = _('Creating environment <b>') + name + '</b>' worker = self.api.conda_create(name=name, pkgs=pkgs, channels=self._active_channels) elif action == C.ACTION_CLONE: clone = dic['clone'] status = (_('Cloning ') + '<i>' + clone + _('</i> into <b>') + name + '</b>') worker = self.api.conda_clone(clone, name=name) elif action == C.ACTION_REMOVE_ENV: status = _('Removing environment <b>') + name + '</b>' worker = self.api.conda_remove(name=name, all_=True) worker.sig_finished.connect(self._conda_process_ready) worker.sig_partial.connect(self._partial_output_ready) self.update_status(hide=True, message=status, progress=None) self._temporal_action_dic = dic return worker # Public API # ------------------------------------------------------------------------- def prepare_model_data(self, packages, apps): """ """ logger.debug('') self._prepare_model_data(output=(packages, apps)) # These should be private methods.... def enable_widgets(self): """ """ self.table.hide_columns() def disable_widgets(self): """ """ self.table.hide_action_columns() def accept_channels_dialog(self): self.button_channels.setFocus() self.button_channels.toggle() def update_actions(self, number_of_actions): """ """ self.button_apply.setVisible(bool(number_of_actions)) self.button_clear.setVisible(bool(number_of_actions)) # --- Non UI API # ------------------------------------------------------------------------- def setup(self, check_updates=False, blacklist=[], metadata={}): """ Setup packages. Main triger method to download repodata, load repodata, prepare and updating the data model. Parameters ---------- check_updates : bool If `True`, checks that the latest repodata is available on the listed channels. If `False`, the data will be loaded from the downloaded files without checking for newer versions. blacklist: list of str List of conda package names to be excluded from the actual package manager view. """ self.sig_packages_busy.emit() if self.busy: logger.debug('Busy...') return else: logger.debug('') if blacklist: self.package_blacklist = [p.lower() for p in blacklist] if metadata: self._metadata = metadata self._current_model_index = self.table.currentIndex() self._current_table_scroll = self.table.verticalScrollBar().value() self.update_status('Updating package index', True) if check_updates: worker = self.api.update_metadata() worker.sig_finished.connect(self._metadata_updated) else: paths = self.api.repodata_files(channels=self._active_channels) self._repodata_updated(paths) def update_domains(self, anaconda_api_url=None, conda_url=None): """ """ logger.debug(str((anaconda_api_url, conda_url))) update = False if anaconda_api_url: if self.conda_api_url != anaconda_api_url: update = True self.conda_api_url = anaconda_api_url self.api.client_set_domain(anaconda_api_url) if conda_url: if self.conda_url != conda_url: update = True self.conda_url = conda_url if update: pass def set_environment(self, name=None, prefix=None): """ This does not update the package manager! """ logger.debug(str((name, prefix))) if prefix and self.api.conda_environment_exists(prefix=prefix): self.prefix = prefix elif name and self.api.conda_environment_exists(name=name): self.prefix = self.get_prefix_envname(name) else: self.prefix = self.root_prefix def set_token(self, token): self.token = token def update_channels(self, channels, active_channels): """ """ logger.debug(str((channels, active_channels))) if sorted(self._active_channels) != sorted(active_channels) or \ sorted(self._channels) != sorted(channels): self._channels = channels self._active_channels = active_channels self.sig_channels_updated.emit(tuple(channels), tuple(active_channels)) self.setup(check_updates=True) def update_style_sheet(self, style_sheet=None, extra_dialogs={}, palette={}): if style_sheet: self.style_sheet = style_sheet self.table.update_style_palette(palette=palette) self.textbox_search.update_style_sheet(style_sheet) self.setStyleSheet(style_sheet) if extra_dialogs: cancel_dialog = extra_dialogs.get('cancel_dialog', None) apply_actions_dialog = extra_dialogs.get('apply_actions_dialog', None) message_box_error = extra_dialogs.get('message_box_error', None) if cancel_dialog: self.cancel_dialog = cancel_dialog if apply_actions_dialog: self.apply_actions_dialog = apply_actions_dialog if message_box_error: self.message_box_error = message_box_error # --- UI API # ------------------------------------------------------------------------- def filter_package(self, value): """ """ self.table.filter_status_changed(value) def show_channels_dialog(self): """ Show the channels dialog. """ button_channels = self.button_channels self.dlg = DialogChannels(self, channels=self._channels, active_channels=self._active_channels, conda_url=self.conda_url) self.dlg.update_style_sheet(style_sheet=self.style_sheet) button_channels.setDisabled(True) self.dlg.sig_channels_updated.connect(self.update_channels) self.dlg.rejected.connect(lambda: button_channels.setEnabled(True)) self.dlg.rejected.connect(button_channels.toggle) self.dlg.rejected.connect(button_channels.setFocus) self.dlg.accepted.connect(self.accept_channels_dialog) geo_tl = button_channels.geometry().topLeft() tl = button_channels.parentWidget().mapToGlobal(geo_tl) x = tl.x() + 2 y = tl.y() + button_channels.height() self.dlg.move(x, y) self.dlg.show() self.dlg.button_add.setFocus() def update_package_index(self): """ """ self.setup(check_updates=True) def search_package(self, text): """ """ self.table.search_string_changed(text) def apply_multiple_actions(self): """ """ logger.debug('') self.conda_errors = [] prefix = self.prefix if prefix == self.root_prefix: name = 'root' elif self.api.conda_environment_exists(prefix=prefix): name = osp.basename(prefix) else: name = prefix actions = self.table.get_actions() if actions is None: return self._multiple_process = deque() pip_actions = actions[C.PIP_PACKAGE] conda_actions = actions[C.CONDA_PACKAGE] pip_remove = pip_actions.get(C.ACTION_REMOVE, []) conda_remove = conda_actions.get(C.ACTION_REMOVE, []) conda_install = conda_actions.get(C.ACTION_INSTALL, []) conda_upgrade = conda_actions.get(C.ACTION_UPGRADE, []) conda_downgrade = conda_actions.get(C.ACTION_DOWNGRADE, []) message = '' template_1 = '<li><b>{0}={1}</b></li>' template_2 = '<li><b>{0}: {1} -> {2}</b></li>' if pip_remove: temp = [ template_1.format(i['name'], i['version_to']) for i in pip_remove ] message += ('The following pip packages will be removed: ' '<ul>' + ''.join(temp) + '</ul>') if conda_remove: temp = [ template_1.format(i['name'], i['version_to']) for i in conda_remove ] message += ('<br>The following conda packages will be removed: ' '<ul>' + ''.join(temp) + '</ul>') if conda_install: temp = [ template_1.format(i['name'], i['version_to']) for i in conda_install ] message += ('<br>The following conda packages will be installed: ' '<ul>' + ''.join(temp) + '</ul>') if conda_downgrade: temp = [ template_2.format(i['name'], i['version_from'], i['version_to']) for i in conda_downgrade ] message += ('<br>The following conda packages will be downgraded: ' '<ul>' + ''.join(temp) + '</ul>') if conda_upgrade: temp = [ template_2.format(i['name'], i['version_from'], i['version_to']) for i in conda_upgrade ] message += ('<br>The following conda packages will be upgraded: ' '<ul>' + ''.join(temp) + '</ul>') message += '<br>' if self.apply_actions_dialog: dlg = self.apply_actions_dialog(message, parent=self) dlg.update_style_sheet(style_sheet=self.style_sheet) reply = dlg.exec_() else: reply = QMessageBox.question(self, 'Proceed with the following actions?', message, buttons=QMessageBox.Ok | QMessageBox.Cancel) if reply: # Pip remove for pkg in pip_remove: status = (_('Removing pip package <b>') + pkg['name'] + '</b>' + _(' from <i>') + name + '</i>') pkgs = [pkg['name']] def trigger(prefix=prefix, pkgs=pkgs): return lambda: self.api.pip_remove(prefix=prefix, pkgs=pkgs) self._multiple_process.append([status, trigger()]) # Process conda actions if conda_remove: status = (_('Removing conda packages <b>') + '</b>' + _(' from <i>') + name + '</i>') pkgs = [i['name'] for i in conda_remove] def trigger(prefix=prefix, pkgs=pkgs): return lambda: self.api.conda_remove(pkgs=pkgs, prefix=prefix) self._multiple_process.append([status, trigger()]) if conda_install: pkgs = [ '{0}={1}'.format(i['name'], i['version_to']) for i in conda_install ] status = (_('Installing conda packages <b>') + '</b>' + _(' on <i>') + name + '</i>') def trigger(prefix=prefix, pkgs=pkgs): return lambda: self.api.conda_install(prefix=prefix, pkgs=pkgs, channels=self. _active_channels, token=self.token) self._multiple_process.append([status, trigger()]) # Conda downgrade if conda_downgrade: status = (_('Downgrading conda packages <b>') + '</b>' + _(' on <i>') + name + '</i>') pkgs = [ '{0}={1}'.format(i['name'], i['version_to']) for i in conda_downgrade ] def trigger(prefix=prefix, pkgs=pkgs): return lambda: self.api.conda_install(prefix=prefix, pkgs=pkgs, channels=self. _active_channels, token=self.token) self._multiple_process.append([status, trigger()]) # Conda update if conda_upgrade: status = (_('Upgrading conda packages <b>') + '</b>' + _(' on <i>') + name + '</i>') pkgs = [ '{0}={1}'.format(i['name'], i['version_to']) for i in conda_upgrade ] def trigger(prefix=prefix, pkgs=pkgs): return lambda: self.api.conda_install(prefix=prefix, pkgs=pkgs, channels=self. _active_channels, token=self.token) self._multiple_process.append([status, trigger()]) self._run_multiple_actions() def clear_actions(self): """ """ self.table.clear_actions() def cancel_process(self): """ Allow user to cancel an ongoing process. """ logger.debug(str('process canceled by user.')) if self.busy: dlg = self.cancel_dialog() reply = dlg.exec_() if reply: self.update_status(hide=False, message='Process cancelled') self.api.conda_terminate() self.api.download_requests_terminate() self.api.conda_clear_lock() self.table.clear_actions() self.sig_process_cancelled.emit() else: QDialog.reject(self) def update_status(self, message=None, hide=True, progress=None, env=False): """ Update status bar, progress bar display and widget visibility message : str Message to display in status bar. hide : bool Enable/Disable widgets. progress : [int, int] Show status bar progress. [0, 0] means spinning statusbar. """ self.busy = hide for widget in self.widgets: widget.setDisabled(hide) self.table.verticalScrollBar().setValue(self._current_table_scroll) self.button_apply.setVisible(False) self.button_clear.setVisible(False) self.progress_bar.setVisible(hide) self.button_cancel.setVisible(hide) if message is not None: self.message = message if self.prefix == self.root_prefix: short_env = 'root' # elif self.api.environment_exists(prefix=self.prefix): # short_env = osp.basename(self.prefix) else: short_env = self.prefix if env: self.message = '{0} (<b>{1}</b>)'.format( self.message, short_env, ) self.status_bar.setText(self.message) if progress is not None: current_progress, max_progress = 0, 0 if progress[1]: max_progress = progress[1] if progress[0]: current_progress = progress[0] self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(max_progress) self.progress_bar.setValue(current_progress) else: self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(0) # --- Conda helpers # ------------------------------------------------------------------------- def get_environment_prefix(self): """ Returns the active environment prefix. """ return self.prefix def get_environment_name(self): """ Returns the active environment name if it is located in the default conda environments directory, otherwise it returns the prefix. """ name = osp.basename(self.prefix) if not (name and self.api.environment_exists(name=name)): name = self.prefix return name def get_environments(self): """ Get a list of conda environments located in the default conda environments directory. """ return self.api.conda_get_envs() def get_prefix_envname(self, name): """ Returns the prefix for a given environment by name. """ return self.api.conda_get_prefix_envname(name) def get_package_versions(self, name): """ """ return self.table.source_model.get_package_versions(name) # --- Conda actions # ------------------------------------------------------------------------- def create_environment(self, name=None, prefix=None, packages=['python']): """ """ # If environment exists already? GUI should take care of this # BUT the api call should simply set that env as the env dic = {} dic['name'] = name dic['prefix'] = prefix dic['pkgs'] = packages dic['dep'] = True # Not really needed but for the moment! dic['action'] = C.ACTION_CREATE return self._run_conda_process(dic['action'], dic) def clone_environment(self, name=None, prefix=None, clone=None): dic = {} dic['name'] = name dic['prefix'] = prefix dic['clone'] = clone dic['pkgs'] = None dic['dep'] = True # Not really needed but for the moment! dic['action'] = C.ACTION_CLONE return self._run_conda_process(dic['action'], dic) def remove_environment(self, name=None, prefix=None): dic = {} dic['name'] = name dic['pkgs'] = None dic['dep'] = True # Not really needed but for the moment! dic['action'] = C.ACTION_REMOVE_ENV return self._run_conda_process(dic['action'], dic) # New api def show_login_dialog(self): pass def show_options_menu(self): pass
class PreferencesDialog(QDialog): """Preferences Dialog for Napari user settings.""" ui_schema = { "call_order": { "ui:widget": "plugins" }, "highlight_thickness": { "ui:widget": "highlight" }, "shortcuts": { "ui:widget": "shortcuts" }, "extension2reader": { "ui:widget": "extension2reader" }, } resized = Signal(QSize) def __init__(self, parent=None): from ...settings import get_settings super().__init__(parent) self.setWindowTitle(trans._("Preferences")) self._settings = get_settings() self._stack = QStackedWidget(self) self._list = QListWidget(self) self._list.setObjectName("Preferences") self._list.currentRowChanged.connect(self._stack.setCurrentIndex) # Set up buttons self._button_cancel = QPushButton(trans._("Cancel")) self._button_cancel.clicked.connect(self.reject) self._button_ok = QPushButton(trans._("OK")) self._button_ok.clicked.connect(self.accept) self._button_ok.setDefault(True) self._button_restore = QPushButton(trans._("Restore defaults")) self._button_restore.clicked.connect(self._restore_default_dialog) # Layout left_layout = QVBoxLayout() left_layout.addWidget(self._list) left_layout.addStretch() left_layout.addWidget(self._button_restore) left_layout.addWidget(self._button_cancel) left_layout.addWidget(self._button_ok) self.setLayout(QHBoxLayout()) self.layout().addLayout(left_layout, 1) self.layout().addWidget(self._stack, 3) # Build dialog from settings self._rebuild_dialog() def keyPressEvent(self, e: 'QKeyEvent'): if e.key() == Qt.Key_Escape: # escape key should just close the window # which implies "accept" e.accept() self.accept() return super().keyPressEvent(e) def resizeEvent(self, event): """Override to emit signal.""" self.resized.emit(event.size()) super().resizeEvent(event) def _rebuild_dialog(self): """Removes settings not to be exposed to user and creates dialog pages.""" # FIXME: this dialog should not need to know about the plugin manager from ...plugins import plugin_manager self._starting_pm_order = plugin_manager.call_order() self._starting_values = self._settings.dict(exclude={'schema_version'}) self._list.clear() while self._stack.count(): self._stack.removeWidget(self._stack.currentWidget()) for field in self._settings.__fields__.values(): if isinstance(field.type_, type) and issubclass( field.type_, BaseModel): self._add_page(field) self._list.setCurrentRow(0) def _add_page(self, field: 'ModelField'): """Builds the preferences widget using the json schema builder. Parameters ---------- field : ModelField subfield for which to create a page. """ from ..._vendor.qt_json_builder.qt_jsonschema_form import WidgetBuilder schema, values = self._get_page_dict(field) name = field.field_info.title or field.name form = WidgetBuilder().create_form(schema, self.ui_schema) # set state values for widget form.widget.state = values # make settings follow state of the form widget form.widget.on_changed.connect( lambda d: getattr(self._settings, name.lower()).update(d)) # need to disable async if octree is enabled. # TODO: this shouldn't live here... if there is a coupling/dependency # between these settings, it should be declared in the settings schema if (name.lower() == 'experimental' and values['octree'] and self._settings.env_settings().get( 'experimental', {}).get('async_') not in (None, '0')): form_layout = form.widget.layout() for i in range(form_layout.count()): wdg = form_layout.itemAt(i, form_layout.FieldRole).widget() if getattr(wdg, '_name') == 'async_': wdg.opacity.setOpacity(0.3) wdg.setDisabled(True) break self._list.addItem(field.field_info.title or field.name) self._stack.addWidget(form) def _get_page_dict(self, field: 'ModelField') -> Tuple[dict, dict, dict]: """Provides the schema, set of values for each setting, and the properties for each setting.""" ftype = cast('BaseModel', field.type_) schema = json.loads(ftype.schema_json()) # find enums: for name, subfield in ftype.__fields__.items(): if isinstance(subfield.type_, EnumMeta): enums = [s.value for s in subfield.type_] # type: ignore schema["properties"][name]["enum"] = enums schema["properties"][name]["type"] = "string" # Need to remove certain properties that will not be displayed on the GUI setting = getattr(self._settings, field.name) with setting.enums_as_values(): values = setting.dict() napari_config = getattr(setting, "NapariConfig", None) if hasattr(napari_config, 'preferences_exclude'): for val in napari_config.preferences_exclude: schema['properties'].pop(val, None) values.pop(val, None) return schema, values def _restore_default_dialog(self): """Launches dialog to confirm restore settings choice.""" response = QMessageBox.question( self, trans._("Restore Settings"), trans._("Are you sure you want to restore default settings?"), QMessageBox.RestoreDefaults | QMessageBox.Cancel, QMessageBox.RestoreDefaults, ) if response == QMessageBox.RestoreDefaults: self._settings.reset() self._rebuild_dialog() # TODO: do we need this? def _restart_required_dialog(self): """Displays the dialog informing user a restart is required.""" QMessageBox.information( self, trans._("Restart required"), trans. _("A restart is required for some new settings to have an effect." ), ) def closeEvent(self, event: 'QCloseEvent') -> None: event.accept() self.accept() def reject(self): """Restores the settings in place when dialog was launched.""" self._settings.update(self._starting_values) # FIXME: this dialog should not need to know about the plugin manager if self._starting_pm_order: from ...plugins import plugin_manager plugin_manager.set_call_order(self._starting_pm_order) super().reject()
class MomentMapsGUI(QDialog): def __init__(self, data, data_collection, parent=None): super(MomentMapsGUI, 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.data = data self.data_collection = data_collection self.parent = parent self.label = '' self.calculateButton = None self.cancelButton = None def display(self): """ Create the popup box with the calculation input area and buttons. :return: """ self.setWindowFlags(self.windowFlags() | Qt.Tool) self.setWindowTitle("Create Moment Map") boldFont = QtGui.QFont() boldFont.setBold(True) # Create calculation 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) hbl1 = QHBoxLayout() hbl1.addWidget(self.data_label) hbl1.addWidget(self.data_combobox) # Create calculation label and input box self.order_label = QLabel("Order:") self.order_label.setFixedWidth(100) self.order_label.setAlignment((Qt.AlignRight | Qt.AlignTop)) self.order_label.setFont(boldFont) self.order_combobox = QComboBox() self.order_combobox.addItems(["1", "2", "3", "4", "5", "6", "7", "8"]) self.order_combobox.setMinimumWidth(200) hbl2 = QHBoxLayout() hbl2.addWidget(self.order_label) hbl2.addWidget(self.order_combobox) # 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(hbl2) vbl.addLayout(hbl5) self.setLayout(vbl) self.setMaximumWidth(700) self.show() def do_calculation(self, order, data_name): # Grab spectral-cube import spectral_cube cube = spectral_cube.SpectralCube(self.data[data_name], wcs=self.data.coords.wcs) cube_moment = cube.moment(order=order, axis=0) self.label = '{}-moment-{}'.format(data_name, order) # 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, cube_moment.value, cube_moment.unit, self.label) # Going to pass in just the value into the overlay as the units aren't # currently used for the overlay area. BUT, this is probably not the # best way to do this. self.parent.add_overlay(cube_moment.value, self.label, display_now=False) def calculate_callback(self): """ Callback for when they hit calculate :return: """ # Determine the data component and order order = int(self.order_combobox.currentText()) data_name = self.data_combobox.currentText() try: self.do_calculation(order, data_name) except Exception as e: show_error_message(str(e), 'Moment Map Error', parent=self) self.close() 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()