def __init__(self, package, parent=None): QSplitter.__init__(self, parent) self.setWindowTitle(_("Tests - %s module") % package.__name__) self.setWindowIcon(get_icon("%s.svg" % package.__name__, "guidata.svg")) test_package_name = "%s.tests" % package.__name__ _temp = __import__(test_package_name) test_package = sys.modules[test_package_name] tests = get_tests(test_package) listwidget = QListWidget(self) listwidget.addItems([osp.basename(test.filename) for test in tests]) self.properties = TestPropertiesWidget(self) self.addWidget(listwidget) self.addWidget(self.properties) self.properties.run_button.clicked.connect( lambda: tests[listwidget.currentRow()].run()) self.properties.quit_button.clicked.connect(self.close) listwidget.currentRowChanged.connect( lambda row: self.properties.set_item(tests[row])) listwidget.itemActivated.connect( lambda: tests[listwidget.currentRow()].run()) listwidget.setCurrentRow(0) QShortcut(QKeySequence("Escape"), self, self.close) self.setSizes([150, 1]) self.setStretchFactor(1, 1) self.resize(QSize(950, 600)) self.properties.set_item(tests[0])
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 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 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 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 SelectFromListDialog(QDialog): """Dialog box for selecting multiple items. The original order of items are preserved in the candidate list. """ def __init__(self, parent=None, allItems=(), selectedItems=()): QDialog.__init__( self, parent, flags=Qt.WindowSystemMenuHint | Qt.WindowTitleHint) self.allItems = allItems self.fromKeys = list(range(len(allItems))) self.selectedKeys = [] for item in selectedItems: key = allItems.index(item) self.fromKeys.remove(key) self.selectedKeys.append(key) self.setAttribute(Qt.WA_DeleteOnClose) fromLabel = QLabel(_("Select from")) self.fromList = QListWidget(self) self.fromList.addItems(allItems[key] for key in self.fromKeys) selectedLabel = QLabel(_("Selected")) self.selectedList = QListWidget(self) self.selectedList.addItems(allItems[key] for key in self.selectedKeys) self.selectButton = QPushButton(_("Select")) self.deselectButton = QPushButton(_("Deselect")) self.selectBox = QDialogButtonBox(Qt.Vertical) self.selectBox.addButton(self.selectButton, QDialogButtonBox.ActionRole) self.selectBox.addButton(self.deselectButton, QDialogButtonBox.ActionRole) self.selectButton.clicked.connect(self.on_select) self.deselectButton.clicked.connect(self.on_deselect) self.buttonBox = QDialogButtonBox(self) self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setStandardButtons( QDialogButtonBox.Cancel|QDialogButtonBox.Ok) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) mainLayout = QGridLayout(self) mainLayout.addWidget(fromLabel, 0, 0) mainLayout.addWidget(selectedLabel, 0, 2) mainLayout.addWidget(self.fromList, 1, 0) mainLayout.addWidget(self.selectBox, 1, 1) mainLayout.addWidget(self.selectedList, 1, 2) mainLayout.addWidget(self.buttonBox, 2, 0, 1, 3) mainLayout.setAlignment(self.selectBox, Qt.AlignCenter) self.setLayout(mainLayout) def on_select(self): if len(self.fromList.selectedItems()): idx = self.fromList.currentRow() key = self.fromKeys.pop(idx) self.selectedKeys.append(key) item = self.fromList.takeItem(idx) self.selectedList.addItem(item) def on_deselect(self): if len(self.selectedList.selectedItems()): idx = self.selectedList.currentRow() item = self.selectedList.takeItem(idx) key = self.selectedKeys.pop(idx) idx = next((i for i, v in enumerate(self.fromKeys) if v > key), len(self.fromKeys)) self.fromKeys.insert(idx, key) self.fromList.insertItem(idx, item)
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 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()
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 ConfigDialog(QDialog): """Spyder configuration ('Preferences') dialog box""" # Signals check_settings = Signal() size_change = Signal(QSize) def __init__(self, parent=None): QDialog.__init__(self, parent) self.main = parent # Widgets self.pages_widget = QStackedWidget() self.pages_widget.setMinimumWidth(600) self.contents_widget = QListWidget() self.button_reset = QPushButton(_('Reset to defaults')) bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply | QDialogButtonBox.Cancel) self.apply_btn = bbox.button(QDialogButtonBox.Apply) # Widgets 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(_('Preferences')) self.setWindowIcon(ima.icon('configure')) self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(1) self.contents_widget.setCurrentRow(0) self.contents_widget.setMinimumWidth(220) self.contents_widget.setMinimumHeight(400) # Layout hsplitter = QSplitter() hsplitter.addWidget(self.contents_widget) hsplitter.addWidget(self.pages_widget) hsplitter.setStretchFactor(0, 1) hsplitter.setStretchFactor(1, 2) btnlayout = QHBoxLayout() btnlayout.addWidget(self.button_reset) btnlayout.addStretch(1) btnlayout.addWidget(bbox) vlayout = QVBoxLayout() vlayout.addWidget(hsplitter) vlayout.addLayout(btnlayout) self.setLayout(vlayout) # Signals and slots if self.main: self.button_reset.clicked.connect(self.main.reset_spyder) self.pages_widget.currentChanged.connect(self.current_page_changed) self.contents_widget.currentRowChanged.connect( self.pages_widget.setCurrentIndex) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) bbox.clicked.connect(self.button_clicked) # Ensures that the config is present on spyder first run CONF.set('main', 'interface_language', load_lang_conf()) def get_current_index(self): """Return current page index""" return self.contents_widget.currentRow() def set_current_index(self, index): """Set current page index""" self.contents_widget.setCurrentRow(index) def get_page(self, index=None): """Return page widget""" if index is None: widget = self.pages_widget.currentWidget() else: widget = self.pages_widget.widget(index) return widget.widget() @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.pages_widget.count()): configpage = self.get_page(index) if not configpage.is_valid(): return configpage.apply_changes() QDialog.accept(self) def button_clicked(self, button): if button is self.apply_btn: # Apply button was clicked configpage = self.get_page() if not configpage.is_valid(): return configpage.apply_changes() def current_page_changed(self, index): widget = self.get_page(index) self.apply_btn.setVisible(widget.apply_callback is not None) self.apply_btn.setEnabled(widget.is_modified) def add_page(self, widget): self.check_settings.connect(widget.check_settings) widget.show_this_page.connect(lambda row=self.contents_widget.count(): self.contents_widget.setCurrentRow(row)) widget.apply_button_enabled.connect(self.apply_btn.setEnabled) scrollarea = QScrollArea(self) scrollarea.setWidgetResizable(True) scrollarea.setWidget(widget) self.pages_widget.addWidget(scrollarea) item = QListWidgetItem(self.contents_widget) try: item.setIcon(widget.get_icon()) except TypeError: pass item.setText(widget.get_name()) item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) item.setSizeHint(QSize(0, 25)) def check_all_settings(self): """This method is called to check all configuration page settings after configuration dialog has been shown""" self.check_settings.emit() def resizeEvent(self, event): """ Reimplement Qt method to be able to save the widget's size from the main application """ QDialog.resizeEvent(self, event) self.size_change.emit(self.size())
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 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 AddFiles(QWidget): """Docstring for AddFiles. """ file_list_changed = Signal(set) def __init__(self, settings: BaseSettings, parent=None, btn_layout=QHBoxLayout): """TODO: to be defined1. """ QWidget.__init__(self, parent) self.settings = settings self.files_to_proceed = set() self.paths = QLineEdit(self) self.selected_files = QListWidget(self) self.selected_files.itemSelectionChanged.connect(self.file_chosen) self.found_button = QPushButton("Find all", self) self.found_button.clicked.connect(self.find_all) self.select_files_button = QPushButton("Select files") self.select_dir_button = QPushButton("Select directory") self.select_files_button.clicked.connect(self.select_files) self.select_dir_button.clicked.connect(self.select_directory) self.delete_button = QPushButton("Remove file", self) self.delete_button.setDisabled(True) self.delete_button.clicked.connect(self.delete_element) self.clean_button = QPushButton("Remove all", self) self.clean_button.clicked.connect(self.clean) layout = QVBoxLayout() layout.addWidget(self.paths) select_layout = btn_layout() select_layout.addWidget(self.select_files_button) select_layout.addWidget(self.select_dir_button) select_layout.addWidget(self.found_button) select_layout.addStretch() select_layout.addWidget(self.clean_button) select_layout.addWidget(self.delete_button) layout.addLayout(select_layout) layout.addWidget(self.selected_files) self.setLayout(layout) def find_all(self): paths = glob(str(self.paths.text())) paths = sorted([ x for x in (set(paths) - self.files_to_proceed) if not os.path.isdir(x) ]) if len(paths) > 0: dialog = AcceptFiles(paths) if dialog.exec_(): new_paths = dialog.get_files() for path in new_paths: size = os.stat(path).st_size size = float(size) / (1024**2) lwi = QListWidgetItem("{:s} ({:.2f} MB)".format( path, size)) lwi.setTextAlignment(Qt.AlignRight) self.selected_files.addItem(lwi) self.files_to_proceed.update(new_paths) self.file_list_changed.emit(self.files_to_proceed) else: QMessageBox.warning(self, "No new files", "No new files found", QMessageBox.Ok) def select_files(self): dial = QFileDialog(self, "Select files") dial.setDirectory( self.settings.get( "io.batch_directory", self.settings.get("io.load_image_directory", str(Path.home())))) dial.setFileMode(QFileDialog.ExistingFiles) if dial.exec_(): self.settings.set("io.batch_directory", os.path.dirname(str(dial.selectedFiles()[0]))) new_paths = sorted( set(map(str, dial.selectedFiles())) - self.files_to_proceed) for path in new_paths: size = os.stat(path).st_size size = float(size) / (1024**2) lwi = QListWidgetItem("{:s} ({:.2f} MB)".format(path, size)) lwi.setTextAlignment(Qt.AlignRight) self.selected_files.addItem(lwi) self.files_to_proceed.update(new_paths) self.file_list_changed.emit(self.files_to_proceed) def select_directory(self): dial = QFileDialog(self, "Select directory") dial.setDirectory( self.settings.get( "io.batch_directory", self.settings.get("io.load_image_directory", str(Path.home())))) dial.setFileMode(QFileDialog.Directory) if dial.exec_(): self.paths.setText(dial.selectedFiles()[0]) self.settings.set("io.batch_directory", str(dial.selectedFiles()[0])) def file_chosen(self): self.delete_button.setEnabled(True) def delete_element(self): item = self.selected_files.takeItem(self.selected_files.currentRow()) path = str(item.text()) path = path[:path.rfind("(") - 1] self.files_to_proceed.remove(path) self.file_list_changed.emit(self.files_to_proceed) if self.selected_files.count() == 0: self.delete_button.setDisabled(True) def clean(self): self.selected_files.clear() self.files_to_proceed.clear() self.file_list_changed.emit(self.files_to_proceed) def get_paths(self): return list(sorted(self.files_to_proceed))
class FileSwitcher(QDialog): """A Sublime-like file switcher.""" sig_goto_file = Signal(int) sig_close_file = Signal(int) # 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] def __init__(self, parent, tabs, data): QDialog.__init__(self, parent) # Variables self.tabs = tabs # Editor stack tabs self.data = data # Editor data 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_editor = 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-z]{0,100}:{0,1}[0-9]{0,100})") # Widgets self.edit = QLineEdit(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) # Setup self.save_initial_state() self.set_dialog_position() self.setup() # --- Properties @property def editors(self): return [self.tabs.widget(index) for index in range(self.tabs.count())] @property def line_count(self): return [editor.get_line_count() for editor in self.editors] @property def save_status(self): return [getattr(td, 'newly_created', False) for td in self.data] @property def paths(self): return [getattr(td, 'filename', None) for td in self.data] @property def filenames(self): return [self.tabs.tabText(index) for index in range(self.tabs.count())] @property def current_path(self): return self.paths_by_editor[self.get_editor()] @property def paths_by_editor(self): return dict(zip(self.editors, self.paths)) @property def editors_by_path(self): return dict(zip(self.paths, self.editors)) @property def filter_text(self): """Get the normalized (lowecase) content of the filter text.""" return to_text_string(self.edit.text()).lower() def save_initial_state(self): """Saves initial cursors and initial active editor.""" paths = self.paths self.initial_editor = self.get_editor() self.initial_cursors = {} for i, editor in enumerate(self.editors): if editor is self.initial_editor: self.initial_path = paths[i] self.initial_cursors[paths[i]] = editor.textCursor() 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 editors = self.editors_by_path for path in self.initial_cursors: cursor = self.initial_cursors[path] if path in editors: self.set_editor_cursor(editors[path], cursor) if self.initial_editor in self.paths_by_editor: index = self.paths.index(self.initial_path) self.sig_goto_file.emit(index) def set_dialog_position(self): """Positions the file switcher dialog in the center of the editor.""" parent = self.parent() geo = parent.geometry() width = self.list.width() # This has been set in setup left = parent.geometry().width()/2 - width/2 top = 0 while parent: geo = parent.geometry() top += geo.top() left += geo.left() parent = parent.parent() # Note: the +1 pixel on the top makes it look better self.move(left, top + self.tabs.tabBar().geometry().height() + 1) def fix_size(self, content, extra=50): """Adjusts the width of the file switcher, based on the content.""" # Update size of dialog based on longest shortened path strings = [] if content: for rich_text in content: label = QLabel(rich_text) label.setTextFormat(Qt.PlainText) strings.append(label.text()) fm = label.fontMetrics() max_width = max([fm.width(s)*1.3 for s in strings]) self.list.setMinimumWidth(max_width + extra) self.set_dialog_position() # --- 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.""" self.select_row(-1) def next_row(self): """Select next row in list widget.""" self.select_row(+1) # --- Helper methods: Editor def get_editor(self, index=None, path=None): """Get editor by index or path. If no path or index specified the current active editor is returned """ if index: return self.tabs.widget(index) elif path: return self.tabs.widget(index) else: return self.parent().get_current_editor() 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_editor() editor.go_to_line(min(line_number, editor.get_line_count())) # --- Helper methods: Outline explorer def get_symbol_list(self): """Get the object explorer data.""" return self.get_editor().highlighter.get_outlineexplorer_data() # --- Handlers def item_selection_changed(self): """List widget item selection change handler.""" row = self.current_row() if self.count() and row >= 0: if self.mode == self.FILE_MODE: try: stack_index = self.paths.index(self.filtered_path[row]) self.sig_goto_file.emit(stack_index) self.goto_line(self.line_number) 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 results = [] trying_for_line_number = ':' in filter_text # Get optional line number if trying_for_line_number: filter_text, line_number = filter_text.split(':') 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>") # 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 + '</big>' if trying_for_line_number: text_item += " [{0:} {1:}]".format(self.line_count[index], _("lines")) text_item += "<br><i>{0:}</i>".format( short_paths[index]) results.append((score_value, index, text_item)) # Sort the obtained scores and populate the list widget self.filtered_path = [] for result in sorted(results): index = result[1] text = result[-1] path = paths[index] item = QListWidgetItem(self.tabs.tabIcon(index), text) item.setToolTip(path) item.setSizeHint(QSize(0, 25)) self.list.addItem(item) self.filtered_path.append(path) # 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) self.fix_size(short_paths) # 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) 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_1 = '<code>{0}<big>{1} {2}</big></code>' template_2 = '<br><code>{0}</code><i>[Line {1}]</i>' 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_1.format(fold_space, token, rich_text) textline += template_2.format(fold_space, line_number) item = QListWidgetItem(icon, textline) item.setSizeHint(QSize(0, 16)) self.list.addItem(item) # 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) # Update list size self.fix_size(lines, extra=125) def setup(self): """Setup list widget content.""" if not self.tabs.count(): 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)
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 ConfigDialog(QDialog): """Spyder configuration ('Preferences') dialog box""" # Signals check_settings = Signal() size_change = Signal(QSize) def __init__(self, parent=None): QDialog.__init__(self, parent) self.main = parent # Widgets self.pages_widget = QStackedWidget() self.pages_widget.setMinimumWidth(600) self.contents_widget = QListWidget() self.button_reset = QPushButton(_('Reset to defaults')) bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply | QDialogButtonBox.Cancel) self.apply_btn = bbox.button(QDialogButtonBox.Apply) self.ok_btn = bbox.button(QDialogButtonBox.Ok) # Widgets 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(_('Preferences')) self.setWindowIcon(ima.icon('configure')) self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(1) self.contents_widget.setCurrentRow(0) self.contents_widget.setMinimumWidth(220) self.contents_widget.setMinimumHeight(400) # Layout hsplitter = QSplitter() hsplitter.addWidget(self.contents_widget) hsplitter.addWidget(self.pages_widget) hsplitter.setStretchFactor(0, 1) hsplitter.setStretchFactor(1, 2) btnlayout = QHBoxLayout() btnlayout.addWidget(self.button_reset) btnlayout.addStretch(1) btnlayout.addWidget(bbox) vlayout = QVBoxLayout() vlayout.addWidget(hsplitter) vlayout.addLayout(btnlayout) self.setLayout(vlayout) # Signals and slots if self.main: self.button_reset.clicked.connect(self.main.reset_spyder) self.pages_widget.currentChanged.connect(self.current_page_changed) self.contents_widget.currentRowChanged.connect( self.pages_widget.setCurrentIndex) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) bbox.clicked.connect(self.button_clicked) # Ensures that the config is present on spyder first run CONF.set('main', 'interface_language', load_lang_conf()) def get_current_index(self): """Return current page index""" return self.contents_widget.currentRow() def set_current_index(self, index): """Set current page index""" self.contents_widget.setCurrentRow(index) def get_page(self, index=None): """Return page widget""" if index is None: widget = self.pages_widget.currentWidget() else: widget = self.pages_widget.widget(index) if widget: return widget.widget() def get_index_by_name(self, name): """Return page index by CONF_SECTION name.""" for idx in range(self.pages_widget.count()): widget = self.pages_widget.widget(idx) widget = widget.widget() if widget.CONF_SECTION == name: return idx else: return None @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.pages_widget.count()): configpage = self.get_page(index) if not configpage.is_valid(): return configpage.apply_changes() QDialog.accept(self) def button_clicked(self, button): if button is self.apply_btn: # Apply button was clicked configpage = self.get_page() if not configpage.is_valid(): return configpage.apply_changes() def current_page_changed(self, index): widget = self.get_page(index) self.apply_btn.setVisible(widget.apply_callback is not None) self.apply_btn.setEnabled(widget.is_modified) def add_page(self, widget): self.check_settings.connect(widget.check_settings) widget.show_this_page.connect(lambda row=self.contents_widget.count(): self.contents_widget.setCurrentRow(row)) widget.apply_button_enabled.connect(self.apply_btn.setEnabled) scrollarea = QScrollArea(self) scrollarea.setWidgetResizable(True) scrollarea.setWidget(widget) self.pages_widget.addWidget(scrollarea) item = QListWidgetItem(self.contents_widget) try: item.setIcon(widget.get_icon()) except TypeError: pass item.setText(widget.get_name()) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) item.setSizeHint(QSize(0, 25)) def check_all_settings(self): """This method is called to check all configuration page settings after configuration dialog has been shown""" self.check_settings.emit() def resizeEvent(self, event): """ Reimplement Qt method to be able to save the widget's size from the main application """ QDialog.resizeEvent(self, event) self.size_change.emit(self.size())
class ConfigDialog(QDialog): """Configuration or preferences dialog box""" # Signals check_settings = Signal() size_change = Signal(QSize) def __init__(self, parent=None, objname=None): QDialog.__init__(self, parent) # If used for data object in tree, the main is the tree widget. self.parent = parent self.objname = objname # Widgets self.pages_widget = QStackedWidget() self.contents_widget = QListWidget() self.button_reset = QPushButton(_('Reset to defaults')) bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply | QDialogButtonBox.Cancel) self.apply_btn = bbox.button(QDialogButtonBox.Apply) # Widgets 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 Ezcad), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) if self.objname is None: self.setWindowTitle(_('Preferences')) else: self.setWindowTitle(_('Preferences of ') + self.objname) self.setWindowIcon(ima.icon('configure')) self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(1) self.contents_widget.setCurrentRow(0) # Layout hsplitter = QSplitter() hsplitter.addWidget(self.contents_widget) hsplitter.addWidget(self.pages_widget) hsplitter.setSizes([150,500]) btnlayout = QHBoxLayout() btnlayout.addWidget(self.button_reset) btnlayout.addStretch(1) btnlayout.addWidget(bbox) vlayout = QVBoxLayout() vlayout.addWidget(hsplitter) vlayout.addLayout(btnlayout) self.setLayout(vlayout) # Signals and slots self.pages_widget.currentChanged.connect(self.current_page_changed) self.contents_widget.currentRowChanged.connect( self.pages_widget.setCurrentIndex) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) bbox.clicked.connect(self.button_clicked) # Ensures that the config is present on ezcad first run CONF.set('main', 'interface_language', load_lang_conf()) def get_current_index(self): """Return current page index""" return self.contents_widget.currentRow() def set_current_index(self, index): """Set current page index""" self.contents_widget.setCurrentRow(index) def get_page(self, index=None): """Return page widget""" if index is None: widget = self.pages_widget.currentWidget() else: widget = self.pages_widget.widget(index) return widget.widget() @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.pages_widget.count()): configpage = self.get_page(index) configpage.apply_changes() QDialog.accept(self) def button_clicked(self, button): if button is self.apply_btn: # Apply button was clicked configpage = self.get_page() configpage.apply_changes() def current_page_changed(self, index): # widget = self.get_page(index) self.apply_btn.setVisible(True) self.apply_btn.setEnabled(True) def add_page(self, widget): scrollarea = QScrollArea(self) scrollarea.setWidgetResizable(True) scrollarea.setWidget(widget) self.pages_widget.addWidget(scrollarea) item = QListWidgetItem(self.contents_widget) try: item.setIcon(widget.get_icon()) except TypeError: pass item.setText(widget.get_name()) item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) item.setSizeHint(QSize(0, 25)) def check_all_settings(self): """This method is called to check all configuration page settings after configuration dialog has been shown""" self.check_settings.emit() def resizeEvent(self, event): """ Reimplement Qt method to be able to save the widget's size from the main application """ QDialog.resizeEvent(self, event) self.size_change.emit(self.size())