class Q7ComboBox(QComboBox): def __init__(self, arg): QComboBox.__init__(self, arg) self.actorlist = QListWidget() self.setModel(self.actorlist.model()) self.setView(self.actorlist) self.view().installEventFilter(self) self.parent = None def setParent(self, parent): self.parent = parent def eventFilter(self, o, e): if e.type() == QEvent.KeyPress: # kmod = e.modifiers() kval = e.key() if kval in [Qt.Key_Z]: path = self.actorlist.currentItem().text() actor = self.parent.findPathObject(path) self.parent.changeCurrentActor([path, actor]) return True if kval in [Qt.Key_H]: path = self.actorlist.currentItem().text() actor = self.parent.findPathObject(path) self.parent.changeCurrentActor([path, actor]) self.parent.hideActor(None) return True return QComboBox.eventFilter(self, o, e) def keyPressEvent(self, event): kmod = event.modifiers() kval = event.key() print(kmod, kval)
def refresh_profiles(self, list_widget: QListWidget, new_values: typing.List[str]): index = self.get_index(list_widget.currentItem(), new_values) list_widget.clear() list_widget.addItems(new_values) if index != -1: list_widget.setCurrentRow(index)
class FileAssociationsWidget(QWidget): """Widget to add applications association to file extensions.""" # This allows validating a single extension entry or a list of comma # separated values (eg `*.json` or `*.json,*.txt,MANIFEST.in`) _EXTENSIONS_LIST_REGEX = (r'(?:(?:\*{1,1}|\w+)\.\w+)' r'(?:,(?:\*{1,1}|\w+)\.\w+){0,20}') sig_data_changed = Signal(dict) def __init__(self, parent=None): """Widget to add applications association to file extensions.""" super(FileAssociationsWidget, self).__init__(parent=parent) # Variables self._data = {} self._dlg_applications = None self._dlg_input = None self._regex = re.compile(self._EXTENSIONS_LIST_REGEX) # Widgets self.label = QLabel( _('Here you can associate which external applications you want' 'to use to open specific file extensions <br> (e.g. .txt ' 'files with Notepad++ or .csv files with Excel).')) self.label_extensions = QLabel(_('File types:')) self.list_extensions = QListWidget() self.button_add = QPushButton(_('Add')) self.button_remove = QPushButton(_('Remove')) self.button_edit = QPushButton(_('Edit')) self.label_applications = QLabel(_('Associated applications:')) self.list_applications = QListWidget() self.button_add_application = QPushButton(_('Add')) self.button_remove_application = QPushButton(_('Remove')) self.button_default = QPushButton(_('Set default')) # Layout layout_extensions = QHBoxLayout() layout_extensions.addWidget(self.list_extensions, 4) layout_buttons_extensions = QVBoxLayout() layout_buttons_extensions.addWidget(self.button_add) layout_buttons_extensions.addWidget(self.button_remove) layout_buttons_extensions.addWidget(self.button_edit) layout_buttons_extensions.addStretch() layout_applications = QHBoxLayout() layout_applications.addWidget(self.list_applications, 4) layout_buttons_applications = QVBoxLayout() layout_buttons_applications.addWidget(self.button_add_application) layout_buttons_applications.addWidget(self.button_remove_application) layout_buttons_applications.addWidget(self.button_default) layout_buttons_applications.addStretch() layout_extensions.addLayout(layout_buttons_extensions, 2) layout_applications.addLayout(layout_buttons_applications, 2) layout = QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.label_extensions) layout.addLayout(layout_extensions) layout.addWidget(self.label_applications) layout.addLayout(layout_applications) self.setLayout(layout) # Signals self.button_add.clicked.connect(lambda: self.add_association()) self.button_remove.clicked.connect(self.remove_association) self.button_edit.clicked.connect(self.edit_association) self.button_add_application.clicked.connect(self.add_application) self.button_remove_application.clicked.connect(self.remove_application) self.button_default.clicked.connect(self.set_default_application) self.list_extensions.currentRowChanged.connect(self.update_extensions) self.list_extensions.itemDoubleClicked.connect(self.edit_association) self.list_applications.currentRowChanged.connect( self.update_applications) self._refresh() self._create_association_dialog() def _refresh(self): """Refresh the status of buttons on widget.""" self.setUpdatesEnabled(False) for widget in [ self.button_remove, self.button_add_application, self.button_edit, self.button_remove_application, self.button_default ]: widget.setDisabled(True) item = self.list_extensions.currentItem() if item: for widget in [ self.button_remove, self.button_add_application, self.button_remove_application, self.button_edit ]: widget.setDisabled(False) self.update_applications() self.setUpdatesEnabled(True) def _add_association(self, value): """Add association helper.""" # Check value is not pressent for row in range(self.list_extensions.count()): item = self.list_extensions.item(row) if item.text().strip() == value.strip(): break else: item = QListWidgetItem(value) self.list_extensions.addItem(item) self.list_extensions.setCurrentItem(item) self._refresh() def _add_application(self, app_name, fpath): """Add application helper.""" app_not_found_text = _(' (Application not found!)') for row in range(self.list_applications.count()): item = self.list_applications.item(row) # Ensure the actual name is checked without the `app not found` # additional text, in case app was not found item_text = item.text().replace(app_not_found_text, '').strip() if item and item_text == app_name: break else: icon = get_application_icon(fpath) if not (os.path.isfile(fpath) or os.path.isdir(fpath)): app_name += app_not_found_text item = QListWidgetItem(icon, app_name) self.list_applications.addItem(item) self.list_applications.setCurrentItem(item) if not (os.path.isfile(fpath) or os.path.isdir(fpath)): item.setToolTip(_('Application not found!')) def _update_extensions(self): """Update extensions list.""" self.list_extensions.clear() for extension, _ in sorted(self._data.items()): self._add_association(extension) # Select first item self.list_extensions.setCurrentRow(0) self.update_extensions() self.update_applications() def _create_association_dialog(self): """Create input extension dialog and save it to for reuse.""" self._dlg_input = InputTextDialog( self, title=_('File association'), label=(_('Enter new file extension. You can add several values ' 'separated by commas.<br>Examples include:') + '<ul><li><code>*.txt</code></li>' + '<li><code>*.json,*,csv</code></li>' + '<li><code>*.json,README.md</code></li></ul>'), ) self._dlg_input.set_regex_validation(self._EXTENSIONS_LIST_REGEX) def load_values(self, data=None): """ Load file associations data. Format {'*.ext': [['Application Name', '/path/to/app/executable']]} `/path/to/app/executable` is an executable app on mac and windows and a .desktop xdg file on linux. """ self._data = data self._update_extensions() def add_association(self, value=None): """Add extension file association.""" if value is None: text, ok_pressed = '', False self._dlg_input.set_text('') if self._dlg_input.exec_(): text = self._dlg_input.text() ok_pressed = True else: match = self._regex.match(value) text, ok_pressed = value, bool(match) if ok_pressed: if text not in self._data: self._data[text] = [] self._add_association(text) self.check_data_changed() def remove_association(self): """Remove extension file association.""" if self._data: if self.current_extension: self._data.pop(self.current_extension) self._update_extensions() self._refresh() self.check_data_changed() def edit_association(self): """Edit text of current selected association.""" old_text = self.current_extension self._dlg_input.set_text(old_text) if self._dlg_input.exec_(): new_text = self._dlg_input.text() if old_text != new_text: values = self._data.pop(self.current_extension) self._data[new_text] = values self._update_extensions() self._refresh() for row in range(self.list_extensions.count()): item = self.list_extensions.item(row) if item.text() == new_text: self.list_extensions.setCurrentItem(item) break self.check_data_changed() def add_application(self): """Remove application to selected extension.""" if self.current_extension: if self._dlg_applications is None: self._dlg_applications = ApplicationsDialog(self) self._dlg_applications.set_extension(self.current_extension) if self._dlg_applications.exec_(): app_name = self._dlg_applications.application_name fpath = self._dlg_applications.application_path self._data[self.current_extension].append((app_name, fpath)) self._add_application(app_name, fpath) self.check_data_changed() def remove_application(self): """Remove application from selected extension.""" current_row = self.list_applications.currentRow() values = self._data.get(self.current_extension) if values and current_row != -1: values.pop(current_row) self.update_extensions() self.update_applications() self.check_data_changed() def set_default_application(self): """ Set the selected item on the application list as default application. """ current_row = self.list_applications.currentRow() if current_row != -1: values = self._data[self.current_extension] value = values.pop(current_row) values.insert(0, value) self._data[self.current_extension] = values self.update_extensions() self.check_data_changed() def update_extensions(self, row=None): """Update extensiosn list after additions or deletions.""" self.list_applications.clear() for extension, values in self._data.items(): if extension.strip() == self.current_extension: for (app_name, fpath) in values: self._add_application(app_name, fpath) break self.list_applications.setCurrentRow(0) self._refresh() def update_applications(self, row=None): """Update application list after additions or deletions.""" current_row = self.list_applications.currentRow() self.button_default.setEnabled(current_row != 0) def check_data_changed(self): """Check if data has changed and emit signal as needed.""" self.sig_data_changed.emit(self._data) @property def current_extension(self): """Return the current selected extension text.""" item = self.list_extensions.currentItem() if item: return item.text() @property def data(self): """Return the current file associations data.""" return self._data.copy()
class PathManager(QDialog): redirect_stdio = Signal(bool) def __init__(self, parent=None, pathlist=None, ro_pathlist=None, not_active_pathlist=None, sync=True): 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) assert isinstance(pathlist, list) self.pathlist = pathlist if not_active_pathlist is None: not_active_pathlist = [] self.not_active_pathlist = not_active_pathlist if ro_pathlist is None: ro_pathlist = [] self.ro_pathlist = ro_pathlist self.last_path = getcwd() self.setWindowTitle(_("PYTHONPATH manager")) self.setWindowIcon(ima.icon('pythonpath')) self.resize(500, 300) self.selection_widgets = [] layout = QVBoxLayout() self.setLayout(layout) top_layout = QHBoxLayout() layout.addLayout(top_layout) self.toolbar_widgets1 = self.setup_top_toolbar(top_layout) self.listwidget = QListWidget(self) self.listwidget.currentRowChanged.connect(self.refresh) self.listwidget.itemChanged.connect(self.update_not_active_pathlist) layout.addWidget(self.listwidget) bottom_layout = QHBoxLayout() layout.addLayout(bottom_layout) self.sync_button = None self.toolbar_widgets2 = self.setup_bottom_toolbar(bottom_layout, sync) # Buttons configuration bbox = QDialogButtonBox(QDialogButtonBox.Close) bbox.rejected.connect(self.reject) bottom_layout.addWidget(bbox) self.update_list() self.refresh() @property def active_pathlist(self): return [path for path in self.pathlist if path not in self.not_active_pathlist] def _add_widgets_to_layout(self, layout, widgets): layout.setAlignment(Qt.AlignLeft) for widget in widgets: layout.addWidget(widget) def setup_top_toolbar(self, layout): toolbar = [] movetop_button = create_toolbutton(self, text=_("Move to top"), icon=ima.icon('2uparrow'), triggered=lambda: self.move_to(absolute=0), text_beside_icon=True) toolbar.append(movetop_button) moveup_button = create_toolbutton(self, text=_("Move up"), icon=ima.icon('1uparrow'), triggered=lambda: self.move_to(relative=-1), text_beside_icon=True) toolbar.append(moveup_button) movedown_button = create_toolbutton(self, text=_("Move down"), icon=ima.icon('1downarrow'), triggered=lambda: self.move_to(relative=1), text_beside_icon=True) toolbar.append(movedown_button) movebottom_button = create_toolbutton(self, text=_("Move to bottom"), icon=ima.icon('2downarrow'), triggered=lambda: self.move_to(absolute=1), text_beside_icon=True) toolbar.append(movebottom_button) self.selection_widgets.extend(toolbar) self._add_widgets_to_layout(layout, toolbar) return toolbar def setup_bottom_toolbar(self, layout, sync=True): toolbar = [] add_button = create_toolbutton(self, text=_('Add path'), icon=ima.icon('edit_add'), triggered=self.add_path, text_beside_icon=True) toolbar.append(add_button) remove_button = create_toolbutton(self, text=_('Remove path'), icon=ima.icon('edit_remove'), triggered=self.remove_path, text_beside_icon=True) toolbar.append(remove_button) self.selection_widgets.append(remove_button) self._add_widgets_to_layout(layout, toolbar) layout.addStretch(1) if os.name == 'nt' and sync: self.sync_button = create_toolbutton(self, text=_("Synchronize..."), icon=ima.icon('fileimport'), triggered=self.synchronize, tip=_("Synchronize Spyder's path list with PYTHONPATH " "environment variable"), text_beside_icon=True) layout.addWidget(self.sync_button) return toolbar @Slot() def synchronize(self): """ Synchronize Spyder's path list with PYTHONPATH environment variable Only apply to: current user, on Windows platforms """ answer = QMessageBox.question(self, _("Synchronize"), _("This will synchronize Spyder's path list with " "<b>PYTHONPATH</b> environment variable for current user, " "allowing you to run your Python modules outside Spyder " "without having to configure sys.path. " "<br>Do you want to clear contents of PYTHONPATH before " "adding Spyder's path list?"), QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) if answer == QMessageBox.Cancel: return elif answer == QMessageBox.Yes: remove = True else: remove = False from spyder.utils.environ import (get_user_env, set_user_env, listdict2envdict) env = get_user_env() if remove: ppath = self.active_pathlist+self.ro_pathlist else: ppath = env.get('PYTHONPATH', []) if not isinstance(ppath, list): ppath = [ppath] ppath = [path for path in ppath if path not in (self.active_pathlist+self.ro_pathlist)] ppath.extend(self.active_pathlist+self.ro_pathlist) env['PYTHONPATH'] = ppath set_user_env(listdict2envdict(env), parent=self) def get_path_list(self): """Return path list (does not include the read-only path list)""" return self.pathlist def update_not_active_pathlist(self, item): path = item.text() if bool(item.checkState()) is True: self.remove_from_not_active_pathlist(path) else: self.add_to_not_active_pathlist(path) def add_to_not_active_pathlist(self, path): if path not in self.not_active_pathlist: self.not_active_pathlist.append(path) def remove_from_not_active_pathlist(self, path): if path in self.not_active_pathlist: self.not_active_pathlist.remove(path) def update_list(self): """Update path list""" self.listwidget.clear() for name in self.pathlist+self.ro_pathlist: item = QListWidgetItem(name) item.setIcon(ima.icon('DirClosedIcon')) if name in self.ro_pathlist: item.setFlags(Qt.NoItemFlags | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) elif name in self.not_active_pathlist: item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Unchecked) else: item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) self.listwidget.addItem(item) self.refresh() def refresh(self, row=None): """Refresh widget""" for widget in self.selection_widgets: widget.setEnabled(self.listwidget.currentItem() is not None) not_empty = self.listwidget.count() > 0 if self.sync_button is not None: self.sync_button.setEnabled(not_empty) def move_to(self, absolute=None, relative=None): index = self.listwidget.currentRow() if absolute is not None: if absolute: new_index = len(self.pathlist)-1 else: new_index = 0 else: new_index = index + relative new_index = max(0, min(len(self.pathlist)-1, new_index)) path = self.pathlist.pop(index) self.pathlist.insert(new_index, path) self.update_list() self.listwidget.setCurrentRow(new_index) @Slot() def remove_path(self): answer = QMessageBox.warning(self, _("Remove path"), _("Do you really want to remove selected path?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: self.pathlist.pop(self.listwidget.currentRow()) self.remove_from_not_active_pathlist( self.listwidget.currentItem().text()) self.update_list() @Slot() def add_path(self): self.redirect_stdio.emit(False) directory = getexistingdirectory(self, _("Select directory"), self.last_path) self.redirect_stdio.emit(True) if directory: directory = osp.abspath(directory) self.last_path = directory if directory in self.pathlist: item = self.listwidget.findItems(directory, Qt.MatchExactly)[0] item.setCheckState(Qt.Checked) answer = QMessageBox.question(self, _("Add path"), _("This directory is already included in Spyder path " "list.<br>Do you want to move it to the top of " "the list?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: self.pathlist.remove(directory) else: return self.pathlist.insert(0, directory) self.update_list()
class MeasurementSettings(QWidget): """ :type settings: Settings """ def __init__(self, settings: PartSettings, parent=None): super().__init__(parent) self.chosen_element: Optional[MeasurementListWidgetItem] = None self.chosen_element_area: Optional[Tuple[AreaType, float]] = None self.settings = settings self.profile_list = QListWidget(self) self.profile_description = QTextEdit(self) self.profile_description.setReadOnly(True) self.profile_options = QListWidget() self.profile_options_chosen = QListWidget() self.measurement_area_choose = QEnumComboBox(enum_class=AreaType) self.per_component = QEnumComboBox(enum_class=PerComponent) self.power_num = QDoubleSpinBox() self.power_num.setDecimals(3) self.power_num.setRange(-100, 100) self.power_num.setValue(1) self.choose_butt = QPushButton("→", self) self.discard_butt = QPushButton("←", self) self.proportion_butt = QPushButton("Ratio", self) self.proportion_butt.setToolTip("Create proportion from two parameter") self.move_up = QPushButton("↑", self) self.move_down = QPushButton("↓", self) self.remove_button = QPushButton("Remove") self.save_butt = QPushButton("Save") self.save_butt.setToolTip( "Set name for set and choose at least one parameter") self.save_butt_with_name = QPushButton( "Save with custom parameters designation") self.save_butt_with_name.setToolTip( "Set name for set and choose at least one parameter") self.reset_butt = QPushButton("Clear") self.soft_reset_butt = QPushButton("Remove user parameters") self.profile_name = QLineEdit(self) self.delete_profile_butt = QPushButton("Delete ") self.export_profiles_butt = QPushButton("Export") self.import_profiles_butt = QPushButton("Import") self.edit_profile_butt = QPushButton("Edit") self.choose_butt.setDisabled(True) self.choose_butt.clicked.connect(self.choose_option) self.discard_butt.setDisabled(True) self.discard_butt.clicked.connect(self.discard_option) self.proportion_butt.setDisabled(True) self.proportion_butt.clicked.connect(self.proportion_action) self.save_butt.setDisabled(True) self.save_butt.clicked.connect(self.save_action) self.save_butt_with_name.setDisabled(True) self.save_butt_with_name.clicked.connect(self.named_save_action) self.profile_name.textChanged.connect(self.name_changed) self.move_down.setDisabled(True) self.move_down.clicked.connect(self.move_down_fun) self.move_up.setDisabled(True) self.move_up.clicked.connect(self.move_up_fun) self.remove_button.setDisabled(True) self.remove_button.clicked.connect(self.remove_element) self.reset_butt.clicked.connect(self.reset_action) self.soft_reset_butt.clicked.connect(self.soft_reset) self.delete_profile_butt.setDisabled(True) self.delete_profile_butt.clicked.connect(self.delete_profile) self.export_profiles_butt.clicked.connect( self.export_measurement_profiles) self.import_profiles_butt.clicked.connect( self.import_measurement_profiles) self.edit_profile_butt.clicked.connect(self.edit_profile) self.profile_list.itemSelectionChanged.connect(self.profile_chosen) self.profile_options.itemSelectionChanged.connect( self.create_selection_changed) self.profile_options_chosen.itemSelectionChanged.connect( self.create_selection_chosen_changed) self.settings.measurement_profiles_changed.connect( self._refresh_profiles) layout = QVBoxLayout() layout.addWidget(QLabel("Measurement set:")) profile_layout = QHBoxLayout() profile_layout.addWidget(self.profile_list) profile_layout.addWidget(self.profile_description) profile_buttons_layout = QHBoxLayout() profile_buttons_layout.addWidget(self.delete_profile_butt) profile_buttons_layout.addWidget(self.export_profiles_butt) profile_buttons_layout.addWidget(self.import_profiles_butt) profile_buttons_layout.addWidget(self.edit_profile_butt) profile_buttons_layout.addStretch() layout.addLayout(profile_layout) layout.addLayout(profile_buttons_layout) heading_layout = QHBoxLayout() # heading_layout.addWidget(QLabel("Create profile"), 1) heading_layout.addWidget(h_line(), 6) layout.addLayout(heading_layout) name_layout = QHBoxLayout() name_layout.addWidget(QLabel("Set name:")) name_layout.addWidget(self.profile_name) name_layout.addStretch() name_layout.addWidget(QLabel("Per component:")) name_layout.addWidget(self.per_component) name_layout.addWidget(QLabel("Area:")) name_layout.addWidget(self.measurement_area_choose) name_layout.addWidget(QLabel("to power:")) name_layout.addWidget(self.power_num) layout.addLayout(name_layout) create_layout = QHBoxLayout() create_layout.addWidget(self.profile_options) butt_op_layout = QVBoxLayout() butt_op_layout.addStretch() butt_op_layout.addWidget(self.choose_butt) butt_op_layout.addWidget(self.discard_butt) butt_op_layout.addWidget(self.proportion_butt) butt_op_layout.addWidget(self.reset_butt) butt_op_layout.addStretch() create_layout.addLayout(butt_op_layout) create_layout.addWidget(self.profile_options_chosen) butt_move_layout = QVBoxLayout() butt_move_layout.addStretch() butt_move_layout.addWidget(self.move_up) butt_move_layout.addWidget(self.move_down) butt_move_layout.addWidget(self.remove_button) butt_move_layout.addStretch() create_layout.addLayout(butt_move_layout) layout.addLayout(create_layout) save_butt_layout = QHBoxLayout() save_butt_layout.addWidget(self.soft_reset_butt) save_butt_layout.addStretch() save_butt_layout.addWidget(self.save_butt) save_butt_layout.addWidget(self.save_butt_with_name) layout.addLayout(save_butt_layout) self.setLayout(layout) for profile in MEASUREMENT_DICT.values(): help_text = profile.get_description() lw = MeasurementListWidgetItem(profile.get_starting_leaf()) lw.setToolTip(help_text) self.profile_options.addItem(lw) self._refresh_profiles() def _refresh_profiles(self): item = self.profile_list.currentItem() items = list(self.settings.measurement_profiles.keys()) try: index = items.index(item.text()) except (ValueError, AttributeError): index = -1 self.profile_list.clear() self.profile_list.addItems(items) self.profile_list.setCurrentRow(index) if self.profile_list.count() == 0: self.export_profiles_butt.setDisabled(True) else: self.export_profiles_butt.setEnabled(True) def remove_element(self): elem = self.profile_options_chosen.currentItem() if elem is None: return index = self.profile_options_chosen.currentRow() self.profile_options_chosen.takeItem(index) if self.profile_options_chosen.count() == 0: self.move_down.setDisabled(True) self.move_up.setDisabled(True) self.remove_button.setDisabled(True) self.discard_butt.setDisabled(True) self.save_butt.setDisabled(True) self.save_butt_with_name.setDisabled(True) def delete_profile(self): item = self.profile_list.currentItem() del self.settings.measurement_profiles[str(item.text())] def profile_chosen(self): self.delete_profile_butt.setEnabled(True) if self.profile_list.count() == 0: self.profile_description.setText("") return item = self.profile_list.currentItem() if item is None: self.profile_description.setText("") return profile = self.settings.measurement_profiles[item.text()] self.profile_description.setText(str(profile)) def create_selection_changed(self): self.choose_butt.setEnabled(True) self.proportion_butt.setEnabled(True) def proportion_action(self): # TODO use get_parameters if self.chosen_element is None: item = self.profile_options.currentItem() self.chosen_element_area = self.get_parameters( deepcopy(item.stat), self.measurement_area_choose.currentEnum(), self.per_component.currentEnum(), self.power_num.value(), ) if self.chosen_element_area is None: return self.chosen_element = item item.setIcon(QIcon(os.path.join(icons_dir, "task-accepted.png"))) elif (self.profile_options.currentItem() == self.chosen_element and self.measurement_area_choose.currentEnum() == self.chosen_element_area.area and self.per_component.currentEnum() == self.chosen_element_area.per_component): self.chosen_element.setIcon(QIcon()) self.chosen_element = None else: item: MeasurementListWidgetItem = self.profile_options.currentItem( ) leaf = self.get_parameters( deepcopy(item.stat), self.measurement_area_choose.currentEnum(), self.per_component.currentEnum(), self.power_num.value(), ) if leaf is None: return lw = MeasurementListWidgetItem( Node(op="/", left=self.chosen_element_area, right=leaf)) lw.setToolTip("User defined") self._add_option(lw) self.chosen_element.setIcon(QIcon()) self.chosen_element = None self.chosen_element_area = None def _add_option(self, item: MeasurementListWidgetItem): for i in range(self.profile_options_chosen.count()): if item.text() == self.profile_options_chosen.item(i).text(): return self.profile_options_chosen.addItem(item) if self.good_name(): self.save_butt.setEnabled(True) self.save_butt_with_name.setEnabled(True) if self.profile_options.count() == 0: self.choose_butt.setDisabled(True) def create_selection_chosen_changed(self): # print(self.profile_options_chosen.count()) self.remove_button.setEnabled(True) if self.profile_options_chosen.count() == 0: self.move_down.setDisabled(True) self.move_up.setDisabled(True) self.remove_button.setDisabled(True) return self.discard_butt.setEnabled(True) if self.profile_options_chosen.currentRow() != 0: self.move_up.setEnabled(True) else: self.move_up.setDisabled(True) if self.profile_options_chosen.currentRow( ) != self.profile_options_chosen.count() - 1: self.move_down.setEnabled(True) else: self.move_down.setDisabled(True) def good_name(self): return str(self.profile_name.text()).strip() != "" def move_down_fun(self): row = self.profile_options_chosen.currentRow() item = self.profile_options_chosen.takeItem(row) self.profile_options_chosen.insertItem(row + 1, item) self.profile_options_chosen.setCurrentRow(row + 1) self.create_selection_chosen_changed() def move_up_fun(self): row = self.profile_options_chosen.currentRow() item = self.profile_options_chosen.takeItem(row) self.profile_options_chosen.insertItem(row - 1, item) self.profile_options_chosen.setCurrentRow(row - 1) self.create_selection_chosen_changed() def name_changed(self): if self.good_name() and self.profile_options_chosen.count() > 0: self.save_butt.setEnabled(True) self.save_butt_with_name.setEnabled(True) else: self.save_butt.setDisabled(True) self.save_butt_with_name.setDisabled(True) def form_dialog(self, arguments): return FormDialog(arguments, settings=self.settings, parent=self) def get_parameters(self, node: Union[Node, Leaf], area: AreaType, component: PerComponent, power: float): if isinstance(node, Node): return node node = node.replace_(power=power) if node.area is None: node = node.replace_(area=area) if node.per_component is None: node = node.replace_(per_component=component) with suppress(KeyError): arguments = MEASUREMENT_DICT[str(node.name)].get_fields() if len(arguments) > 0 and len(node.dict) == 0: dial = self.form_dialog(arguments) if dial.exec_(): node = node._replace(dict=dial.get_values()) else: return return node def choose_option(self): selected_item = self.profile_options.currentItem() # selected_row = self.profile_options.currentRow() if not isinstance(selected_item, MeasurementListWidgetItem): raise ValueError( f"Current item (type: {type(selected_item)} is not instance of MeasurementListWidgetItem" ) node = deepcopy(selected_item.stat) # noinspection PyTypeChecker node = self.get_parameters(node, self.measurement_area_choose.currentEnum(), self.per_component.currentEnum(), self.power_num.value()) if node is None: return lw = MeasurementListWidgetItem(node) lw.setToolTip(selected_item.toolTip()) self._add_option(lw) def discard_option(self): selected_item: MeasurementListWidgetItem = self.profile_options_chosen.currentItem( ) # selected_row = self.profile_options_chosen.currentRow() lw = MeasurementListWidgetItem(deepcopy(selected_item.stat)) lw.setToolTip(selected_item.toolTip()) self.create_selection_chosen_changed() for i in range(self.profile_options.count()): if lw.text() == self.profile_options.item(i).text(): return self.profile_options.addItem(lw) def edit_profile(self): item = self.profile_list.currentItem() if item is None: return profile = self.settings.measurement_profiles[str(item.text())] self.profile_options_chosen.clear() self.profile_name.setText(item.text()) for ch in profile.chosen_fields: self.profile_options_chosen.addItem( MeasurementListWidgetItem(ch.calculation_tree)) # self.gauss_img.setChecked(profile.use_gauss_image) self.save_butt.setEnabled(True) self.save_butt_with_name.setEnabled(True) def save_action(self): if self.profile_name.text() in self.settings.measurement_profiles: ret = QMessageBox.warning( self, "Profile exist", "Profile exist\nWould you like to overwrite it?", QMessageBox.No | QMessageBox.Yes, ) if ret == QMessageBox.No: return selected_values = [] for i in range(self.profile_options_chosen.count()): element: MeasurementListWidgetItem = self.profile_options_chosen.item( i) selected_values.append( MeasurementEntry(element.text(), element.stat)) stat_prof = MeasurementProfile(self.profile_name.text(), selected_values) self.settings.measurement_profiles[stat_prof.name] = stat_prof self.settings.dump() self.export_profiles_butt.setEnabled(True) def named_save_action(self): if self.profile_name.text() in self.settings.measurement_profiles: ret = QMessageBox.warning( self, "Profile exist", "Profile exist\nWould you like to overwrite it?", QMessageBox.No | QMessageBox.Yes, ) if ret == QMessageBox.No: return selected_values = [] for i in range(self.profile_options_chosen.count()): txt = str(self.profile_options_chosen.item(i).text()) selected_values.append((txt, str, txt)) val_dialog = MultipleInput("Set fields name", list(selected_values), parent=self) if val_dialog.exec_(): selected_values = [] for i in range(self.profile_options_chosen.count()): element: MeasurementListWidgetItem = self.profile_options_chosen.item( i) selected_values.append( MeasurementEntry(val_dialog.result[element.text()], element.stat)) stat_prof = MeasurementProfile(self.profile_name.text(), selected_values) self.settings.measurement_profiles[stat_prof.name] = stat_prof self.export_profiles_butt.setEnabled(True) def reset_action(self): self.profile_options.clear() self.profile_options_chosen.clear() self.profile_name.setText("") self.save_butt.setDisabled(True) self.save_butt_with_name.setDisabled(True) self.move_down.setDisabled(True) self.move_up.setDisabled(True) self.proportion_butt.setDisabled(True) self.choose_butt.setDisabled(True) self.discard_butt.setDisabled(True) for profile in MEASUREMENT_DICT.values(): help_text = profile.get_description() lw = MeasurementListWidgetItem(profile.get_starting_leaf()) lw.setToolTip(help_text) self.profile_options.addItem(lw) def soft_reset(self): # TODO rim should not be removed shift = 0 for i in range(self.profile_options.count()): item = self.profile_options.item(i - shift) if str(item.text()) not in MEASUREMENT_DICT: self.profile_options.takeItem(i - shift) if item == self.chosen_element: self.chosen_element = None del item shift += 1 self.create_selection_changed() def export_measurement_profiles(self): exp = ExportDialog(self.settings.measurement_profiles, StringViewer, parent=self) if not exp.exec_(): return dial = PSaveDialog( "Measurement profile (*.json)", settings=self.settings, path="io.export_directory", caption="Export settings profiles", ) dial.selectFile("measurements_profile.json") if dial.exec_(): file_path = str(dial.selectedFiles()[0]) data = { x: self.settings.measurement_profiles[x] for x in exp.get_export_list() } with open(file_path, "w", encoding="utf-8") as ff: json.dump(data, ff, cls=self.settings.json_encoder_class, indent=2) def import_measurement_profiles(self): dial = PLoadDialog( "Measurement profile (*.json)", settings=self.settings, path="io.export_directory", caption="Import settings profiles", parent=self, ) if dial.exec_(): file_path = str(dial.selectedFiles()[0]) stat, err = self.settings.load_part(file_path) if err: QMessageBox.warning( self, "Import error", "error during importing, part of data were filtered.") measurement_dict = self.settings.measurement_profiles imp = ImportDialog(stat, measurement_dict, StringViewer) if not imp.exec_(): return for original_name, final_name in imp.get_import_list(): measurement_dict[final_name] = stat[original_name] self.settings.dump()
class PMGListCtrl(BaseExtendedWidget): def __init__(self, layout_dir: str, title: str, initial_value: List[List[str]], new_id_func: Callable = None): super().__init__(layout_dir) self.choices = [] self.text_list = [] lab_title = QLabel(text=title) layout = QHBoxLayout() self.central_layout.addWidget(lab_title) self.on_check_callback = None self.list_widget = QListWidget() self.list_widget.mouseDoubleClickEvent = self.on_listwidget_double_cicked self.set_value(initial_value) layout_tools = QVBoxLayout() self.button_add_item = QPushButton('+') self.button_delete_item = QPushButton('-') self.button_delete_item.clicked.connect(self.delete_row) self.button_add_item.clicked.connect(self.add_row) self.button_add_item.setMaximumWidth(20) self.button_delete_item.setMaximumWidth(20) layout_tools.addWidget(self.button_add_item) layout_tools.addWidget(self.button_delete_item) layout.addLayout(layout_tools) layout.addWidget(self.list_widget) self.central_layout.addLayout(layout) self.data = initial_value self.new_id_func = new_id_func self.text_edit = QLineEdit(parent=self.list_widget) self.text_edit.setWindowFlags(self.text_edit.windowFlags() | Qt.Dialog | Qt.FramelessWindowHint) self.text_edit.hide() self.completer = QCompleter() self.text_edit.setCompleter(self.completer) self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) def set_completions(self, completions: List[str]): """ 设置补全内容 Args: completions: Returns: """ self.completer.setModel(QStringListModel(completions)) def new_id(self): if callable(self.new_id_func): return self.new_id_func() else: return None def add_row(self): self.data = self.get_value() self.data[0].append(self.new_id()) self.data[1].append('Unnamed') self.list_widget.addItem(QListWidgetItem('Unnamed')) def delete_row(self): index = self.list_widget.currentIndex().row() self.data[0].pop(index) self.data[1].pop(index) self.list_widget.takeItem(index) def on_listwidget_double_cicked(self, evt: QMouseEvent): print('edit', evt) pos = evt.globalPos() current_item: QListWidgetItem = self.list_widget.currentItem() def set_value(): current_item.setText(self.text_edit.text()) self.text_edit.hide() self.text_edit.returnPressed.disconnect(set_value) item: QListWidgetItem = self.list_widget.currentItem() self.text_edit.setGeometry(pos.x(), pos.y(), 200, 20) self.text_edit.returnPressed.connect(set_value) self.text_edit.show() # self.list_widget.editItem(item) def get_value(self): text = [] for i in range(self.list_widget.count()): text.append(self.list_widget.item(i).text()) self.data[1] = text assert len(self.data[1]) == len(self.data[0]), repr(self.data) return self.data def set_value(self, data: List[List[str]]): self.list_widget.clear() self.list_widget.addItems(data[1]) self.data = data for index in range(self.list_widget.count()): item: QListWidgetItem = self.list_widget.item(index)
class FileSwitcher(QDialog): """A Sublime-like file switcher.""" sig_goto_file = Signal(int, object) # Constants that define the mode in which the list widget is working # FILE_MODE is for a list of files, SYMBOL_MODE if for a list of symbols # in a given file when using the '@' symbol. FILE_MODE, SYMBOL_MODE = [1, 2] MAX_WIDTH = 600 def __init__(self, parent, plugin, tabs, data, icon): QDialog.__init__(self, parent) # Variables self.plugins_tabs = [] self.plugins_data = [] self.plugins_instances = [] self.add_plugin(plugin, tabs, data, icon) self.plugin = None # Last plugin with focus self.mode = self.FILE_MODE # By default start in this mode self.initial_cursors = None # {fullpath: QCursor} self.initial_path = None # Fullpath of initial active editor self.initial_widget = None # Initial active editor self.line_number = None # Selected line number in filer self.is_visible = False # Is the switcher visible? help_text = _("Press <b>Enter</b> to switch files or <b>Esc</b> to " "cancel.<br><br>Type to filter filenames.<br><br>" "Use <b>:number</b> to go to a line, e.g. " "<b><code>main:42</code></b><br>" "Use <b>@symbol_text</b> to go to a symbol, e.g. " "<b><code>@init</code></b>" "<br><br> Press <b>Ctrl+W</b> to close current tab.<br>") # Either allow searching for a line number or a symbol but not both regex = QRegExp("([A-Za-z0-9_]{0,100}@[A-Za-z0-9_]{0,100})|" + "([A-Za-z0-9_]{0,100}:{0,1}[0-9]{0,100})") # Widgets self.edit = FilesFilterLine(self) self.help = HelperToolButton() self.list = QListWidget(self) self.filter = KeyPressFilter() regex_validator = QRegExpValidator(regex, self.edit) # Widgets setup self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint) self.setWindowOpacity(0.95) self.edit.installEventFilter(self.filter) self.edit.setValidator(regex_validator) self.help.setToolTip(help_text) self.list.setItemDelegate(HTMLDelegate(self)) # Layout edit_layout = QHBoxLayout() edit_layout.addWidget(self.edit) edit_layout.addWidget(self.help) layout = QVBoxLayout() layout.addLayout(edit_layout) layout.addWidget(self.list) self.setLayout(layout) # Signals self.rejected.connect(self.restore_initial_state) self.filter.sig_up_key_pressed.connect(self.previous_row) self.filter.sig_down_key_pressed.connect(self.next_row) self.edit.returnPressed.connect(self.accept) self.edit.textChanged.connect(self.setup) self.list.itemSelectionChanged.connect(self.item_selection_changed) self.list.clicked.connect(self.edit.setFocus) # --- Properties @property def widgets(self): widgets = [] for tabs, plugin in self.plugins_tabs: widgets += [(tabs.widget(index), plugin) for index in range(tabs.count())] return widgets @property def line_count(self): line_count = [] for widget in self.widgets: try: current_line_count = widget[0].get_line_count() except AttributeError: current_line_count = 0 line_count.append(current_line_count) return line_count @property def save_status(self): save_status = [] for da, icon in self.plugins_data: save_status += [getattr(td, 'newly_created', False) for td in da] return save_status @property def paths(self): paths = [] for da, icon in self.plugins_data: paths += [getattr(td, 'filename', None) for td in da] return paths @property def filenames(self): filenames = [] for da, icon in self.plugins_data: filenames += [os.path.basename(getattr(td, 'filename', None)) for td in da] return filenames @property def icons(self): icons = [] for da, icon in self.plugins_data: icons += [icon for td in da] return icons @property def current_path(self): return self.paths_by_widget[self.get_widget()] @property def paths_by_widget(self): widgets = [w[0] for w in self.widgets] return dict(zip(widgets, self.paths)) @property def widgets_by_path(self): widgets = [w[0] for w in self.widgets] return dict(zip(self.paths, widgets)) @property def filter_text(self): """Get the normalized (lowecase) content of the filter text.""" return to_text_string(self.edit.text()).lower() def set_search_text(self, _str): self.edit.setText(_str) def save_initial_state(self): """Save initial cursors and initial active widget.""" paths = self.paths self.initial_widget = self.get_widget() self.initial_cursors = {} for i, editor in enumerate(self.widgets): if editor is self.initial_widget: self.initial_path = paths[i] # This try is needed to make the fileswitcher work with # plugins that does not have a textCursor. try: self.initial_cursors[paths[i]] = editor.textCursor() except AttributeError: pass def accept(self): self.is_visible = False QDialog.accept(self) self.list.clear() def restore_initial_state(self): """Restores initial cursors and initial active editor.""" self.list.clear() self.is_visible = False widgets = self.widgets_by_path if not self.edit.clicked_outside: for path in self.initial_cursors: cursor = self.initial_cursors[path] if path in widgets: self.set_editor_cursor(widgets[path], cursor) if self.initial_widget in self.paths_by_widget: index = self.paths.index(self.initial_path) self.sig_goto_file.emit(index) def set_dialog_position(self): """Positions the file switcher dialog.""" parent = self.parent() geo = parent.geometry() width = self.list.width() # This has been set in setup left = parent.geometry().width()/2 - width/2 # Note: the +1 pixel on the top makes it look better if isinstance(parent, QMainWindow): top = (parent.toolbars_menu.geometry().height() + parent.menuBar().geometry().height() + 1) else: top = self.plugins_tabs[0][0].tabBar().geometry().height() + 1 while parent: geo = parent.geometry() top += geo.top() left += geo.left() parent = parent.parent() self.move(left, top) def get_item_size(self, content): """ Get the max size (width and height) for the elements of a list of strings as a QLabel. """ strings = [] if content: for rich_text in content: label = QLabel(rich_text) label.setTextFormat(Qt.PlainText) strings.append(label.text()) fm = label.fontMetrics() return (max([fm.width(s) * 1.3 for s in strings]), fm.height()) def fix_size(self, content): """ Adjusts the width and height of the file switcher based on the relative size of the parent and content. """ # Update size of dialog based on relative size of the parent if content: width, height = self.get_item_size(content) # Width parent = self.parent() relative_width = parent.geometry().width() * 0.65 if relative_width > self.MAX_WIDTH: relative_width = self.MAX_WIDTH self.list.setMinimumWidth(relative_width) # Height if len(content) < 8: max_entries = len(content) else: max_entries = 8 max_height = height * max_entries * 2.5 self.list.setMinimumHeight(max_height) # Resize self.list.resize(relative_width, self.list.height()) # --- Helper methods: List widget def count(self): """Gets the item count in the list widget.""" return self.list.count() def current_row(self): """Returns the current selected row in the list widget.""" return self.list.currentRow() def set_current_row(self, row): """Sets the current selected row in the list widget.""" return self.list.setCurrentRow(row) def select_row(self, steps): """Select row in list widget based on a number of steps with direction. Steps can be positive (next rows) or negative (previous rows). """ row = self.current_row() + steps if 0 <= row < self.count(): self.set_current_row(row) def previous_row(self): """Select previous row in list widget.""" if self.mode == self.SYMBOL_MODE: self.select_row(-1) return prev_row = self.current_row() - 1 if prev_row >= 0: title = self.list.item(prev_row).text() else: title = '' if prev_row == 0 and '</b></big><br>' in title: self.list.scrollToTop() elif '</b></big><br>' in title: # Select the next previous row, the one following is a title self.select_row(-2) else: self.select_row(-1) def next_row(self): """Select next row in list widget.""" if self.mode == self.SYMBOL_MODE: self.select_row(+1) return next_row = self.current_row() + 1 if next_row < self.count(): if '</b></big><br>' in self.list.item(next_row).text(): # Select the next next row, the one following is a title self.select_row(+2) else: self.select_row(+1) def get_stack_index(self, stack_index, plugin_index): """Get the real index of the selected item.""" other_plugins_count = sum([other_tabs[0].count() \ for other_tabs in \ self.plugins_tabs[:plugin_index]]) real_index = stack_index - other_plugins_count return real_index # --- Helper methods: Widget def get_widget(self, index=None, path=None, tabs=None): """Get widget by index. If no tabs and index specified the current active widget is returned. """ if index and tabs: return tabs.widget(index) elif path and tabs: return tabs.widget(index) elif self.plugin: index = self.plugins_instances.index(self.plugin) return self.plugins_tabs[index][0].currentWidget() else: return self.plugins_tabs[0][0].currentWidget() def set_editor_cursor(self, editor, cursor): """Set the cursor of an editor.""" pos = cursor.position() anchor = cursor.anchor() new_cursor = QTextCursor() if pos == anchor: new_cursor.movePosition(pos) else: new_cursor.movePosition(anchor) new_cursor.movePosition(pos, QTextCursor.KeepAnchor) editor.setTextCursor(cursor) def goto_line(self, line_number): """Go to specified line number in current active editor.""" if line_number: line_number = int(line_number) editor = self.get_widget() try: editor.go_to_line(min(line_number, editor.get_line_count())) except AttributeError: pass # --- Helper methods: Outline explorer def get_symbol_list(self): """Get the list of symbols present in the file.""" try: oedata = self.get_widget().get_outlineexplorer_data() except AttributeError: oedata = {} return oedata # --- Handlers def item_selection_changed(self): """List widget item selection change handler.""" row = self.current_row() if self.count() and row >= 0: if '</b></big><br>' in self.list.currentItem().text() and row == 0: self.next_row() if self.mode == self.FILE_MODE: try: stack_index = self.paths.index(self.filtered_path[row]) self.plugin = self.widgets[stack_index][1] plugin_index = self.plugins_instances.index(self.plugin) # Count the real index in the tabWidget of the # current plugin real_index = self.get_stack_index(stack_index, plugin_index) self.sig_goto_file.emit(real_index, self.plugin.get_current_tab_manager()) self.goto_line(self.line_number) try: self.plugin.switch_to_plugin() self.raise_() except AttributeError: # The widget using the fileswitcher is not a plugin pass self.edit.setFocus() except ValueError: pass else: line_number = self.filtered_symbol_lines[row] self.goto_line(line_number) def setup_file_list(self, filter_text, current_path): """Setup list widget content for file list display.""" short_paths = shorten_paths(self.paths, self.save_status) paths = self.paths icons = self.icons results = [] trying_for_line_number = ':' in filter_text # Get optional line number if trying_for_line_number: filter_text, line_number = filter_text.split(':') # Get all the available filenames scores = get_search_scores('', self.filenames, template="<b>{0}</b>") else: line_number = None # Get all available filenames and get the scores for # "fuzzy" matching scores = get_search_scores(filter_text, self.filenames, template="<b>{0}</b>") # Get max width to determine if shortpaths should be used max_width = self.get_item_size(paths)[0] self.fix_size(paths) # Build the text that will appear on the list widget for index, score in enumerate(scores): text, rich_text, score_value = score if score_value != -1: text_item = '<big>' + rich_text.replace('&', '') + '</big>' if trying_for_line_number: text_item += " [{0:} {1:}]".format(self.line_count[index], _("lines")) if max_width > self.list.width(): text_item += u"<br><i>{0:}</i>".format(short_paths[index]) else: text_item += u"<br><i>{0:}</i>".format(paths[index]) if (trying_for_line_number and self.line_count[index] != 0 or not trying_for_line_number): results.append((score_value, index, text_item)) # Sort the obtained scores and populate the list widget self.filtered_path = [] plugin = None for result in sorted(results): index = result[1] path = paths[index] icon = icons[index] text = '' try: title = self.widgets[index][1].get_plugin_title().split(' - ') if plugin != title[0]: plugin = title[0] text += '<br><big><b>' + plugin + '</b></big><br>' item = QListWidgetItem(text) item.setToolTip(path) item.setSizeHint(QSize(0, 25)) item.setFlags(Qt.ItemIsEditable) self.list.addItem(item) self.filtered_path.append(path) except: # The widget using the fileswitcher is not a plugin pass text = '' text += result[-1] item = QListWidgetItem(icon, text) item.setToolTip(path) item.setSizeHint(QSize(0, 25)) self.list.addItem(item) self.filtered_path.append(path) # To adjust the delegate layout for KDE themes self.list.files_list = True # Move selected item in list accordingly and update list size if current_path in self.filtered_path: self.set_current_row(self.filtered_path.index(current_path)) elif self.filtered_path: self.set_current_row(0) # If a line number is searched look for it self.line_number = line_number self.goto_line(line_number) def setup_symbol_list(self, filter_text, current_path): """Setup list widget content for symbol list display.""" # Get optional symbol name filter_text, symbol_text = filter_text.split('@') # Fetch the Outline explorer data, get the icons and values oedata = self.get_symbol_list() icons = get_python_symbol_icons(oedata) # The list of paths here is needed in order to have the same # point of measurement for the list widget size as in the file list # See issue 4648 paths = self.paths # Update list size self.fix_size(paths) symbol_list = process_python_symbol_data(oedata) line_fold_token = [(item[0], item[2], item[3]) for item in symbol_list] choices = [item[1] for item in symbol_list] scores = get_search_scores(symbol_text, choices, template="<b>{0}</b>") # Build the text that will appear on the list widget results = [] lines = [] self.filtered_symbol_lines = [] for index, score in enumerate(scores): text, rich_text, score_value = score line, fold_level, token = line_fold_token[index] lines.append(text) if score_value != -1: results.append((score_value, line, text, rich_text, fold_level, icons[index], token)) template = '{0}{1}' for (score, line, text, rich_text, fold_level, icon, token) in sorted(results): fold_space = ' '*(fold_level) line_number = line + 1 self.filtered_symbol_lines.append(line_number) textline = template.format(fold_space, rich_text) item = QListWidgetItem(icon, textline) item.setSizeHint(QSize(0, 16)) self.list.addItem(item) # To adjust the delegate layout for KDE themes self.list.files_list = False # Move selected item in list accordingly # NOTE: Doing this is causing two problems: # 1. It makes the cursor to auto-jump to the last selected # symbol after opening or closing a different file # 2. It moves the cursor to the first symbol by default, # which is very distracting. # That's why this line is commented! # self.set_current_row(0) def setup(self): """Setup list widget content.""" if len(self.plugins_tabs) == 0: self.close() return self.list.clear() current_path = self.current_path filter_text = self.filter_text # Get optional line or symbol to define mode and method handler trying_for_symbol = ('@' in self.filter_text) if trying_for_symbol: self.mode = self.SYMBOL_MODE self.setup_symbol_list(filter_text, current_path) else: self.mode = self.FILE_MODE self.setup_file_list(filter_text, current_path) # Set position according to size self.set_dialog_position() def add_plugin(self, plugin, tabs, data, icon): """Add a plugin to display its files.""" self.plugins_tabs.append((tabs, plugin)) self.plugins_data.append((data, icon)) self.plugins_instances.append(plugin)
class FileSwitcher(QDialog): """A Sublime-like file switcher.""" sig_goto_file = Signal(int, object) # Constants that define the mode in which the list widget is working # FILE_MODE is for a list of files, SYMBOL_MODE if for a list of symbols # in a given file when using the '@' symbol. FILE_MODE, SYMBOL_MODE = [1, 2] MAX_WIDTH = 600 def __init__(self, parent, plugin, tabs, data, icon): QDialog.__init__(self, parent) # Variables self.plugins_tabs = [] self.plugins_data = [] self.plugins_instances = [] self.add_plugin(plugin, tabs, data, icon) self.plugin = None # Last plugin with focus self.mode = self.FILE_MODE # By default start in this mode self.initial_cursors = None # {fullpath: QCursor} self.initial_path = None # Fullpath of initial active editor self.initial_widget = None # Initial active editor self.line_number = None # Selected line number in filer self.is_visible = False # Is the switcher visible? help_text = _("Press <b>Enter</b> to switch files or <b>Esc</b> to " "cancel.<br><br>Type to filter filenames.<br><br>" "Use <b>:number</b> to go to a line, e.g. " "<b><code>main:42</code></b><br>" "Use <b>@symbol_text</b> to go to a symbol, e.g. " "<b><code>@init</code></b>" "<br><br> Press <b>Ctrl+W</b> to close current tab.<br>") # Either allow searching for a line number or a symbol but not both regex = QRegExp("([A-Za-z0-9_]{0,100}@[A-Za-z0-9_]{0,100})|" + "([A-Za-z0-9_]{0,100}:{0,1}[0-9]{0,100})") # Widgets self.edit = FilesFilterLine(self) self.help = HelperToolButton() self.list = QListWidget(self) self.filter = KeyPressFilter() regex_validator = QRegExpValidator(regex, self.edit) # Widgets setup self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint) self.setWindowOpacity(0.95) self.edit.installEventFilter(self.filter) self.edit.setValidator(regex_validator) self.help.setToolTip(help_text) self.list.setItemDelegate(HTMLDelegate(self)) # Layout edit_layout = QHBoxLayout() edit_layout.addWidget(self.edit) edit_layout.addWidget(self.help) layout = QVBoxLayout() layout.addLayout(edit_layout) layout.addWidget(self.list) self.setLayout(layout) # Signals self.rejected.connect(self.restore_initial_state) self.filter.sig_up_key_pressed.connect(self.previous_row) self.filter.sig_down_key_pressed.connect(self.next_row) self.edit.returnPressed.connect(self.accept) self.edit.textChanged.connect(self.setup) self.list.itemSelectionChanged.connect(self.item_selection_changed) self.list.clicked.connect(self.edit.setFocus) # --- Properties @property def widgets(self): widgets = [] for plugin in self.plugins_instances: tabs = self.get_plugin_tabwidget(plugin) widgets += [(tabs.widget(index), plugin) for index in range(tabs.count())] return widgets @property def line_count(self): line_count = [] for widget in self.widgets: try: current_line_count = widget[0].get_line_count() except AttributeError: current_line_count = 0 line_count.append(current_line_count) return line_count @property def save_status(self): save_status = [] for da, icon in self.plugins_data: save_status += [getattr(td, 'newly_created', False) for td in da] return save_status @property def paths(self): paths = [] for plugin in self.plugins_instances: da = self.get_plugin_data(plugin) paths += [getattr(td, 'filename', None) for td in da] return paths @property def filenames(self): filenames = [] for plugin in self.plugins_instances: da = self.get_plugin_data(plugin) filenames += [ os.path.basename(getattr(td, 'filename', None)) for td in da ] return filenames @property def icons(self): icons = [] for da, icon in self.plugins_data: icons += [icon for td in da] return icons @property def current_path(self): return self.paths_by_widget[self.get_widget()] @property def paths_by_widget(self): widgets = [w[0] for w in self.widgets] return dict(zip(widgets, self.paths)) @property def widgets_by_path(self): widgets = [w[0] for w in self.widgets] return dict(zip(self.paths, widgets)) @property def filter_text(self): """Get the normalized (lowecase) content of the filter text.""" return to_text_string(self.edit.text()).lower() def set_search_text(self, _str): self.edit.setText(_str) def save_initial_state(self): """Save initial cursors and initial active widget.""" paths = self.paths self.initial_widget = self.get_widget() self.initial_cursors = {} for i, editor in enumerate(self.widgets): if editor is self.initial_widget: self.initial_path = paths[i] # This try is needed to make the fileswitcher work with # plugins that does not have a textCursor. try: self.initial_cursors[paths[i]] = editor.textCursor() except AttributeError: pass def accept(self): self.is_visible = False QDialog.accept(self) self.list.clear() def restore_initial_state(self): """Restores initial cursors and initial active editor.""" self.list.clear() self.is_visible = False widgets = self.widgets_by_path if not self.edit.clicked_outside: for path in self.initial_cursors: cursor = self.initial_cursors[path] if path in widgets: self.set_editor_cursor(widgets[path], cursor) if self.initial_widget in self.paths_by_widget: index = self.paths.index(self.initial_path) self.sig_goto_file.emit(index) def set_dialog_position(self): """Positions the file switcher dialog.""" parent = self.parent() geo = parent.geometry() width = self.list.width() # This has been set in setup left = parent.geometry().width() / 2 - width / 2 # Note: the +1 pixel on the top makes it look better if isinstance(parent, QMainWindow): top = (parent.toolbars_menu.geometry().height() + parent.menuBar().geometry().height() + 1) else: top = self.plugins_tabs[0][0].tabBar().geometry().height() + 1 while parent: geo = parent.geometry() top += geo.top() left += geo.left() parent = parent.parent() self.move(left, top) def get_item_size(self, content): """ Get the max size (width and height) for the elements of a list of strings as a QLabel. """ strings = [] if content: for rich_text in content: label = QLabel(rich_text) label.setTextFormat(Qt.PlainText) strings.append(label.text()) fm = label.fontMetrics() return (max([fm.width(s) * 1.3 for s in strings]), fm.height()) def fix_size(self, content): """ Adjusts the width and height of the file switcher based on the relative size of the parent and content. """ # Update size of dialog based on relative size of the parent if content: width, height = self.get_item_size(content) # Width parent = self.parent() relative_width = parent.geometry().width() * 0.65 if relative_width > self.MAX_WIDTH: relative_width = self.MAX_WIDTH self.list.setMinimumWidth(relative_width) # Height if len(content) < 8: max_entries = len(content) else: max_entries = 8 max_height = height * max_entries * 2.5 self.list.setMinimumHeight(max_height) # Resize self.list.resize(relative_width, self.list.height()) # --- Helper methods: List widget def count(self): """Gets the item count in the list widget.""" return self.list.count() def current_row(self): """Returns the current selected row in the list widget.""" return self.list.currentRow() def set_current_row(self, row): """Sets the current selected row in the list widget.""" return self.list.setCurrentRow(row) def select_row(self, steps): """Select row in list widget based on a number of steps with direction. Steps can be positive (next rows) or negative (previous rows). """ row = self.current_row() + steps if 0 <= row < self.count(): self.set_current_row(row) def previous_row(self): """Select previous row in list widget.""" if self.mode == self.SYMBOL_MODE: self.select_row(-1) return prev_row = self.current_row() - 1 if prev_row >= 0: title = self.list.item(prev_row).text() else: title = '' if prev_row == 0 and '</b></big><br>' in title: self.list.scrollToTop() elif '</b></big><br>' in title: # Select the next previous row, the one following is a title self.select_row(-2) else: self.select_row(-1) def next_row(self): """Select next row in list widget.""" if self.mode == self.SYMBOL_MODE: self.select_row(+1) return next_row = self.current_row() + 1 if next_row < self.count(): if '</b></big><br>' in self.list.item(next_row).text(): # Select the next next row, the one following is a title self.select_row(+2) else: self.select_row(+1) def get_stack_index(self, stack_index, plugin_index): """Get the real index of the selected item.""" other_plugins_count = sum([other_tabs[0].count() \ for other_tabs in \ self.plugins_tabs[:plugin_index]]) real_index = stack_index - other_plugins_count return real_index # --- Helper methods: Widget def get_plugin_data(self, plugin): """Get the data object of the plugin's current tab manager.""" # The data object is named "data" in the editor plugin while it is # named "clients" in the notebook plugin. try: data = plugin.get_current_tab_manager().data except AttributeError: data = plugin.get_current_tab_manager().clients return data def get_plugin_tabwidget(self, plugin): """Get the tabwidget of the plugin's current tab manager.""" # The tab widget is named "tabs" in the editor plugin while it is # named "tabwidget" in the notebook plugin. try: tabwidget = plugin.get_current_tab_manager().tabs except AttributeError: tabwidget = plugin.get_current_tab_manager().tabwidget return tabwidget def get_widget(self, index=None, path=None, tabs=None): """Get widget by index. If no tabs and index specified the current active widget is returned. """ if (index and tabs) or (path and tabs): return tabs.widget(index) elif self.plugin: return self.get_plugin_tabwidget(self.plugin).currentWidget() else: return self.plugins_tabs[0][0].currentWidget() def set_editor_cursor(self, editor, cursor): """Set the cursor of an editor.""" pos = cursor.position() anchor = cursor.anchor() new_cursor = QTextCursor() if pos == anchor: new_cursor.movePosition(pos) else: new_cursor.movePosition(anchor) new_cursor.movePosition(pos, QTextCursor.KeepAnchor) editor.setTextCursor(cursor) def goto_line(self, line_number): """Go to specified line number in current active editor.""" if line_number: line_number = int(line_number) try: self.plugin.go_to_line(line_number) except AttributeError: pass # --- Helper methods: Outline explorer def get_symbol_list(self): """Get the list of symbols present in the file.""" try: oedata = self.get_widget().get_outlineexplorer_data() except AttributeError: oedata = {} return oedata # --- Handlers def item_selection_changed(self): """List widget item selection change handler.""" row = self.current_row() if self.count() and row >= 0: if '</b></big><br>' in self.list.currentItem().text() and row == 0: self.next_row() if self.mode == self.FILE_MODE: try: stack_index = self.paths.index(self.filtered_path[row]) self.plugin = self.widgets[stack_index][1] plugin_index = self.plugins_instances.index(self.plugin) # Count the real index in the tabWidget of the # current plugin real_index = self.get_stack_index(stack_index, plugin_index) self.sig_goto_file.emit( real_index, self.plugin.get_current_tab_manager()) self.goto_line(self.line_number) try: self.plugin.switch_to_plugin() self.raise_() except AttributeError: # The widget using the fileswitcher is not a plugin pass self.edit.setFocus() except ValueError: pass else: line_number = self.filtered_symbol_lines[row] self.goto_line(line_number) def setup_file_list(self, filter_text, current_path): """Setup list widget content for file list display.""" short_paths = shorten_paths(self.paths, self.save_status) paths = self.paths icons = self.icons results = [] trying_for_line_number = ':' in filter_text # Get optional line number if trying_for_line_number: filter_text, line_number = filter_text.split(':') if line_number == '': line_number = None # Get all the available filenames scores = get_search_scores('', self.filenames, template="<b>{0}</b>") else: line_number = None # Get all available filenames and get the scores for # "fuzzy" matching scores = get_search_scores(filter_text, self.filenames, template="<b>{0}</b>") # Get max width to determine if shortpaths should be used max_width = self.get_item_size(paths)[0] self.fix_size(paths) # Build the text that will appear on the list widget for index, score in enumerate(scores): text, rich_text, score_value = score if score_value != -1: text_item = '<big>' + rich_text.replace('&', '') + '</big>' if trying_for_line_number: text_item += " [{0:} {1:}]".format(self.line_count[index], _("lines")) if max_width > self.list.width(): text_item += u"<br><i>{0:}</i>".format(short_paths[index]) else: text_item += u"<br><i>{0:}</i>".format(paths[index]) if (trying_for_line_number and self.line_count[index] != 0 or not trying_for_line_number): results.append((score_value, index, text_item)) # Sort the obtained scores and populate the list widget self.filtered_path = [] plugin = None for result in sorted(results): index = result[1] path = paths[index] icon = icons[index] text = '' try: title = self.widgets[index][1].get_plugin_title().split(' - ') if plugin != title[0]: plugin = title[0] text += '<br><big><b>' + plugin + '</b></big><br>' item = QListWidgetItem(text) item.setToolTip(path) item.setSizeHint(QSize(0, 25)) item.setFlags(Qt.ItemIsEditable) self.list.addItem(item) self.filtered_path.append(path) except: # The widget using the fileswitcher is not a plugin pass text = '' text += result[-1] item = QListWidgetItem(icon, text) item.setToolTip(path) item.setSizeHint(QSize(0, 25)) self.list.addItem(item) self.filtered_path.append(path) # To adjust the delegate layout for KDE themes self.list.files_list = True # Move selected item in list accordingly and update list size if current_path in self.filtered_path: self.set_current_row(self.filtered_path.index(current_path)) elif self.filtered_path: self.set_current_row(0) # If a line number is searched look for it self.line_number = line_number self.goto_line(line_number) def setup_symbol_list(self, filter_text, current_path): """Setup list widget content for symbol list display.""" # Get optional symbol name filter_text, symbol_text = filter_text.split('@') # Fetch the Outline explorer data, get the icons and values oedata = self.get_symbol_list() icons = get_python_symbol_icons(oedata) # The list of paths here is needed in order to have the same # point of measurement for the list widget size as in the file list # See issue 4648 paths = self.paths # Update list size self.fix_size(paths) symbol_list = process_python_symbol_data(oedata) line_fold_token = [(item[0], item[2], item[3]) for item in symbol_list] choices = [item[1] for item in symbol_list] scores = get_search_scores(symbol_text, choices, template="<b>{0}</b>") # Build the text that will appear on the list widget results = [] lines = [] self.filtered_symbol_lines = [] for index, score in enumerate(scores): text, rich_text, score_value = score line, fold_level, token = line_fold_token[index] lines.append(text) if score_value != -1: results.append((score_value, line, text, rich_text, fold_level, icons[index], token)) template = '{0}{1}' for (score, line, text, rich_text, fold_level, icon, token) in sorted(results): fold_space = ' ' * (fold_level) line_number = line + 1 self.filtered_symbol_lines.append(line_number) textline = template.format(fold_space, rich_text) item = QListWidgetItem(icon, textline) item.setSizeHint(QSize(0, 16)) self.list.addItem(item) # To adjust the delegate layout for KDE themes self.list.files_list = False # Select edit line when using symbol search initially. # See issue 5661 self.edit.setFocus() # Move selected item in list accordingly # NOTE: Doing this is causing two problems: # 1. It makes the cursor to auto-jump to the last selected # symbol after opening or closing a different file # 2. It moves the cursor to the first symbol by default, # which is very distracting. # That's why this line is commented! # self.set_current_row(0) def setup(self): """Setup list widget content.""" if len(self.plugins_tabs) == 0: self.close() return self.list.clear() current_path = self.current_path filter_text = self.filter_text # Get optional line or symbol to define mode and method handler trying_for_symbol = ('@' in self.filter_text) if trying_for_symbol: self.mode = self.SYMBOL_MODE self.setup_symbol_list(filter_text, current_path) else: self.mode = self.FILE_MODE self.setup_file_list(filter_text, current_path) # Set position according to size self.set_dialog_position() def show(self): """ Override Qt method to force an update of the fileswitcher before showing it. See Issue #5317 and PR #5389. """ self.setup() super(FileSwitcher, self).show() def add_plugin(self, plugin, tabs, data, icon): """Add a plugin to display its files.""" self.plugins_tabs.append((tabs, plugin)) self.plugins_data.append((data, icon)) self.plugins_instances.append(plugin)
class EditableList(QWidget): """ Editable list with label, add, delete and edit buttons. """ sig_item_edited = Signal(object) sig_item_removed = Signal(object) sig_item_selected = Signal() def __init__(self, title=None, items_text=[], duplicates=False, normalize=True, min_items=1, confirm_remove=True, regex=None, tooltip=None): super(EditableList, self).__init__() self.duplicates = duplicates self.items_text = items_text self.normalize = normalize self.min_items = min_items self.confirm_remove = confirm_remove # Widgets self.label_title = QLabel(title) self.button_add = QPushButton(qta.icon('fa.plus'), '') self.button_remove = QPushButton(qta.icon('fa.minus'), '') self.button_edit = QPushButton(qta.icon('fa.edit'), '') self.list = QListWidget() self.delegate = EditableListDelegate(regex=regex, tooltip=tooltip) # Widget setup self.list.setItemDelegate(self.delegate) self.setMaximumHeight(self._height() * 4) # Layout label_buttons_layout = QHBoxLayout() label_buttons_layout.addWidget(self.label_title, 0) label_buttons_layout.addStretch() label_buttons_layout.addWidget(self.button_add) label_buttons_layout.addWidget(self.button_remove) label_buttons_layout.addWidget(self.button_edit) layout = QVBoxLayout() layout.addLayout(label_buttons_layout) layout.addWidget(self.list) self.setLayout(layout) # Signals self.button_add.clicked.connect(self.add) self.button_edit.clicked.connect(self.edit) self.button_remove.clicked.connect(self.remove) self.delegate.closeEditor.connect(self.check_value) self.list.currentItemChanged.connect(self.refresh) self.list.itemSelectionChanged.connect(self.sig_item_selected) # Setup self.setup() # Expose list methods self.clear = self.list.clear self.currentItem = self.list.currentItem self.setCurrentRow = self.list.setCurrentRow self.currentRow = self.list.currentRow self.item = self.list.item self.count = self.list.count def _height(self): """ Get the height for the row in the widget based on OS font metrics. """ return self.fontMetrics().height() * 2 def setup(self): """ Initial setup for populating items if any. """ # TODO: Check against regex and raise error accordingly! new_items = [] for text in self.items_text: if self.normalize: text = text.lower() new_items.append(text) self.items_text = new_items if not self.duplicates: if len(set(self.items_text)) != len(self.items_text): raise Exception('The list cannot contains duplicates.') for item in self.items_text: item = QListWidgetItem(item) item.extra_data = None item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) self.add(item) item.setSizeHint(QSize(item.sizeHint().width(), self._height())) self.refresh() def get_texts(self): """ Returns the list of texts in the list, excluding the ones under edition. """ row = self.list.currentRow() texts = [] for i in range(self.list.count()): item = self.list.item(i) if item: text = item.text().lower() if self.normalize else item.text() texts.append(text) # Check for duplicates. But the entered text already is part of the # items, so that needs to be removed to make the check if texts and row != -1: texts.pop(row) return texts def add(self, item=None): """ Return the text of all items in the list, except the current one being edited. """ if item: if item.text() in self.get_texts() and not self.duplicates: raise Exception else: self.list.addItem(item) else: item = QListWidgetItem() item.extra_data = None item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable) self.list.addItem(item) self.list.setCurrentItem(item) item.setSizeHint(QSize(item.sizeHint().width(), self._height())) self.edit() self.refresh() def remove(self): """ Remove item fron the list. """ row = self.list.currentRow() item = self.list.currentItem() if item is None: self.refresh() else: text = item.text() if self.confirm_remove: message = ("Are you sure you want to remove<br>" "<strong>{0}</strong>?".format(text)) reply = QMessageBox.question(self, 'Remove item', message, QMessageBox.Yes, QMessageBox.No) remove_item = reply == QMessageBox.Yes else: remove_item = True if row != -1 and remove_item: item = self.list.takeItem(row) self.sig_item_removed.emit(item) self.refresh() def edit(self): """ Start editing item from the list. """ self.button_remove.setDisabled(True) self.button_add.setDisabled(True) self.button_edit.setDisabled(True) item = self.current_item() if item: item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable) item._text = item.text() self.list.editItem(item) def current_item(self): """ Return the current selected item. """ item = None row = self.list.currentRow() if row != -1: item = self.list.item(row) return item def check_value(self): """ After editing an item check the value is valid and return to edit mode if it does not pass, or accept the value and add it. """ texts = self.get_texts() item = self.current_item() self._temp_item = item if item: text = item.text().lower() if self.normalize else item.text() row = self.list.currentRow() if text.strip() == '': self.list.takeItem(row) if text.strip() in texts and not self.duplicates: self.edit() else: self.sig_item_edited.emit(item) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) def refresh(self): """ Refresh the enabled status of the actions buttons. """ current_row = self.list.currentRow() if self.list.count() == 0: self.button_edit.setDisabled(True) self.button_remove.setDisabled(True) elif self.list.count() != 0 and current_row == -1: self.button_edit.setDisabled(True) self.button_remove.setDisabled(True) elif self.list.count() == self.min_items: self.button_add.setDisabled(False) self.button_edit.setDisabled(False) self.button_remove.setDisabled(True) elif self.list.count() != 0 and current_row != -1: self.button_add.setDisabled(False) self.button_edit.setDisabled(False) self.button_remove.setDisabled(False) for i in range(self.list.count()): item = self.list.item(i) item.setSizeHint(QSize(item.sizeHint().width(), self._height()))
class PreferencesDialog(QDialog): """Preferences Dialog for Napari user settings.""" ui_schema = { "call_order": { "ui:widget": "plugins" }, } 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")) # 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) # Make widget self.make_dialog() self._list.setCurrentRow(0) 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.""" # 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']) # 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.""" widget = ConfirmDialog( parent=self, text=trans._("Are you sure you want to restore default settings?"), ) widget.valueChanged.connect(self._reset_widgets) widget.exec_() def _reset_widgets(self): """Deletes the widgets and rebuilds with defaults.""" self.close() self._list.clear() for n in range(self._stack.count()): widget = self._stack.removeWidget(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. 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) 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. """ builder = WidgetBuilder() form = builder.create_form(schema, self.ui_schema) # set state values for widget form.widget.state = values form.widget.on_changed.connect(lambda d: self.check_differences( d, self._values_dict[schema["title"].lower()], )) 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 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. """ 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 except: # noqa: E722 continue
class SegmentationInfoDialog(QWidget): def __init__(self, settings: StackSettings, set_parameters: Callable[[str, dict], None], additional_text=None): """ :param settings: :param set_parameters: Function which set parameters of chosen in dialog. :param additional_text: Additional text on top of Window. """ super().__init__() self.settings = settings self.parameters_dict = None self.set_parameters = set_parameters self.components = QListWidget() self.components.currentItemChanged.connect(self.change_component_info) self.description = QPlainTextEdit() self.description.setReadOnly(True) self.close_btn = QPushButton("Close") self.close_btn.clicked.connect(self.close) self.set_parameters_btn = QPushButton("Reuse parameters") self.set_parameters_btn.clicked.connect(self.set_parameter_action) self.additional_text_label = QLabel(additional_text) layout = QGridLayout() layout.addWidget(self.additional_text_label, 0, 0, 1, 2) if not additional_text: self.additional_text_label.setVisible(False) layout.addWidget(QLabel("Components:"), 1, 0) layout.addWidget(QLabel("segmentation parameters:"), 1, 1) layout.addWidget(self.components, 2, 0) layout.addWidget(self.description, 2, 1) layout.addWidget(self.close_btn, 3, 0) layout.addWidget(self.set_parameters_btn, 3, 1) self.setLayout(layout) self.setWindowTitle("Parameters preview") def set_parameters_dict(self, val: Optional[Dict[int, SegmentationProfile]]): self.parameters_dict = val def set_additional_text(self, text): self.additional_text_label.setText(text) self.additional_text_label.setVisible(bool(text)) @property def get_parameters(self): if self.parameters_dict: return self.parameters_dict return self.settings.components_parameters_dict def change_component_info(self): if self.components.currentItem() is None: return text = self.components.currentItem().text() parameters = self.get_parameters[int(text)] if parameters is None: self.description.setPlainText("None") else: self.description.setPlainText( f"Component {text}\n" + parameters.pretty_print(mask_algorithm_dict)) def set_parameter_action(self): if self.components.currentItem() is None: return text = self.components.currentItem().text() parameters = self.get_parameters[int(text)] self.set_parameters(parameters.algorithm, parameters.values) def event(self, event: QEvent): if event.type() == QEvent.WindowActivate: index = self.components.currentRow() self.components.clear() self.components.addItems(list(map(str, self.get_parameters.keys()))) self.components.setCurrentRow(index) return super().event(event)
class ExportDialog(QDialog): def __init__(self, export_dict, viewer): super(ExportDialog, self).__init__() self.setWindowTitle("Export") self.export_dict = export_dict self.viewer = viewer() self.list_view = QListWidget() self.check_state = np.zeros(len(export_dict), dtype=np.bool) self.check_state[...] = True for el in sorted(export_dict.keys()): item = QListWidgetItem(el) # noinspection PyTypeChecker item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) self.list_view.addItem(item) self.checked_num = len(export_dict) self.export_btn = QPushButton("Export") self.cancel_btn = QPushButton("Cancel") self.check_btn = QPushButton("Check all") self.uncheck_btn = QPushButton("Uncheck all") self.cancel_btn.clicked.connect(self.close) self.export_btn.clicked.connect(self.accept) self.check_btn.clicked.connect(self.check_all) self.uncheck_btn.clicked.connect(self.uncheck_all) self.list_view.itemSelectionChanged.connect(self.preview) self.list_view.itemChanged.connect(self.checked_change) layout = QVBoxLayout() info_layout = QHBoxLayout() info_layout.addWidget(self.list_view) info_layout.addWidget(self.viewer) layout.addLayout(info_layout) btn_layout = QHBoxLayout() btn_layout.addWidget(self.check_btn) btn_layout.addWidget(self.uncheck_btn) btn_layout.addStretch() btn_layout.addWidget(self.export_btn) btn_layout.addWidget(self.cancel_btn) layout.addLayout(btn_layout) self.setLayout(layout) def checked_change(self, item): if item.checkState() == Qt.Unchecked: self.checked_num -= 1 else: self.checked_num += 1 if self.checked_num == 0: self.export_btn.setDisabled(True) else: self.export_btn.setEnabled(True) def get_checked(self): res = [] for i in range(self.list_view.count()): it = self.list_view.item(i) if it.checkState() == Qt.Checked: res.append(str(it.text())) return res def preview(self): name = str(self.list_view.currentItem().text()) self.viewer.preview_object(self.export_dict[name]) def check_change(self): item = self.list_view.currentItem() # type: QListWidgetItem index = self.list_view.currentRow() checked = item.checkState() == Qt.Checked self.check_state[index] = checked self.export_btn.setEnabled(np.any(self.check_state)) def uncheck_all(self): for index in range(self.list_view.count()): item = self.list_view.item(index) item.setCheckState(Qt.Unchecked) self.check_state[...] = False self.export_btn.setDisabled(True) self.checked_num = 0 def check_all(self): for index in range(self.list_view.count()): item = self.list_view.item(index) item.setCheckState(Qt.Checked) self.checked_num = len(self.export_dict) self.check_state[...] = True self.export_btn.setDisabled(False) def get_export_list(self): res = [] for num in range(self.list_view.count()): item = self.list_view.item(num) if item.checkState() == Qt.Checked: res.append(str(item.text())) return res
class CreatePlan(QWidget): plan_created = Signal() plan_node_changed = Signal() def __init__(self, settings: PartSettings): super().__init__() self.settings = settings self.save_translate_dict: typing.Dict[str, SaveBase] = { x.get_short_name(): x for x in save_dict.values() } self.plan = PlanPreview(self) self.save_plan_btn = QPushButton("Save") self.clean_plan_btn = QPushButton("Remove all") self.remove_btn = QPushButton("Remove") self.update_element_chk = QCheckBox("Update element") self.change_root = EnumComboBox(RootType) self.save_choose = QComboBox() self.save_choose.addItem("<none>") self.save_choose.addItems(list(self.save_translate_dict.keys())) self.save_btn = QPushButton("Save") self.segment_profile = QListWidget() self.pipeline_profile = QListWidget() self.segment_stack = QTabWidget() self.segment_stack.addTab(self.segment_profile, "Profile") self.segment_stack.addTab(self.pipeline_profile, "Pipeline") self.generate_mask_btn = QPushButton("Add mask") self.generate_mask_btn.setToolTip("Mask need to have unique name") self.mask_name = QLineEdit() self.mask_operation = EnumComboBox(MaskOperation) self.chanel_num = QSpinBox() self.choose_channel_for_measurements = QComboBox() self.choose_channel_for_measurements.addItems( ["Same as segmentation"] + [str(x + 1) for x in range(MAX_CHANNEL_NUM)]) self.units_choose = EnumComboBox(Units) self.units_choose.set_value(self.settings.get("units_value", Units.nm)) self.chanel_num.setRange(0, 10) self.expected_node_type = None self.save_constructor = None self.chose_profile_btn = QPushButton("Add Profile") self.get_big_btn = QPushButton("Leave the biggest") self.get_big_btn.hide() self.add_new_segmentation_btn = QPushButton("Add new profile") self.get_big_btn.setDisabled(True) self.add_new_segmentation_btn.setDisabled(True) self.measurements_list = QListWidget(self) self.measurement_name_prefix = QLineEdit(self) self.add_calculation_btn = QPushButton("Add measurement calculation") self.information = QTextEdit() self.information.setReadOnly(True) self.protect = False self.mask_set = set() self.calculation_plan = CalculationPlan() self.plan.set_plan(self.calculation_plan) self.segmentation_mask = MaskWidget(settings) self.file_mask = FileMask() self.change_root.currentIndexChanged.connect(self.change_root_type) self.save_choose.currentTextChanged.connect(self.save_changed) self.measurements_list.currentTextChanged.connect( self.show_measurement) self.segment_profile.currentTextChanged.connect(self.show_segment) self.measurements_list.currentTextChanged.connect( self.show_measurement_info) self.segment_profile.currentTextChanged.connect(self.show_segment_info) self.pipeline_profile.currentTextChanged.connect( self.show_segment_info) self.pipeline_profile.currentTextChanged.connect(self.show_segment) self.mask_name.textChanged.connect(self.mask_name_changed) self.generate_mask_btn.clicked.connect(self.create_mask) self.clean_plan_btn.clicked.connect(self.clean_plan) self.remove_btn.clicked.connect(self.remove_element) self.mask_name.textChanged.connect(self.mask_text_changed) self.chose_profile_btn.clicked.connect(self.add_segmentation) self.get_big_btn.clicked.connect(self.add_leave_biggest) self.add_calculation_btn.clicked.connect(self.add_measurement) self.save_plan_btn.clicked.connect(self.add_calculation_plan) # self.forgot_mask_btn.clicked.connect(self.forgot_mask) # self.cmap_save_btn.clicked.connect(self.save_to_cmap) self.save_btn.clicked.connect(self.add_save_to_project) self.update_element_chk.stateChanged.connect(self.mask_text_changed) self.update_element_chk.stateChanged.connect(self.show_measurement) self.update_element_chk.stateChanged.connect(self.show_segment) self.update_element_chk.stateChanged.connect(self.update_names) self.segment_stack.currentChanged.connect( self.change_segmentation_table) plan_box = QGroupBox("Prepare workflow:") lay = QVBoxLayout() lay.addWidget(self.plan) bt_lay = QGridLayout() bt_lay.setSpacing(0) bt_lay.addWidget(self.save_plan_btn, 0, 0) bt_lay.addWidget(self.clean_plan_btn, 0, 1) bt_lay.addWidget(self.remove_btn, 1, 0) bt_lay.addWidget(self.update_element_chk, 1, 1) lay.addLayout(bt_lay) plan_box.setLayout(lay) plan_box.setStyleSheet(group_sheet) other_box = QGroupBox("Other operations:") other_box.setContentsMargins(0, 0, 0, 0) bt_lay = QVBoxLayout() bt_lay.setSpacing(0) bt_lay.addWidget(QLabel("Root type:")) bt_lay.addWidget(self.change_root) bt_lay.addStretch(1) bt_lay.addWidget(QLabel("Saving:")) bt_lay.addWidget(self.save_choose) bt_lay.addWidget(self.save_btn) other_box.setLayout(bt_lay) other_box.setStyleSheet(group_sheet) mask_box = QGroupBox("Use mask from:") mask_box.setStyleSheet(group_sheet) self.mask_stack = QTabWidget() self.mask_stack.addTab(stretch_widget(self.file_mask), "File") self.mask_stack.addTab(stretch_widget(self.segmentation_mask), "Current segmentation") self.mask_stack.addTab(stretch_widget(self.mask_operation), "Operations on masks") self.mask_stack.setTabToolTip( 2, "Allows to create mask which is based on masks previously added to plan." ) lay = QGridLayout() lay.setSpacing(0) lay.addWidget(self.mask_stack, 0, 0, 1, 2) label = QLabel("Mask name:") label.setToolTip( "Needed if you would like to reuse this mask in tab 'Operations on masks'" ) self.mask_name.setToolTip( "Needed if you would like to reuse this mask in tab 'Operations on masks'" ) lay.addWidget(label, 1, 0) lay.addWidget(self.mask_name, 1, 1) lay.addWidget(self.generate_mask_btn, 2, 0, 1, 2) mask_box.setLayout(lay) segment_box = QGroupBox("Segmentation:") segment_box.setStyleSheet(group_sheet) lay = QVBoxLayout() lay.setSpacing(0) lay.addWidget(self.segment_stack) lay.addWidget(self.chose_profile_btn) lay.addWidget(self.get_big_btn) lay.addWidget(self.add_new_segmentation_btn) segment_box.setLayout(lay) measurement_box = QGroupBox("Set of measurements:") measurement_box.setStyleSheet(group_sheet) lay = QGridLayout() lay.setSpacing(0) lay.addWidget(self.measurements_list, 0, 0, 1, 2) lab = QLabel("Name prefix:") lab.setToolTip("Prefix added before each column name") lay.addWidget(lab, 1, 0) lay.addWidget(self.measurement_name_prefix, 1, 1) lay.addWidget(QLabel("Channel:"), 2, 0) lay.addWidget(self.choose_channel_for_measurements, 2, 1) lay.addWidget(QLabel("Units:")) lay.addWidget(self.units_choose, 3, 1) lay.addWidget(self.add_calculation_btn, 4, 0, 1, 2) measurement_box.setLayout(lay) info_box = QGroupBox("Information") info_box.setStyleSheet(group_sheet) lay = QVBoxLayout() lay.addWidget(self.information) info_box.setLayout(lay) layout = QGridLayout() fst_col = QVBoxLayout() fst_col.addWidget(plan_box, 1) fst_col.addWidget(mask_box) layout.addWidget(plan_box, 0, 0, 5, 1) # layout.addWidget(plan_box, 0, 0, 3, 1) # layout.addWidget(mask_box, 3, 0, 2, 1) # layout.addWidget(segmentation_mask_box, 1, 1) layout.addWidget(mask_box, 0, 2, 1, 2) layout.addWidget(other_box, 0, 1) layout.addWidget(segment_box, 1, 1, 1, 2) layout.addWidget(measurement_box, 1, 3) layout.addWidget(info_box, 3, 1, 1, 3) self.setLayout(layout) self.generate_mask_btn.setDisabled(True) self.chose_profile_btn.setDisabled(True) self.add_calculation_btn.setDisabled(True) self.mask_allow = False self.segment_allow = False self.file_mask_allow = False self.node_type = NodeType.root self.node_name = "" self.plan_node_changed.connect(self.mask_text_changed) self.plan.changed_node.connect(self.node_type_changed) self.plan_node_changed.connect(self.show_segment) self.plan_node_changed.connect(self.show_measurement) self.plan_node_changed.connect(self.mask_stack_change) self.mask_stack.currentChanged.connect(self.mask_stack_change) self.file_mask.value_changed.connect(self.mask_stack_change) self.mask_name.textChanged.connect(self.mask_stack_change) self.node_type_changed() def change_root_type(self): value: RootType = self.change_root.get_value() self.calculation_plan.set_root_type(value) self.plan.update_view() def change_segmentation_table(self): index = self.segment_stack.currentIndex() text = self.segment_stack.tabText(index) if self.update_element_chk.isChecked(): self.chose_profile_btn.setText("Replace " + text) else: self.chose_profile_btn.setText("Add " + text) self.segment_profile.setCurrentItem(None) self.pipeline_profile.setCurrentItem(None) def save_changed(self, text): text = str(text) if text == "<none>": self.save_btn.setText("Save") self.save_btn.setToolTip("Choose file type") self.expected_node_type = None self.save_constructor = None else: save_class = self.save_translate_dict.get(text, None) if save_class is None: self.save_choose.setCurrentText("<none>") return self.save_btn.setText(f"Save to {save_class.get_short_name()}") self.save_btn.setToolTip("Choose mask create in plan view") if save_class.need_mask(): self.expected_node_type = NodeType.mask elif save_class.need_segmentation(): self.expected_node_type = NodeType.segment else: self.expected_node_type = NodeType.root self.save_constructor = Save self.save_activate() def save_activate(self): self.save_btn.setDisabled(True) if self.node_type == self.expected_node_type: self.save_btn.setEnabled(True) return def segmentation_from_project(self): self.calculation_plan.add_step(Operations.reset_to_base) self.plan.update_view() def update_names(self): if self.update_element_chk.isChecked(): self.chose_profile_btn.setText("Replace Profile") self.add_calculation_btn.setText("Replace set of measurements") self.generate_mask_btn.setText("Replace mask") else: self.chose_profile_btn.setText("Add Profile") self.add_calculation_btn.setText("Add set of measurements") self.generate_mask_btn.setText("Generate mask") def node_type_changed(self): # self.cmap_save_btn.setDisabled(True) self.save_btn.setDisabled(True) self.node_name = "" if self.plan.currentItem() is None: self.mask_allow = False self.file_mask_allow = False self.segment_allow = False self.remove_btn.setDisabled(True) self.plan_node_changed.emit() logging.debug("[node_type_changed] return") return node_type = self.calculation_plan.get_node_type() self.node_type = node_type if node_type in [ NodeType.file_mask, NodeType.mask, NodeType.segment, NodeType.measurement, NodeType.save ]: self.remove_btn.setEnabled(True) else: self.remove_btn.setEnabled(False) if node_type == NodeType.mask or node_type == NodeType.file_mask: self.mask_allow = False self.segment_allow = True self.file_mask_allow = False self.node_name = self.calculation_plan.get_node().operation.name elif node_type == NodeType.segment: self.mask_allow = True self.segment_allow = False self.file_mask_allow = False self.save_btn.setEnabled(True) # self.cmap_save_btn.setEnabled(True) elif node_type == NodeType.root: self.mask_allow = False self.segment_allow = True self.file_mask_allow = True elif node_type == NodeType.none or node_type == NodeType.measurement or node_type == NodeType.save: self.mask_allow = False self.segment_allow = False self.file_mask_allow = False self.save_activate() self.plan_node_changed.emit() def add_save_to_project(self): save_class = self.save_translate_dict.get( self.save_choose.currentText(), None) if save_class is None: QMessageBox.warning(self, "Save problem", "Not found save class") dial = FormDialog([ AlgorithmProperty("suffix", "File suffix", ""), AlgorithmProperty("directory", "Sub directory", "") ] + save_class.get_fields()) if dial.exec(): values = dial.get_values() suffix = values["suffix"] directory = values["directory"] del values["suffix"] del values["directory"] save_elem = Save(suffix, directory, save_class.get_name(), save_class.get_short_name(), values) if self.update_element_chk.isChecked(): self.calculation_plan.replace_step(save_elem) else: self.calculation_plan.add_step(save_elem) self.plan.update_view() def create_mask(self): text = str(self.mask_name.text()).strip() if text != "" and text in self.mask_set: QMessageBox.warning(self, "Already exists", "Mask with this name already exists", QMessageBox.Ok) return if _check_widget(self.mask_stack, EnumComboBox): # existing mask mask_dialog = TwoMaskDialog if self.mask_operation.get_value( ) == MaskOperation.mask_intersection: # Mask intersection MaskConstruct = MaskIntersection else: MaskConstruct = MaskSum dial = mask_dialog(self.mask_set) if not dial.exec(): return names = dial.get_result() mask_ob = MaskConstruct(text, *names) elif _check_widget(self.mask_stack, MaskWidget): mask_ob = MaskCreate(text, self.segmentation_mask.get_mask_property()) elif _check_widget(self.mask_stack, FileMask): mask_ob = self.file_mask.get_value(text) else: raise ValueError("Unknowsn widget") if self.update_element_chk.isChecked(): node = self.calculation_plan.get_node() name = node.operation.name if name in self.calculation_plan.get_reused_mask( ) and name != text: QMessageBox.warning( self, "Cannot remove", f"Cannot remove mask '{name}' from plan because it is used in other elements" ) return self.mask_set.remove(name) self.mask_set.add(mask_ob.name) self.calculation_plan.replace_step(mask_ob) else: self.mask_set.add(mask_ob.name) self.calculation_plan.add_step(mask_ob) self.plan.update_view() self.mask_text_changed() def mask_stack_change(self): node_type = self.calculation_plan.get_node_type() if self.update_element_chk.isChecked() and node_type not in [ NodeType.mask, NodeType.file_mask ]: self.generate_mask_btn.setDisabled(True) text = self.mask_name.text() update = self.update_element_chk.isChecked() if self.node_type == NodeType.none: self.generate_mask_btn.setDisabled(True) return operation = self.calculation_plan.get_node().operation if (not update and isinstance(operation, (MaskMapper, MaskBase)) and self.calculation_plan.get_node().operation.name == text): self.generate_mask_btn.setDisabled(True) return if _check_widget(self.mask_stack, EnumComboBox): # reuse mask if len(self.mask_set) > 1 and ( (not update and node_type == NodeType.root) or (update and node_type == NodeType.file_mask)): self.generate_mask_btn.setEnabled(True) else: self.generate_mask_btn.setEnabled(False) self.generate_mask_btn.setToolTip( "Need at least two named mask and root selected") elif _check_widget(self.mask_stack, MaskWidget): # mask from segmentation if (not update and node_type == NodeType.segment) or ( update and node_type == NodeType.mask): self.generate_mask_btn.setEnabled(True) else: self.generate_mask_btn.setEnabled(False) self.generate_mask_btn.setToolTip("Select segmentation") else: if (not update and node_type == NodeType.root) or ( update and node_type == NodeType.file_mask): self.generate_mask_btn.setEnabled(self.file_mask.is_valid()) else: self.generate_mask_btn.setEnabled(False) self.generate_mask_btn.setToolTip("Need root selected") def mask_name_changed(self, text): if str(text) in self.mask_set: self.generate_mask_btn.setDisabled(True) else: self.generate_mask_btn.setDisabled(False) def add_leave_biggest(self): profile = self.calculation_plan.get_node().operation profile.leave_biggest_swap() self.calculation_plan.replace_step(profile) self.plan.update_view() def add_segmentation(self): if self.segment_stack.currentIndex() == 0: text = str(self.segment_profile.currentItem().text()) if text not in self.settings.segmentation_profiles: self.refresh_all_profiles() return profile = self.settings.segmentation_profiles[text] if self.update_element_chk.isChecked(): self.calculation_plan.replace_step(profile) else: self.calculation_plan.add_step(profile) self.plan.update_view() else: # self.segment_stack.currentIndex() == 1 text = self.pipeline_profile.currentItem().text() segmentation_pipeline = self.settings.segmentation_pipelines[text] pos = self.calculation_plan.current_pos[:] old_pos = self.calculation_plan.current_pos[:] for el in segmentation_pipeline.mask_history: self.calculation_plan.add_step(el.segmentation) self.plan.update_view() node = self.calculation_plan.get_node(pos) pos.append(len(node.children) - 1) self.calculation_plan.set_position(pos) self.calculation_plan.add_step(MaskCreate( "", el.mask_property)) self.plan.update_view() pos.append(0) self.calculation_plan.set_position(pos) self.calculation_plan.add_step(segmentation_pipeline.segmentation) self.calculation_plan.set_position(old_pos) self.plan.update_view() def add_measurement(self): text = str(self.measurements_list.currentItem().text()) measurement_copy = deepcopy(self.settings.measurement_profiles[text]) prefix = str(self.measurement_name_prefix.text()).strip() channel = self.choose_channel_for_measurements.currentIndex() - 1 measurement_copy.name_prefix = prefix # noinspection PyTypeChecker measurement_calculate = MeasurementCalculate( channel=channel, statistic_profile=measurement_copy, name_prefix=prefix, units=self.units_choose.get_value()) if self.update_element_chk.isChecked(): self.calculation_plan.replace_step(measurement_calculate) else: self.calculation_plan.add_step(measurement_calculate) self.plan.update_view() def remove_element(self): conflict_mask, used_mask = self.calculation_plan.get_file_mask_names() if len(conflict_mask) > 0: logging.info("Mask in use") QMessageBox.warning( self, "In use", "Masks {} are used in other places".format( ", ".join(conflict_mask))) return self.mask_set -= used_mask self.calculation_plan.remove_step() self.plan.update_view() def clean_plan(self): self.calculation_plan = CalculationPlan() self.plan.set_plan(self.calculation_plan) self.node_type_changed() self.mask_set = set() def mask_text_changed(self): name = str(self.mask_name.text()).strip() self.generate_mask_btn.setDisabled(True) # load mask from file if not self.update_element_chk.isChecked(): # generate mask from segmentation if self.mask_allow and (name == "" or name not in self.mask_set): self.generate_mask_btn.setEnabled(True) else: if self.node_type != NodeType.file_mask and self.node_type != NodeType.mask: return # generate mask from segmentation if self.node_type == NodeType.mask and ( name == "" or name == self.node_name or name not in self.mask_set): self.generate_mask_btn.setEnabled(True) def add_calculation_plan(self, text=None): if text is None or isinstance(text, bool): text, ok = QInputDialog.getText(self, "Plan title", "Set plan title") else: text, ok = QInputDialog.getText( self, "Plan title", "Set plan title. Previous ({}) is already in use".format(text), text=text) text = text.strip() if ok: if text == "": QMessageBox.information( self, "Name cannot be empty", "Name cannot be empty, Please set correct name", QMessageBox.Ok) self.add_calculation_plan() return if text in self.settings.batch_plans: res = QMessageBox.information( self, "Name already in use", "Name already in use. Would like to overwrite?", QMessageBox.Yes | QMessageBox.No, ) if res == QMessageBox.No: self.add_calculation_plan(text) return plan = copy(self.calculation_plan) plan.set_name(text) self.settings.batch_plans[text] = plan self.settings.dump() self.plan_created.emit() @staticmethod def get_index(item: QListWidgetItem, new_values: typing.List[str]) -> int: if item is None: return -1 text = item.text() try: return new_values.index(text) except ValueError: return -1 @staticmethod def refresh_profiles(list_widget: QListWidget, new_values: typing.List[str], index: int): list_widget.clear() list_widget.addItems(new_values) if index != -1: list_widget.setCurrentRow(index) def showEvent(self, event): self.refresh_all_profiles() def refresh_all_profiles(self): new_measurements = list( sorted(self.settings.measurement_profiles.keys())) new_segment = list(sorted(self.settings.segmentation_profiles.keys())) new_pipelines = list( sorted(self.settings.segmentation_pipelines.keys())) measurement_index = self.get_index( self.measurements_list.currentItem(), new_measurements) segment_index = self.get_index(self.segment_profile.currentItem(), new_segment) pipeline_index = self.get_index(self.pipeline_profile.currentItem(), new_pipelines) self.protect = True self.refresh_profiles(self.measurements_list, new_measurements, measurement_index) self.refresh_profiles(self.segment_profile, new_segment, segment_index) self.refresh_profiles(self.pipeline_profile, new_pipelines, pipeline_index) self.protect = False def show_measurement_info(self, text=None): if self.protect: return if text is None: if self.measurements_list.currentItem() is not None: text = str(self.measurements_list.currentItem().text()) else: return profile = self.settings.measurement_profiles[text] self.information.setText(str(profile)) def show_measurement(self): if self.update_element_chk.isChecked(): if self.node_type == NodeType.measurement: self.add_calculation_btn.setEnabled(True) else: self.add_calculation_btn.setDisabled(True) else: if self.measurements_list.currentItem() is not None: self.add_calculation_btn.setEnabled(self.mask_allow) else: self.add_calculation_btn.setDisabled(True) def show_segment_info(self, text=None): if self.protect: return if text == "": return if self.segment_stack.currentIndex() == 0: if text is None: if self.segment_profile.currentItem() is not None: text = str(self.segment_profile.currentItem().text()) else: return profile = self.settings.segmentation_profiles[text] else: if text is None: if self.pipeline_profile.currentItem() is not None: text = str(self.pipeline_profile.currentItem().text()) else: return profile = self.settings.segmentation_pipelines[text] self.information.setText(profile.pretty_print(analysis_algorithm_dict)) def show_segment(self): if self.update_element_chk.isChecked( ) and self.segment_stack.currentIndex() == 0: self.get_big_btn.setDisabled(True) if self.node_type == NodeType.segment: self.chose_profile_btn.setEnabled(True) else: self.chose_profile_btn.setDisabled(True) else: if self.node_type == NodeType.segment: self.get_big_btn.setEnabled(True) else: self.get_big_btn.setDisabled(True) if self.segment_stack.currentIndex() == 0: if self.segment_profile.currentItem() is not None: self.chose_profile_btn.setEnabled(self.segment_allow) else: self.chose_profile_btn.setDisabled(True) else: if self.pipeline_profile.currentItem() is not None: self.chose_profile_btn.setEnabled(self.segment_allow) else: self.chose_profile_btn.setDisabled(True) def edit_plan(self): plan = self.sender().plan_to_edit # type: CalculationPlan self.calculation_plan = copy(plan) self.plan.set_plan(self.calculation_plan) self.mask_set.clear() self.calculation_plan.set_position([]) self.mask_set.update(self.calculation_plan.get_mask_names())
class CalculateInfo(QWidget): """ "widget to show information about plans and allow to se plan details :type settings: Settings """ plan_to_edit_signal = Signal() def __init__(self, settings: PartSettings): super(CalculateInfo, self).__init__() self.settings = settings self.calculate_plans = QListWidget(self) self.plan_view = PlanPreview(self) self.delete_plan_btn = QPushButton("Delete") self.edit_plan_btn = QPushButton("Edit") self.export_plans_btn = QPushButton("Export") self.import_plans_btn = QPushButton("Import") info_layout = QVBoxLayout() info_butt_layout = QGridLayout() info_butt_layout.setSpacing(0) info_butt_layout.addWidget(self.delete_plan_btn, 1, 1) info_butt_layout.addWidget(self.edit_plan_btn, 0, 1) info_butt_layout.addWidget(self.export_plans_btn, 1, 0) info_butt_layout.addWidget(self.import_plans_btn, 0, 0) info_layout.addLayout(info_butt_layout) info_chose_layout = QVBoxLayout() info_chose_layout.setSpacing(2) info_chose_layout.addWidget(QLabel("List of workflows:")) info_chose_layout.addWidget(self.calculate_plans) info_chose_layout.addWidget(QLabel("Preview:")) info_chose_layout.addWidget(self.plan_view) info_layout.addLayout(info_chose_layout) self.setLayout(info_layout) self.calculate_plans.addItems( list(sorted(self.settings.batch_plans.keys()))) self.protect = False self.plan_to_edit = None self.plan_view.header().close() self.calculate_plans.currentTextChanged.connect(self.plan_preview) self.delete_plan_btn.clicked.connect(self.delete_plan) self.edit_plan_btn.clicked.connect(self.edit_plan) self.export_plans_btn.clicked.connect(self.export_plans) self.import_plans_btn.clicked.connect(self.import_plans) def update_plan_list(self): new_plan_list = list(sorted(self.settings.batch_plans.keys())) if self.calculate_plans.currentItem() is not None: text = str(self.calculate_plans.currentItem().text()) try: index = new_plan_list.index(text) except ValueError: index = -1 else: index = -1 self.protect = True self.calculate_plans.clear() self.calculate_plans.addItems(new_plan_list) if index != -1: self.calculate_plans.setCurrentRow(index) else: pass # self.plan_view.setText("") self.protect = False def export_plans(self): choose = ExportDialog(self.settings.batch_plans, PlanPreview) if not choose.exec_(): return dial = QFileDialog(self, "Export calculation plans") dial.setFileMode(QFileDialog.AnyFile) dial.setAcceptMode(QFileDialog.AcceptSave) dial.setDirectory( dial.setDirectory( self.settings.get("io.batch_plan_directory", str(Path.home())))) dial.setNameFilter("Calculation plans (*.json)") dial.setDefaultSuffix("json") dial.selectFile("calculation_plans.json") dial.setHistory(dial.history() + self.settings.get_path_history()) if dial.exec_(): file_path = str(dial.selectedFiles()[0]) self.settings.set("io.batch_plan_directory", os.path.dirname(file_path)) self.settings.add_path_history(os.path.dirname(file_path)) data = { x: self.settings.batch_plans[x] for x in choose.get_export_list() } with open(file_path, "w") as ff: json.dump(data, ff, cls=self.settings.json_encoder_class, indent=2) def import_plans(self): dial = QFileDialog(self, "Import calculation plans") dial.setFileMode(QFileDialog.ExistingFile) dial.setAcceptMode(QFileDialog.AcceptOpen) dial.setDirectory( self.settings.get("io.open_directory", str(Path.home()))) dial.setNameFilter("Calculation plans (*.json)") dial.setDefaultSuffix("json") dial.setHistory(dial.history() + self.settings.get_path_history()) if dial.exec_(): file_path = dial.selectedFiles()[0] plans, err = self.settings.load_part(file_path) self.settings.set("io.batch_plan_directory", os.path.dirname(file_path)) self.settings.add_path_history(os.path.dirname(file_path)) if err: QMessageBox.warning( self, "Import error", "error during importing, part of data were filtered.") choose = ImportDialog(plans, self.settings.batch_plans, PlanPreview) if choose.exec_(): for original_name, final_name in choose.get_import_list(): self.settings.batch_plans[final_name] = plans[ original_name] self.update_plan_list() def delete_plan(self): if self.calculate_plans.currentItem() is None: return text = str(self.calculate_plans.currentItem().text()) if text == "": return if text in self.settings.batch_plans: del self.settings.batch_plans[text] self.update_plan_list() self.plan_view.clear() def edit_plan(self): if self.calculate_plans.currentItem() is None: return text = str(self.calculate_plans.currentItem().text()) if text == "": return if text in self.settings.batch_plans: self.plan_to_edit = self.settings.batch_plans[text] self.plan_to_edit_signal.emit() def plan_preview(self, text): if self.protect: return text = str(text) if text.strip() == "": return plan = self.settings.batch_plans[str(text)] # type: CalculationPlan self.plan_view.set_plan(plan)
class ApplicationsDialog(QDialog): """Dialog for selection of installed system/user applications.""" def __init__(self, parent=None): """Dialog for selection of installed system/user applications.""" super(ApplicationsDialog, self).__init__(parent=parent) # Widgets self.label = QLabel() self.label_browse = QLabel() self.edit_filter = QLineEdit() self.list = QListWidget() self.button_browse = QPushButton(_('Browse...')) self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_ok = self.button_box.button(QDialogButtonBox.Ok) self.button_cancel = self.button_box.button(QDialogButtonBox.Cancel) # Widget setup self.setWindowTitle(_('Applications')) self.edit_filter.setPlaceholderText(_('Type to filter by name')) self.list.setIconSize(QSize(16, 16)) # FIXME: Use metrics # Layout layout = QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.edit_filter) layout.addWidget(self.list) layout_browse = QHBoxLayout() layout_browse.addWidget(self.button_browse) layout_browse.addWidget(self.label_browse) layout.addLayout(layout_browse) layout.addSpacing(12) # FIXME: Use metrics layout.addWidget(self.button_box) self.setLayout(layout) # Signals self.edit_filter.textChanged.connect(self.filter) self.button_browse.clicked.connect(lambda x: self.browse()) self.button_ok.clicked.connect(self.accept) self.button_cancel.clicked.connect(self.reject) self.list.currentItemChanged.connect(self._refresh) self._refresh() self.setup() def setup(self, applications=None): """Load installed applications.""" QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) self.list.clear() if applications is None: apps = get_installed_applications() else: apps = applications for app in sorted(apps, key=lambda x: x.lower()): fpath = apps[app] icon = get_application_icon(fpath) item = QListWidgetItem(icon, app) item.setToolTip(fpath) item.fpath = fpath self.list.addItem(item) # FIXME: Use metrics self.list.setMinimumWidth(self.list.sizeHintForColumn(0) + 24) QApplication.restoreOverrideCursor() self._refresh() def _refresh(self): """Refresh the status of buttons on widget.""" self.button_ok.setEnabled(self.list.currentRow() != -1) def browse(self, fpath=None): """Prompt user to select an application not found on the list.""" app = None item = None if sys.platform == 'darwin': if fpath is None: basedir = '/Applications/' filters = _('Applications (*.app)') title = _('Select application') fpath, __ = getopenfilename(self, title, basedir, filters) if fpath and fpath.endswith('.app') and os.path.isdir(fpath): app = os.path.basename(fpath).split('.app')[0] for row in range(self.list.count()): item = self.list.item(row) if app == item.text() and fpath == item.fpath: break else: item = None elif os.name == 'nt': if fpath is None: basedir = 'C:\\' filters = _('Applications (*.exe *.bat *.com)') title = _('Select application') fpath, __ = getopenfilename(self, title, basedir, filters) if fpath: check_1 = fpath.endswith('.bat') and is_text_file(fpath) check_2 = (fpath.endswith(('.exe', '.com')) and not is_text_file(fpath)) if check_1 or check_2: app = os.path.basename(fpath).capitalize().rsplit('.')[0] for row in range(self.list.count()): item = self.list.item(row) if app == item.text() and fpath == item.fpath: break else: item = None else: if fpath is None: basedir = '/' filters = _('Applications (*.desktop)') title = _('Select application') fpath, __ = getopenfilename(self, title, basedir, filters) if fpath and fpath.endswith(('.desktop')) and is_text_file(fpath): entry_data = parse_linux_desktop_entry(fpath) app = entry_data['name'] for row in range(self.list.count()): item = self.list.item(row) if app == item.text() and fpath == item.fpath: break else: item = None if fpath: if item: self.list.setCurrentItem(item) elif app: icon = get_application_icon(fpath) item = QListWidgetItem(icon, app) item.fpath = fpath self.list.addItem(item) self.list.setCurrentItem(item) self.list.setFocus() self._refresh() def filter(self, text): """Filter the list of applications based on text.""" text = self.edit_filter.text().lower().strip() for row in range(self.list.count()): item = self.list.item(row) item.setHidden(text not in item.text().lower()) self._refresh() def set_extension(self, extension): """Set the extension on the label of the dialog.""" self.label.setText( _('Choose the application for files of type ') + extension) @property def application_path(self): """Return the selected application path to executable.""" item = self.list.currentItem() path = item.fpath if item else '' return path @property def application_name(self): """Return the selected application name.""" item = self.list.currentItem() text = item.text() if item else '' return text
class PreferencesDialog(QDialog): """Preferences Dialog for Napari user settings.""" valueChanged = Signal() ui_schema = { "call_order": { "ui:widget": "plugins" }, "highlight_thickness": { "ui:widget": "highlight" }, } 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")) # 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) # 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): """Deletes the widgets and rebuilds with defaults.""" self.close() self.valueChanged.emit() self._list.clear() for n in range(self._stack.count()): widget = self._stack.removeWidget(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) 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 PreferencesDialog(QDialog): """Preferences Dialog for Napari user settings.""" resized = Signal(QSize) 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")) # 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) # Make widget self.make_dialog() self._list.setCurrentRow(0) 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.""" # Because there are multiple pages, need to keep a list of values sets. self._values_orig_set_list = [] self._values_set_list = [] for _key, setting in SETTINGS.schemas().items(): schema = json.loads(setting['json_schema']) # 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 self._values_orig_set_list.append(set(values.items())) self._values_set_list.append(set(values.items())) # Only add pages if there are any properties to add. if properties: self.add_page(schema, values) def restore_defaults(self): """Launches dialog to confirm restore settings choice.""" widget = ConfirmDialog( parent=self, text=trans._("Are you sure you want to restore default settings?"), ) widget.valueChanged.connect(self._reset_widgets) widget.exec_() def _reset_widgets(self): """Deletes the widgets and rebuilds with defaults.""" self.close() self._list.clear() for n in range(self._stack.count()): widget = self._stack.removeWidget(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. for n in range(self._stack.count()): # Must set the current row so that the proper set list is updated # in check differences. self._list.setCurrentRow(n) self.check_differences( self._values_orig_set_list[n], self._values_set_list[n], ) 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. """ builder = WidgetBuilder() form = builder.create_form(schema, {}) # set state values for widget form.widget.state = values form.widget.on_changed.connect(lambda d: self.check_differences( set(d.items()), self._values_set_list[self._list.currentIndex().row()], )) return form def check_differences(self, new_set, values_set): """Changes settings in settings manager with changes from dialog. Parameters ---------- new_set : set The set of new values, with tuples of key value pairs for each setting. values_set : set The old set of values. """ page = self._list.currentItem().text().split(" ")[0].lower() different_values = list(new_set - values_set) if len(different_values) > 0: # change the values in SETTINGS for val in different_values: try: setattr(SETTINGS._settings[page], val[0], val[1]) self._values_set_list[ self._list.currentIndex().row()] = new_set except: # noqa: E722 continue
class PathManager(QDialog): redirect_stdio = Signal(bool) def __init__(self, parent=None, pathlist=None, ro_pathlist=None, not_active_pathlist=None, sync=True): 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) assert isinstance(pathlist, list) self.pathlist = pathlist if not_active_pathlist is None: not_active_pathlist = [] self.not_active_pathlist = not_active_pathlist if ro_pathlist is None: ro_pathlist = [] self.ro_pathlist = ro_pathlist self.last_path = getcwd_or_home() self.setWindowTitle(_("PYTHONPATH manager")) self.setWindowIcon(ima.icon('pythonpath')) self.resize(500, 300) self.selection_widgets = [] layout = QVBoxLayout() self.setLayout(layout) top_layout = QHBoxLayout() layout.addLayout(top_layout) self.toolbar_widgets1 = self.setup_top_toolbar(top_layout) self.listwidget = QListWidget(self) self.listwidget.currentRowChanged.connect(self.refresh) self.listwidget.itemChanged.connect(self.update_not_active_pathlist) layout.addWidget(self.listwidget) bottom_layout = QHBoxLayout() layout.addLayout(bottom_layout) self.sync_button = None self.toolbar_widgets2 = self.setup_bottom_toolbar(bottom_layout, sync) # Buttons configuration bbox = QDialogButtonBox(QDialogButtonBox.Close) bbox.rejected.connect(self.reject) bottom_layout.addWidget(bbox) self.update_list() self.refresh() @property def active_pathlist(self): return [ path for path in self.pathlist if path not in self.not_active_pathlist ] def _add_widgets_to_layout(self, layout, widgets): layout.setAlignment(Qt.AlignLeft) for widget in widgets: layout.addWidget(widget) def setup_top_toolbar(self, layout): toolbar = [] movetop_button = create_toolbutton( self, text=_("Move to top"), icon=ima.icon('2uparrow'), triggered=lambda: self.move_to(absolute=0), text_beside_icon=True) toolbar.append(movetop_button) moveup_button = create_toolbutton( self, text=_("Move up"), icon=ima.icon('1uparrow'), triggered=lambda: self.move_to(relative=-1), text_beside_icon=True) toolbar.append(moveup_button) movedown_button = create_toolbutton( self, text=_("Move down"), icon=ima.icon('1downarrow'), triggered=lambda: self.move_to(relative=1), text_beside_icon=True) toolbar.append(movedown_button) movebottom_button = create_toolbutton( self, text=_("Move to bottom"), icon=ima.icon('2downarrow'), triggered=lambda: self.move_to(absolute=1), text_beside_icon=True) toolbar.append(movebottom_button) self.selection_widgets.extend(toolbar) self._add_widgets_to_layout(layout, toolbar) return toolbar def setup_bottom_toolbar(self, layout, sync=True): toolbar = [] add_button = create_toolbutton(self, text=_('Add path'), icon=ima.icon('edit_add'), triggered=self.add_path, text_beside_icon=True) toolbar.append(add_button) remove_button = create_toolbutton(self, text=_('Remove path'), icon=ima.icon('edit_remove'), triggered=self.remove_path, text_beside_icon=True) toolbar.append(remove_button) self.selection_widgets.append(remove_button) self._add_widgets_to_layout(layout, toolbar) layout.addStretch(1) if os.name == 'nt' and sync: self.sync_button = create_toolbutton( self, text=_("Synchronize..."), icon=ima.icon('fileimport'), triggered=self.synchronize, tip=_("Synchronize Spyder's path list with PYTHONPATH " "environment variable"), text_beside_icon=True) layout.addWidget(self.sync_button) return toolbar @Slot() def synchronize(self): """ Synchronize Spyder's path list with PYTHONPATH environment variable Only apply to: current user, on Windows platforms """ answer = QMessageBox.question( self, _("Synchronize"), _("This will synchronize Spyder's path list with " "<b>PYTHONPATH</b> environment variable for current user, " "allowing you to run your Python modules outside Spyder " "without having to configure sys.path. " "<br>Do you want to clear contents of PYTHONPATH before " "adding Spyder's path list?"), QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) if answer == QMessageBox.Cancel: return elif answer == QMessageBox.Yes: remove = True else: remove = False from spyder.utils.environ import (get_user_env, set_user_env, listdict2envdict) env = get_user_env() if remove: ppath = self.active_pathlist + self.ro_pathlist else: ppath = env.get('PYTHONPATH', []) if not isinstance(ppath, list): ppath = [ppath] ppath = [ path for path in ppath if path not in (self.active_pathlist + self.ro_pathlist) ] ppath.extend(self.active_pathlist + self.ro_pathlist) env['PYTHONPATH'] = ppath set_user_env(listdict2envdict(env), parent=self) def get_path_list(self): """Return path list (does not include the read-only path list)""" return self.pathlist def update_not_active_pathlist(self, item): path = item.text() if bool(item.checkState()) is True: self.remove_from_not_active_pathlist(path) else: self.add_to_not_active_pathlist(path) def add_to_not_active_pathlist(self, path): if path not in self.not_active_pathlist: self.not_active_pathlist.append(path) def remove_from_not_active_pathlist(self, path): if path in self.not_active_pathlist: self.not_active_pathlist.remove(path) def update_list(self): """Update path list""" self.listwidget.clear() for name in self.pathlist + self.ro_pathlist: item = QListWidgetItem(name) item.setIcon(ima.icon('DirClosedIcon')) if name in self.ro_pathlist: item.setFlags(Qt.NoItemFlags | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) elif name in self.not_active_pathlist: item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Unchecked) else: item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) self.listwidget.addItem(item) self.refresh() def refresh(self, row=None): """Refresh widget""" for widget in self.selection_widgets: widget.setEnabled(self.listwidget.currentItem() is not None) not_empty = self.listwidget.count() > 0 if self.sync_button is not None: self.sync_button.setEnabled(not_empty) def move_to(self, absolute=None, relative=None): index = self.listwidget.currentRow() if absolute is not None: if absolute: new_index = len(self.pathlist) - 1 else: new_index = 0 else: new_index = index + relative new_index = max(0, min(len(self.pathlist) - 1, new_index)) path = self.pathlist.pop(index) self.pathlist.insert(new_index, path) self.update_list() self.listwidget.setCurrentRow(new_index) @Slot() def remove_path(self): answer = QMessageBox.warning( self, _("Remove path"), _("Do you really want to remove selected path?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: self.pathlist.pop(self.listwidget.currentRow()) self.remove_from_not_active_pathlist( self.listwidget.currentItem().text()) self.update_list() @Slot() def add_path(self): self.redirect_stdio.emit(False) directory = getexistingdirectory(self, _("Select directory"), self.last_path) self.redirect_stdio.emit(True) if directory: is_unicode = False if PY2: try: directory.decode('ascii') except UnicodeEncodeError: is_unicode = True if is_unicode: QMessageBox.warning( self, _("Add path"), _("You are using Python 2 and the path" " selected has Unicode characters. " "The new path will not be added."), QMessageBox.Ok) return directory = osp.abspath(directory) self.last_path = directory if directory in self.pathlist: item = self.listwidget.findItems(directory, Qt.MatchExactly)[0] item.setCheckState(Qt.Checked) answer = QMessageBox.question( self, _("Add path"), _("This directory is already included in Spyder " "path list.<br>Do you want to move it to the " "top of the list?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: self.pathlist.remove(directory) else: return self.pathlist.insert(0, directory) self.update_list()
class PathManager(QDialog): """Path manager dialog.""" redirect_stdio = Signal(bool) sig_path_changed = Signal(object) def __init__(self, parent, path=None, read_only_path=None, not_active_path=None, sync=True): """Path manager dialog.""" super(PathManager, self).__init__(parent) assert isinstance(path, (tuple, None)) self.path = path or () self.read_only_path = read_only_path or () self.not_active_path = not_active_path or () self.last_path = getcwd_or_home() self.original_path_dict = None # Widgets self.add_button = None self.remove_button = None self.movetop_button = None self.moveup_button = None self.movedown_button = None self.movebottom_button = None self.sync_button = None self.selection_widgets = [] self.top_toolbar_widgets = self._setup_top_toolbar() self.bottom_toolbar_widgets = self._setup_bottom_toolbar() self.listwidget = QListWidget(self) self.bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_ok = self.bbox.button(QDialogButtonBox.Ok) # Widget setup # 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.setWindowTitle(_("PYTHONPATH manager")) self.setWindowIcon(ima.icon('pythonpath')) self.resize(500, 300) self.sync_button.setVisible(os.name == 'nt' and sync) # Layouts top_layout = QHBoxLayout() self._add_widgets_to_layout(self.top_toolbar_widgets, top_layout) bottom_layout = QHBoxLayout() self._add_widgets_to_layout(self.bottom_toolbar_widgets, bottom_layout) bottom_layout.addWidget(self.bbox) layout = QVBoxLayout() layout.addLayout(top_layout) layout.addWidget(self.listwidget) layout.addLayout(bottom_layout) self.setLayout(layout) # Signals self.listwidget.currentRowChanged.connect(lambda x: self.refresh()) self.listwidget.itemChanged.connect(lambda x: self.refresh()) self.bbox.accepted.connect(self.accept) self.bbox.rejected.connect(self.reject) # Setup self.setup() def _add_widgets_to_layout(self, widgets, layout): """Helper to add toolbar widgets to top and bottom layout.""" layout.setAlignment(Qt.AlignLeft) for widget in widgets: if widget is None: layout.addStretch(1) else: layout.addWidget(widget) def _setup_top_toolbar(self): """Create top toolbar and actions.""" self.movetop_button = create_toolbutton( self, text=_("Move to top"), icon=ima.icon('2uparrow'), triggered=lambda: self.move_to(absolute=0), text_beside_icon=True) self.moveup_button = create_toolbutton( self, text=_("Move up"), icon=ima.icon('1uparrow'), triggered=lambda: self.move_to(relative=-1), text_beside_icon=True) self.movedown_button = create_toolbutton( self, text=_("Move down"), icon=ima.icon('1downarrow'), triggered=lambda: self.move_to(relative=1), text_beside_icon=True) self.movebottom_button = create_toolbutton( self, text=_("Move to bottom"), icon=ima.icon('2downarrow'), triggered=lambda: self.move_to(absolute=1), text_beside_icon=True) toolbar = [ self.movetop_button, self.moveup_button, self.movedown_button, self.movebottom_button ] self.selection_widgets.extend(toolbar) return toolbar def _setup_bottom_toolbar(self): """Create bottom toolbar and actions.""" self.add_button = create_toolbutton( self, text=_('Add path'), icon=ima.icon('edit_add'), triggered=lambda x: self.add_path(), text_beside_icon=True) self.remove_button = create_toolbutton( self, text=_('Remove path'), icon=ima.icon('edit_remove'), triggered=lambda x: self.remove_path(), text_beside_icon=True) self.sync_button = create_toolbutton( self, text=_("Synchronize..."), icon=ima.icon('fileimport'), triggered=self.synchronize, tip=_("Synchronize Spyder's path list with PYTHONPATH " "environment variable"), text_beside_icon=True) self.selection_widgets.append(self.remove_button) return [self.add_button, self.remove_button, None, self.sync_button] def _create_item(self, path): """Helper to create a new list item.""" item = QListWidgetItem(path) item.setIcon(ima.icon('DirClosedIcon')) if path in self.read_only_path: item.setFlags(Qt.NoItemFlags | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) elif path in self.not_active_path: item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Unchecked) else: item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) return item @property def editable_bottom_row(self): """Maximum bottom row count that is editable.""" read_only_count = len(self.read_only_path) if read_only_count == 0: max_row = self.listwidget.count() - 1 else: max_row = self.listwidget.count() - read_only_count - 1 return max_row def setup(self): """Populate list widget.""" self.listwidget.clear() for path in self.path + self.read_only_path: item = self._create_item(path) self.listwidget.addItem(item) self.listwidget.setCurrentRow(0) self.original_path_dict = self.get_path_dict() self.refresh() @Slot() def synchronize(self): """ Synchronize Spyder's path list with PYTHONPATH environment variable Only apply to: current user, on Windows platforms. """ answer = QMessageBox.question( self, _("Synchronize"), _("This will synchronize Spyder's path list with " "<b>PYTHONPATH</b> environment variable for the current user, " "allowing you to run your Python modules outside Spyder " "without having to configure sys.path. " "<br>" "Do you want to clear contents of PYTHONPATH before " "adding Spyder's path list?"), QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) if answer == QMessageBox.Cancel: return elif answer == QMessageBox.Yes: remove = True else: remove = False from spyder.utils.environ import (get_user_env, listdict2envdict, set_user_env) env = get_user_env() # Includes read only paths active_path = tuple(k for k, v in self.get_path_dict(True).items() if v) if remove: ppath = active_path else: ppath = env.get('PYTHONPATH', []) if not isinstance(ppath, list): ppath = [ppath] ppath = tuple(p for p in ppath if p not in active_path) ppath = ppath + active_path env['PYTHONPATH'] = list(ppath) set_user_env(listdict2envdict(env), parent=self) def get_path_dict(self, read_only=False): """ Return an ordered dict with the path entries as keys and the active state as the value. If `read_only` is True, the read_only entries are also included. `read_only` entry refers to the project path entry. """ odict = OrderedDict() for row in range(self.listwidget.count()): item = self.listwidget.item(row) path = item.text() if path in self.read_only_path and not read_only: continue odict[path] = item.checkState() == Qt.Checked return odict def refresh(self): """Refresh toolbar widgets.""" enabled = self.listwidget.currentItem() is not None for widget in self.selection_widgets: widget.setEnabled(enabled) # Disable buttons based on row row = self.listwidget.currentRow() disable_widgets = [] # Move up/top disabled for top item if row == 0: disable_widgets.extend([self.movetop_button, self.moveup_button]) # Move down/bottom disabled for bottom item if row == self.editable_bottom_row: disable_widgets.extend( [self.movebottom_button, self.movedown_button]) for widget in disable_widgets: widget.setEnabled(False) self.sync_button.setEnabled(self.listwidget.count() > 0) # Ok button only enabled if actual changes occur self.button_ok.setEnabled( self.original_path_dict != self.get_path_dict()) def check_path(self, path): """Check that the path is not a [site|dist]-packages folder.""" if os.name == 'nt': pat = re.compile(r'.*lib/(?:site|dist)-packages.*') else: pat = re.compile(r'.*lib/python.../(?:site|dist)-packages.*') path_norm = path.replace('\\', '/') return pat.match(path_norm) is None @Slot() def add_path(self, directory=None): """ Add path to list widget. If `directory` is provided, the folder dialog is overridden. """ if directory is None: self.redirect_stdio.emit(False) directory = getexistingdirectory(self, _("Select directory"), self.last_path) self.redirect_stdio.emit(True) if PY2: is_unicode = False try: directory.decode('ascii') except (UnicodeEncodeError, UnicodeDecodeError): is_unicode = True if is_unicode: QMessageBox.warning( self, _("Add path"), _("You are using Python 2 and the selected path has " "Unicode characters." "<br> " "Therefore, this path will not be added."), QMessageBox.Ok) return directory = osp.abspath(directory) self.last_path = directory if directory in self.get_path_dict(): item = self.listwidget.findItems(directory, Qt.MatchExactly)[0] item.setCheckState(Qt.Checked) answer = QMessageBox.question( self, _("Add path"), _("This directory is already included in the list." "<br> " "Do you want to move it to the top of it?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: item = self.listwidget.takeItem(self.listwidget.row(item)) self.listwidget.insertItem(0, item) self.listwidget.setCurrentRow(0) else: if self.check_path(directory): item = self._create_item(directory) self.listwidget.insertItem(0, item) self.listwidget.setCurrentRow(0) else: answer = QMessageBox.warning( self, _("Add path"), _("This directory cannot be added to the path!" "<br><br>" "If you want to set a different Python interpreter, " "please go to <tt>Preferences > Main interpreter</tt>" "."), QMessageBox.Ok) self.refresh() @Slot() def remove_path(self, force=False): """ Remove path from list widget. If `force` is True, the message box is overridden. """ if self.listwidget.currentItem(): if not force: answer = QMessageBox.warning( self, _("Remove path"), _("Do you really want to remove the selected path?"), QMessageBox.Yes | QMessageBox.No) if force or answer == QMessageBox.Yes: self.listwidget.takeItem(self.listwidget.currentRow()) self.refresh() def move_to(self, absolute=None, relative=None): """Move items of list widget.""" index = self.listwidget.currentRow() if absolute is not None: if absolute: new_index = self.listwidget.count() - 1 else: new_index = 0 else: new_index = index + relative new_index = max(0, min(self.editable_bottom_row, new_index)) item = self.listwidget.takeItem(index) self.listwidget.insertItem(new_index, item) self.listwidget.setCurrentRow(new_index) self.refresh() def current_row(self): """Returns the current row of the list.""" return self.listwidget.currentRow() def set_current_row(self, row): """Set the current row of the list.""" self.listwidget.setCurrentRow(row) def row_check_state(self, row): """Return the checked state for item in row.""" item = self.listwidget.item(row) return item.checkState() def set_row_check_state(self, row, value): """Set the current checked state for item in row.""" item = self.listwidget.item(row) item.setCheckState(value) def count(self): """Return the number of items.""" return self.listwidget.count() def accept(self): """Override Qt method.""" path_dict = self.get_path_dict() if self.original_path_dict != path_dict: self.sig_path_changed.emit(path_dict) super(PathManager, self).accept()