def test_focus(qtbot): """Check that window manager is working""" line_edit = QLineEdit() qtbot.addWidget(line_edit) if PYQT5: with qtbot.waitExposed(line_edit): # Supported only by PyQt5 line_edit.show() else: qtbot.waitForWindowShown(line_edit) # Works with Pyside2 line_edit.show() line_edit.setFocus() qtbot.waitUntil(lambda: line_edit.hasFocus()) assert line_edit.hasFocus()
def startEdit(self, tabIndex: int) -> None: self.editingTabIndex = tabIndex rect: QRect = self.tabRect(tabIndex) topMargin = 3 leftMargin = 6 lineEdit = QLineEdit(self) lineEdit.setAlignment(Qt.AlignCenter) lineEdit.move(rect.left() + leftMargin, rect.top() + topMargin) lineEdit.resize(rect.width() - 2 * leftMargin, rect.height() - 2 * topMargin) lineEdit.setText(self.tabText(tabIndex)) lineEdit.selectAll() lineEdit.setFocus() lineEdit.show() lineEdit.editingFinished.connect(self.finishEdit) self.lineEdit = lineEdit
class EditableLineEdit(QWidget): """ """ sig_text_changed = Signal(object, object) # old_text, new_text def __init__(self, title, text, regex=None, allow_empty=False): super(EditableLineEdit, self).__init__() self._label = QLabel(title) self._text = QLineEdit() self.button_edit = QPushButton() self.allow_empty = allow_empty self.regex = regex self.qregex = None self.button_edit.setIcon(qta.icon('fa.edit')) self._text.setText(text) layout = QVBoxLayout() layout.addWidget(self._label) layout_h = QHBoxLayout() layout_h.addWidget(self._text) layout_h.addWidget(self.button_edit) layout.addLayout(layout_h) self.setLayout(layout) self._text.setDisabled(True) self.button_edit.clicked.connect(self.edit) self.last_text = self._text.text() self.set_regex(regex) # def focusOutEvent(self, event): # """ # Qt override. # FIXME: # """ # super(EditableLineEdit, self).focusOutEvent(event) # event = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_Escape) # self.keyPressEvent(event) def keyPressEvent(self, event): """ Qt override. """ super(EditableLineEdit, self).keyPressEvent(event) key = event.key() if key in [Qt.Key_Enter, Qt.Key_Return]: self.check_text() elif key in [Qt.Key_Escape]: self._text.setText(self.last_text) self.check_text(escaped=True) # --- Public API # ------------------------------------------------------------------------- def text(self): return self._text.text() def setText(self, text): self.set_text(text) def set_text(self, text): """ """ self._text.setText(text) def set_label_text(self, text): """ """ self.label.setText(text) def set_regex(self, regex): """ """ if regex: self.regex = regex self.qregex = QRegExp(regex) validator = QRegExpValidator(self.qregex) self._text.setValidator(validator) def check_text(self, escaped=False): """ """ self._text.setDisabled(True) self.button_edit.setDisabled(False) new_text = self._text.text() if not self.allow_empty and len(new_text) == 0: self.edit() if self.last_text != new_text and not escaped: self.sig_text_changed.emit(self.last_text, new_text) self.last_text = new_text def edit(self): """ """ self._text.setDisabled(False) self.button_edit.setDisabled(True) self._text.setFocus() self._text.setCursorPosition(len(self._text.text())) self.last_text = self._text.text()
class SpyderErrorDialog(QDialog): """Custom error dialog for error reporting.""" def __init__(self, parent=None, is_report=False): QDialog.__init__(self, parent) self.is_report = is_report self.setWindowTitle(_("Issue reporter")) # To save the traceback sent to the internal console self.error_traceback = "" # Dialog main label if self.is_report: title = _("Please fill the following information") else: title = _("Spyder has encountered an internal problem!") main_label = QLabel( _("<h3>{title}</h3>" "Before reporting this problem, <i>please</i> consult our " "comprehensive " "<b><a href=\"{trouble_url}\">Troubleshooting Guide</a></b> " "which should help solve most issues, and search for " "<b><a href=\"{project_url}\">known bugs</a></b> " "matching your error message or problem description for a " "quicker solution.").format(title=title, trouble_url=__trouble_url__, project_url=__project_url__)) main_label.setOpenExternalLinks(True) main_label.setWordWrap(True) main_label.setAlignment(Qt.AlignJustify) main_label.setStyleSheet('font-size: 12px;') # Issue title self.title = QLineEdit() self.title.textChanged.connect(self._contents_changed) self.title_chars_label = QLabel( _("{} more characters " "to go...").format(TITLE_MIN_CHARS)) form_layout = QFormLayout() form_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) red_asterisk = '<font color="Red">*</font>' title_label = QLabel(_("<b>Title</b>: {}").format(red_asterisk)) form_layout.setWidget(0, QFormLayout.LabelRole, title_label) form_layout.setWidget(0, QFormLayout.FieldRole, self.title) # Description steps_header = QLabel( _("<b>Steps to reproduce:</b> {}").format(red_asterisk)) steps_text = QLabel( _("Please enter a detailed step-by-step " "description (in English) of what led up to " "the problem below. Issue reports without a " "clear way to reproduce them will be closed.")) steps_text.setWordWrap(True) steps_text.setAlignment(Qt.AlignJustify) steps_text.setStyleSheet('font-size: 12px;') # Field to input the description of the problem self.input_description = DescriptionWidget(self) # Only allow to submit to Github if we have a long enough description self.input_description.textChanged.connect(self._contents_changed) # Widget to show errors self.details = ShowErrorWidget(self) self.details.set_pythonshell_font(get_font()) self.details.hide() # Label to show missing chars self.initial_chars = len(self.input_description.toPlainText()) self.desc_chars_label = QLabel( _("{} more characters " "to go...").format(DESC_MIN_CHARS)) # Checkbox to dismiss future errors self.dismiss_box = QCheckBox( _("Hide all future errors during this " "session")) if self.is_report: self.dismiss_box.hide() # Dialog buttons gh_icon = ima.icon('github') self.submit_btn = QPushButton(gh_icon, _('Submit to Github')) self.submit_btn.setEnabled(False) self.submit_btn.clicked.connect(self._submit_to_github) self.details_btn = QPushButton(_('Show details')) self.details_btn.clicked.connect(self._show_details) if self.is_report: self.details_btn.hide() self.close_btn = QPushButton(_('Close')) if self.is_report: self.close_btn.clicked.connect(self.reject) # Buttons layout buttons_layout = QHBoxLayout() buttons_layout.addWidget(self.submit_btn) buttons_layout.addWidget(self.details_btn) buttons_layout.addWidget(self.close_btn) # Main layout layout = QVBoxLayout() layout.addWidget(main_label) layout.addSpacing(20) layout.addLayout(form_layout) layout.addWidget(self.title_chars_label) layout.addSpacing(12) layout.addWidget(steps_header) layout.addSpacing(-1) layout.addWidget(steps_text) layout.addSpacing(1) layout.addWidget(self.input_description) layout.addWidget(self.details) layout.addWidget(self.desc_chars_label) layout.addSpacing(15) layout.addWidget(self.dismiss_box) layout.addSpacing(15) layout.addLayout(buttons_layout) layout.setContentsMargins(25, 20, 25, 10) self.setLayout(layout) self.resize(570, 600) self.title.setFocus() # Set Tab key focus order self.setTabOrder(self.title, self.input_description) def _submit_to_github(self): """Action to take when pressing the submit button.""" # Get reference to the main window if self.parent() is not None: if getattr(self.parent(), 'main', False): # This covers the case when the dialog is attached # to the internal console main = self.parent().main else: # Else the dialog is attached to the main window # directly main = self.parent() else: main = None # Getting description and traceback title = self.title.text() description = self.input_description.toPlainText() traceback = self.error_traceback[:-1] # Remove last EOL # Render issue if main is not None: issue_text = main.render_issue(description=description, traceback=traceback) else: issue_text = description try: if main is None: org = 'ccordoba12' else: org = 'spyder-ide' github_backend = GithubBackend(org, 'spyder', parent_widget=main) github_report = github_backend.send_report(title, issue_text) if github_report: self.close() except Exception: ret = QMessageBox.question( self, _('Error'), _("An error occurred while trying to send the issue to " "Github automatically. Would you like to open it " "manually?<br><br>" "If so, please make sure to paste your clipboard " "into the issue report box that will appear in a new " "browser tab before clicking <i>Submit</i> on that " "page.")) if ret in [QMessageBox.Yes, QMessageBox.Ok]: QApplication.clipboard().setText(issue_text) issue_body = ( " \n<!--- *** BEFORE SUBMITTING: PASTE CLIPBOARD HERE " "TO COMPLETE YOUR REPORT *** ---!>\n") if main is not None: main.report_issue(body=issue_body, title=title, open_webpage=True) else: pass def append_traceback(self, text): """Append text to the traceback, to be displayed in details.""" self.error_traceback += text def _show_details(self): """Show traceback on its own dialog""" if self.details.isVisible(): self.details.hide() self.details_btn.setText(_('Show details')) else: self.resize(570, 700) self.details.document().setPlainText('') self.details.append_text_to_shell(self.error_traceback, error=True, prompt=False) self.details.show() self.details_btn.setText(_('Hide details')) def _contents_changed(self): """Activate submit_btn.""" desc_chars = (len(self.input_description.toPlainText()) - self.initial_chars) if desc_chars < DESC_MIN_CHARS: self.desc_chars_label.setText(u"{} {}".format( DESC_MIN_CHARS - desc_chars, _("more characters to go..."))) else: self.desc_chars_label.setText(_("Description complete; thanks!")) title_chars = len(self.title.text()) if title_chars < TITLE_MIN_CHARS: self.title_chars_label.setText(u"{} {}".format( TITLE_MIN_CHARS - title_chars, _("more characters to go..."))) else: self.title_chars_label.setText(_("Title complete; thanks!")) submission_enabled = (desc_chars >= DESC_MIN_CHARS and title_chars >= TITLE_MIN_CHARS) self.submit_btn.setEnabled(submission_enabled) def set_title(self, title): """Set the title for the report.""" self.title.setText(title) def set_description(self, description): """Set the description for the report.""" self.input_description.setPlainText(description) def set_color_scheme(self, color_scheme): """Set the color scheme for the description input.""" self.input_description.set_color_scheme(color_scheme)
class DlgGitHubLogin(QDialog): """Dialog to submit error reports to Github.""" def __init__(self, parent, username): super(DlgGitHubLogin, self).__init__(parent) title = _("Sign in to Github") self.resize(366, 248) self.setWindowTitle(title) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) # Header html = ('<html><head/><body><p align="center"><img src="{mark}"/></p>' '<p align="center">{title}</p></body></html>') mark = GH_MARK_NORMAL if self.palette().base().color().lightness() < 128: mark = GH_MARK_LIGHT lbl_html = QLabel(html.format(mark=mark, title=title)) # Tabs tabs = QTabWidget() # Basic form layout basic_form_layout = QFormLayout() basic_form_layout.setContentsMargins(-1, 0, -1, -1) lbl_user = QLabel(_("Username:"******"Password: "******"Basic authentication")) # Token form layout token_form_layout = QFormLayout() token_form_layout.setContentsMargins(-1, 0, -1, -1) lbl_token = QLabel("Token: ") token_form_layout.setWidget(1, QFormLayout.LabelRole, lbl_token) self.le_token = QLineEdit() self.le_token.setEchoMode(QLineEdit.Password) self.le_token.textChanged.connect(self.update_btn_state) token_form_layout.setWidget(1, QFormLayout.FieldRole, self.le_token) # Token auth tab token_auth = QWidget() token_layout = QVBoxLayout() token_layout.addLayout(token_form_layout) token_layout.addStretch(1) token_auth.setLayout(token_layout) tabs.addTab(token_auth, _("Token authentication")) # Sign in button self.bt_sign_in = QPushButton(_("Sign in")) self.bt_sign_in.clicked.connect(self.accept) self.bt_sign_in.setDisabled(True) # Main layout layout = QVBoxLayout() layout.addWidget(lbl_html) layout.addWidget(tabs) layout.addWidget(self.bt_sign_in) self.setLayout(layout) # Final adjustments if username: self.le_user.setText(username) self.le_password.setFocus() else: self.le_user.setFocus() self.setFixedSize(self.width(), self.height()) self.le_password.installEventFilter(self) self.le_user.installEventFilter(self) def eventFilter(self, obj, event): interesting_objects = [self.le_password, self.le_user] if obj in interesting_objects and event.type() == QEvent.KeyPress: if (event.key() == Qt.Key_Return and event.modifiers() & Qt.ControlModifier and self.bt_sign_in.isEnabled()): self.accept() return True return False def update_btn_state(self): user = to_text_string(self.le_user.text()).strip() != '' password = to_text_string(self.le_password.text()).strip() != '' token = to_text_string(self.le_token.text()).strip() != '' enable = (user and password) or token self.bt_sign_in.setEnabled(enable) @classmethod def login(cls, parent, username): dlg = DlgGitHubLogin(parent, username) if dlg.exec_() == dlg.Accepted: user = dlg.le_user.text() password = dlg.le_password.text() token = dlg.le_token.text() if token != '': return (token, ) else: return user, password return None, None
class QtPluginDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.refresh_state = RefreshState.DONE self.already_installed = set() installer_type = "mamba" if running_as_constructor_app() else "pip" self.installer = Installer(installer=installer_type) self.setup_ui() self.installer.set_output_widget(self.stdout_text) self.installer.started.connect(self._on_installer_start) self.installer.finished.connect(self._on_installer_done) self.refresh() def _on_installer_start(self): self.cancel_all_btn.setVisible(True) self.working_indicator.show() self.process_error_indicator.hide() self.close_btn.setDisabled(True) def _on_installer_done(self, exit_code): self.working_indicator.hide() if exit_code: self.process_error_indicator.show() self.cancel_all_btn.setVisible(False) self.close_btn.setDisabled(False) self.refresh() def closeEvent(self, event): if self.close_btn.isEnabled(): super().closeEvent(event) event.ignore() def refresh(self): if self.refresh_state != RefreshState.DONE: self.refresh_state = RefreshState.OUTDATED return self.refresh_state = RefreshState.REFRESHING self.installed_list.clear() self.available_list.clear() # fetch installed from npe2 import PluginManager from ...plugins import plugin_manager plugin_manager.discover() # since they might not be loaded yet self.already_installed = set() def _add_to_installed(distname, enabled, npe_version=1): norm_name = normalized_name(distname or '') if distname: try: meta = metadata(distname) except PackageNotFoundError: self.refresh_state = RefreshState.OUTDATED return # a race condition has occurred and the package is uninstalled by another thread if len(meta) == 0: # will not add builtins. return self.already_installed.add(norm_name) else: meta = {} self.installed_list.addItem( PackageMetadata( metadata_version="1.0", name=norm_name, version=meta.get('version', ''), summary=meta.get('summary', ''), home_page=meta.get('url', ''), author=meta.get('author', ''), license=meta.get('license', ''), ), installed=True, enabled=enabled, npe_version=npe_version, ) pm2 = PluginManager.instance() for manifest in pm2.iter_manifests(): distname = normalized_name(manifest.name or '') if distname in self.already_installed or distname == 'napari': continue enabled = not pm2.is_disabled(manifest.name) _add_to_installed(distname, enabled, npe_version=2) for ( plugin_name, _mod_name, distname, ) in plugin_manager.iter_available(): # not showing these in the plugin dialog if plugin_name in ('napari_plugin_engine', ): continue if distname in self.already_installed: continue _add_to_installed(distname, not plugin_manager.is_blocked(plugin_name)) self.installed_label.setText( trans._( "Installed Plugins ({amount})", amount=len(self.already_installed), )) # fetch available plugins settings = get_settings() use_hub = (running_as_bundled_app() or running_as_constructor_app() or settings.plugins.plugin_api.name == "napari_hub") if use_hub: conda_forge = running_as_constructor_app() self.worker = create_worker(iter_hub_plugin_info, conda_forge=conda_forge) else: self.worker = create_worker(iter_napari_plugin_info) self.worker.yielded.connect(self._handle_yield) self.worker.finished.connect(self.working_indicator.hide) self.worker.finished.connect(self._update_count_in_label) self.worker.finished.connect(self._end_refresh) self.worker.start() def setup_ui(self): self.resize(1080, 640) vlay_1 = QVBoxLayout(self) self.h_splitter = QSplitter(self) vlay_1.addWidget(self.h_splitter) self.h_splitter.setOrientation(Qt.Horizontal) self.v_splitter = QSplitter(self.h_splitter) self.v_splitter.setOrientation(Qt.Vertical) self.v_splitter.setMinimumWidth(500) installed = QWidget(self.v_splitter) lay = QVBoxLayout(installed) lay.setContentsMargins(0, 2, 0, 2) self.installed_label = QLabel(trans._("Installed Plugins")) self.packages_filter = QLineEdit() self.packages_filter.setPlaceholderText(trans._("filter...")) self.packages_filter.setMaximumWidth(350) self.packages_filter.setClearButtonEnabled(True) mid_layout = QVBoxLayout() mid_layout.addWidget(self.packages_filter) mid_layout.addWidget(self.installed_label) lay.addLayout(mid_layout) self.installed_list = QPluginList(installed, self.installer) self.packages_filter.textChanged.connect(self.installed_list.filter) lay.addWidget(self.installed_list) uninstalled = QWidget(self.v_splitter) lay = QVBoxLayout(uninstalled) lay.setContentsMargins(0, 2, 0, 2) self.avail_label = QLabel(trans._("Available Plugins")) mid_layout = QHBoxLayout() mid_layout.addWidget(self.avail_label) mid_layout.addStretch() lay.addLayout(mid_layout) self.available_list = QPluginList(uninstalled, self.installer) self.packages_filter.textChanged.connect(self.available_list.filter) lay.addWidget(self.available_list) self.stdout_text = QTextEdit(self.v_splitter) self.stdout_text.setReadOnly(True) self.stdout_text.setObjectName("pip_install_status") self.stdout_text.hide() buttonBox = QHBoxLayout() self.working_indicator = QLabel(trans._("loading ..."), self) sp = self.working_indicator.sizePolicy() sp.setRetainSizeWhenHidden(True) self.working_indicator.setSizePolicy(sp) self.process_error_indicator = QLabel(self) self.process_error_indicator.setObjectName("error_label") self.process_error_indicator.hide() load_gif = str(Path(napari.resources.__file__).parent / "loading.gif") mov = QMovie(load_gif) mov.setScaledSize(QSize(18, 18)) self.working_indicator.setMovie(mov) mov.start() visibility_direct_entry = not running_as_constructor_app() self.direct_entry_edit = QLineEdit(self) self.direct_entry_edit.installEventFilter(self) self.direct_entry_edit.setPlaceholderText( trans._('install by name/url, or drop file...')) self.direct_entry_edit.setVisible(visibility_direct_entry) self.direct_entry_btn = QPushButton(trans._("Install"), self) self.direct_entry_btn.setVisible(visibility_direct_entry) self.direct_entry_btn.clicked.connect(self._install_packages) self.show_status_btn = QPushButton(trans._("Show Status"), self) self.show_status_btn.setFixedWidth(100) self.cancel_all_btn = QPushButton(trans._("cancel all actions"), self) self.cancel_all_btn.setObjectName("remove_button") self.cancel_all_btn.setVisible(False) self.cancel_all_btn.clicked.connect(lambda: self.installer.cancel()) self.close_btn = QPushButton(trans._("Close"), self) self.close_btn.clicked.connect(self.accept) self.close_btn.setObjectName("close_button") buttonBox.addWidget(self.show_status_btn) buttonBox.addWidget(self.working_indicator) buttonBox.addWidget(self.direct_entry_edit) buttonBox.addWidget(self.direct_entry_btn) if not visibility_direct_entry: buttonBox.addStretch() buttonBox.addWidget(self.process_error_indicator) buttonBox.addSpacing(20) buttonBox.addWidget(self.cancel_all_btn) buttonBox.addSpacing(20) buttonBox.addWidget(self.close_btn) buttonBox.setContentsMargins(0, 0, 4, 0) vlay_1.addLayout(buttonBox) self.show_status_btn.setCheckable(True) self.show_status_btn.setChecked(False) self.show_status_btn.toggled.connect(self._toggle_status) self.v_splitter.setStretchFactor(1, 2) self.h_splitter.setStretchFactor(0, 2) self.packages_filter.setFocus() def _update_count_in_label(self): count = self.available_list.count() self.avail_label.setText( trans._("Available Plugins ({count})", count=count)) def _end_refresh(self): refresh_state = self.refresh_state self.refresh_state = RefreshState.DONE if refresh_state == RefreshState.OUTDATED: self.refresh() def eventFilter(self, watched, event): if event.type() == QEvent.DragEnter: # we need to accept this event explicitly to be able # to receive QDropEvents! event.accept() if event.type() == QEvent.Drop: md = event.mimeData() if md.hasUrls(): files = [url.toLocalFile() for url in md.urls()] self.direct_entry_edit.setText(files[0]) return True return super().eventFilter(watched, event) def _toggle_status(self, show): if show: self.show_status_btn.setText(trans._("Hide Status")) self.stdout_text.show() else: self.show_status_btn.setText(trans._("Show Status")) self.stdout_text.hide() def _install_packages(self, packages: Sequence[str] = ()): if not packages: _packages = self.direct_entry_edit.text() if os.path.exists(_packages): packages = [_packages] else: packages = _packages.split() self.direct_entry_edit.clear() if packages: self.installer.install(packages) def _handle_yield(self, data: Tuple[PackageMetadata, bool]): project_info, is_available = data if project_info.name in self.already_installed: self.installed_list.tag_outdated(project_info, is_available) else: self.available_list.addItem(project_info) if not is_available: self.available_list.tag_unavailable(project_info) self.filter() def filter(self, text: str = None) -> None: """Filter by text or set current text as filter.""" if text is None: text = self.packages_filter.text() else: self.packages_filter.setText(text) self.installed_list.filter(text) self.available_list.filter(text)
class Switcher(QDialog): """ A multi purpose switcher. Example ------- SwitcherItem: [title description <shortcut> section] SwitcherItem: [title description <shortcut> section] SwitcherSeparator: [---------------------------------------] SwitcherItem: [title description <shortcut> section] SwitcherItem: [title description <shortcut> section] """ # Dismissed switcher sig_rejected = Signal() # Search/Filter text changes sig_text_changed = Signal(TEXT_TYPES[-1]) # Current item changed sig_item_changed = Signal(object) # List item selected, mode and cleaned search text sig_item_selected = Signal(object, TEXT_TYPES[-1], TEXT_TYPES[-1], ) sig_mode_selected = Signal(TEXT_TYPES[-1]) _MAX_NUM_ITEMS = 15 _MIN_WIDTH = 580 _MIN_HEIGHT = 200 _MAX_HEIGHT = 390 _ITEM_WIDTH = _MIN_WIDTH - 20 def __init__(self, parent, help_text=None, item_styles=ITEM_STYLES, item_separator_styles=ITEM_SEPARATOR_STYLES): """Multi purpose switcher.""" super(Switcher, self).__init__(parent) self._modes = {} self._mode_on = '' self._item_styles = item_styles self._item_separator_styles = item_separator_styles # Widgets self.edit = QLineEdit(self) self.list = QListView(self) self.model = QStandardItemModel(self.list) self.proxy = SwitcherProxyModel(self.list) self.filter = KeyPressFilter() # Widgets setup self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint) self.setWindowOpacity(0.95) # self.setMinimumHeight(self._MIN_HEIGHT) self.setMaximumHeight(self._MAX_HEIGHT) self.edit.installEventFilter(self.filter) self.edit.setPlaceholderText(help_text if help_text else '') self.list.setMinimumWidth(self._MIN_WIDTH) self.list.setItemDelegate(SwitcherDelegate(self)) self.list.setFocusPolicy(Qt.NoFocus) self.list.setSelectionBehavior(self.list.SelectItems) self.list.setSelectionMode(self.list.SingleSelection) self.list.setVerticalScrollMode(QAbstractItemView.ScrollPerItem) self.proxy.setSourceModel(self.model) self.list.setModel(self.proxy) # Layout layout = QVBoxLayout() layout.addWidget(self.edit) layout.addWidget(self.list) self.setLayout(layout) # Signals self.filter.sig_up_key_pressed.connect(self.previous_row) self.filter.sig_down_key_pressed.connect(self.next_row) self.filter.sig_enter_key_pressed.connect(self.enter) self.edit.textChanged.connect(self.setup) self.edit.textChanged.connect(self.sig_text_changed) self.edit.returnPressed.connect(self.enter) self.list.clicked.connect(self.enter) self.list.clicked.connect(self.edit.setFocus) self.list.selectionModel().currentChanged.connect( self.current_item_changed) self.edit.setFocus() # --- Helper methods def _add_item(self, item, last_item=True): """Perform common actions when adding items.""" item.set_width(self._ITEM_WIDTH) self.model.appendRow(item) if last_item: # Only set the current row to the first item when the added item is # the last one in order to prevent performance issues when # adding multiple items self.set_current_row(0) self.set_height() self.setup_sections() # --- API def clear(self): """Remove all items from the list and clear the search text.""" self.set_placeholder_text('') self.model.beginResetModel() self.model.clear() self.model.endResetModel() self.setMinimumHeight(self._MIN_HEIGHT) def set_placeholder_text(self, text): """Set the text appearing on the empty line edit.""" self.edit.setPlaceholderText(text) def add_mode(self, token, description): """Add mode by token key and description.""" if len(token) == 1: self._modes[token] = description else: raise Exception('Token must be of length 1!') def get_mode(self): """Get the current mode the switcher is in.""" return self._mode_on def remove_mode(self, token): """Remove mode by token key.""" if token in self._modes: self._modes.pop(token) def clear_modes(self): """Delete all modes spreviously defined.""" del self._modes self._modes = {} def add_item(self, icon=None, title=None, description=None, shortcut=None, section=None, data=None, tool_tip=None, action_item=False, last_item=True): """Add switcher list item.""" item = SwitcherItem( parent=self.list, icon=icon, title=title, description=description, data=data, shortcut=shortcut, section=section, action_item=action_item, tool_tip=tool_tip, styles=self._item_styles ) self._add_item(item, last_item=last_item) def add_separator(self): """Add separator item.""" item = SwitcherSeparatorItem(parent=self.list, styles=self._item_separator_styles) self._add_item(item) def setup(self): """Set-up list widget content based on the filtering.""" # Check exited mode mode = self._mode_on if mode: search_text = self.search_text()[len(mode):] else: search_text = self.search_text() # Check exited mode if self.search_text() == '': self._mode_on = '' self.clear() self.proxy.set_filter_by_score(False) self.sig_mode_selected.emit(self._mode_on) return # Check entered mode for key in self._modes: if self.search_text().startswith(key) and not mode: self._mode_on = key self.sig_mode_selected.emit(key) return # Filter by text titles = [] for row in range(self.model.rowCount()): item = self.model.item(row) if isinstance(item, SwitcherItem): title = item.get_title() else: title = '' titles.append(title) search_text = clean_string(search_text) scores = get_search_scores(to_text_string(search_text), titles, template=u"<b>{0}</b>") for idx, (title, rich_title, score_value) in enumerate(scores): item = self.model.item(idx) if not self._is_separator(item) and not item.is_action_item(): rich_title = rich_title.replace(" ", " ") item.set_rich_title(rich_title) item.set_score(score_value) self.proxy.set_filter_by_score(True) self.setup_sections() if self.count(): self.set_current_row(0) else: self.set_current_row(-1) self.set_height() def setup_sections(self): """Set-up which sections appear on the item list.""" mode = self._mode_on if mode: search_text = self.search_text()[len(mode):] else: search_text = self.search_text() if search_text: for row in range(self.model.rowCount()): item = self.model.item(row) if isinstance(item, SwitcherItem): item.set_section_visible(False) else: sections = [] for row in range(self.model.rowCount()): item = self.model.item(row) if isinstance(item, SwitcherItem): sections.append(item.get_section()) item.set_section_visible(bool(search_text)) else: sections.append('') if row != 0: visible = sections[row] != sections[row - 1] if not self._is_separator(item): item.set_section_visible(visible) else: item.set_section_visible(True) self.proxy.sortBy('_score') self.sig_item_changed.emit(self.current_item()) def set_height(self): """Set height taking into account the number of items.""" if self.count() >= self._MAX_NUM_ITEMS: switcher_height = self._MAX_HEIGHT elif self.count() != 0 and self.current_item(): current_item = self.current_item() item_height = current_item.get_height() list_height = item_height * (self.count() + 3) edit_height = self.edit.height() spacing_height = self.layout().spacing() * 4 switcher_height = list_height + edit_height + spacing_height switcher_height = max(switcher_height, self._MIN_HEIGHT) else: switcher_height = self._MIN_HEIGHT self.setFixedHeight(int(switcher_height)) def set_position(self, top): """Set the position of the dialog.""" parent = self.parent() if parent is not None: geo = parent.geometry() width = self.list.width() # This has been set in setup left = parent.geometry().width()/2 - width/2 while parent: geo = parent.geometry() top += geo.top() left += geo.left() parent = parent.parent() self.move(round(left), top) @Slot(QModelIndex, QModelIndex) def current_item_changed(self, current, previous): """Handle item selection.""" self.sig_item_changed.emit(self.current_item()) # --- Qt overrides # ------------------------------------------------------------------------ @Slot() @Slot(QListWidgetItem) def enter(self, itemClicked=None): """Override Qt method.""" row = self.current_row() model_index = self.proxy.mapToSource(self.proxy.index(row, 0)) item = self.model.item(model_index.row()) if item: mode = self._mode_on self.sig_item_selected.emit(item, mode, self.search_text()[len(mode):]) def accept(self): """Override Qt method.""" super(Switcher, self).accept() def reject(self): """Override Qt method.""" self.set_search_text('') self.sig_rejected.emit() super(Switcher, self).reject() def resizeEvent(self, event): """Override Qt method.""" super(Switcher, self).resizeEvent(event) # --- Helper methods: Lineedit widget def search_text(self): """Get the normalized (lowecase) content of the search text.""" return to_text_string(self.edit.text()).lower() def set_search_text(self, string): """Set the content of the search text.""" self.edit.setText(string) # --- Helper methods: List widget def _is_separator(self, item): """Check if item is an separator item (SwitcherSeparatorItem).""" return isinstance(item, SwitcherSeparatorItem) 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 count(self): """Get the item count in the list widget.""" return self.proxy.rowCount() def current_row(self): """Return the current selected row in the list widget.""" return self.list.currentIndex().row() def current_item(self): """Return the current selected item in the list widget.""" row = self.current_row() model_index = self.proxy.mapToSource(self.proxy.index(row, 0)) item = self.model.item(model_index.row()) return item def set_current_row(self, row): """Set the current selected row in the list widget.""" proxy_index = self.proxy.index(row, 0) selection_model = self.list.selectionModel() # https://doc.qt.io/qt-5/qitemselectionmodel.html#SelectionFlag-enum selection_model.setCurrentIndex( proxy_index, selection_model.ClearAndSelect) # Ensure that the selected item is visible self.list.scrollTo(proxy_index, QAbstractItemView.EnsureVisible) def previous_row(self): """Select previous row in list widget.""" steps = 1 prev_row = self.current_row() - steps if prev_row == -1: self.set_current_row(self.count() - 1) else: if prev_row >= 0: # Need to map the filtered list to the actual model items list_index = self.proxy.index(prev_row, 0) model_index = self.proxy.mapToSource(list_index) item = self.model.item(model_index.row(), 0) if self._is_separator(item): steps += 1 self._select_row(-steps) def next_row(self): """Select next row in list widget.""" steps = 1 next_row = self.current_row() + steps # Need to map the filtered list to the actual model items list_index = self.proxy.index(next_row, 0) model_index = self.proxy.mapToSource(list_index) item = self.model.item(model_index.row(), 0) if next_row >= self.count(): self.set_current_row(0) else: if item: if self._is_separator(item): steps += 1 self._select_row(steps)
class DlgGitHubLogin(QDialog): """Dialog to submit error reports to Github.""" def __init__(self, parent, username, password, token, remember=False, remember_token=False): QDialog.__init__(self, parent) title = _("Sign in to Github") self.resize(415, 375) self.setWindowTitle(title) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) # Header html = ('<html><head/><body><p align="center">' '{title}</p></body></html>') lbl_html = QLabel(html.format(title=title)) lbl_html.setStyleSheet('font-size: 16px;') # Tabs self.tabs = QTabWidget() # Basic form layout basic_form_layout = QFormLayout() basic_form_layout.setContentsMargins(-1, 0, -1, -1) basic_lbl_msg = QLabel( _("For regular users, i.e. users <b>without</b>" " two-factor authentication enabled")) basic_lbl_msg.setWordWrap(True) basic_lbl_msg.setAlignment(Qt.AlignJustify) lbl_user = QLabel(_("Username:"******"", QWidget()) lbl_password = QLabel(_("Password: "******"Remember me")) self.cb_remember.setToolTip( _("Spyder will save your credentials " "safely")) self.cb_remember.setChecked(remember) basic_form_layout.setWidget(4, QFormLayout.FieldRole, self.cb_remember) # Basic auth tab basic_auth = QWidget() basic_layout = QVBoxLayout() basic_layout.addSpacerItem(QSpacerItem(QSpacerItem(0, 8))) basic_layout.addWidget(basic_lbl_msg) basic_layout.addSpacerItem( QSpacerItem(QSpacerItem(0, 50, vPolicy=QSizePolicy.Expanding))) basic_layout.addLayout(basic_form_layout) basic_layout.addSpacerItem( QSpacerItem(QSpacerItem(0, 50, vPolicy=QSizePolicy.Expanding))) basic_auth.setLayout(basic_layout) self.tabs.addTab(basic_auth, _("Password Only")) # Token form layout token_form_layout = QFormLayout() token_form_layout.setContentsMargins(-1, 0, -1, -1) token_lbl_msg = QLabel( _("For users <b>with</b> two-factor " "authentication enabled, or who prefer a " "per-app token authentication.<br><br>" "You can go <b><a href=\"{}\">here</a></b> " "and click \"Generate token\" at the bottom " "to create a new token to use for this, with " "the appropriate permissions.").format(TOKEN_URL)) token_lbl_msg.setOpenExternalLinks(True) token_lbl_msg.setWordWrap(True) token_lbl_msg.setAlignment(Qt.AlignJustify) lbl_token = QLabel("Token: ") token_form_layout.setWidget(1, QFormLayout.LabelRole, lbl_token) self.le_token = QLineEdit() self.le_token.setEchoMode(QLineEdit.Password) self.le_token.textChanged.connect(self.update_btn_state) token_form_layout.setWidget(1, QFormLayout.FieldRole, self.le_token) self.cb_remember_token = None # Same validation as with cb_remember if self.is_keyring_available() and valid_py_os: self.cb_remember_token = QCheckBox(_("Remember token")) self.cb_remember_token.setToolTip( _("Spyder will save your " "token safely")) self.cb_remember_token.setChecked(remember_token) token_form_layout.setWidget(3, QFormLayout.FieldRole, self.cb_remember_token) # Token auth tab token_auth = QWidget() token_layout = QVBoxLayout() token_layout.addSpacerItem(QSpacerItem(QSpacerItem(0, 8))) token_layout.addWidget(token_lbl_msg) token_layout.addSpacerItem( QSpacerItem(QSpacerItem(0, 50, vPolicy=QSizePolicy.Expanding))) token_layout.addLayout(token_form_layout) token_layout.addSpacerItem( QSpacerItem(QSpacerItem(0, 50, vPolicy=QSizePolicy.Expanding))) token_auth.setLayout(token_layout) self.tabs.addTab(token_auth, _("Access Token")) # Sign in button self.bt_sign_in = QPushButton(_("Sign in")) self.bt_sign_in.clicked.connect(self.accept) self.bt_sign_in.setDisabled(True) # Main layout layout = QVBoxLayout() layout.addWidget(lbl_html) layout.addWidget(self.tabs) layout.addWidget(self.bt_sign_in) self.setLayout(layout) # Final adjustments if username and password: self.le_user.setText(username) self.le_password.setText(password) self.bt_sign_in.setFocus() elif username: self.le_user.setText(username) self.le_password.setFocus() elif token: self.le_token.setText(token) else: self.le_user.setFocus() self.setFixedSize(self.width(), self.height()) self.le_password.installEventFilter(self) self.le_user.installEventFilter(self) self.tabs.currentChanged.connect(self.update_btn_state) def eventFilter(self, obj, event): interesting_objects = [self.le_password, self.le_user] if obj in interesting_objects and event.type() == QEvent.KeyPress: if (event.key() == Qt.Key_Return and event.modifiers() & Qt.ControlModifier and self.bt_sign_in.isEnabled()): self.accept() return True return False def update_btn_state(self): user = to_text_string(self.le_user.text()).strip() != '' password = to_text_string(self.le_password.text()).strip() != '' token = to_text_string(self.le_token.text()).strip() != '' enable = ((user and password and self.tabs.currentIndex() == 0) or (token and self.tabs.currentIndex() == 1)) self.bt_sign_in.setEnabled(enable) def is_keyring_available(self): """Check if keyring is available for password storage.""" try: import keyring # analysis:ignore return True except Exception: return False @classmethod def login(cls, parent, username, password, token, remember, remember_token): dlg = DlgGitHubLogin(parent, username, password, token, remember, remember_token) if dlg.exec_() == dlg.Accepted: user = dlg.le_user.text() password = dlg.le_password.text() token = dlg.le_token.text() if dlg.cb_remember: remember = dlg.cb_remember.isChecked() else: remember = False if dlg.cb_remember_token: remember_token = dlg.cb_remember_token.isChecked() else: remember_token = False credentials = dict(username=user, password=password, token=token, remember=remember, remember_token=remember_token) return credentials return dict(username=None, password=None, token=None, remember=False, remember_token=False)
class AuthenticationDialog(DialogBase): FORGOT_USERNAME_URL = 'https://anaconda.org/account/forgot_username' FORGOT_PASWORD_URL = 'https://anaconda.org/account/forgot_password' REGISTER_URL = 'https://anaconda.org' def __init__(self, api, parent=None): super(AuthenticationDialog, self).__init__(parent) self.api = api self._parent = parent self.token = None self.error = None self.tracker = GATracker() # Widgets self.label_username = QLabel('Username:'******'Password:'******'You can register ') self.label_signin_text = QLabel('<hr><br><b>Already a member? ' 'Sign in!</b><br>') # For styling purposes the label next to a ButtonLink is also a button # so they align adequately self.button_register_text = ButtonLabel('You can register by ' 'visiting the') self.button_register = ButtonLink('Anaconda Cloud') self.button_register_after_text = ButtonLabel('website.') self.label_information = QLabel(''' <strong>Anaconda Cloud</strong> is where packages, notebooks, and <br> environments are shared. It provides powerful <br> collaboration and package management for open <br> source and private projects.<br> ''') self.label_message = QLabel('') self.button_forgot_username = ButtonLink('I forgot my username') self.button_forgot_password = ButtonLink('I forgot my password') self.button_login = QPushButton('Login') self.button_cancel = ButtonCancel('Cancel') self.bbox = QDialogButtonBox(Qt.Horizontal) # Widgets setup self.bbox.addButton(self.button_cancel, QDialogButtonBox.RejectRole) self.bbox.addButton(self.button_login, QDialogButtonBox.AcceptRole) self.text_username.setAttribute(Qt.WA_MacShowFocusRect, False) self.text_password.setAttribute(Qt.WA_MacShowFocusRect, False) self.setMinimumWidth(260) self.setWindowTitle('Sign in') # This allows to completely style the dialog with css using the frame self.text_password.setEchoMode(QLineEdit.Password) self.label_message.setVisible(False) # Layout grid_layout = QGridLayout() grid_layout.addWidget(self.label_username, 0, 0) grid_layout.addWidget(self.text_username, 0, 1) grid_layout.addWidget(self.label_password, 1, 0) grid_layout.addWidget(self.text_password, 1, 1) main_layout = QVBoxLayout() main_layout.addWidget(self.label_information) register_layout = QHBoxLayout() register_layout.addWidget(self.button_register_text, 0) register_layout.addWidget(self.button_register, 0, Qt.AlignLeft) register_layout.addWidget(self.button_register_after_text, 0, Qt.AlignLeft) register_layout.addStretch() register_layout.setContentsMargins(0, 0, 0, 0) main_layout.addLayout(register_layout) main_layout.addWidget(self.label_signin_text) main_layout.addLayout(grid_layout) main_layout.addSpacing(5) main_layout.addWidget(self.label_message) main_layout.addWidget(self.button_forgot_username, 0, Qt.AlignRight) main_layout.addWidget(self.button_forgot_password, 0, Qt.AlignRight) main_layout.addSpacing(15) main_layout.addWidget(self.bbox) self.setLayout(main_layout) # Signals self.button_forgot_username.clicked.connect( lambda: self.open_url(self.FORGOT_USERNAME_URL)) self.button_forgot_password.clicked.connect( lambda: self.open_url(self.FORGOT_PASWORD_URL)) self.button_register.clicked.connect( lambda: self.open_url(self.REGISTER_URL)) self.text_username.textEdited.connect(self.check_text) self.text_password.textEdited.connect(self.check_text) self.button_login.clicked.connect(self.login) self.button_cancel.clicked.connect(self.reject) # Setup self.check_text() self.update_style_sheet() self.text_username.setFocus() @property def username(self): return self.text_username.text() def update_style_sheet(self, style_sheet=None): if style_sheet is None: style_sheet = load_style_sheet() self.setStyleSheet(style_sheet) def check_text(self): """ Check that `username` and `password` are not empty and disabel/enable buttons accordingly. """ username = self.text_username.text() password = self.text_password.text() if len(username) == 0 or len(password) == 0: self.button_login.setDisabled(True) else: self.button_login.setDisabled(False) def login(self): self.button_login.setEnabled(False) QApplication.setOverrideCursor(Qt.WaitCursor) self.label_message.setText('') worker = self.api.client_login(self.text_username.text(), self.text_password.text(), 'Anaconda Navigator', '') worker.sig_finished.connect(self._finished) def _finished(self, worker, output, error): """ Method called when the anaconda-client Api has finished a process that runs in a separate worker thread. """ token = output if token: self.token = token self.accept() elif error: username = self.text_username.text() bold_username = '******'.format(username) # The error might come in (error_message, http_error) format try: error_message = eval(str(error))[0] except Exception: error_message = str(error) error_message = error_message.lower().capitalize() error_message = error_message.split(', ')[0] error_text = '<i>{0}</i>'.format(error_message) error_text = error_text.replace(username, bold_username) self.label_message.setText(error_text) self.label_message.setVisible(True) if error_message: domain = self.api.client_domain() label = '{0}/{1}: {2}'.format(domain, username, error_message.lower()) self.tracker.track_event('authenticate', 'login failed', label=label) self.text_password.setFocus() self.text_password.selectAll() self.button_login.setDisabled(False) self.check_text() QApplication.restoreOverrideCursor() def open_url(self, url): self.tracker.track_event('content', 'click', url) QDesktopServices.openUrl(QUrl(url))
class AppUpdateDialog(QDialog): def __init__(self, parent): super(AppUpdateDialog, self).__init__() self.parent = parent self.setWindowTitle(config.thisTranslation["App_Updater"]) self.layout = QVBoxLayout() self.latestVersion = UpdateUtil.getLatestVersion() self.currentVersion = UpdateUtil.getCurrentVersion() if not config.internet: error = QLabel( config.thisTranslation["Could_not_connect_to_internet"]) error.setStyleSheet("color: rgb(253, 128, 8);") self.layout.addWidget(error) else: if UpdateUtil.currentIsLatest(self.currentVersion, self.latestVersion): self.uptodate = True else: self.uptodate = False if not self.uptodate: self.layout.addWidget( QLabel("{0}: {1}".format( config.thisTranslation["Latest_version"], self.latestVersion))) self.layout.addWidget( QLabel("{0}: {1}".format( config.thisTranslation["Current_version"], self.currentVersion))) self.updateNowButton = QPushButton( config.thisTranslation["Update_now"]) self.updateNowButton.setEnabled(True) self.updateNowButton.clicked.connect(self.updateNow) if self.uptodate: ubaUptodate = QLabel(config.thisTranslation["UBA_is_uptodate"]) if config.theme == "dark": ubaUptodate.setStyleSheet("color: green;") else: ubaUptodate.setStyleSheet("color: blue;") self.layout.addWidget(ubaUptodate) else: self.layout.addWidget(self.updateNowButton) self.layout.addWidget( QLabel("{0}: {1}".format( config.thisTranslation["Last_check"], DateUtil.formattedLocalDate( UpdateUtil.lastAppUpdateCheckDateObject())))) self.layout.addWidget( QLabel("{0}: {1}".format( config.thisTranslation["Next_check"], DateUtil.formattedLocalDate( DateUtil.addDays( UpdateUtil.lastAppUpdateCheckDateObject(), int(config.daysElapseForNextAppUpdateCheck)))))) row = QHBoxLayout() row.addWidget( QLabel("{0}:".format( config.thisTranslation["Days_between_checks"]))) self.daysInput = QLineEdit() self.daysInput.setText(str(config.daysElapseForNextAppUpdateCheck)) self.daysInput.setMaxLength(3) self.daysInput.setMaximumWidth(40) row.addWidget(self.daysInput) self.layout.addLayout(row) buttons = QDialogButtonBox.Ok self.buttonBox = QDialogButtonBox(buttons) self.buttonBox.accepted.connect(self.setDaysElapse) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) self.layout.addWidget(self.buttonBox) self.setLayout(self.layout) if config.internet: self.updateNowButton.setFocus() if self.uptodate: self.daysInput.setFocus() else: # self.setTabOrder(self.updateNowButton, self.daysInput) # self.setTabOrder(self.daysInput, self.updateNowButton) self.updateNowButton.setFocus() def updateNow(self): debug = False self.updateNowButton.setText(config.thisTranslation["Updating"]) self.updateNowButton.setEnabled(False) UpdateUtil.updateUniqueBibleApp(self.parent, debug) self.close() def setDaysElapse(self): digits = TextUtil.getDigits(self.daysInput.text()) if digits == '': digits = '0' config.daysElapseForNextAppUpdateCheck = digits
class MainWindowTyping(QMainWindow): """Main Window.""" def __init__(self): """Main Window UI를 설정한다.""" super().__init__() self.setWindowTitle(f"Typing Number - {VER}") icon = QIcon() icon.addPixmap(QPixmap(r'ok_64x64.ico'), QIcon.Normal, QIcon.Off) self.setWindowIcon(icon) self.setMinimumSize(800, 100) # Typing Info self.typing = ManageTyping() # Setup StatusBar self.statusBar().showMessage("") self.statusBar().addPermanentWidget(VLine()) self.label_typing_avg = QLabel("평균타수 : 0.0타/초", self) self.statusBar().addPermanentWidget(self.label_typing_avg) self.statusBar().addPermanentWidget(VLine()) self.label_total_time = QLabel("총 연습시간", self) self.statusBar().addPermanentWidget(self.label_total_time) # Setup LineEdit self.line_for_number = QLineEdit(self) self.line_for_typing = QLineEdit(self) self.line_for_error = QLineEdit(self) self.setup_lineedit() vbox = QVBoxLayout() vbox.addWidget(self.line_for_number) vbox.addWidget(self.line_for_typing) vbox.addWidget(self.line_for_error) central_widget = QWidget(self) central_widget.setLayout(vbox) self.setCentralWidget(central_widget) self.new_game() def setup_lineedit(self): """LineEdit를 설정한다.""" mask = (("X" + " " * self.typing.n_space_between_chars) * self.typing.n_char_in_game) self.line_for_number.setInputMask(mask) self.line_for_typing.setInputMask(mask) self.line_for_error.setInputMask(mask) self.line_for_number.setReadOnly(True) self.line_for_error.setReadOnly(True) self.line_for_error.setStyleSheet( "QLineEdit { border:none; color: red; }") self.line_for_typing.textChanged.connect(self.update_typing) def display_result(self): """연습결과를 보여준다.""" typing_per_sec = self.typing.typing_per_sec_prev() typing_per_sec_avg = self.typing.typing_per_sec_avg() self.statusBar().showMessage(f'현재타수 : {typing_per_sec:.1f}타/초') self.label_typing_avg.setText(f'평균타수 : {typing_per_sec_avg:.1f}타/초') self.label_total_time.setText("총 연습시간 : " + self.typing.str_of_total_time()) def new_game(self): """연습에 필요한 문자열 만든다.""" self.line_for_typing.setText("") self.line_for_typing.setCursorPosition(0) self.typing.new_game() self.line_for_number.setText(self.typing.chars_answer) self.line_for_typing.setFocus() self.display_result() def update_typing(self): """Typing 정보를 갱신한다.""" typing = self.typing typing.update_time() is_correct, err_str = typing.check_result(self.line_for_typing.text()) if is_correct: self.new_game() self.line_for_error.setText("") self.line_for_error.setText(err_str)
class SpyderErrorDialog(QDialog): """Custom error dialog for error reporting.""" def __init__(self, parent=None, is_report=False): QDialog.__init__(self, parent) self.is_report = is_report self.setWindowTitle(_("Issue reporter")) self.setModal(True) # To save the traceback sent to the internal console self.error_traceback = "" # Dialog main label if self.is_report: title = _("Please fill the following information") else: title = _("Spyder has encountered an internal problem!") main_label = QLabel( _("<h3>{title}</h3>" "Before reporting this problem, <i>please</i> consult our " "comprehensive " "<b><a href=\"{trouble_url}\">Troubleshooting Guide</a></b> " "which should help solve most issues, and search for " "<b><a href=\"{project_url}\">known bugs</a></b> " "matching your error message or problem description for a " "quicker solution." ).format(title=title, trouble_url=__trouble_url__, project_url=__project_url__)) main_label.setOpenExternalLinks(True) main_label.setWordWrap(True) main_label.setAlignment(Qt.AlignJustify) main_label.setStyleSheet('font-size: 12px;') # Issue title self.title = QLineEdit() self.title.textChanged.connect(self._contents_changed) self.title_chars_label = QLabel(_("{} more characters " "to go...").format(TITLE_MIN_CHARS)) form_layout = QFormLayout() form_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) red_asterisk = '<font color="Red">*</font>' title_label = QLabel(_("<b>Title</b>: {}").format(red_asterisk)) form_layout.setWidget(0, QFormLayout.LabelRole, title_label) form_layout.setWidget(0, QFormLayout.FieldRole, self.title) # Description steps_header = QLabel( _("<b>Steps to reproduce:</b> {}").format(red_asterisk)) steps_text = QLabel(_("Please enter a detailed step-by-step " "description (in English) of what led up to " "the problem below. Issue reports without a " "clear way to reproduce them will be closed.")) steps_text.setWordWrap(True) steps_text.setAlignment(Qt.AlignJustify) steps_text.setStyleSheet('font-size: 12px;') # Field to input the description of the problem self.input_description = DescriptionWidget(self) # Only allow to submit to Github if we have a long enough description self.input_description.textChanged.connect(self._contents_changed) # Widget to show errors self.details = ShowErrorWidget(self) self.details.set_pythonshell_font(get_font()) self.details.hide() # Label to show missing chars self.initial_chars = len(self.input_description.toPlainText()) self.desc_chars_label = QLabel(_("{} more characters " "to go...").format(DESC_MIN_CHARS)) # Checkbox to dismiss future errors self.dismiss_box = QCheckBox(_("Hide all future errors during this " "session")) if self.is_report: self.dismiss_box.hide() # Dialog buttons gh_icon = ima.icon('github') self.submit_btn = QPushButton(gh_icon, _('Submit to Github')) self.submit_btn.setEnabled(False) self.submit_btn.clicked.connect(self._submit_to_github) self.details_btn = QPushButton(_('Show details')) self.details_btn.clicked.connect(self._show_details) if self.is_report: self.details_btn.hide() self.close_btn = QPushButton(_('Close')) if self.is_report: self.close_btn.clicked.connect(self.reject) # Buttons layout buttons_layout = QHBoxLayout() buttons_layout.addWidget(self.submit_btn) buttons_layout.addWidget(self.details_btn) buttons_layout.addWidget(self.close_btn) # Main layout layout = QVBoxLayout() layout.addWidget(main_label) layout.addSpacing(20) layout.addLayout(form_layout) layout.addWidget(self.title_chars_label) layout.addSpacing(12) layout.addWidget(steps_header) layout.addSpacing(-1) layout.addWidget(steps_text) layout.addSpacing(1) layout.addWidget(self.input_description) layout.addWidget(self.details) layout.addWidget(self.desc_chars_label) layout.addSpacing(15) layout.addWidget(self.dismiss_box) layout.addSpacing(15) layout.addLayout(buttons_layout) layout.setContentsMargins(25, 20, 25, 10) self.setLayout(layout) self.resize(570, 600) self.title.setFocus() # Set Tab key focus order self.setTabOrder(self.title, self.input_description) def _submit_to_github(self): """Action to take when pressing the submit button.""" # Get reference to the main window if self.parent() is not None: if getattr(self.parent(), 'main', False): # This covers the case when the dialog is attached # to the internal console main = self.parent().main else: # Else the dialog is attached to the main window # directly main = self.parent() else: main = None # Getting description and traceback title = self.title.text() description = self.input_description.toPlainText() traceback = self.error_traceback[:-1] # Remove last EOL # Render issue if main is not None: issue_text = main.render_issue(description=description, traceback=traceback) else: issue_text = description try: if main is None: org = 'ccordoba12' else: org = 'spyder-ide' github_backend = GithubBackend(org, 'spyder', parent_widget=main) github_report = github_backend.send_report(title, issue_text) if github_report: self.close() except Exception: ret = QMessageBox.question( self, _('Error'), _("An error occurred while trying to send the issue to " "Github automatically. Would you like to open it " "manually?<br><br>" "If so, please make sure to paste your clipboard " "into the issue report box that will appear in a new " "browser tab before clicking <i>Submit</i> on that " "page.")) if ret in [QMessageBox.Yes, QMessageBox.Ok]: QApplication.clipboard().setText(issue_text) issue_body = ( " \n<!--- *** BEFORE SUBMITTING: PASTE CLIPBOARD HERE " "TO COMPLETE YOUR REPORT *** ---!>\n") if main is not None: main.report_issue(body=issue_body, title=title, open_webpage=True) else: pass def append_traceback(self, text): """Append text to the traceback, to be displayed in details.""" self.error_traceback += text def _show_details(self): """Show traceback on its own dialog""" if self.details.isVisible(): self.details.hide() self.details_btn.setText(_('Show details')) else: self.resize(570, 700) self.details.document().setPlainText('') self.details.append_text_to_shell(self.error_traceback, error=True, prompt=False) self.details.show() self.details_btn.setText(_('Hide details')) def _contents_changed(self): """Activate submit_btn.""" desc_chars = (len(self.input_description.toPlainText()) - self.initial_chars) if desc_chars < DESC_MIN_CHARS: self.desc_chars_label.setText( u"{} {}".format(DESC_MIN_CHARS - desc_chars, _("more characters to go..."))) else: self.desc_chars_label.setText(_("Description complete; thanks!")) title_chars = len(self.title.text()) if title_chars < TITLE_MIN_CHARS: self.title_chars_label.setText( u"{} {}".format(TITLE_MIN_CHARS - title_chars, _("more characters to go..."))) else: self.title_chars_label.setText(_("Title complete; thanks!")) submission_enabled = (desc_chars >= DESC_MIN_CHARS and title_chars >= TITLE_MIN_CHARS) self.submit_btn.setEnabled(submission_enabled)
class MiniControl(QWidget): def __init__(self, parent): super().__init__() self.setWindowTitle(config.thisTranslation["remote_control"]) self.parent = parent # specify window size if config.qtMaterial and config.qtMaterialTheme: self.resizeWindow(1 / 2, 1 / 3) else: self.resizeWindow(2 / 5, 1 / 3) self.resizeEvent = (lambda old_method: (lambda event: (self.onResized(event), old_method(event))[-1]))( self.resizeEvent) # setup interface self.setupUI() # window appearance def resizeWindow(self, widthFactor, heightFactor): availableGeometry = QGuiApplication.instance().desktop( ).availableGeometry() self.setMinimumWidth(500) self.resize(availableGeometry.width() * widthFactor, availableGeometry.height() * heightFactor) def onResized(self, event): pass def closeEvent(self, event): config.miniControl = False # manage key capture def event(self, event): if event.type() == QEvent.KeyRelease: if event.modifiers() == Qt.ControlModifier: if event.key() == Qt.Key_B: self.tabs.setCurrentIndex(0) elif event.key() == Qt.Key_T: self.tabs.setCurrentIndex(1) elif event.key() == Qt.Key_C: self.tabs.setCurrentIndex(2) elif event.key() == Qt.Key_L: self.tabs.setCurrentIndex(3) elif event.key() == Qt.Key_D: self.tabs.setCurrentIndex(4) elif event.key() == Qt.Key_Escape: self.close() return QWidget.event(self, event) # setup ui def setupUI(self): mainLayout = QGridLayout() commandBox = QVBoxLayout() commandBox.setSpacing(3) commandBar = QWidget() commandLayout1 = QBoxLayout(QBoxLayout.LeftToRight) commandLayout1.setSpacing(5) self.searchLineEdit = QLineEdit() self.searchLineEdit.setClearButtonEnabled(True) self.searchLineEdit.setToolTip( config.thisTranslation["enter_command_here"]) self.searchLineEdit.returnPressed.connect(self.searchLineEntered) self.searchLineEdit.setFixedWidth(300) commandLayout1.addWidget(self.searchLineEdit) enterButton = QPushButton(config.thisTranslation["enter"]) enterButton.setFixedWidth(100) enterButton.clicked.connect(self.searchLineEntered) commandLayout1.addWidget(enterButton) commandLayout1.addStretch() commandBox.addLayout(commandLayout1) commandLayout2 = QBoxLayout(QBoxLayout.LeftToRight) commandLayout2.setSpacing(5) keys = [ '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', ':', '-', ',', '.', ' ', '<', 'X' ] for key in keys: button = QPushButton(key) button.setMaximumWidth(30) button.clicked.connect(partial(self.keyEntryAction, key)) commandLayout2.addWidget(button) commandLayout2.addStretch() commandBox.addLayout(commandLayout2) if config.isTtsInstalled: ttsLayout = QBoxLayout(QBoxLayout.LeftToRight) ttsLayout.setSpacing(5) self.languageCombo = QComboBox() ttsLayout.addWidget(self.languageCombo) if config.espeak: languages = TtsLanguages().isoLang2epeakLang else: languages = TtsLanguages().isoLang2qlocaleLang self.languageCodes = list(languages.keys()) for code in self.languageCodes: self.languageCombo.addItem(languages[code][1]) # Check if selected tts engine has the language user specify. if not (config.ttsDefaultLangauge in self.languageCodes): config.ttsDefaultLangauge = "en" # Set initial item initialIndex = self.languageCodes.index(config.ttsDefaultLangauge) self.languageCombo.setCurrentIndex(initialIndex) # setting tts default language here is confusing; better place in menu #setDefaultButton = QPushButton(config.thisTranslation["setDefault"]) #setDefaultButton.setFixedWidth(130) #setDefaultButton.clicked.connect(self.setTtsDefaultLanguage) #ttsLayout.addWidget(setDefaultButton) speakButton = QPushButton(config.thisTranslation["speak"]) speakButton.setFixedWidth(100) speakButton.clicked.connect(self.speakCommandFieldText) ttsLayout.addWidget(speakButton) stopButton = QPushButton(config.thisTranslation["stop"]) stopButton.setFixedWidth(100) stopButton.clicked.connect( self.parent.textCommandParser.stopTtsAudio) ttsLayout.addWidget(stopButton) ttsLayout.addStretch() commandBox.addLayout(ttsLayout) commandBar.setLayout(commandBox) mainLayout.addWidget(commandBar, 0, 0, Qt.AlignCenter) self.tabs = QTabWidget() self.tabs.currentChanged.connect(self.tabChanged) mainLayout.addWidget(self.tabs, 1, 0, Qt.AlignCenter) parser = BibleVerseParser(config.parserStandarisation) self.bookMap = parser.standardAbbreviation bookNums = list(self.bookMap.keys()) bookNumGps = [ bookNums[0:10], bookNums[10:20], bookNums[20:30], bookNums[30:39], bookNums[39:49], bookNums[49:59], bookNums[59:66], ] bible = QWidget() bible_layout = QVBoxLayout() bible_layout.setMargin(0) bible_layout.setSpacing(0) for bookNumGp in bookNumGps[0:5]: gp = QWidget() layout = self.newRowLayout() for bookNum in bookNumGp: text = self.bookMap[bookNum] button = QPushButton(text) button.clicked.connect(partial(self.bibleBookAction, bookNum)) layout.addWidget(button) gp.setLayout(layout) bible_layout.addWidget(gp) for bookNumGp in bookNumGps[5:]: gp = QWidget() layout = self.newRowLayout() for bookNum in bookNumGp: text = self.bookMap[bookNum] button = QPushButton(text) button.clicked.connect(partial(self.bibleBookAction, bookNum)) layout.addWidget(button) gp.setLayout(layout) bible_layout.addWidget(gp) bible_layout.addStretch() bible.setLayout(bible_layout) self.tabs.addTab(bible, config.thisTranslation["bible"]) bibles_box = QWidget() box_layout = QVBoxLayout() box_layout.setMargin(0) box_layout.setSpacing(0) row_layout = self.newRowLayout() biblesSqlite = BiblesSqlite() bibles = biblesSqlite.getBibleList() count = 0 for bible in bibles: button = QPushButton(bible) button.clicked.connect(partial(self.bibleAction, bible)) row_layout.addWidget(button) count += 1 if count > 6: count = 0 box_layout.addLayout(row_layout) row_layout = self.newRowLayout() box_layout.addLayout(row_layout) box_layout.addStretch() bibles_box.setLayout(box_layout) self.tabs.addTab(bibles_box, config.thisTranslation["translations"]) commentaries_box = QWidget() box_layout = QVBoxLayout() box_layout.setMargin(0) box_layout.setSpacing(0) row_layout = self.newRowLayout() commentaries = Commentary().getCommentaryList() count = 0 for commentary in commentaries: button = QPushButton(commentary) button.clicked.connect(partial(self.commentaryAction, commentary)) row_layout.addWidget(button) count += 1 if count > 6: count = 0 box_layout.addLayout(row_layout) row_layout = self.newRowLayout() box_layout.addLayout(row_layout) box_layout.addStretch() commentaries_box.setLayout(box_layout) self.tabs.addTab(commentaries_box, config.thisTranslation["commentaries"]) lexicons_box = QWidget() box_layout = QVBoxLayout() box_layout.setMargin(0) box_layout.setSpacing(0) row_layout = self.newRowLayout() lexicons = LexiconData().lexiconList count = 0 for lexicon in lexicons: button = QPushButton(lexicon) button.clicked.connect(partial(self.lexiconAction, lexicon)) row_layout.addWidget(button) count += 1 if count > 6: count = 0 box_layout.addLayout(row_layout) row_layout = self.newRowLayout() box_layout.addLayout(row_layout) box_layout.addStretch() lexicons_box.setLayout(box_layout) self.tabs.addTab(lexicons_box, config.thisTranslation["lexicons"]) dictionaries_box = QWidget() box_layout = QVBoxLayout() box_layout.setMargin(0) box_layout.setSpacing(0) row_layout = self.newRowLayout() dictionaries = IndexesSqlite().dictionaryList count = 0 for dictionary in dictionaries: button = QPushButton(dictionary[0]) button.setToolTip(dictionary[1]) button.clicked.connect( partial(self.dictionaryAction, dictionary[0])) row_layout.addWidget(button) count += 1 if count > 6: count = 0 box_layout.addLayout(row_layout) row_layout = self.newRowLayout() box_layout.addLayout(row_layout) box_layout.addStretch() dictionaries_box.setLayout(box_layout) self.tabs.addTab(dictionaries_box, config.thisTranslation["dictionaries"]) self.tabs.setCurrentIndex(config.miniControlInitialTab) self.setLayout(mainLayout) def newRowLayout(self): row_layout = QHBoxLayout() row_layout.setSpacing(0) row_layout.setMargin(0) return row_layout def tabChanged(self, index): prefix = "" if index == 0: prefix = "BIBLE:::{0}:::".format(config.mainText) elif index == 1: prefix = "TEXT:::" elif index == 2: prefix = "COMMENTARY:::{0}:::".format(config.commentaryText) elif index == 3: prefix = "LEXICON:::" elif index == 4: prefix = "SEARCHTOOL:::" if not config.clearCommandEntry: self.searchLineEdit.setText(prefix) def searchLineEntered(self): searchString = self.searchLineEdit.text() self.parent.textCommandLineEdit.setText(searchString) self.parent.runTextCommand(searchString) self.searchLineEdit.setFocus() #def setTtsDefaultLanguage(self): #config.ttsDefaultLangauge = self.languageCodes[self.languageCombo.currentIndex()] def speakCommandFieldText(self): text = self.searchLineEdit.text() if ":::" in text: text = text.split(":::")[-1] command = "SPEAK:::{0}:::{1}".format( self.languageCodes[self.languageCombo.currentIndex()], text) self.runCommmand(command) def bibleBookAction(self, book): command = "{0} ".format(self.bookMap[book]) self.runCommmand(command) self.searchLineEdit.setFocus() def keyEntryAction(self, key): text = self.searchLineEdit.text() if key == "X": text = "" elif key == "<": text = text[:-1] else: text += key self.searchLineEdit.setText(text) def bibleAction(self, bible): command = "BIBLE:::{0}:::{1}".format( bible, self.parent.verseReference("main")[1]) self.runCommmand(command) command = "_bibleinfo:::{0}".format(bible) self.parent.runTextCommand(command) def commentaryAction(self, commentary): command = "COMMENTARY:::{0}:::{1}".format( commentary, self.parent.verseReference("main")[1]) self.runCommmand(command) command = "_commentaryinfo:::{0}".format(commentary) self.parent.runTextCommand(command) def lexiconAction(self, lexicon): command = "LEXICON:::{0}:::{1}".format( lexicon, TextCommandParser.last_lexicon_entry) self.runCommmand(command) def dictionaryAction(self, dictionary): command = "SEARCHTOOL:::{0}:::{1}".format( dictionary, TextCommandParser.last_text_search) self.runCommmand(command) def runCommmand(self, command): self.searchLineEdit.setText(command) self.parent.runTextCommand(command) self.parent.textCommandLineEdit.setText(command)
class MasterControl(QWidget): def __init__(self, parent, initialTab=0, b=config.mainB, c=config.mainC, v=config.mainV, text=config.mainText): super().__init__() self.isRefreshing = True self.parent = parent # set title self.setWindowTitle(config.thisTranslation["controlPanel"]) if config.restrictControlPanelWidth and config.screenWidth > config.masterControlWidth: self.setFixedWidth(config.masterControlWidth) # setup item option lists self.setupResourceLists() # setup interface self.text = text self.setupUI(b, c, v, text, initialTab) # setup keyboard shortcuts self.setupKeyboardShortcuts() self.isRefreshing = False def setupKeyboardShortcuts(self): for index, shortcut in enumerate( (sc.openControlPanelTab0, sc.openControlPanelTab1, sc.openControlPanelTab2, sc.openControlPanelTab3, sc.openControlPanelTab4, sc.openControlPanelTab5)): shortcut = QShortcut(QKeySequence(shortcut), self) shortcut.activated.connect( lambda index=index: self.tabs.setCurrentIndex(index)) # manage key capture def event(self, event): if event.type() == QEvent.KeyRelease: if event.key() == Qt.Key_Escape: self.hide() if isinstance(event, QEvent): return QWidget.event(self, event) def closeEvent(self, event): # Control panel is designed for frequent use # Hiding it instead of closing may save time from reloading event.ignore() self.hide() def setupResourceLists(self): self.parent.setupResourceLists() # bible versions self.textList = self.parent.textList self.textFullNameList = self.parent.textFullNameList self.strongBibles = self.parent.strongBibles if self.parent.versionCombo is not None and config.menuLayout in ( "classic", "focus", "aleph"): for index, fullName in enumerate(self.textFullNameList): self.parent.versionCombo.setItemData(index, fullName, Qt.ToolTipRole) # commentaries self.commentaryList = self.parent.commentaryList #self.commentaryFullNameList = [Commentary(module).commentaryInfo() for module in self.commentaryList] self.commentaryFullNameList = self.parent.commentaryFullNameList # reference book # menu10_dialog self.referenceBookList = self.parent.referenceBookList # topic # menu5_topics self.topicListAbb = self.parent.topicListAbb self.topicList = self.parent.topicList # lexicon # context1_originalLexicon self.lexiconList = self.parent.lexiconList # dictionary # context1_dict self.dictionaryListAbb = self.parent.dictionaryListAbb self.dictionaryList = self.parent.dictionaryList # encyclopedia # context1_encyclopedia self.encyclopediaListAbb = self.parent.encyclopediaListAbb self.encyclopediaList = self.parent.encyclopediaList # 3rd-party dictionary # menu5_3rdDict self.thirdPartyDictionaryList = self.parent.thirdPartyDictionaryList # pdf list self.pdfList = self.parent.pdfList # docx list self.docxList = self.parent.docxList def setupUI(self, b, c, v, text, initialTab): mainLayout = QVBoxLayout() mainLayout.addWidget(self.sharedWidget()) mainLayout.addWidget(self.tabWidget(b, c, v, text, initialTab)) self.setLayout(mainLayout) def sharedWidget(self): sharedWidget = QWidget() sharedWidgetLayout = QVBoxLayout() subLayout = QHBoxLayout() subLayout.addWidget(self.commandFieldWidget()) subLayout.addWidget(self.autoCloseCheckBox()) sharedWidgetLayout.addLayout(subLayout) sharedWidget.setLayout(sharedWidgetLayout) return sharedWidget def updateBibleTabText(self, reference): self.tabs.setTabText(0, reference) def tabWidget(self, b, c, v, text, initialTab): self.tabs = QTabWidget() # 0 self.bibleTab = BibleExplorer(self, (b, c, v, text)) self.tabs.addTab(self.bibleTab, config.thisTranslation["cp0"]) self.tabs.setTabToolTip(0, sc.openControlPanelTab0) # 1 libraryTab1 = LibraryLauncher(self) self.tabs.addTab(libraryTab1, config.thisTranslation["cp1"]) self.tabs.setTabToolTip(1, sc.openControlPanelTab1) # 2 libraryTab2 = Library2Launcher(self) self.tabs.addTab(libraryTab2, config.thisTranslation["cp2"]) self.tabs.setTabToolTip(2, sc.openControlPanelTab2) # 3 self.toolTab = SearchLauncher(self) self.tabs.addTab(self.toolTab, config.thisTranslation["cp3"]) self.tabs.setTabToolTip(3, sc.openControlPanelTab3) # 4 self.historyTab = HistoryLauncher(self) self.tabs.addTab(self.historyTab, config.thisTranslation["cp4"]) self.tabs.setTabToolTip(4, sc.openControlPanelTab4) # 5 self.miscellaneousTab = MiscellaneousLauncher(self) self.tabs.addTab(self.miscellaneousTab, config.thisTranslation["cp5"]) self.tabs.setTabToolTip(5, sc.openControlPanelTab5) # 6 if config.isVlcInstalled: mediaTab = MediaLauncher(self) self.tabs.addTab(mediaTab, config.thisTranslation["mediaPlayer"]) self.tabs.setTabToolTip(6, sc.openControlPanelTab6) #7 self.morphologyTab = MorphologyLauncher(self) self.tabs.addTab(self.morphologyTab, config.thisTranslation["cp7"]) self.tabs.setTabToolTip(7, sc.openControlPanelTab7) # set action with changing tabs self.tabs.currentChanged.connect(self.tabChanged) # set initial tab self.tabs.setCurrentIndex(initialTab) return self.tabs def commandFieldWidget(self): self.commandField = QLineEdit() self.commandField.setClearButtonEnabled(True) self.commandField.setToolTip( config.thisTranslation["enter_command_here"]) self.commandField.returnPressed.connect(self.commandEntered) return self.commandField def autoCloseCheckBox(self): checkbox = QCheckBox() checkbox.setText(config.thisTranslation["autoClose"]) checkbox.setToolTip(config.thisTranslation["autoCloseToolTip"]) checkbox.setChecked(config.closeControlPanelAfterRunningCommand) checkbox.stateChanged.connect( self.closeControlPanelAfterRunningCommandChanged) return checkbox # Common layout def buttonsWidget(self, buttonElementTupleTuple, r2l=False, translation=True): buttons = QWidget() buttonsLayouts = QVBoxLayout() buttonsLayouts.setSpacing(3) for buttonElementTuple in buttonElementTupleTuple: buttonsLayouts.addLayout( self.buttonsLayout(buttonElementTuple, r2l, translation)) buttons.setLayout(buttonsLayouts) return buttons def buttonsLayout(self, buttonElementTuple, r2l=False, translation=True): buttonsLayout = QBoxLayout( QBoxLayout.RightToLeft if r2l else QBoxLayout.LeftToRight) buttonsLayout.setSpacing(5) for label, action in buttonElementTuple: buttonLabel = config.thisTranslation[ label] if translation else label button = QPushButton(buttonLabel) button.clicked.connect(action) buttonsLayout.addWidget(button) return buttonsLayout def comboFeatureLayout(self, feature, combo, action): # QGridLayout: https://stackoverflow.com/questions/61451279/how-does-setcolumnstretch-and-setrowstretch-works layout = QGridLayout() layout.setSpacing(5) # combo layout.addWidget(combo, 0, 0, 1, 3) # button button = QPushButton(config.thisTranslation[feature]) button.clicked.connect(action) layout.addWidget(button, 0, 3, 1, 1) return layout # Actions def closeControlPanelAfterRunningCommandChanged(self): config.closeControlPanelAfterRunningCommand = not config.closeControlPanelAfterRunningCommand def updateBCVText(self, b, c, v, text): self.bibleTab.updateBCVText(b, c, v, text) def commandEntered(self): command = self.commandField.text() self.runTextCommand(command, False) def runTextCommand(self, command, printCommand=True, reloadMainWindow=False): if printCommand: self.commandField.setText(command) self.parent.textCommandLineEdit.setText(command) self.parent.runTextCommand(command) if reloadMainWindow: self.parent.reloadCurrentRecord() if config.closeControlPanelAfterRunningCommand and not self.isRefreshing: self.hide() def tabChanged(self, index): self.isRefreshing = True # refresh content if index == 4: self.historyTab.refresh() elif index == 5: self.miscellaneousTab.refresh() # set focus if index == 3: self.toolTab.searchField.setFocus() if config.contextItem: self.toolTab.searchField.setText(config.contextItem) config.contextItem = "" elif self.parent.mainView.currentWidget().selectedText(): self.toolTab.searchField.setText( self.parent.mainView.currentWidget().selectedText()) elif self.parent.studyView.currentWidget().selectedText(): self.toolTab.searchField.setText( self.parent.studyView.currentWidget().selectedText()) else: self.commandField.setFocus() self.isRefreshing = False def displayMessage(self, message="", title="UniqueBible"): reply = QMessageBox.information(self, title, message)
class MiniControl(QWidget): def __init__(self, parent, selectedTab=0): super().__init__() self.textButtonStyleEnabled = "QPushButton {background-color: #151B54; color: white;} QPushButton:hover {background-color: #333972;} QPushButton:pressed { background-color: #515790;}" self.textButtonStyleDisabled = "QPushButton {background-color: #323232; color: #323232;} QPushButton:hover {background-color: #323232;} QPushButton:pressed { background-color: #323232;}" self.setWindowTitle(config.thisTranslation["remote_control"]) self.parent = parent self.devotionals = sorted( glob.glob(config.marvelData + "/devotionals/*.devotional")) # specify window size if config.qtMaterial and config.qtMaterialTheme: self.resizeWindow(1 / 2, 1 / 3) else: self.resizeWindow(2 / 5, 1 / 3) self.resizeEvent = (lambda old_method: (lambda event: (self.onResized(event), old_method(event))[-1]))( self.resizeEvent) self.bibleButtons = {} self.commentaryButtons = {} self.bookIntroButtons = {} # setup interface self.bible_layout = None self.setupUI() self.tabs.setCurrentIndex(selectedTab) # window appearance def resizeWindow(self, widthFactor, heightFactor): availableGeometry = QGuiApplication.instance().desktop( ).availableGeometry() self.setMinimumWidth(500) self.resize(availableGeometry.width() * widthFactor, availableGeometry.height() * heightFactor) def onResized(self, event): pass def closeEvent(self, event): config.miniControl = False # manage key capture def event(self, event): if event.type() == QEvent.KeyRelease: if event.modifiers() == Qt.ControlModifier: if event.key() == Qt.Key_B: self.tabs.setCurrentIndex(0) elif event.key() == Qt.Key_T: self.tabs.setCurrentIndex(1) elif event.key() == Qt.Key_C: self.tabs.setCurrentIndex(2) elif event.key() == Qt.Key_L: self.tabs.setCurrentIndex(3) elif event.key() == Qt.Key_D: self.tabs.setCurrentIndex(4) elif event.key() == Qt.Key_O: self.tabs.setCurrentIndex(5) elif event.key() == Qt.Key_Escape: self.close() return QWidget.event(self, event) # setup ui def setupUI(self): textButtonStyle = "QPushButton {background-color: #151B54; color: white;} QPushButton:hover {background-color: #333972;} QPushButton:pressed { background-color: #515790;}" mainLayout = QGridLayout() commandBox = QVBoxLayout() commandBox.setSpacing(3) commandBar = QWidget() commandLayout1 = QBoxLayout(QBoxLayout.LeftToRight) commandLayout1.setSpacing(5) self.searchLineEdit = QLineEdit() self.searchLineEdit.setClearButtonEnabled(True) self.searchLineEdit.setToolTip( config.thisTranslation["enter_command_here"]) self.searchLineEdit.returnPressed.connect(self.searchLineEntered) self.searchLineEdit.setFixedWidth(450) commandLayout1.addWidget(self.searchLineEdit) enterButton = QPushButton(config.thisTranslation["enter"]) enterButton.setFixedWidth(100) enterButton.clicked.connect(self.searchLineEntered) commandLayout1.addWidget(enterButton) # commandLayout1.addStretch() commandBox.addLayout(commandLayout1) if config.showMiniKeyboardInMiniControl: commandLayout2 = QBoxLayout(QBoxLayout.LeftToRight) commandLayout2.setSpacing(5) keys = [ '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', ':', '-', ',', '.', ' ', '<', 'X' ] for key in keys: button = QPushButton(key) button.setMaximumWidth(30) button.clicked.connect(partial(self.keyEntryAction, key)) commandLayout2.addWidget(button) commandLayout2.addStretch() commandBox.addLayout(commandLayout2) if config.showMiniKeyboardInMiniControl and config.isTtsInstalled: ttsLayout = QBoxLayout(QBoxLayout.LeftToRight) ttsLayout.setSpacing(5) self.languageCombo = QComboBox() ttsLayout.addWidget(self.languageCombo) if config.espeak: languages = TtsLanguages().isoLang2epeakLang else: languages = TtsLanguages().isoLang2qlocaleLang self.languageCodes = list(languages.keys()) for code in self.languageCodes: self.languageCombo.addItem(languages[code][1]) # Check if selected tts engine has the language user specify. if not (config.ttsDefaultLangauge in self.languageCodes): config.ttsDefaultLangauge = "en" # Set initial item initialIndex = self.languageCodes.index(config.ttsDefaultLangauge) self.languageCombo.setCurrentIndex(initialIndex) # setting tts default language here is confusing; better place in menu #setDefaultButton = QPushButton(config.thisTranslation["setDefault"]) #setDefaultButton.setFixedWidth(130) #setDefaultButton.clicked.connect(self.setTtsDefaultLanguage) #ttsLayout.addWidget(setDefaultButton) speakButton = QPushButton(config.thisTranslation["speak"]) speakButton.setFixedWidth(100) speakButton.clicked.connect(self.speakCommandFieldText) ttsLayout.addWidget(speakButton) stopButton = QPushButton(config.thisTranslation["stop"]) stopButton.setFixedWidth(100) stopButton.clicked.connect( self.parent.textCommandParser.stopTtsAudio) ttsLayout.addWidget(stopButton) ttsLayout.addStretch() commandBox.addLayout(ttsLayout) commandBar.setLayout(commandBox) mainLayout.addWidget(commandBar, 0, 0, Qt.AlignCenter) self.tabs = QTabWidget() self.tabs.currentChanged.connect(self.tabChanged) mainLayout.addWidget(self.tabs, 1, 0, Qt.AlignCenter) parser = BibleVerseParser(config.parserStandarisation) self.bookMap = parser.standardAbbreviation bookNums = list(self.bookMap.keys()) self.bookNumGps = [ bookNums[0:10], bookNums[10:20], bookNums[20:30], bookNums[30:39], bookNums[39:49], bookNums[49:59], bookNums[59:69], bookNums[69:79], bookNums[79:86], bookNums[86:94], bookNums[94:99], bookNums[99:104], bookNums[104:110], bookNums[110:119], bookNums[119:124], bookNums[124:129], bookNums[129:139], bookNums[139:149], bookNums[149:159], bookNums[159:169], bookNums[169:174], bookNums[174:179], bookNums[179:189], bookNums[189:199], ] # Bible books tab self.bible = QWidget() self.populateBooksButtons(config.mainText) self.tabs.addTab(self.bible, config.thisTranslation["bible"]) # Bible translations tab self.biblesBox = QWidget() self.biblesBoxContainer = QVBoxLayout() collectionsLayout = self.newRowLayout() if len(config.bibleCollections) > 0: button = QPushButton("All") button.setStyleSheet(textButtonStyle) button.clicked.connect(partial(self.selectCollection, "All")) collectionsLayout.addWidget(button) count = 0 for collection in sorted(config.bibleCollections.keys()): button = QPushButton(collection) button.setStyleSheet(textButtonStyle) button.clicked.connect( partial(self.selectCollection, collection)) collectionsLayout.addWidget(button) count += 1 if count > 5: count = 0 self.biblesBoxContainer.addLayout(collectionsLayout) collectionsLayout = self.newRowLayout() self.biblesBoxContainer.addLayout(collectionsLayout) self.bibleBoxWidget = QWidget() self.bibleBoxLayout = QVBoxLayout() self.bibleBoxLayout.setContentsMargins(0, 0, 0, 0) self.bibleBoxLayout.setSpacing(1) row_layout = self.newRowLayout() row_layout.setContentsMargins(0, 0, 0, 0) row_layout.setSpacing(1) biblesSqlite = BiblesSqlite() bibles = biblesSqlite.getBibleList() count = 0 for bible in bibles: button = QPushButton(bible) if bible in config.bibleDescription: button.setToolTip("{0}".format(config.bibleDescription[bible])) button.clicked.connect(partial(self.bibleAction, bible)) row_layout.addWidget(button) count += 1 if count > 6: count = 0 self.bibleBoxLayout.addLayout(row_layout) row_layout = self.newRowLayout() self.bibleButtons[bible] = button self.bibleBoxLayout.addLayout(row_layout) self.bibleBoxLayout.addStretch() self.biblesBoxContainer.addLayout(self.bibleBoxLayout) self.biblesBoxContainer.addStretch() self.biblesBox.setLayout(self.biblesBoxContainer) self.tabs.addTab(self.biblesBox, config.thisTranslation["translations"]) # Commentaries tab commentaries_box = QWidget() box_layout = QVBoxLayout() box_layout.setContentsMargins(0, 0, 0, 0) box_layout.setSpacing(1) row_layout = self.newRowLayout() button = QPushButton(config.thisTranslation["activeOnly"]) button.setStyleSheet(textButtonStyle) button.clicked.connect(self.activeCommentaries) row_layout.addWidget(button) box_layout.addLayout(row_layout) row_layout = self.newRowLayout() commentaries = Commentary().getCommentaryList() count = 0 for commentary in commentaries: button = QPushButton(commentary) button.setToolTip(Commentary.fileLookup[commentary]) button.clicked.connect(partial(self.commentaryAction, commentary)) self.commentaryButtons[commentary] = button row_layout.addWidget(button) count += 1 if count > 6: count = 0 box_layout.addLayout(row_layout) row_layout = self.newRowLayout() box_layout.addLayout(row_layout) box_layout.addStretch() commentaries_box.setLayout(box_layout) self.tabs.addTab(commentaries_box, config.thisTranslation["commentaries"]) # Lexicons tab lexicons_box = QWidget() box_layout = QVBoxLayout() box_layout.setContentsMargins(0, 0, 0, 0) box_layout.setSpacing(1) row_layout = self.newRowLayout() lexicons = LexiconData().lexiconList count = 0 for lexicon in lexicons: button = QPushButton(lexicon) if lexicon in config.lexiconDescription: button.setToolTip("{0}".format( config.lexiconDescription[lexicon])) button.clicked.connect(partial(self.lexiconAction, lexicon)) row_layout.addWidget(button) count += 1 if count > 6: count = 0 box_layout.addLayout(row_layout) row_layout = self.newRowLayout() box_layout.addLayout(row_layout) box_layout.addStretch() lexicons_box.setLayout(box_layout) self.tabs.addTab(lexicons_box, config.thisTranslation["lexicons"]) # Dictionaries tab dictionaries_box = QWidget() box_layout = QVBoxLayout() box_layout.setContentsMargins(0, 0, 0, 0) box_layout.setSpacing(1) row_layout = self.newRowLayout() dictionaries = IndexesSqlite().dictionaryList count = 0 for dictionary in dictionaries: button = QPushButton(dictionary[0]) button.setToolTip(dictionary[1]) button.clicked.connect( partial(self.dictionaryAction, dictionary[0])) row_layout.addWidget(button) count += 1 if count > 6: count = 0 box_layout.addLayout(row_layout) row_layout = self.newRowLayout() box_layout.addLayout(row_layout) box_layout.addStretch() dictionaries_box.setLayout(box_layout) self.tabs.addTab(dictionaries_box, config.thisTranslation["dictionaries"]) # Book intros tab bookIntros_box = QWidget() box_layout = QVBoxLayout() box_layout.setContentsMargins(0, 0, 0, 0) box_layout.setSpacing(1) row_layout = self.newRowLayout() button = QPushButton(config.thisTranslation["activeOnly"]) button.setStyleSheet(textButtonStyle) button.clicked.connect(self.activeBookIntros) row_layout.addWidget(button) box_layout.addLayout(row_layout) row_layout = self.newRowLayout() commentaries = Commentary().getCommentaryList() count = 0 for commentary in commentaries: button = QPushButton(commentary) button.setToolTip(Commentary.fileLookup[commentary]) button.clicked.connect(partial(self.bookIntroAction, commentary)) self.bookIntroButtons[commentary] = button row_layout.addWidget(button) count += 1 if count > 6: count = 0 box_layout.addLayout(row_layout) row_layout = self.newRowLayout() box_layout.addLayout(row_layout) box_layout.addStretch() bookIntros_box.setLayout(box_layout) self.tabs.addTab(bookIntros_box, config.thisTranslation["bookIntro"]) # Devotionals tab if len(self.devotionals) > 0: devotionals_box = QWidget() box_layout = QVBoxLayout() box_layout.setContentsMargins(0, 0, 0, 0) box_layout.setSpacing(1) row_layout = self.newRowLayout() count = 0 for file in self.devotionals: name = Path(file).stem button = QPushButton(name) # button.setToolTip(dictionary[1]) button.clicked.connect(partial(self.devotionalAction, name)) row_layout.addWidget(button) count += 1 if count > 2: count = 0 box_layout.addLayout(row_layout) row_layout.addStretch() row_layout = self.newRowLayout() for i in range(count, 3): button = QPushButton("") row_layout.addWidget(button) box_layout.addLayout(row_layout) box_layout.addStretch() devotionals_box.setLayout(box_layout) self.tabs.addTab(devotionals_box, config.thisTranslation["devotionals"]) self.tabs.setCurrentIndex(config.miniControlInitialTab) self.setLayout(mainLayout) def populateBooksButtons(self, bibleName): books = Bible(bibleName).getBookList() if self.bible_layout is not None: while self.bible_layout.count(): child = self.bible_layout.takeAt(0) if child.widget(): child.widget().deleteLater() else: self.bible_layout = QVBoxLayout() self.bible_layout.setContentsMargins(0, 0, 0, 0) self.bible_layout.setSpacing(1) for bookNumGp in self.bookNumGps: gp = QWidget() layout = self.newRowLayout() for bookNum in bookNumGp: if int(bookNum) in books: text = self.bookMap[bookNum] button = QPushButton(text) if config.developer: button.setToolTip("{0} - {1}".format( BibleBooks.eng[bookNum][1], bookNum)) else: button.setToolTip("{0}".format( BibleBooks.eng[bookNum][1])) button.clicked.connect( partial(self.bibleBookAction, bookNum)) layout.addWidget(button) gp.setLayout(layout) self.bible_layout.addWidget(gp) # for bookNumGp in self.bookNumGps[5:]: # gp = QWidget() # layout = self.newRowLayout() # for bookNum in bookNumGp: # text = self.bookMap[bookNum] # button = QPushButton(text) # button.clicked.connect(partial(self.bibleBookAction, bookNum)) # layout.addWidget(button) # gp.setLayout(layout) # bible_layout.addWidget(gp) self.bible_layout.addStretch() self.bible.setLayout(self.bible_layout) def newRowLayout(self): row_layout = QHBoxLayout() row_layout.setContentsMargins(0, 0, 0, 0) row_layout.setSpacing(1) return row_layout def tabChanged(self, index): prefix = "" if index == 0: prefix = "BIBLE:::{0}:::".format(config.mainText) elif index == 1: prefix = "TEXT:::" elif index == 2: prefix = "COMMENTARY:::{0}:::".format(config.commentaryText) elif index == 3: prefix = "LEXICON:::" elif index == 4: prefix = "SEARCHTOOL:::" if not config.clearCommandEntry: self.searchLineEdit.setText(prefix) def searchLineEntered(self): saveOpenBibleWindowContentOnNextTab = config.openBibleWindowContentOnNextTab searchString = self.searchLineEdit.text() if ":::" not in searchString or ":::{0}:::".format( config.mainText) in searchString: config.openBibleWindowContentOnNextTab = False self.parent.textCommandLineEdit.setText(searchString) self.parent.runTextCommand(searchString) self.searchLineEdit.setFocus() self.populateBooksButtons(config.mainText) config.openBibleWindowContentOnNextTab = saveOpenBibleWindowContentOnNextTab #def setTtsDefaultLanguage(self): #config.ttsDefaultLangauge = self.languageCodes[self.languageCombo.currentIndex()] def speakCommandFieldText(self): text = self.searchLineEdit.text() if ":::" in text: text = text.split(":::")[-1] command = "SPEAK:::{0}:::{1}".format( self.languageCodes[self.languageCombo.currentIndex()], text) self.runCommmand(command) def bibleBookAction(self, book): command = "{0} ".format(self.bookMap[book]) self.runCommmand(command) self.searchLineEdit.setFocus() def keyEntryAction(self, key): text = self.searchLineEdit.text() if key == "X": text = "" elif key == "<": text = text[:-1] else: text += key self.searchLineEdit.setText(text) def bibleAction(self, bible): command = "BIBLE:::{0}:::{1}".format( bible, self.parent.verseReference("main")[1]) self.runCommmand(command) command = "_bibleinfo:::{0}".format(bible) self.parent.runTextCommand(command) self.populateBooksButtons(config.mainText) def commentaryAction(self, commentary): command = "COMMENTARY:::{0}:::{1}".format( commentary, self.parent.verseReference("main")[1]) self.runCommmand(command) command = "_commentaryinfo:::{0}".format(commentary) self.parent.runTextCommand(command) def bookIntroAction(self, commentary): command = "COMMENTARY:::{0}:::{1}".format( commentary, BibleBooks().eng[str(config.mainB)][-1]) self.runCommmand(command) command = "_commentaryinfo:::{0}".format(commentary) self.parent.runTextCommand(command) def devotionalAction(self, devotional): command = "DEVOTIONAL:::{0}".format(devotional) self.runCommmand(command) def lexiconAction(self, lexicon): searchString = self.searchLineEdit.text() if ":::" not in searchString: TextCommandParser.last_lexicon_entry = searchString command = "SEARCHLEXICON:::{0}:::{1}".format( lexicon, TextCommandParser.last_lexicon_entry) self.runCommmand(command) def dictionaryAction(self, dictionary): searchString = self.searchLineEdit.text() if ":::" not in searchString: TextCommandParser.last_text_search = searchString command = "SEARCHTOOL:::{0}:::{1}".format( dictionary, TextCommandParser.last_text_search) self.runCommmand(command) def runCommmand(self, command): self.searchLineEdit.setText(command) self.parent.runTextCommand(command) self.parent.textCommandLineEdit.setText(command) self.populateBooksButtons(config.mainText) def selectCollection(self, collection): if not collection == "All": biblesInCollection = config.bibleCollections[collection] for bible in self.bibleButtons.keys(): button = self.bibleButtons[bible] if collection == "All": button.setEnabled(True) button.setStyleSheet("") else: if bible in biblesInCollection: button.setEnabled(True) button.setStyleSheet("") else: button.setEnabled(False) button.setStyleSheet(self.textButtonStyleDisabled) def activeCommentaries(self): activeCommentaries = [ item[0] for item in Commentary().getCommentaryListThatHasBookAndChapter( config.mainB, config.mainC) ] for commentary in self.commentaryButtons.keys(): button = self.commentaryButtons[commentary] if commentary in activeCommentaries: button.setEnabled(True) button.setStyleSheet("") else: button.setEnabled(False) button.setStyleSheet(self.textButtonStyleDisabled) def activeBookIntros(self): activeCommentaries = [ item[0] for item in Commentary().getCommentaryListThatHasBookAndChapter( config.mainB, 0) ] for commentary in self.bookIntroButtons.keys(): button = self.bookIntroButtons[commentary] if commentary in activeCommentaries: button.setEnabled(True) button.setStyleSheet("") else: button.setEnabled(False) button.setStyleSheet(self.textButtonStyleDisabled)
class AuthenticationDialog(DialogBase): """Login dialog.""" # See https://github.com/Anaconda-Platform/anaconda-server settings USER_RE = QRegExp('^[A-Za-z0-9_][A-Za-z0-9_-]+$') FORGOT_USERNAME_URL = 'account/forgot_username' FORGOT_PASSWORD_URL = 'account/forgot_password' sig_authentication_succeeded = Signal() sig_authentication_failed = Signal() sig_url_clicked = Signal(object) def __init__(self, api, parent=None): """Login dialog.""" super(AuthenticationDialog, self).__init__(parent) self._parent = parent self.config = CONF self.api = api self.token = None self.error = None self.tracker = GATracker() self.forgot_username_url = None self.forgot_password_url = None # Widgets self.label_username = QLabel('Username:'******'Password:'******'<hr><br><b>Already a member? ' 'Sign in!</b><br>') # For styling purposes the label next to a ButtonLink is also a button # so they align adequately self.button_register_text = ButtonLabel('You can register by ' 'visiting the ') self.button_register = ButtonLink('Anaconda Cloud') self.button_register_after_text = ButtonLabel('website.') self.label_information = QLabel(''' <strong>Anaconda Cloud</strong> is where packages, notebooks, and <br> environments are shared. It provides powerful <br> collaboration and package management for open <br> source and private projects.<br> ''') self.label_message = QLabel('') self.button_forgot_username = ButtonLink('I forgot my username') self.button_forgot_password = ButtonLink('I forgot my password') self.button_login = ButtonPrimary('Login') self.button_cancel = ButtonNormal('Cancel') # Widgets setup self.button_login.setDefault(True) username_validator = QRegExpValidator(self.USER_RE) self.text_username.setValidator(username_validator) self.setMinimumWidth(260) self.setWindowTitle('Sign in') # This allows to completely style the dialog with css using the frame self.text_password.setEchoMode(QLineEdit.Password) self.label_message.setVisible(False) # Layout grid_layout = QVBoxLayout() grid_layout.addWidget(self.label_username) # grid_layout.addWidget(SpacerVertical()) grid_layout.addWidget(self.text_username) grid_layout.addWidget(SpacerVertical()) grid_layout.addWidget(self.label_password) # grid_layout.addWidget(SpacerVertical()) grid_layout.addWidget(self.text_password) main_layout = QVBoxLayout() main_layout.addWidget(self.label_information) register_layout = QHBoxLayout() register_layout.addWidget(self.button_register_text) register_layout.addWidget(self.button_register) register_layout.addWidget(self.button_register_after_text) register_layout.addStretch() main_layout.addLayout(register_layout) main_layout.addWidget(self.label_signin_text) main_layout.addLayout(grid_layout) main_layout.addWidget(SpacerVertical()) main_layout.addWidget(self.label_message) main_layout.addWidget(self.button_forgot_username, 0, Qt.AlignRight) main_layout.addWidget(self.button_forgot_password, 0, Qt.AlignRight) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout_buttons.addWidget(self.button_cancel) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_login) main_layout.addWidget(SpacerVertical()) main_layout.addWidget(SpacerVertical()) main_layout.addLayout(layout_buttons) self.setLayout(main_layout) # Signals self.text_username.textEdited.connect(self.check_text) self.text_password.textEdited.connect(self.check_text) self.button_login.clicked.connect(self.login) self.button_cancel.clicked.connect(self.reject) # Setup self.check_text() self.update_style_sheet() self.text_username.setFocus() self.setup() def setup(self): """Setup login dialog.""" self.update_links() def update_links(self): """Update links.""" for button in [ self.button_forgot_username, self.button_forgot_password, self.button_register, ]: try: button.disconnect() except TypeError: # pragma: no cover pass # TODO, get this from anaconda client directly? # from binstar_client.utils import get_config, set_config # config = get_config() anaconda_api_url = self.config.get('main', 'anaconda_api_url', None) if anaconda_api_url: # Remove api if using a subdomain base_url = anaconda_api_url.lower().replace('//api.', '//') self.base_url = base_url # Remove api if not using a subdomain parts = base_url.lower().split('/') if parts[-1] == 'api': base_url = '/'.join(parts[:-1]) self.forgot_username_url = (base_url + '/' + self.FORGOT_USERNAME_URL) self.forgot_password_url = (base_url + '/' + self.FORGOT_PASSWORD_URL) self.button_register.clicked.connect( lambda: self.open_url(base_url)) self.button_forgot_username.clicked.connect( lambda: self.open_url(self.forgot_username_url)) self.button_forgot_password.clicked.connect( lambda: self.open_url(self.forgot_password_url)) @property def username(self): """Return the logged username.""" return self.text_username.text().lower() def update_style_sheet(self, style_sheet=None): """Update custom css style sheet.""" if style_sheet is None: style_sheet = load_style_sheet() self.setStyleSheet(style_sheet) def check_text(self): """Check that `username` and `password` are valid. If not empty and disable/enable buttons accordingly. """ username = self.text_username.text() password = self.text_password.text() if len(username) == 0 or len(password) == 0: self.button_login.setDisabled(True) else: self.button_login.setDisabled(False) def login(self): """Try to log the user in the specified anaconda api endpoint.""" self.button_login.setEnabled(False) self.text_username.setText(self.text_username.text().lower()) QApplication.setOverrideCursor(Qt.WaitCursor) self.label_message.setText('') worker = self.api.login(self.text_username.text().lower(), self.text_password.text()) worker.sig_finished.connect(self._finished) def _finished(self, worker, output, error): """Callback for the login procedure after worker has finished.""" token = output if token: self.token = token self.sig_authentication_succeeded.emit() self.accept() elif error: username = self.text_username.text().lower() bold_username = '******'.format(username) # The error might come in (error_message, http_error) format try: error_message = ast.literal_eval(str(error))[0] except Exception: # pragma: no cover error_message = str(error) error_message = error_message.lower().capitalize() error_message = error_message.split(', ')[0] error_text = '<i>{0}</i>'.format(error_message) error_text = error_text.replace(username, bold_username) self.label_message.setText(error_text) self.label_message.setVisible(True) if error_message: domain = self.api.client_domain() label = '{0}/{1}: {2}'.format(domain, username, error_message.lower()) self.tracker.track_event( 'authenticate', 'login failed', label=label, ) self.text_password.setFocus() self.text_password.selectAll() self.sig_authentication_failed.emit() self.button_login.setDisabled(False) self.check_text() QApplication.restoreOverrideCursor() def open_url(self, url): """Open given url in the default browser and log the action.""" self.tracker.track_event('content', 'click', url) self.sig_url_clicked.emit(url) QDesktopServices.openUrl(QUrl(url))
class QtPluginDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.installer = Installer() self.setup_ui() self.installer.set_output_widget(self.stdout_text) self.installer.process.started.connect(self._on_installer_start) self.installer.process.finished.connect(self._on_installer_done) self.refresh() def _on_installer_start(self): self.show_status_btn.setChecked(True) self.working_indicator.show() self.process_error_indicator.hide() def _on_installer_done(self, exit_code, exit_status): self.working_indicator.hide() if exit_code: self.process_error_indicator.show() else: self.show_status_btn.setChecked(False) self.refresh() self.plugin_sorter.refresh() def refresh(self): self.installed_list.clear() self.available_list.clear() # fetch installed from ...plugins import plugin_manager plugin_manager.discover() # since they might not be loaded yet already_installed = set() for plugin_name, mod_name, distname in plugin_manager.iter_available(): # not showing these in the plugin dialog if plugin_name in ('napari_plugin_engine', ): continue if distname: already_installed.add(distname) meta = standard_metadata(distname) else: meta = {} self.installed_list.addItem( ProjectInfo( normalized_name(distname or ''), meta.get('version', ''), meta.get('url', ''), meta.get('summary', ''), meta.get('author', ''), meta.get('license', ''), ), plugin_name=plugin_name, enabled=plugin_name in plugin_manager.plugins, ) # self.v_splitter.setSizes([70 * self.installed_list.count(), 10, 10]) # fetch available plugins self.worker = create_worker(iter_napari_plugin_info) def _handle_yield(project_info): if project_info.name in already_installed: self.installed_list.tag_outdated(project_info) else: self.available_list.addItem(project_info) self.worker.yielded.connect(_handle_yield) self.worker.finished.connect(self.working_indicator.hide) self.worker.finished.connect(self._update_count_in_label) self.worker.start() def setup_ui(self): self.resize(1080, 640) vlay_1 = QVBoxLayout(self) self.h_splitter = QSplitter(self) vlay_1.addWidget(self.h_splitter) self.h_splitter.setOrientation(Qt.Horizontal) self.v_splitter = QSplitter(self.h_splitter) self.v_splitter.setOrientation(Qt.Vertical) self.v_splitter.setMinimumWidth(500) self.plugin_sorter = QtPluginSorter(parent=self.h_splitter) self.plugin_sorter.layout().setContentsMargins(2, 0, 0, 0) self.plugin_sorter.hide() installed = QWidget(self.v_splitter) lay = QVBoxLayout(installed) lay.setContentsMargins(0, 2, 0, 2) self.installed_label = QLabel(trans._("Installed Plugins")) self.installed_filter = QLineEdit() self.installed_filter.setPlaceholderText("search...") self.installed_filter.setMaximumWidth(350) self.installed_filter.setClearButtonEnabled(True) mid_layout = QHBoxLayout() mid_layout.addWidget(self.installed_label) mid_layout.addWidget(self.installed_filter) mid_layout.addStretch() lay.addLayout(mid_layout) self.installed_list = QPluginList(installed, self.installer) self.installed_filter.textChanged.connect(self.installed_list.filter) lay.addWidget(self.installed_list) uninstalled = QWidget(self.v_splitter) lay = QVBoxLayout(uninstalled) lay.setContentsMargins(0, 2, 0, 2) self.avail_label = QLabel(trans._("Available Plugins")) self.avail_filter = QLineEdit() self.avail_filter.setPlaceholderText("search...") self.avail_filter.setMaximumWidth(350) self.avail_filter.setClearButtonEnabled(True) mid_layout = QHBoxLayout() mid_layout.addWidget(self.avail_label) mid_layout.addWidget(self.avail_filter) mid_layout.addStretch() lay.addLayout(mid_layout) self.available_list = QPluginList(uninstalled, self.installer) self.avail_filter.textChanged.connect(self.available_list.filter) lay.addWidget(self.available_list) self.stdout_text = QTextEdit(self.v_splitter) self.stdout_text.setReadOnly(True) self.stdout_text.setObjectName("pip_install_status") self.stdout_text.hide() buttonBox = QHBoxLayout() self.working_indicator = QLabel(trans._("loading ..."), self) sp = self.working_indicator.sizePolicy() sp.setRetainSizeWhenHidden(True) self.working_indicator.setSizePolicy(sp) self.process_error_indicator = QLabel(self) self.process_error_indicator.setObjectName("error_label") self.process_error_indicator.hide() load_gif = str(Path(napari.resources.__file__).parent / "loading.gif") mov = QMovie(load_gif) mov.setScaledSize(QSize(18, 18)) self.working_indicator.setMovie(mov) mov.start() self.direct_entry_edit = QLineEdit(self) self.direct_entry_edit.installEventFilter(self) self.direct_entry_edit.setPlaceholderText( trans._('install by name/url, or drop file...')) self.direct_entry_btn = QPushButton(trans._("Install"), self) self.direct_entry_btn.clicked.connect(self._install_packages) self.show_status_btn = QPushButton(trans._("Show Status"), self) self.show_status_btn.setFixedWidth(100) self.show_sorter_btn = QPushButton(trans._("<< Show Sorter"), self) self.close_btn = QPushButton(trans._("Close"), self) self.close_btn.clicked.connect(self.accept) buttonBox.addWidget(self.show_status_btn) buttonBox.addWidget(self.working_indicator) buttonBox.addWidget(self.direct_entry_edit) buttonBox.addWidget(self.direct_entry_btn) buttonBox.addWidget(self.process_error_indicator) buttonBox.addSpacing(60) buttonBox.addWidget(self.show_sorter_btn) buttonBox.addWidget(self.close_btn) buttonBox.setContentsMargins(0, 0, 4, 0) vlay_1.addLayout(buttonBox) self.show_status_btn.setCheckable(True) self.show_status_btn.setChecked(False) self.show_status_btn.toggled.connect(self._toggle_status) self.show_sorter_btn.setCheckable(True) self.show_sorter_btn.setChecked(False) self.show_sorter_btn.toggled.connect(self._toggle_sorter) self.v_splitter.setStretchFactor(1, 2) self.h_splitter.setStretchFactor(0, 2) self.avail_filter.setFocus() def _update_count_in_label(self): count = self.available_list.count() self.avail_label.setText( trans._("Available Plugins ({count})", count=count)) def eventFilter(self, watched, event): if event.type() == QEvent.DragEnter: # we need to accept this event explicitly to be able # to receive QDropEvents! event.accept() if event.type() == QEvent.Drop: md = event.mimeData() if md.hasUrls(): files = [url.toLocalFile() for url in md.urls()] self.direct_entry_edit.setText(files[0]) return True return super().eventFilter(watched, event) def _toggle_sorter(self, show): if show: self.show_sorter_btn.setText(trans._(">> Hide Sorter")) self.plugin_sorter.show() else: self.show_sorter_btn.setText(trans._("<< Show Sorter")) self.plugin_sorter.hide() def _toggle_status(self, show): if show: self.show_status_btn.setText(trans._("Hide Status")) self.stdout_text.show() else: self.show_status_btn.setText(trans._("Show Status")) self.stdout_text.hide() def _install_packages(self, packages: Sequence[str] = ()): if not packages: _packages = self.direct_entry_edit.text() if os.path.exists(_packages): packages = [_packages] else: packages = _packages.split() self.direct_entry_edit.clear() if packages: self.installer.install(packages)
class ValidatedDialog(QDialog): """ A dialog for creating a validated new value. Performs validation of name against a provided. Can be used to select from the list or for creating a new value that is not on the list. """ INVALID_COLOR = QColor(255, 235, 235) def __init__( self, title="Title", description="Description", unique_names=None, choose_from_list=False, ): QDialog.__init__(self) self.setModal(True) self.setWindowTitle(title) # self.setMinimumWidth(250) # self.setMinimumHeight(150) if unique_names is None: unique_names = [] self.unique_names = unique_names self.choose_from_list = choose_from_list self.layout = QFormLayout() self.layout.setSizeConstraint(QLayout.SetFixedSize) label = QLabel(description) label.setAlignment(Qt.AlignHCenter) self.layout.addRow(self.createSpace(5)) self.layout.addRow(label) self.layout.addRow(self.createSpace(10)) buttons = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self) self.ok_button = buttons.button(QDialogButtonBox.Ok) self.ok_button.setEnabled(False) if choose_from_list: self.param_name_combo = QComboBox() self.param_name.currentIndexChanged.connect(self.validateChoice) for item in unique_names: self.param_name_combo.addItem(item) self.layout.addRow("Job:", self.param_name_combo) else: self.param_name = QLineEdit(self) self.param_name.setFocus() self.param_name.textChanged.connect(self.validateName) self.validColor = self.param_name.palette().color( self.param_name.backgroundRole()) self.layout.addRow("Name:", self.param_name) self.layout.addRow(self.createSpace(10)) self.layout.addRow(buttons) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) self.setLayout(self.layout) def notValid(self, msg): """Called when the name is not valid.""" self.ok_button.setEnabled(False) palette = self.param_name.palette() palette.setColor(self.param_name.backgroundRole(), self.INVALID_COLOR) self.param_name.setToolTip(msg) self.param_name.setPalette(palette) def valid(self): """Called when the name is valid.""" self.ok_button.setEnabled(True) palette = self.param_name.palette() palette.setColor(self.param_name.backgroundRole(), self.validColor) self.param_name.setToolTip("") self.param_name.setPalette(palette) def validateName(self, value): """Called to perform validation of a name. For specific needs override this function and call valid() and notValid(msg).""" value = str(value) if value == "": self.notValid("Can not be empty!") elif not value.find(" ") == -1: self.notValid("No spaces allowed!") elif value in self.unique_names: self.notValid("Name must be unique!") else: self.valid() def validateChoice(self, choice): """Only called when using selection mode.""" self.ok_button.setEnabled(not choice == "") def getName(self): """Return the new name chosen by the user""" if self.choose_from_list: return str(self.param_name_combo.currentText()) else: return str(self.param_name.text()) def showAndTell(self): """Shows the dialog and returns the result""" if self.exec_(): return str(self.getName()).strip() return "" def createSpace(self, size=5): """Creates a widget that can be used as spacing on a panel.""" qw = QWidget() qw.setMinimumSize(QSize(size, size)) return qw
class KernelConnectionDialog(QDialog, SpyderConfigurationAccessor): """Dialog to connect to existing kernels (either local or remote).""" CONF_SECTION = 'existing-kernel' def __init__(self, parent=None): super(KernelConnectionDialog, self).__init__(parent) self.setWindowTitle(_('Connect to an existing kernel')) main_label = QLabel( _("<p>Please select the JSON connection file (<i>e.g.</i> " "<tt>kernel-1234.json</tt>) of the existing kernel, and enter " "the SSH information if connecting to a remote machine. " "To learn more about starting external kernels and connecting " "to them, see <a href=\"https://docs.spyder-ide.org/" "ipythonconsole.html#connect-to-an-external-kernel\">" "our documentation</a>.</p>")) main_label.setWordWrap(True) main_label.setAlignment(Qt.AlignJustify) main_label.setOpenExternalLinks(True) # Connection file cf_label = QLabel(_('Connection file:')) self.cf = QLineEdit() self.cf.setPlaceholderText(_('Kernel connection file path')) self.cf.setMinimumWidth(350) cf_open_btn = QPushButton(_('Browse')) cf_open_btn.clicked.connect(self.select_connection_file) cf_layout = QHBoxLayout() cf_layout.addWidget(cf_label) cf_layout.addWidget(self.cf) cf_layout.addWidget(cf_open_btn) # Remote kernel groupbox self.rm_group = QGroupBox(_("This is a remote kernel (via SSH)")) # SSH connection hn_label = QLabel(_('Hostname:')) self.hn = QLineEdit() pn_label = QLabel(_('Port:')) self.pn = QLineEdit() self.pn.setMaximumWidth(75) un_label = QLabel(_('Username:'******'Password:'******'SSH keyfile:')) self.pw = QLineEdit() self.pw.setEchoMode(QLineEdit.Password) self.pw_radio.toggled.connect(self.pw.setEnabled) self.kf_radio.toggled.connect(self.pw.setDisabled) self.kf = QLineEdit() kf_open_btn = QPushButton(_('Browse')) kf_open_btn.clicked.connect(self.select_ssh_key) kf_layout = QHBoxLayout() kf_layout.addWidget(self.kf) kf_layout.addWidget(kf_open_btn) kfp_label = QLabel(_('Passphase:')) self.kfp = QLineEdit() self.kfp.setPlaceholderText(_('Optional')) self.kfp.setEchoMode(QLineEdit.Password) self.kf_radio.toggled.connect(self.kf.setEnabled) self.kf_radio.toggled.connect(self.kfp.setEnabled) self.kf_radio.toggled.connect(kf_open_btn.setEnabled) self.kf_radio.toggled.connect(kfp_label.setEnabled) self.pw_radio.toggled.connect(self.kf.setDisabled) self.pw_radio.toggled.connect(self.kfp.setDisabled) self.pw_radio.toggled.connect(kf_open_btn.setDisabled) self.pw_radio.toggled.connect(kfp_label.setDisabled) # SSH layout ssh_layout = QGridLayout() ssh_layout.addWidget(hn_label, 0, 0, 1, 2) ssh_layout.addWidget(self.hn, 0, 2) ssh_layout.addWidget(pn_label, 0, 3) ssh_layout.addWidget(self.pn, 0, 4) ssh_layout.addWidget(un_label, 1, 0, 1, 2) ssh_layout.addWidget(self.un, 1, 2, 1, 3) # SSH authentication layout auth_layout = QGridLayout() auth_layout.addWidget(self.pw_radio, 1, 0) auth_layout.addWidget(pw_label, 1, 1) auth_layout.addWidget(self.pw, 1, 2) auth_layout.addWidget(self.kf_radio, 2, 0) auth_layout.addWidget(kf_label, 2, 1) auth_layout.addLayout(kf_layout, 2, 2) auth_layout.addWidget(kfp_label, 3, 1) auth_layout.addWidget(self.kfp, 3, 2) auth_group.setLayout(auth_layout) # Remote kernel layout rm_layout = QVBoxLayout() rm_layout.addLayout(ssh_layout) rm_layout.addSpacerItem(QSpacerItem(QSpacerItem(0, 8))) rm_layout.addWidget(auth_group) self.rm_group.setLayout(rm_layout) self.rm_group.setCheckable(True) self.rm_group.toggled.connect(self.pw_radio.setChecked) # Ok and Cancel buttons self.accept_btns = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self) self.accept_btns.accepted.connect(self.save_connection_settings) self.accept_btns.accepted.connect(self.accept) self.accept_btns.rejected.connect(self.reject) # Save connection settings checkbox self.save_layout = QCheckBox(self) self.save_layout.setText(_("Save connection settings")) btns_layout = QHBoxLayout() btns_layout.addWidget(self.save_layout) btns_layout.addWidget(self.accept_btns) # Dialog layout layout = QVBoxLayout(self) layout.addWidget(main_label) layout.addSpacerItem(QSpacerItem(QSpacerItem(0, 8))) layout.addLayout(cf_layout) layout.addSpacerItem(QSpacerItem(QSpacerItem(0, 12))) layout.addWidget(self.rm_group) layout.addLayout(btns_layout) self.cf.setFocus() self.load_connection_settings() def load_connection_settings(self): """Load the user's previously-saved kernel connection settings.""" existing_kernel = self.get_conf("settings", {}) connection_file_path = existing_kernel.get("json_file_path", "") is_remote = existing_kernel.get("is_remote", False) username = existing_kernel.get("username", "") hostname = existing_kernel.get("hostname", "") port = str(existing_kernel.get("port", 22)) is_ssh_kf = existing_kernel.get("is_ssh_keyfile", False) ssh_kf = existing_kernel.get("ssh_key_file_path", "") if connection_file_path != "": self.cf.setText(connection_file_path) if username != "": self.un.setText(username) if hostname != "": self.hn.setText(hostname) if ssh_kf != "": self.kf.setText(ssh_kf) self.rm_group.setChecked(is_remote) self.pn.setText(port) self.kf_radio.setChecked(is_ssh_kf) self.pw_radio.setChecked(not is_ssh_kf) try: import keyring ssh_passphrase = keyring.get_password("spyder_remote_kernel", "ssh_key_passphrase") ssh_password = keyring.get_password("spyder_remote_kernel", "ssh_password") if ssh_passphrase: self.kfp.setText(ssh_passphrase) if ssh_password: self.pw.setText(ssh_password) except Exception: pass def save_connection_settings(self): """Save user's kernel connection settings.""" if not self.save_layout.isChecked(): return is_ssh_key = bool(self.kf_radio.isChecked()) connection_settings = { "json_file_path": self.cf.text(), "is_remote": self.rm_group.isChecked(), "username": self.un.text(), "hostname": self.hn.text(), "port": self.pn.text(), "is_ssh_keyfile": is_ssh_key, "ssh_key_file_path": self.kf.text() } self.set_conf("settings", connection_settings) try: import keyring if is_ssh_key: keyring.set_password("spyder_remote_kernel", "ssh_key_passphrase", self.kfp.text()) else: keyring.set_password("spyder_remote_kernel", "ssh_password", self.pw.text()) except Exception: pass def select_connection_file(self): cf = getopenfilename(self, _('Select kernel connection file'), jupyter_runtime_dir(), '*.json;;*.*')[0] self.cf.setText(cf) def select_ssh_key(self): kf = getopenfilename(self, _('Select SSH keyfile'), get_home_dir(), '*.pem;;*')[0] self.kf.setText(kf) @staticmethod def get_connection_parameters(parent=None, dialog=None): if not dialog: dialog = KernelConnectionDialog(parent) result = dialog.exec_() is_remote = bool(dialog.rm_group.isChecked()) accepted = result == QDialog.Accepted if is_remote: def falsy_to_none(arg): return arg if arg else None if dialog.hn.text() and dialog.un.text(): port = dialog.pn.text() if dialog.pn.text() else '22' hostname = "{0}@{1}:{2}".format(dialog.un.text(), dialog.hn.text(), port) else: hostname = None if dialog.pw_radio.isChecked(): password = falsy_to_none(dialog.pw.text()) keyfile = None elif dialog.kf_radio.isChecked(): keyfile = falsy_to_none(dialog.kf.text()) password = falsy_to_none(dialog.kfp.text()) else: # imposible? keyfile = None password = None return (dialog.cf.text(), hostname, keyfile, password, accepted) else: path = dialog.cf.text() _dir, filename = osp.dirname(path), osp.basename(path) if _dir == '' and not filename.endswith('.json'): path = osp.join(jupyter_runtime_dir(), 'kernel-' + path + '.json') return (path, None, None, None, accepted)
def generate_context_menu(self, pos: QPoint): """ Generate a context menu for contextMenuEvent Parameters ---------- pos : QPoint The point where the context menu was requested """ model_menu = QMenu() skip_text = "skip me" # Add filterbox to the context menu txt_box = QLineEdit(model_menu) txt_box.setPlaceholderText("Filter") txt_box.setClearButtonEnabled(True) txt_box_action = QWidgetAction(model_menu) txt_box_action.setDefaultWidget(txt_box) model_menu.addAction(txt_box_action) # Add result treeview to the context menu tree_view = QTreeWidget(model_menu) tree_view.header().close() tree_view_action = QWidgetAction(model_menu) tree_view_action.setDefaultWidget(tree_view) model_menu.addAction(tree_view_action) top_level_items = {} for cat in self._scene.registry.categories(): item = QTreeWidgetItem(tree_view) item.setText(0, cat) item.setData(0, Qt.UserRole, skip_text) top_level_items[cat] = item registry = self._scene.registry for model, category in registry.registered_models_category_association( ).items(): self.parent = top_level_items[category] item = QTreeWidgetItem(self.parent) item.setText(0, model) item.setData(0, Qt.UserRole, model) tree_view.expandAll() def click_handler(item): model_name = item.data(0, Qt.UserRole) if model_name == skip_text: return type_ = self._scene.registry.create(model_name) if type_: node = self._scene.create_node(type_) pos_view = self.mapToScene(pos) node.graphics_object.setPos(pos_view) self._scene.node_placed.emit(node) else: logger.debug("Model not found") model_menu.close() tree_view.itemClicked.connect(click_handler) # Setup filtering def filter_handler(text): for name, top_lvl_item in top_level_items.items(): for i in range(top_lvl_item.childCount()): child = top_lvl_item.child(i) model_name = child.data(0, Qt.UserRole) child.setHidden(text not in model_name) txt_box.textChanged.connect(filter_handler) # make sure the text box gets focus so the user doesn't have to click on it txt_box.setFocus() return model_menu
class PlotNameWidget(QWidget): """A widget to display the plot name, and edit and close buttons This widget is added to the table widget to support the renaming and close buttons, as well as the direct renaming functionality. """ def __init__(self, presenter, plot_number, parent=None): super(PlotNameWidget, self).__init__(parent) self.presenter = presenter self.plot_number = plot_number self.mutex = QMutex() self.line_edit = QLineEdit(self.presenter.get_plot_name_from_number(plot_number)) self.line_edit.setReadOnly(True) self.line_edit.setFrame(False) self.line_edit.setStyleSheet("* { background-color: rgba(0, 0, 0, 0); }") self.line_edit.setAttribute(Qt.WA_TransparentForMouseEvents, True) self.line_edit.editingFinished.connect(self.rename_plot) shown_icon = get_icon('mdi.eye') self.hide_button = QPushButton(shown_icon, "") self.hide_button.setToolTip('Hide') self.hide_button.setFlat(True) self.hide_button.setMaximumWidth(self.hide_button.iconSize().width() * 5 / 3) self.hide_button.clicked.connect(self.toggle_visibility) rename_icon = get_icon('mdi.square-edit-outline') self.rename_button = QPushButton(rename_icon, "") self.rename_button.setToolTip('Rename') self.rename_button.setFlat(True) self.rename_button.setMaximumWidth(self.rename_button.iconSize().width() * 5 / 3) self.rename_button.setCheckable(True) self.rename_button.toggled.connect(self.rename_button_toggled) close_icon = get_icon('mdi.close') self.close_button = QPushButton(close_icon, "") self.close_button.setToolTip('Delete') self.close_button.setFlat(True) self.close_button.setMaximumWidth(self.close_button.iconSize().width() * 5 / 3) self.close_button.clicked.connect(lambda: self.close_pressed(self.plot_number)) self.layout = QHBoxLayout() # Get rid of the top and bottom margins - the button provides # some natural margin anyway. Get rid of right margin and # reduce spacing to get buttons closer together. self.layout.setContentsMargins(5, 0, 0, 0) self.layout.setSpacing(0) self.layout.addWidget(self.line_edit) self.layout.addWidget(self.hide_button) self.layout.addWidget(self.rename_button) self.layout.addWidget(self.close_button) self.layout.sizeHint() self.setLayout(self.layout) def set_plot_name(self, new_name): """ Sets the internally stored and displayed plot name :param new_name: The name to set """ self.line_edit.setText(new_name) def close_pressed(self, plot_number): """ Close the plot with the given name :param plot_number: The unique number in GlobalFigureManager """ self.presenter.close_single_plot(plot_number) def rename_button_toggled(self, checked): """ If the rename button is pressed from being unchecked then make the line edit item editable :param checked: True if the rename toggle is now pressed """ if checked: self.toggle_plot_name_editable(True, toggle_rename_button=False) def toggle_plot_name_editable(self, editable, toggle_rename_button=True): """ Set the line edit item to be editable or not editable. If editable move the cursor focus to the editable name and highlight it all. :param editable: If true make the plot name editable, else make it read only :param toggle_rename_button: If true also toggle the rename button state """ self.line_edit.setReadOnly(not editable) self.line_edit.setAttribute(Qt.WA_TransparentForMouseEvents, not editable) # This is a sneaky way to avoid the issue of two calls to # this toggle method, by effectively disabling the button # press in edit mode. self.rename_button.setAttribute(Qt.WA_TransparentForMouseEvents, editable) if toggle_rename_button: self.rename_button.setChecked(editable) if editable: self.line_edit.setFocus() self.line_edit.selectAll() else: self.line_edit.setSelection(0, 0) def toggle_visibility(self): """ Calls the presenter to hide the selected plot """ self.presenter.toggle_plot_visibility(self.plot_number) def set_visibility_icon(self, is_shown): """ Change the widget icon between shown and hidden :param is_shown: True if plot is shown, false if hidden """ if is_shown: self.hide_button.setIcon(get_icon('mdi.eye')) self.hide_button.setToolTip('Hide') else: self.hide_button.setIcon(get_icon('mdi.eye', 'lightgrey')) self.hide_button.setToolTip('Show') def rename_plot(self): """ Called when the editing is finished, gets the presenter to do the real renaming of the plot """ self.presenter.rename_figure(self.plot_number, self.line_edit.text()) self.toggle_plot_name_editable(False)
class DlgGitHubLogin(QDialog): """Dialog to submit error reports to Github.""" def __init__(self, parent, username, password, token, remember=False, remember_token=False): QDialog.__init__(self, parent) title = _("Sign in to Github") self.resize(415, 375) self.setWindowTitle(title) self.setWindowFlags( self.windowFlags() & ~Qt.WindowContextHelpButtonHint) # Header html = ('<html><head/><body><p align="center">' '{title}</p></body></html>') lbl_html = QLabel(html.format(title=title)) lbl_html.setStyleSheet('font-size: 16px;') # Tabs self.tabs = QTabWidget() # Basic form layout basic_form_layout = QFormLayout() basic_form_layout.setContentsMargins(-1, 0, -1, -1) basic_lbl_msg = QLabel(_("For regular users, i.e. users <b>without</b>" " two-factor authentication enabled")) basic_lbl_msg.setWordWrap(True) basic_lbl_msg.setAlignment(Qt.AlignJustify) lbl_user = QLabel(_("Username:"******"", QWidget()) lbl_password = QLabel(_("Password: "******"Remember me")) self.cb_remember.setToolTip(_("Spyder will save your credentials " "safely")) self.cb_remember.setChecked(remember) basic_form_layout.setWidget(4, QFormLayout.FieldRole, self.cb_remember) # Basic auth tab basic_auth = QWidget() basic_layout = QVBoxLayout() basic_layout.addSpacerItem(QSpacerItem(QSpacerItem(0, 8))) basic_layout.addWidget(basic_lbl_msg) basic_layout.addSpacerItem( QSpacerItem(QSpacerItem(0, 50, vPolicy=QSizePolicy.Expanding))) basic_layout.addLayout(basic_form_layout) basic_layout.addSpacerItem( QSpacerItem(QSpacerItem(0, 50, vPolicy=QSizePolicy.Expanding))) basic_auth.setLayout(basic_layout) self.tabs.addTab(basic_auth, _("Password Only")) # Token form layout token_form_layout = QFormLayout() token_form_layout.setContentsMargins(-1, 0, -1, -1) token_lbl_msg = QLabel(_("For users <b>with</b> two-factor " "authentication enabled, or who prefer a " "per-app token authentication.<br><br>" "You can go <b><a href=\"{}\">here</a></b> " "and click \"Generate token\" at the bottom " "to create a new token to use for this, with " "the appropriate permissions.").format( TOKEN_URL)) token_lbl_msg.setOpenExternalLinks(True) token_lbl_msg.setWordWrap(True) token_lbl_msg.setAlignment(Qt.AlignJustify) lbl_token = QLabel("Token: ") token_form_layout.setWidget(1, QFormLayout.LabelRole, lbl_token) self.le_token = QLineEdit() self.le_token.setEchoMode(QLineEdit.Password) self.le_token.textChanged.connect(self.update_btn_state) token_form_layout.setWidget(1, QFormLayout.FieldRole, self.le_token) self.cb_remember_token = None # Same validation as with cb_remember if self.is_keyring_available() and valid_py_os: self.cb_remember_token = QCheckBox(_("Remember token")) self.cb_remember_token.setToolTip(_("Spyder will save your " "token safely")) self.cb_remember_token.setChecked(remember_token) token_form_layout.setWidget(3, QFormLayout.FieldRole, self.cb_remember_token) # Token auth tab token_auth = QWidget() token_layout = QVBoxLayout() token_layout.addSpacerItem(QSpacerItem(QSpacerItem(0, 8))) token_layout.addWidget(token_lbl_msg) token_layout.addSpacerItem( QSpacerItem(QSpacerItem(0, 50, vPolicy=QSizePolicy.Expanding))) token_layout.addLayout(token_form_layout) token_layout.addSpacerItem( QSpacerItem(QSpacerItem(0, 50, vPolicy=QSizePolicy.Expanding))) token_auth.setLayout(token_layout) self.tabs.addTab(token_auth, _("Access Token")) # Sign in button self.bt_sign_in = QPushButton(_("Sign in")) self.bt_sign_in.clicked.connect(self.accept) self.bt_sign_in.setDisabled(True) # Main layout layout = QVBoxLayout() layout.addWidget(lbl_html) layout.addWidget(self.tabs) layout.addWidget(self.bt_sign_in) self.setLayout(layout) # Final adjustments if username and password: self.le_user.setText(username) self.le_password.setText(password) self.bt_sign_in.setFocus() elif username: self.le_user.setText(username) self.le_password.setFocus() elif token: self.le_token.setText(token) else: self.le_user.setFocus() self.setFixedSize(self.width(), self.height()) self.le_password.installEventFilter(self) self.le_user.installEventFilter(self) self.tabs.currentChanged.connect(self.update_btn_state) def eventFilter(self, obj, event): interesting_objects = [self.le_password, self.le_user] if obj in interesting_objects and event.type() == QEvent.KeyPress: if (event.key() == Qt.Key_Return and event.modifiers() & Qt.ControlModifier and self.bt_sign_in.isEnabled()): self.accept() return True return False def update_btn_state(self): user = to_text_string(self.le_user.text()).strip() != '' password = to_text_string(self.le_password.text()).strip() != '' token = to_text_string(self.le_token.text()).strip() != '' enable = ((user and password and self.tabs.currentIndex() == 0) or (token and self.tabs.currentIndex() == 1)) self.bt_sign_in.setEnabled(enable) def is_keyring_available(self): """Check if keyring is available for password storage.""" try: import keyring # analysis:ignore return True except Exception: return False @classmethod def login(cls, parent, username, password, token, remember, remember_token): dlg = DlgGitHubLogin(parent, username, password, token, remember, remember_token) if dlg.exec_() == dlg.Accepted: user = dlg.le_user.text() password = dlg.le_password.text() token = dlg.le_token.text() if dlg.cb_remember: remember = dlg.cb_remember.isChecked() else: remember = False if dlg.cb_remember_token: remember_token = dlg.cb_remember_token.isChecked() else: remember_token = False credentials = dict(username=user, password=password, token=token, remember=remember, remember_token=remember_token) return credentials return dict(username=None, password=None, token=None, remember=False, remember_token=False)
class NoteEditor(QMainWindow): def __init__(self, parent, noteType, noteFileName="", b=None, c=None, v=None): super().__init__() self.parent, self.noteType = parent, noteType self.noteFileName = noteFileName if not self.noteType == "file": if v: self.b, self.c, self.v = b, c, v else: self.b, self.c, self.v = config.studyB, config.studyC, config.studyV # default - "Rich" mode for editing self.html = True # default - text is not modified; no need for saving new content self.parent.noteSaved = True config.noteOpened = True config.lastOpenedNote = (noteType, b, c, v) # specify window size self.resizeWindow(2/3, 2/3) # setup interface self.setupMenuBar() self.addToolBarBreak() self.setupToolBar() if config.hideNoteEditorStyleToolbar: self.toolBar.hide() self.addToolBarBreak() self.setupTextUtility() if config.hideNoteEditorTextUtility: self.ttsToolbar.hide() self.translateToolbar.hide() self.setupLayout() # display content when first launched self.displayInitialContent() self.editor.setFocus() # specify window title self.updateWindowTitle() # re-implementing close event, when users close this widget def closeEvent(self, event): if self.parent.noteSaved: config.noteOpened = False event.accept() if config.lastOpenedNote and config.openBibleNoteAfterEditorClosed: #if config.lastOpenedNote[0] == "file": # self.parent.externalFileButtonClicked() if config.lastOpenedNote[0] == "book": self.parent.openStudyBookNote() elif config.lastOpenedNote[0] == "chapter": self.parent.openStudyChapterNote() elif config.lastOpenedNote[0] == "verse": self.parent.openStudyVerseNote() else: if self.parent.warningNotSaved(): self.parent.noteSaved = True config.noteOpened = False event.accept() else: self.parent.bringToForeground(self) event.ignore() # re-implement keyPressEvent, control+S for saving file def keyPressEvent(self, event): keys = { Qt.Key_O: self.openFileDialog, Qt.Key_S: self.saveNote, Qt.Key_B: self.format_bold, Qt.Key_I: self.format_italic, Qt.Key_U: self.format_underline, Qt.Key_M: self.format_custom, Qt.Key_D: self.format_clear, Qt.Key_F: self.focusSearchField, } key = event.key() if event.modifiers() == Qt.ControlModifier and key in keys: keys[key]() # window appearance def resizeWindow(self, widthFactor, heightFactor): availableGeometry = QGuiApplication.instance().desktop().availableGeometry() self.resize(availableGeometry.width() * widthFactor, availableGeometry.height() * heightFactor) def updateWindowTitle(self): if self.noteType == "file": if self.noteFileName: *_, title = os.path.split(self.noteFileName) else: title = "NEW" else: title = self.parent.bcvToVerseReference(self.b, self.c, self.v) if self.noteType == "book": title, *_ = title.split(" ") elif self.noteType == "chapter": title, *_ = title.split(":") mode = {True: "rich", False: "plain"} notModified = {True: "", False: " [modified]"} self.setWindowTitle("Note Editor ({1} mode) - {0}{2}".format(title, mode[self.html], notModified[self.parent.noteSaved])) # switching between "rich" & "plain" mode def switchMode(self): if self.html: note = self.editor.toHtml() note = re.sub("<body style={0}[ ]*?font-family:[ ]*?'[^']*?';[ ]*?font-size:[ ]*?[0-9]+?pt;".format('"'), "<body style={0}font-family:'{1}'; font-size:{2}pt;".format('"', config.font, config.fontSize), note) self.editor.setPlainText(note) self.html = False self.updateWindowTitle() else: note = self.editor.toPlainText() self.editor.setHtml(note) self.html = True self.updateWindowTitle() # without this hide / show command below, QTextEdit does not update the text in some devices self.hide() self.show() def setupMenuBar(self): if config.toolBarIconFullSize: self.setupMenuBarFullIconSize() else: self.setupMenuBarStandardIconSize() def setupMenuBarStandardIconSize(self): self.menuBar = QToolBar() self.menuBar.setWindowTitle(config.thisTranslation["note_title"]) self.menuBar.setContextMenuPolicy(Qt.PreventContextMenu) # In QWidget, self.menuBar is treated as the menubar without the following line # In QMainWindow, the following line adds the configured QToolBar as part of the toolbar of the main window self.addToolBar(self.menuBar) newButton = QPushButton() newButton.setToolTip("{0}\n[Ctrl/Cmd + N]".format(config.thisTranslation["menu7_create"])) newButtonFile = os.path.join("htmlResources", "newfile.png") newButton.setIcon(QIcon(newButtonFile)) newButton.clicked.connect(self.newNoteFile) self.menuBar.addWidget(newButton) openButton = QPushButton() openButton.setToolTip("{0}\n[Ctrl/Cmd + O]".format(config.thisTranslation["menu7_open"])) openButtonFile = os.path.join("htmlResources", "open.png") openButton.setIcon(QIcon(openButtonFile)) openButton.clicked.connect(self.openFileDialog) self.menuBar.addWidget(openButton) self.menuBar.addSeparator() saveButton = QPushButton() saveButton.setToolTip("{0}\n[Ctrl/Cmd + S]".format(config.thisTranslation["note_save"])) saveButtonFile = os.path.join("htmlResources", "save.png") saveButton.setIcon(QIcon(saveButtonFile)) saveButton.clicked.connect(self.saveNote) self.menuBar.addWidget(saveButton) saveAsButton = QPushButton() saveAsButton.setToolTip(config.thisTranslation["note_saveAs"]) saveAsButtonFile = os.path.join("htmlResources", "saveas.png") saveAsButton.setIcon(QIcon(saveAsButtonFile)) saveAsButton.clicked.connect(self.openSaveAsDialog) self.menuBar.addWidget(saveAsButton) self.menuBar.addSeparator() toolBarButton = QPushButton() toolBarButton.setToolTip(config.thisTranslation["note_print"]) toolBarButtonFile = os.path.join("htmlResources", "print.png") toolBarButton.setIcon(QIcon(toolBarButtonFile)) toolBarButton.clicked.connect(self.printNote) self.menuBar.addWidget(toolBarButton) self.menuBar.addSeparator() switchButton = QPushButton() switchButton.setToolTip(config.thisTranslation["note_mode"]) switchButtonFile = os.path.join("htmlResources", "switch.png") switchButton.setIcon(QIcon(switchButtonFile)) switchButton.clicked.connect(self.switchMode) self.menuBar.addWidget(switchButton) self.menuBar.addSeparator() # decreaseFontSizeButton = QPushButton() # decreaseFontSizeButton.setToolTip(config.thisTranslation["menu2_smaller"]) # decreaseFontSizeButtonFile = os.path.join("htmlResources", "fontMinus.png") # decreaseFontSizeButton.setIcon(QIcon(decreaseFontSizeButtonFile)) # decreaseFontSizeButton.clicked.connect(self.decreaseNoteEditorFontSize) # self.menuBar.addWidget(decreaseFontSizeButton) # # increaseFontSizeButton = QPushButton() # increaseFontSizeButton.setToolTip(config.thisTranslation["menu2_larger"]) # increaseFontSizeButtonFile = os.path.join("htmlResources", "fontPlus.png") # increaseFontSizeButton.setIcon(QIcon(increaseFontSizeButtonFile)) # increaseFontSizeButton.clicked.connect(self.increaseNoteEditorFontSize) # self.menuBar.addWidget(increaseFontSizeButton) # self.menuBar.addSeparator() self.searchLineEdit = QLineEdit() self.searchLineEdit.setClearButtonEnabled(True) self.searchLineEdit.setToolTip(config.thisTranslation["menu5_search"]) self.searchLineEdit.setMaximumWidth(400) self.searchLineEdit.returnPressed.connect(self.searchLineEntered) self.menuBar.addWidget(self.searchLineEdit) self.menuBar.addSeparator() toolBarButton = QPushButton() toolBarButton.setToolTip(config.thisTranslation["note_toolbar"]) toolBarButtonFile = os.path.join("htmlResources", "toolbar.png") toolBarButton.setIcon(QIcon(toolBarButtonFile)) toolBarButton.clicked.connect(self.toggleToolbar) self.menuBar.addWidget(toolBarButton) toolBarButton = QPushButton() toolBarButton.setToolTip(config.thisTranslation["note_textUtility"]) toolBarButtonFile = os.path.join("htmlResources", "textUtility.png") toolBarButton.setIcon(QIcon(toolBarButtonFile)) toolBarButton.clicked.connect(self.toggleTextUtility) self.menuBar.addWidget(toolBarButton) self.menuBar.addSeparator() def setupMenuBarFullIconSize(self): self.menuBar = QToolBar() self.menuBar.setWindowTitle(config.thisTranslation["note_title"]) self.menuBar.setContextMenuPolicy(Qt.PreventContextMenu) # In QWidget, self.menuBar is treated as the menubar without the following line # In QMainWindow, the following line adds the configured QToolBar as part of the toolbar of the main window self.addToolBar(self.menuBar) iconFile = os.path.join("htmlResources", "newfile.png") self.menuBar.addAction(QIcon(iconFile), "{0}\n[Ctrl/Cmd + N]".format(config.thisTranslation["menu7_create"]), self.newNoteFile) iconFile = os.path.join("htmlResources", "open.png") self.menuBar.addAction(QIcon(iconFile), "{0}\n[Ctrl/Cmd + O]".format(config.thisTranslation["menu7_open"]), self.openFileDialog) self.menuBar.addSeparator() iconFile = os.path.join("htmlResources", "save.png") self.menuBar.addAction(QIcon(iconFile), "{0}\n[Ctrl/Cmd + S]".format(config.thisTranslation["note_save"]), self.saveNote) iconFile = os.path.join("htmlResources", "saveas.png") self.menuBar.addAction(QIcon(iconFile), config.thisTranslation["note_saveAs"], self.openSaveAsDialog) self.menuBar.addSeparator() iconFile = os.path.join("htmlResources", "print.png") self.menuBar.addAction(QIcon(iconFile), config.thisTranslation["note_print"], self.printNote) self.menuBar.addSeparator() iconFile = os.path.join("htmlResources", "switch.png") self.menuBar.addAction(QIcon(iconFile), config.thisTranslation["note_mode"], self.switchMode) self.menuBar.addSeparator() # iconFile = os.path.join("htmlResources", "fontMinus.png") # self.menuBar.addAction(QIcon(iconFile), config.thisTranslation["menu2_smaller"], self.decreaseNoteEditorFontSize) # # iconFile = os.path.join("htmlResources", "fontPlus.png") # self.menuBar.addAction(QIcon(iconFile), config.thisTranslation["menu2_larger"], self.increaseNoteEditorFontSize) # self.menuBar.addSeparator() self.searchLineEdit = QLineEdit() self.searchLineEdit.setToolTip("{0}\n[Ctrl/Cmd + F]".format(config.thisTranslation["menu5_search"])) self.searchLineEdit.setMaximumWidth(400) self.searchLineEdit.returnPressed.connect(self.searchLineEntered) self.menuBar.addWidget(self.searchLineEdit) self.menuBar.addSeparator() iconFile = os.path.join("htmlResources", "toolbar.png") self.menuBar.addAction(QIcon(iconFile), config.thisTranslation["note_toolbar"], self.toggleToolbar) iconFile = os.path.join("htmlResources", "textUtility.png") self.menuBar.addAction(QIcon(iconFile), config.thisTranslation["note_textUtility"], self.toggleTextUtility) self.menuBar.addSeparator() def toggleToolbar(self): if config.hideNoteEditorStyleToolbar: self.toolBar.show() config.hideNoteEditorStyleToolbar = False else: self.toolBar.hide() config.hideNoteEditorStyleToolbar = True def toggleTextUtility(self): if config.hideNoteEditorTextUtility: self.ttsToolbar.show() self.translateToolbar.show() config.hideNoteEditorTextUtility = False else: self.ttsToolbar.hide() self.translateToolbar.hide() config.hideNoteEditorTextUtility = True def printNote(self): #document = QTextDocument("Sample Page") document = self.editor.document() printer = QPrinter() myPrintDialog = QPrintDialog(printer, self) if myPrintDialog.exec_() == QDialog.Accepted: return document.print_(printer) def setupToolBar(self): if config.toolBarIconFullSize: self.setupToolBarFullIconSize() else: self.setupToolBarStandardIconSize() def setupToolBarStandardIconSize(self): self.toolBar = QToolBar() self.toolBar.setWindowTitle(config.thisTranslation["noteTool_title"]) self.toolBar.setContextMenuPolicy(Qt.PreventContextMenu) # self.toolBar can be treated as an individual widget and positioned with a specified layout # In QMainWindow, the following line adds the configured QToolBar as part of the toolbar of the main window self.addToolBar(self.toolBar) items = ( ("noteTool_textFont", "font.png", self.format_font), ("noteTool_textColor", "textColor.png", self.format_textColor), ("noteTool_textBackgroundColor", "textBgColor.png", self.format_textBackgroundColor), ) for item in items: toolTip, icon, action = item self.parent.addStandardIconButton(toolTip, icon, action, self.toolBar) self.toolBar.addSeparator() items = ( ("noteTool_header1", "header1.png", self.format_header1), ("noteTool_header2", "header2.png", self.format_header2), ("noteTool_header3", "header3.png", self.format_header3), ) for item in items: toolTip, icon, action = item self.parent.addStandardIconButton(toolTip, icon, action, self.toolBar) self.toolBar.addSeparator() items = ( ("{0}\n[Ctrl/Cmd + B]".format(config.thisTranslation["noteTool_bold"]), "bold.png", self.format_bold), ("{0}\n[Ctrl/Cmd + I]".format(config.thisTranslation["noteTool_italic"]), "italic.png", self.format_italic), ("{0}\n[Ctrl/Cmd + U]".format(config.thisTranslation["noteTool_underline"]), "underline.png", self.format_underline), ) for item in items: toolTip, icon, action = item self.parent.addStandardIconButton(toolTip, icon, action, self.toolBar, translation=False) self.toolBar.addSeparator() items = ( ("noteTool_superscript", "superscript.png", self.format_superscript), ("noteTool_subscript", "subscript.png", self.format_subscript), ) for item in items: toolTip, icon, action = item self.parent.addStandardIconButton(toolTip, icon, action, self.toolBar) self.toolBar.addSeparator() self.parent.addStandardIconButton("{0}\n[Ctrl/Cmd + M]\n\n{1}\n* {4}\n* {5}\n* {6}\n\n{2}\n*1 {4}\n*2 {5}\n*3 {6}\n\n{3}\n{10}{4}|{5}|{6}{11}\n{10}{7}|{8}|{9}{11}".format(config.thisTranslation["noteTool_trans0"], config.thisTranslation["noteTool_trans1"], config.thisTranslation["noteTool_trans2"], config.thisTranslation["noteTool_trans3"], config.thisTranslation["noteTool_no1"], config.thisTranslation["noteTool_no2"], config.thisTranslation["noteTool_no3"], config.thisTranslation["noteTool_no4"], config.thisTranslation["noteTool_no5"], config.thisTranslation["noteTool_no6"], "{", "}"), "custom.png", self.format_custom, self.toolBar, translation=False) self.toolBar.addSeparator() items = ( ("noteTool_left", "align_left.png", self.format_left), ("noteTool_centre", "align_center.png", self.format_center), ("noteTool_right", "align_right.png", self.format_right), ("noteTool_justify", "align_justify.png", self.format_justify), ) for item in items: toolTip, icon, action = item self.parent.addStandardIconButton(toolTip, icon, action, self.toolBar) self.toolBar.addSeparator() self.parent.addStandardIconButton("{0}\n[Ctrl/Cmd + D]".format(config.thisTranslation["noteTool_delete"]), "clearFormat.png", self.format_clear, self.toolBar, translation=False) self.toolBar.addSeparator() items = ( ("noteTool_hyperlink", "hyperlink.png", self.openHyperlinkDialog), ("noteTool_externalImage", "gallery.png", self.openImageDialog), ) for item in items: toolTip, icon, action = item self.parent.addStandardIconButton(toolTip, icon, action, self.toolBar) self.toolBar.addSeparator() items = ( ("noteTool_image", "addImage.png", self.addInternalImage), ("noteTool_exportImage", "export.png", self.exportNoteImages), ) for item in items: toolTip, icon, action = item self.parent.addStandardIconButton(toolTip, icon, action, self.toolBar) self.toolBar.addSeparator() def setupToolBarFullIconSize(self): self.toolBar = QToolBar() self.toolBar.setWindowTitle(config.thisTranslation["noteTool_title"]) self.toolBar.setContextMenuPolicy(Qt.PreventContextMenu) # self.toolBar can be treated as an individual widget and positioned with a specified layout # In QMainWindow, the following line adds the configured QToolBar as part of the toolbar of the main window self.addToolBar(self.toolBar) iconFile = os.path.join("htmlResources", "font.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_textFont"], self.format_font) iconFile = os.path.join("htmlResources", "textColor.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_textColor"], self.format_textColor) iconFile = os.path.join("htmlResources", "textBgColor.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_textBackgroundColor"], self.format_textBackgroundColor) self.toolBar.addSeparator() iconFile = os.path.join("htmlResources", "header1.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_header1"], self.format_header1) iconFile = os.path.join("htmlResources", "header2.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_header2"], self.format_header2) iconFile = os.path.join("htmlResources", "header3.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_header3"], self.format_header3) self.toolBar.addSeparator() iconFile = os.path.join("htmlResources", "bold.png") self.toolBar.addAction(QIcon(iconFile), "{0}\n[Ctrl/Cmd + B]".format(config.thisTranslation["noteTool_bold"]), self.format_bold) iconFile = os.path.join("htmlResources", "italic.png") self.toolBar.addAction(QIcon(iconFile), "{0}\n[Ctrl/Cmd + I]".format(config.thisTranslation["noteTool_italic"]), self.format_italic) iconFile = os.path.join("htmlResources", "underline.png") self.toolBar.addAction(QIcon(iconFile), "{0}\n[Ctrl/Cmd + U]".format(config.thisTranslation["noteTool_underline"]), self.format_underline) self.toolBar.addSeparator() iconFile = os.path.join("htmlResources", "custom.png") self.toolBar.addAction(QIcon(iconFile), "{0}\n[Ctrl/Cmd + M]\n\n{1}\n* {4}\n* {5}\n* {6}\n\n{2}\n*1 {4}\n*2 {5}\n*3 {6}\n\n{3}\n{10}{4}|{5}|{6}{11}\n{10}{7}|{8}|{9}{11}".format(config.thisTranslation["noteTool_trans0"], config.thisTranslation["noteTool_trans1"], config.thisTranslation["noteTool_trans2"], config.thisTranslation["noteTool_trans3"], config.thisTranslation["noteTool_no1"], config.thisTranslation["noteTool_no2"], config.thisTranslation["noteTool_no3"], config.thisTranslation["noteTool_no4"], config.thisTranslation["noteTool_no5"], config.thisTranslation["noteTool_no6"], "{", "}"), self.format_custom) self.toolBar.addSeparator() iconFile = os.path.join("htmlResources", "align_left.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_left"], self.format_left) iconFile = os.path.join("htmlResources", "align_center.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_centre"], self.format_center) iconFile = os.path.join("htmlResources", "align_right.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_right"], self.format_right) iconFile = os.path.join("htmlResources", "align_justify.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_justify"], self.format_justify) self.toolBar.addSeparator() iconFile = os.path.join("htmlResources", "clearFormat.png") self.toolBar.addAction(QIcon(iconFile), "{0}\n[Ctrl/Cmd + D]".format(config.thisTranslation["noteTool_delete"]), self.format_clear) self.toolBar.addSeparator() iconFile = os.path.join("htmlResources", "hyperlink.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_hyperlink"], self.openHyperlinkDialog) iconFile = os.path.join("htmlResources", "gallery.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_externalImage"], self.openImageDialog) self.toolBar.addSeparator() iconFile = os.path.join("htmlResources", "addImage.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_image"], self.addInternalImage) iconFile = os.path.join("htmlResources", "export.png") self.toolBar.addAction(QIcon(iconFile), config.thisTranslation["noteTool_exportImage"], self.exportNoteImages) self.toolBar.addSeparator() def setupLayout(self): self.editor = QTextEdit() self.editor.setStyleSheet("font-family:'{0}'; font-size:{1}pt;".format(config.font, config.fontSize)); self.editor.textChanged.connect(self.textChanged) self.setCentralWidget(self.editor) #self.layout = QGridLayout() #self.layout.setMenuBar(self.menuBar) #self.layout.addWidget(self.toolBar, 0, 0) #self.layout.addWidget(self.editor, 1, 0) #self.setLayout(self.layout) # adjustment of note editor font size def increaseNoteEditorFontSize(self): if self.html: self.editor.selectAll() config.noteEditorFontSize += 1 self.editor.setFontPointSize(config.noteEditorFontSize) self.hide() self.show() def decreaseNoteEditorFontSize(self): if self.html and not config.noteEditorFontSize == 0: self.editor.selectAll() config.noteEditorFontSize -= 1 self.editor.setFontPointSize(config.noteEditorFontSize) self.hide() self.show() # search field entered def searchLineEntered(self): searchString = self.searchLineEdit.text() if searchString: cursor = self.editor.document().find(searchString, self.editor.textCursor()) if cursor: self.editor.setTextCursor(cursor) self.hide() self.show() def focusSearchField(self): self.searchLineEdit.setFocus() # track if the text being modified def textChanged(self): if self.parent.noteSaved: self.parent.noteSaved = False self.updateWindowTitle() # display content when first launched def displayInitialContent(self): if self.noteType == "file": if self.noteFileName: self.openNoteFile(self.noteFileName) else: self.newNoteFile() else: self.openBibleNote() self.editor.selectAll() self.editor.setFontPointSize(config.noteEditorFontSize) self.editor.moveCursor(QTextCursor.Start, QTextCursor.MoveAnchor) self.parent.noteSaved = True def getEmptyPage(self): strict = '' if config.includeStrictDocTypeInNote: strict = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">' return """{4}<html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li {0} white-space: pre-wrap; {1} </style></head><body style="font-family:'{2}'; font-size:{3}pt; font-weight:400; font-style:normal;"> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html>"""\ .format("{", "}", config.font, config.fontSize, strict) # load chapter / verse notes from sqlite database def openBibleNote(self): if self.noteType == "book": note = NoteService.getBookNote(self.b) elif self.noteType == "chapter": note = NoteService.getChapterNote(self.b, self.c) elif self.noteType == "verse": note = NoteService.getVerseNote(self.b, self.c, self.v) if note == config.thisTranslation["empty"]: note = self.getEmptyPage() else: note = self.fixNoteFont(note) if self.html: self.editor.setHtml(note) else: self.editor.setPlainText(note) # File I / O def newNoteFile(self): if self.parent.noteSaved: self.newNoteFileAction() elif self.parent.warningNotSaved(): self.newNoteFileAction() def newNoteFileAction(self): self.noteType = "file" self.noteFileName = "" #self.editor.clear() defaultText = self.getEmptyPage() if self.html: self.editor.setHtml(defaultText) else: self.editor.setPlainText(defaultText) self.parent.noteSaved = True self.updateWindowTitle() self.hide() self.show() def openFileDialog(self): if self.parent.noteSaved: self.openFileDialogAction() elif self.parent.warningNotSaved(): self.openFileDialogAction() def openFileDialogAction(self): options = QFileDialog.Options() fileName, filtr = QFileDialog.getOpenFileName(self, config.thisTranslation["menu7_open"], "notes", "UniqueBible.app Note Files (*.uba);;HTML Files (*.html);;HTM Files (*.htm);;All Files (*)", "", options) if fileName: self.openNoteFile(fileName) def openNoteFile(self, fileName): try: f = open(fileName, "r", encoding="utf-8") except: print("Failed to open '{0}'".format(fileName)) note = f.read() f.close() self.noteType = "file" self.noteFileName = fileName note = self.fixNoteFont(note) if self.html: self.editor.setHtml(note) else: self.editor.setPlainText(note) self.parent.noteSaved = True self.updateWindowTitle() self.hide() self.show() def saveNote(self): if self.html: note = self.editor.toHtml() else: note = self.editor.toPlainText() note = self.fixNoteFont(note) if self.noteType == "book": NoteService.saveBookNote(self.b, note) if config.openBibleNoteAfterSave: self.parent.openBookNote(self.b,) self.parent.noteSaved = True self.updateWindowTitle() elif self.noteType == "chapter": NoteService.saveChapterNote(self.b, self.c, note) if config.openBibleNoteAfterSave: self.parent.openChapterNote(self.b, self.c) self.parent.noteSaved = True self.updateWindowTitle() elif self.noteType == "verse": NoteService.saveVerseNote(self.b, self.c, self.v, note) if config.openBibleNoteAfterSave: self.parent.openVerseNote(self.b, self.c, self.v) self.parent.noteSaved = True self.updateWindowTitle() elif self.noteType == "file": if self.noteFileName == "": self.openSaveAsDialog() else: self.saveAsNote(self.noteFileName) def openSaveAsDialog(self): if self.noteFileName: *_, defaultName = os.path.split(self.noteFileName) else: defaultName = "new.uba" options = QFileDialog.Options() fileName, filtr = QFileDialog.getSaveFileName(self, config.thisTranslation["note_saveAs"], os.path.join("notes", defaultName), "UniqueBible.app Note Files (*.uba);;HTML Files (*.html);;HTM Files (*.htm);;All Files (*)", "", options) if fileName: if not "." in os.path.basename(fileName): fileName = fileName + ".uba" self.saveAsNote(fileName) def saveAsNote(self, fileName): if self.html: note = self.editor.toHtml() else: note = self.editor.toPlainText() note = self.fixNoteFont(note) f = open(fileName, "w", encoding="utf-8") f.write(note) f.close() self.noteFileName = fileName self.parent.addExternalFileHistory(fileName) self.parent.setExternalFileButton() self.parent.noteSaved = True self.updateWindowTitle() def fixNoteFont(self, note): note = re.sub("<body style={0}[ ]*?font-family:[ ]*?'[^']*?';[ ]*?font-size:[ ]*?[0-9]+?pt;".format('"'), "<body style={0}font-family:'{1}'; font-size:{2}pt;".format('"', config.font, config.fontSize), note) if not config.includeStrictDocTypeInNote: note = re.sub("""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">\n""", "", note) return note # formatting styles def format_clear(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: selectedText = """<span style="font-family:'{0}'; font-size:{1}pt;">{2}</span>""".format(config.font, config.fontSize, selectedText) self.editor.insertHtml(selectedText) else: selectedText = re.sub("<[^\n<>]*?>", "", selectedText) self.editor.insertPlainText(selectedText) else: self.selectTextFirst() def format_header1(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: self.editor.insertHtml("<h1>{0}</h1>".format(selectedText)) else: self.editor.insertPlainText("<h1>{0}</h1>".format(selectedText)) else: self.selectTextFirst() def format_header2(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: self.editor.insertHtml("<h2>{0}</h2>".format(selectedText)) else: self.editor.insertPlainText("<h2>{0}</h2>".format(selectedText)) else: self.selectTextFirst() def format_header3(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: self.editor.insertHtml("<h3>{0}</h3>".format(selectedText)) else: self.editor.insertPlainText("<h3>{0}</h3>".format(selectedText)) else: self.selectTextFirst() def format_font(self): selectedText = self.editor.textCursor().selectedText() if selectedText: ok, font = QFontDialog.getFont(QFont(config.font, config.fontSize), self) if ok: if self.html: self.editor.setCurrentFont(font) else: fontFamily, fontSize, i1, i2, fontWeight, italic, underline, strikeout, *_ = font.key().split(",") spanTag = """<span style="font-family:'{0}'; font-size:{1}pt;""".format(fontFamily, fontSize) # add font weight if fontWeight == "25": spanTag += " font-weight:200;" elif fontWeight == "75": spanTag += " font-weight:600;" # add italic style if italic == "1": spanTag += " font-style:italic;" # add both underline and strikeout style if underline == "1" and strikeout == "1": spanTag += " text-decoration: underline line-through;" # add underline style elif underline == "1": spanTag += " text-decoration: underline;" # add strikeout style elif strikeout == "1": spanTag += " text-decoration: line-through;" # close tag spanTag += '">' self.editor.insertPlainText("{0}{1}</span>".format(spanTag, selectedText)) else: self.selectTextFirst() def format_textColor(self): selectedText = self.editor.textCursor().selectedText() if selectedText: color = QColorDialog.getColor(Qt.darkRed, self) if color.isValid(): if self.html: self.editor.setTextColor(color) else: self.editor.insertPlainText('<span style="color:{0};">{1}</span>'.format(color.name(), self.editor.textCursor().selectedText())) else: self.selectTextFirst() def format_textBackgroundColor(self): selectedText = self.editor.textCursor().selectedText() if selectedText: color = QColorDialog.getColor(Qt.yellow, self) if color.isValid(): if self.html: self.editor.setTextBackgroundColor(color) else: self.editor.insertPlainText('<span style="background-color:{0};">{1}</span>'.format(color.name(), selectedText)) else: self.selectTextFirst() def format_bold(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: # Reference: https://doc.qt.io/qt-5/qfont.html#Weight-enum # Bold = 75 self.editor.setFontWeight(75) else: self.editor.insertPlainText("<b>{0}</b>".format(selectedText)) else: self.selectTextFirst() def format_italic(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: self.editor.setFontItalic(True) else: self.editor.insertPlainText("<i>{0}</i>".format(selectedText)) else: self.selectTextFirst() def format_underline(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: self.editor.setFontUnderline(True) else: self.editor.insertPlainText("<u>{0}</u>".format(selectedText)) else: self.selectTextFirst() def format_superscript(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: self.editor.insertHtml("<sup>{0}</sup>".format(selectedText)) else: self.editor.insertPlainText("<sup>{0}</sup>".format(selectedText)) else: self.selectTextFirst() def format_subscript(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: self.editor.insertHtml("<sub>{0}</sub>".format(selectedText)) else: self.editor.insertPlainText("<sub>{0}</sub>".format(selectedText)) else: self.selectTextFirst() def format_center(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: self.editor.setAlignment(Qt.AlignCenter) else: self.editor.insertPlainText("<div style='text-align:center;'>{0}</div>".format(selectedText)) else: self.selectTextFirst() def format_justify(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: self.editor.setAlignment(Qt.AlignJustify) else: self.editor.insertPlainText("<div style='text-align:justify;'>{0}</div>".format(selectedText)) else: self.selectTextFirst() def format_left(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: self.editor.setAlignment(Qt.AlignLeft) else: self.editor.insertPlainText("<div style='text-align:left;'>{0}</div>".format(selectedText)) else: self.selectTextFirst() def format_right(self): selectedText = self.editor.textCursor().selectedText() if selectedText: if self.html: self.editor.setAlignment(Qt.AlignRight) else: self.editor.insertPlainText("<div style='text-align:right;'>{0}</div>".format(selectedText)) else: self.selectTextFirst() def format_custom(self): selectedText = self.editor.textCursor().selectedText() if selectedText: selectedText = self.customFormat(selectedText) if self.html: self.editor.insertHtml(selectedText) else: self.editor.insertPlainText(selectedText) else: self.selectTextFirst() def customFormat(self, text): # QTextEdit's line break character by pressing ENTER in plain & html mode " " # please note that " " is not an empty string text = text.replace(" ", "\n") text = re.sub("^\*[0-9]+? (.*?)$", r"<ol><li>\1</li></ol>", text, flags=re.M) text = text.replace("</ol>\n<ol>", "\n") text = re.sub("^\* (.*?)$", r"<ul><li>\1</li></ul>", text, flags=re.M) text = text.replace("</ul>\n<ul>", "\n") text = re.sub("^{.*?}$", self.formatHTMLTable, text, flags=re.M) text = text.replace("</table>\n<table>", "\n") # add style to table here # please note that QTextEdit supports HTML 4, rather than HTML 5 # take this old reference: https://www.w3schools.com/tags/tag_table.asp text = text.replace('<table>', '<table border="1" cellpadding="5">') # convert back to QTextEdit linebreak text = text.replace("\n", " ") # wrap with default font and font-size text = """<span style="font-family:'{0}'; font-size:{1}pt;">{2}</span>""".format(config.font, config.fontSize, text) return text def formatHTMLTable(self, match): row = match.group()[1:-1] row = "".join(["<td>{0}</td>".format(cell) for cell in row.split("|")]) return "<table><tr>{0}</tr></table>".format(row) def addInternalImage(self): self.openImageDialog(external=False) def openImageDialog(self, external=True): options = QFileDialog.Options() fileName, filtr = QFileDialog.getOpenFileName(self, config.thisTranslation["html_open"], self.parent.openFileNameLabel.text(), "JPG Files (*.jpg);;JPEG Files (*.jpeg);;PNG Files (*.png);;GIF Files (*.gif);;BMP Files (*.bmp);;All Files (*)", "", options) if fileName: if external: self.linkExternalImage(fileName) else: self.embedImage(fileName) def embedImage(self, fileName): name, extension = os.path.splitext(os.path.basename(fileName)) with open(fileName, "rb") as fileObject: binaryData = fileObject.read() encodedData = base64.b64encode(binaryData) asciiString = encodedData.decode('ascii') imageTag = '<img src="data:image/{2};base64,{0}" alt="{1}">'.format(asciiString, name, extension[1:]) if self.html: self.editor.insertHtml(imageTag) else: self.editor.insertPlainText(imageTag) def exportNoteImages(self): options = QFileDialog.DontResolveSymlinks | QFileDialog.ShowDirsOnly directory = QFileDialog.getExistingDirectory(self, config.thisTranslation["select_a_folder"], self.parent.directoryLabel.text(), options) if directory: if self.html: htmlText = self.editor.toHtml() else: htmlText = self.editor.toPlainText() searchPattern = r'src=(["{0}])data:image/([^<>]+?);[ ]*?base64,[ ]*?([^ <>]+?)\1'.format("'") for counter, value in enumerate(re.findall(searchPattern, htmlText)): *_, ext, asciiString = value binaryString = asciiString.encode("ascii") binaryData = base64.b64decode(binaryString) imageFilePath = os.path.join(directory, "image{0}.{1}".format(counter + 1, ext)) with open(imageFilePath, "wb") as fileObject: fileObject.write(binaryData) def linkExternalImage(self, fileName): imageTag = '<img src="{0}" alt="UniqueBible.app">'.format(fileName) if self.html: self.editor.insertHtml(imageTag) else: self.editor.insertPlainText(imageTag) def openHyperlinkDialog(self): selectedText = self.editor.textCursor().selectedText() if selectedText: text, ok = QInputDialog.getText(self, "UniqueBible.app", config.thisTranslation["noteTool_hyperlink"], QLineEdit.Normal, selectedText) if ok and text != '': hyperlink = '<a href="{0}">{1}</a>'.format(text, selectedText) hyperlink = """<span style="font-family:'{0}'; font-size:{1}pt;">{2}</span>""".format(config.font, config.fontSize, hyperlink) if self.html: self.editor.insertHtml(hyperlink) else: self.editor.insertPlainText(hyperlink) else: self.selectTextFirst() def setupTextUtility(self): self.ttsToolbar = QToolBar() self.ttsToolbar.setWindowTitle(config.thisTranslation["noteTool_title"]) self.ttsToolbar.setContextMenuPolicy(Qt.PreventContextMenu) # self.toolBar can be treated as an individual widget and positioned with a specified layout # In QMainWindow, the following line adds the configured QToolBar as part of the toolbar of the main window self.addToolBar(self.ttsToolbar) self.languageCombo = QComboBox() self.ttsToolbar.addWidget(self.languageCombo) if config.espeak: languages = TtsLanguages().isoLang2epeakLang else: languages = TtsLanguages().isoLang2qlocaleLang self.languageCodes = list(languages.keys()) for code in self.languageCodes: self.languageCombo.addItem(languages[code][1]) # Check if selected tts engine has the language user specify. if not (config.ttsDefaultLangauge in self.languageCodes): config.ttsDefaultLangauge = "en" # Set initial item initialIndex = self.languageCodes.index(config.ttsDefaultLangauge) self.languageCombo.setCurrentIndex(initialIndex) button = QPushButton(config.thisTranslation["speak"]) button.setToolTip(config.thisTranslation["speak"]) button.clicked.connect(self.speakText) self.ttsToolbar.addWidget(button) button = QPushButton(config.thisTranslation["stop"]) button.setToolTip(config.thisTranslation["stop"]) button.clicked.connect(self.parent.textCommandParser.stopTtsAudio) self.ttsToolbar.addWidget(button) self.translateToolbar = QToolBar() self.translateToolbar.setWindowTitle(config.thisTranslation["noteTool_title"]) self.translateToolbar.setContextMenuPolicy(Qt.PreventContextMenu) # self.toolBar can be treated as an individual widget and positioned with a specified layout # In QMainWindow, the following line adds the configured QToolBar as part of the toolbar of the main window self.addToolBar(self.translateToolbar) self.fromLanguageCombo = QComboBox() self.translateToolbar.addWidget(self.fromLanguageCombo) self.fromLanguageCombo.addItems(["[Auto]"] +Translator.fromLanguageNames) initialIndex = 0 self.fromLanguageCombo.setCurrentIndex(initialIndex) button = QPushButton(config.thisTranslation["context1_translate"]) button.setToolTip(config.thisTranslation["context1_translate"]) button.clicked.connect(self.translateText) self.translateToolbar.addWidget(button) self.toLanguageCombo = QComboBox() self.translateToolbar.addWidget(self.toLanguageCombo) self.toLanguageCombo.addItems(Translator.toLanguageNames) initialIndex = Translator.toLanguageNames.index(config.userLanguage) self.toLanguageCombo.setCurrentIndex(initialIndex) def speakText(self): text = self.editor.textCursor().selectedText() if text: if config.isTtsInstalled: if ":::" in text: text = text.split(":::")[-1] command = "SPEAK:::{0}:::{1}".format(self.languageCodes[self.languageCombo.currentIndex()], text) self.parent.runTextCommand(command) else: self.displayMessage(config.thisTranslation["message_noSupport"]) else: self.selectTextFirst() def translateText(self): text = self.editor.textCursor().selectedText() if text: translator = Translator() if translator.language_translator is not None: fromLanguage = Translator.fromLanguageCodes[self.fromLanguageCombo.currentIndex() - 1] if self.fromLanguageCombo.currentIndex() != 0 else translator.identify(text) toLanguage = Translator.toLanguageCodes[self.toLanguageCombo.currentIndex()] result = translator.translate(text, fromLanguage, toLanguage) self.editor.insertPlainText(result) else: self.displayMessage(config.thisTranslation["ibmWatsonNotEnalbed"]) webbrowser.open("https://github.com/eliranwong/UniqueBible/wiki/IBM-Watson-Language-Translator") else: self.selectTextFirst() def selectTextFirst(self): self.displayMessage(config.thisTranslation["selectTextFirst"]) def displayMessage(self, message="", title="UniqueBible"): reply = QMessageBox.information(self, title, message)
class PlotNameWidget(QWidget): """A widget to display the plot name, and edit and close buttons This widget is added to the table widget to support the renaming and close buttons, as well as the direct renaming functionality. """ def __init__(self, presenter, plot_number, parent=None): super(PlotNameWidget, self).__init__(parent) self.presenter = presenter self.plot_number = plot_number self.mutex = QMutex() self.line_edit = QLineEdit( self.presenter.get_plot_name_from_number(plot_number)) self.line_edit.setReadOnly(True) self.line_edit.setFrame(False) # changes the line edit to look like normal even when self.line_edit.setStyleSheet( """* { background-color: rgba(0, 0, 0, 0); } QLineEdit:disabled { color: black; }""") self.line_edit.setAttribute(Qt.WA_TransparentForMouseEvents, True) self.line_edit.editingFinished.connect(self.rename_plot) # Disabling the line edit prevents it from temporarily # grabbing focus when changing code editors - this triggered # the editingFinished signal, which was causing #26305 self.line_edit.setDisabled(True) shown_icon = get_icon('mdi.eye') self.hide_button = QPushButton(shown_icon, "") self.hide_button.setToolTip('Hide') self.hide_button.setFlat(True) self.hide_button.setMaximumWidth(self.hide_button.iconSize().width() * 5 / 3) self.hide_button.clicked.connect(self.toggle_visibility) rename_icon = get_icon('mdi.square-edit-outline') self.rename_button = QPushButton(rename_icon, "") self.rename_button.setToolTip('Rename') self.rename_button.setFlat(True) self.rename_button.setMaximumWidth( self.rename_button.iconSize().width() * 5 / 3) self.rename_button.setCheckable(True) self.rename_button.toggled.connect(self.rename_button_toggled) close_icon = get_icon('mdi.close') self.close_button = QPushButton(close_icon, "") self.close_button.setToolTip('Delete') self.close_button.setFlat(True) self.close_button.setMaximumWidth( self.close_button.iconSize().width() * 5 / 3) self.close_button.clicked.connect( lambda: self.close_pressed(self.plot_number)) self.layout = QHBoxLayout() # Get rid of the top and bottom margins - the button provides # some natural margin anyway. Get rid of right margin and # reduce spacing to get buttons closer together. self.layout.setContentsMargins(5, 0, 0, 0) self.layout.setSpacing(0) self.layout.addWidget(self.line_edit) self.layout.addWidget(self.hide_button) self.layout.addWidget(self.rename_button) self.layout.addWidget(self.close_button) self.layout.sizeHint() self.setLayout(self.layout) def set_plot_name(self, new_name): """ Sets the internally stored and displayed plot name :param new_name: The name to set """ self.line_edit.setText(new_name) def close_pressed(self, plot_number): """ Close the plot with the given name :param plot_number: The unique number in GlobalFigureManager """ self.presenter.close_single_plot(plot_number) def rename_button_toggled(self, checked): """ If the rename button is pressed from being unchecked then make the line edit item editable :param checked: True if the rename toggle is now pressed """ if checked: self.toggle_plot_name_editable(True, toggle_rename_button=False) def toggle_plot_name_editable(self, editable, toggle_rename_button=True): """ Set the line edit item to be editable or not editable. If editable move the cursor focus to the editable name and highlight it all. :param editable: If true make the plot name editable, else make it read only :param toggle_rename_button: If true also toggle the rename button state """ self.line_edit.setReadOnly(not editable) self.line_edit.setDisabled(not editable) self.line_edit.setAttribute(Qt.WA_TransparentForMouseEvents, not editable) # This is a sneaky way to avoid the issue of two calls to # this toggle method, by effectively disabling the button # press in edit mode. self.rename_button.setAttribute(Qt.WA_TransparentForMouseEvents, editable) if toggle_rename_button: self.rename_button.setChecked(editable) if editable: self.line_edit.setFocus() self.line_edit.selectAll() else: self.line_edit.setSelection(0, 0) def toggle_visibility(self): """ Calls the presenter to hide the selected plot """ self.presenter.toggle_plot_visibility(self.plot_number) def set_visibility_icon(self, is_shown): """ Change the widget icon between shown and hidden :param is_shown: True if plot is shown, false if hidden """ if is_shown: self.hide_button.setIcon(get_icon('mdi.eye')) self.hide_button.setToolTip('Hide') else: self.hide_button.setIcon(get_icon('mdi.eye', 'lightgrey')) self.hide_button.setToolTip('Show') def rename_plot(self): """ Called when the editing is finished, gets the presenter to do the real renaming of the plot """ self.presenter.rename_figure(self.plot_number, self.line_edit.text()) self.toggle_plot_name_editable(False)
class SpyderErrorDialog(QDialog): """Custom error dialog for error reporting.""" def __init__(self, parent=None, is_report=False): QDialog.__init__(self, parent) self.is_report = is_report # Set to true to run tests on the dialog. This is the default # in the test function at the end of this file. self._testing = False self.setWindowTitle(_("Issue reporter")) self._github_org = 'spyder-ide' self._github_repo = 'spyder' # To save the traceback sent to the internal console self.error_traceback = "" # Dialog main label if self.is_report: title = _("Please fill the following information") else: title = _("Spyder has encountered an internal problem!") self.main_label = QLabel( _("<h3>{title}</h3>" "Before reporting this problem, <i>please</i> consult our " "comprehensive " "<b><a href=\"{trouble_url}\">Troubleshooting Guide</a></b> " "which should help solve most issues, and search for " "<b><a href=\"{project_url}\">known bugs</a></b> " "matching your error message or problem description for a " "quicker solution.").format(title=title, trouble_url=__trouble_url__, project_url=__project_url__)) self.main_label.setOpenExternalLinks(True) self.main_label.setWordWrap(True) self.main_label.setAlignment(Qt.AlignJustify) self.main_label.setStyleSheet('font-size: 12px;') # Issue title self.title = QLineEdit() self.title.textChanged.connect(self._contents_changed) self.title_chars_label = QLabel( _("{} more characters " "to go...").format(TITLE_MIN_CHARS)) form_layout = QFormLayout() form_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) red_asterisk = '<font color="Red">*</font>' title_label = QLabel(_("<b>Title</b>: {}").format(red_asterisk)) form_layout.setWidget(0, QFormLayout.LabelRole, title_label) form_layout.setWidget(0, QFormLayout.FieldRole, self.title) # Description steps_header = QLabel( _("<b>Steps to reproduce:</b> {}").format(red_asterisk)) self.steps_text = QLabel( _("Please enter a detailed step-by-step " "description (in English) of what led up to " "the problem below. Issue reports without a " "clear way to reproduce them will be closed.")) self.steps_text.setWordWrap(True) self.steps_text.setAlignment(Qt.AlignJustify) self.steps_text.setStyleSheet('font-size: 12px;') # Field to input the description of the problem self.input_description = DescriptionWidget(self) # Only allow to submit to Github if we have a long enough description self.input_description.textChanged.connect(self._contents_changed) # Widget to show errors self.details = ShowErrorWidget(self) self.details.set_pythonshell_font(get_font()) self.details.hide() self.description_minimum_length = DESC_MIN_CHARS self.require_minimum_length = True # Label to show missing chars self.initial_chars = len(self.input_description.toPlainText()) self.desc_chars_label = QLabel( _("{} more characters " "to go...").format(self.description_minimum_length)) # Checkbox to dismiss future errors self.dismiss_box = QCheckBox( _("Hide all future errors during this " "session")) if self.is_report: self.dismiss_box.hide() # Dialog buttons gh_icon = ima.icon('github') self.submit_btn = QPushButton(gh_icon, _('Submit to Github')) self.submit_btn.setEnabled(False) self.submit_btn.clicked.connect(self._submit_to_github) self.details_btn = QPushButton(_('Show details')) self.details_btn.clicked.connect(self._show_details) if self.is_report: self.details_btn.hide() self.close_btn = QPushButton(_('Close')) if self.is_report: self.close_btn.clicked.connect(self.reject) # Buttons layout buttons_layout = QHBoxLayout() buttons_layout.addWidget(self.submit_btn) buttons_layout.addWidget(self.details_btn) buttons_layout.addWidget(self.close_btn) # Main layout layout = QVBoxLayout() layout.addWidget(self.main_label) layout.addSpacing(20) layout.addLayout(form_layout) layout.addWidget(self.title_chars_label) layout.addSpacing(12) layout.addWidget(steps_header) layout.addSpacing(-1) layout.addWidget(self.steps_text) layout.addSpacing(1) layout.addWidget(self.input_description) layout.addWidget(self.details) layout.addWidget(self.desc_chars_label) layout.addSpacing(15) layout.addWidget(self.dismiss_box) layout.addSpacing(15) layout.addLayout(buttons_layout) layout.setContentsMargins(25, 20, 25, 10) self.setLayout(layout) self.resize(570, 600) self.title.setFocus() # Set Tab key focus order self.setTabOrder(self.title, self.input_description) @staticmethod def render_issue(description='', traceback=''): """ Render issue content. Parameters ---------- description: str Description to include in issue message. traceback: str Traceback text. """ # Get component versions versions = get_versions() # Get dependencies if they haven't beed computed yet. if not dependencies.DEPENDENCIES: try: dependencies.declare_dependencies() except ValueError: pass # Get git revision for development version revision = '' if versions['revision']: revision = versions['revision'] # Make a description header in case no description is supplied if not description: description = "### What steps reproduce the problem?" # Make error section from traceback and add appropriate reminder header if traceback: error_section = ("### Traceback\n" "```python-traceback\n" "{}\n" "```".format(traceback)) else: error_section = '' issue_template = """\ ## Description {description} {error_section} ## Versions * Spyder version: {spyder_version} {commit} * Python version: {python_version} * Qt version: {qt_version} * {qt_api_name} version: {qt_api_version} * Operating System: {os_name} {os_version} ### Dependencies ``` {dependencies} ``` """.format(description=description, error_section=error_section, spyder_version=versions['spyder'], commit=revision, python_version=versions['python'], qt_version=versions['qt'], qt_api_name=versions['qt_api'], qt_api_version=versions['qt_api_ver'], os_name=versions['system'], os_version=versions['release'], dependencies=dependencies.status()) return issue_template @staticmethod def open_web_report(body, title=None): """ Open a new issue on Github with prefilled information. Parameters ---------- body: str The body content of the report. title: str or None, optional The title of the report. Default is None. """ url = QUrl(__project_url__ + '/issues/new') query = QUrlQuery() query.addQueryItem("body", quote(body)) if title: query.addQueryItem("title", quote(title)) url.setQuery(query) QDesktopServices.openUrl(url) def set_require_minimum_length(self, state): """Remove the requirement for minimum length.""" self.require_minimum_length = state if state: self._contents_changed() else: self.desc_chars_label.setText('') def set_github_repo_org(self, repo_fullname): """Set the report Github organization and repository.""" org, repo = repo_fullname.split('/') self._github_org = org self._github_repo = repo def _submit_to_github(self): """Action to take when pressing the submit button.""" # Getting description and traceback title = self.title.text() description = self.input_description.toPlainText() traceback = self.error_traceback[:-1] # Remove last EOL # Render issue issue_text = self.render_issue(description=description, traceback=traceback) try: org = self._github_org if not self._testing else 'ccordoba12' repo = self._github_repo github_backend = GithubBackend(org, repo, parent_widget=self) github_report = github_backend.send_report(title, issue_text) if github_report: self.close() except Exception: ret = QMessageBox.question( self, _('Error'), _("An error occurred while trying to send the issue to " "Github automatically. Would you like to open it " "manually?<br><br>" "If so, please make sure to paste your clipboard " "into the issue report box that will appear in a new " "browser tab before clicking <i>Submit</i> on that " "page."), ) if ret in [QMessageBox.Yes, QMessageBox.Ok]: QApplication.clipboard().setText(issue_text) issue_body = ( " \n<!--- *** BEFORE SUBMITTING: PASTE CLIPBOARD HERE " "TO COMPLETE YOUR REPORT *** ---!>\n") self.open_web_report(body=issue_body, title=title) def append_traceback(self, text): """Append text to the traceback, to be displayed in details.""" self.error_traceback += text def _show_details(self): """Show traceback on its own dialog""" if self.details.isVisible(): self.details.hide() self.details_btn.setText(_('Show details')) else: self.resize(570, 700) self.details.document().setPlainText('') self.details.append_text_to_shell(self.error_traceback, error=True, prompt=False) self.details.show() self.details_btn.setText(_('Hide details')) def _contents_changed(self): """Activate submit_btn.""" if not self.require_minimum_length: return desc_chars = (len(self.input_description.toPlainText()) - self.initial_chars) if desc_chars < self.description_minimum_length: self.desc_chars_label.setText(u"{} {}".format( self.description_minimum_length - desc_chars, _("more characters to go..."))) else: self.desc_chars_label.setText(_("Description complete; thanks!")) title_chars = len(self.title.text()) if title_chars < TITLE_MIN_CHARS: self.title_chars_label.setText(u"{} {}".format( TITLE_MIN_CHARS - title_chars, _("more characters to go..."))) else: self.title_chars_label.setText(_("Title complete; thanks!")) submission_enabled = (desc_chars >= self.description_minimum_length and title_chars >= TITLE_MIN_CHARS) self.submit_btn.setEnabled(submission_enabled) def set_title(self, title): """Set the title for the report.""" self.title.setText(title) def set_description(self, description): """Set the description for the report.""" self.input_description.setPlainText(description) def set_color_scheme(self, color_scheme): """Set the color scheme for the description input.""" self.input_description.set_color_scheme(color_scheme)
class MasterControl(QWidget): def __init__(self, parent, initialTab=0, b=config.mainB, c=config.mainC, v=config.mainV, text=config.mainText): super().__init__() self.isRefreshing = True self.parent = parent # set title self.setWindowTitle(config.thisTranslation["controlPanel"]) if config.restrictControlPanelWidth and config.screenWidth > config.masterControlWidth: self.setFixedWidth(config.masterControlWidth) # setup item option lists self.setupResourceLists() # setup interface self.text = text self.setupUI(b, c, v, text, initialTab) self.isRefreshing = False # manage key capture def event(self, event): if event.type() == QEvent.KeyRelease: if event.modifiers() == Qt.ControlModifier: if event.key() == Qt.Key_B: self.tabs.setCurrentIndex(0) elif event.key() == Qt.Key_L: self.tabs.setCurrentIndex(1) elif event.key() == Qt.Key_F: self.tabs.setCurrentIndex(2) elif event.key() == Qt.Key_Y: self.tabs.setCurrentIndex(3) elif event.key() == Qt.Key_M: self.tabs.setCurrentIndex(4) elif event.key() == Qt.Key_Escape: self.hide() return QWidget.event(self, event) def closeEvent(self, event): # Control panel is designed for frequent use # Hiding it instead of closing may save time from reloading event.ignore() self.hide() def setupResourceLists(self): self.parent.setupResourceLists() # bible versions self.textList = self.parent.textList self.textFullNameList = self.parent.textFullNameList self.strongBibles = self.parent.strongBibles if self.parent.versionCombo is not None and config.menuLayout in ( "classic", "focus", "aleph"): for index, fullName in enumerate(self.textFullNameList): self.parent.versionCombo.setItemData(index, fullName, Qt.ToolTipRole) # commentaries self.commentaryList = self.parent.commentaryList #self.commentaryFullNameList = [Commentary(module).commentaryInfo() for module in self.commentaryList] self.commentaryFullNameList = self.parent.commentaryFullNameList # reference book # menu10_dialog self.referenceBookList = self.parent.referenceBookList # topic # menu5_topics self.topicListAbb = self.parent.topicListAbb self.topicList = self.parent.topicList # lexicon # context1_originalLexicon self.lexiconList = self.parent.lexiconList # dictionary # context1_dict self.dictionaryListAbb = self.parent.dictionaryListAbb self.dictionaryList = self.parent.dictionaryList # encyclopedia # context1_encyclopedia self.encyclopediaListAbb = self.parent.encyclopediaListAbb self.encyclopediaList = self.parent.encyclopediaList # 3rd-party dictionary # menu5_3rdDict self.thirdPartyDictionaryList = self.parent.thirdPartyDictionaryList # def setupItemLists(self): # # bible versions # self.textList = BiblesSqlite().getBibleList() # self.textFullNameList = [Bible(text).bibleInfo() for text in self.textList] # self.strongBibles = [text for text in self.textList if Bible(text).bibleStrong()] # if self.parent.versionCombo is not None and config.menuLayout in ("classic", "focus", "aleph"): # for index, fullName in enumerate(self.textFullNameList): # self.parent.versionCombo.setItemData(index, fullName, Qt.ToolTipRole) # # commentaries # self.commentaryList = Commentary().getCommentaryList() # #self.commentaryFullNameList = [Commentary(module).commentaryInfo() for module in self.commentaryList] # self.commentaryFullNameList = [] # for module in self.commentaryList: # info = Commentary(module).commentaryInfo() # if info == "https://Marvel.Bible Commentary" and module in Commentary.marvelCommentaries: # info = Commentary.marvelCommentaries[module] # self.commentaryFullNameList.append(info) # # reference book # # menu10_dialog # bookData = BookData() # self.referenceBookList = [book for book, *_ in bookData.getBookList()] # # open database # indexes = IndexesSqlite() # # topic # # menu5_topics # topicDictAbb2Name = {abb: name for abb, name in indexes.topicList} # self.topicListAbb = list(topicDictAbb2Name.keys()) # topicDict = {name: abb for abb, name in indexes.topicList} # self.topicList = list(topicDict.keys()) # # lexicon # # context1_originalLexicon # self.lexiconList = LexiconData().lexiconList # # dictionary # # context1_dict # dictionaryDictAbb2Name = {abb: name for abb, name in indexes.dictionaryList} # self.dictionaryListAbb = list(dictionaryDictAbb2Name.keys()) # dictionaryDict = {name: abb for abb, name in indexes.dictionaryList} # self.dictionaryList = list(dictionaryDict.keys()) # # encyclopedia # # context1_encyclopedia # encyclopediaDictAbb2Name = {abb: name for abb, name in indexes.encyclopediaList} # self.encyclopediaListAbb = list(encyclopediaDictAbb2Name.keys()) # encyclopediaDict = {name: abb for abb, name in indexes.encyclopediaList} # self.encyclopediaList = list(encyclopediaDict.keys()) # # 3rd-party dictionary # # menu5_3rdDict # self.thirdPartyDictionaryList = ThirdPartyDictionary(self.parent.textCommandParser.isThridPartyDictionary(config.thirdDictionary)).moduleList def setupUI(self, b, c, v, text, initialTab): mainLayout = QVBoxLayout() mainLayout.addWidget(self.sharedWidget()) mainLayout.addWidget(self.tabWidget(b, c, v, text, initialTab)) self.setLayout(mainLayout) def sharedWidget(self): sharedWidget = QWidget() sharedWidgetLayout = QVBoxLayout() subLayout = QHBoxLayout() subLayout.addWidget(self.commandFieldWidget()) subLayout.addWidget(self.autoCloseCheckBox()) sharedWidgetLayout.addLayout(subLayout) sharedWidget.setLayout(sharedWidgetLayout) return sharedWidget def updateBibleTabText(self, reference): self.tabs.setTabText(0, reference) def tabWidget(self, b, c, v, text, initialTab): self.tabs = QTabWidget() # 0 self.bibleTab = BibleExplorer(self, (b, c, v, text)) self.tabs.addTab(self.bibleTab, config.thisTranslation["cp0"]) self.tabs.setTabToolTip(0, config.thisTranslation["cp0Tip"]) # 1 libraryTab = LibraryLauncher(self) self.tabs.addTab(libraryTab, config.thisTranslation["cp1"]) self.tabs.setTabToolTip(1, config.thisTranslation["cp1Tip"]) # 2 self.toolTab = SearchLauncher(self) self.tabs.addTab(self.toolTab, config.thisTranslation["cp2"]) self.tabs.setTabToolTip(2, config.thisTranslation["cp2Tip"]) # 3 self.historyTab = HistoryLauncher(self) self.tabs.addTab(self.historyTab, config.thisTranslation["cp3"]) self.tabs.setTabToolTip(3, config.thisTranslation["cp3Tip"]) # 4 self.miscellaneousTab = MiscellaneousLauncher(self) self.tabs.addTab(self.miscellaneousTab, config.thisTranslation["cp4"]) self.tabs.setTabToolTip(4, config.thisTranslation["cp4Tip"]) # set action with changing tabs self.tabs.currentChanged.connect(self.tabChanged) # set initial tab self.tabs.setCurrentIndex(initialTab) return self.tabs def commandFieldWidget(self): self.commandField = QLineEdit() self.commandField.setClearButtonEnabled(True) self.commandField.setToolTip( config.thisTranslation["enter_command_here"]) self.commandField.returnPressed.connect(self.commandEntered) return self.commandField def autoCloseCheckBox(self): checkbox = QCheckBox() checkbox.setText(config.thisTranslation["autoClose"]) checkbox.setToolTip(config.thisTranslation["autoCloseToolTip"]) checkbox.setChecked(config.closeControlPanelAfterRunningCommand) checkbox.stateChanged.connect( self.closeControlPanelAfterRunningCommandChanged) return checkbox # Common layout def buttonsWidget(self, buttonElementTupleTuple, r2l=False, translation=True): buttons = QWidget() buttonsLayouts = QVBoxLayout() buttonsLayouts.setSpacing(3) for buttonElementTuple in buttonElementTupleTuple: buttonsLayouts.addLayout( self.buttonsLayout(buttonElementTuple, r2l, translation)) buttons.setLayout(buttonsLayouts) return buttons def buttonsLayout(self, buttonElementTuple, r2l=False, translation=True): buttonsLayout = QBoxLayout( QBoxLayout.RightToLeft if r2l else QBoxLayout.LeftToRight) buttonsLayout.setSpacing(5) for label, action in buttonElementTuple: buttonLabel = config.thisTranslation[ label] if translation else label button = QPushButton(buttonLabel) button.clicked.connect(action) buttonsLayout.addWidget(button) return buttonsLayout def comboFeatureLayout(self, feature, combo, action): # QGridLayout: https://stackoverflow.com/questions/61451279/how-does-setcolumnstretch-and-setrowstretch-works layout = QGridLayout() layout.setSpacing(5) # combo layout.addWidget(combo, 0, 0, 1, 3) # button button = QPushButton(config.thisTranslation[feature]) button.clicked.connect(action) layout.addWidget(button, 0, 3, 1, 1) return layout # Actions def closeControlPanelAfterRunningCommandChanged(self): config.closeControlPanelAfterRunningCommand = not config.closeControlPanelAfterRunningCommand def updateBCVText(self, b, c, v, text): self.bibleTab.updateBCVText(b, c, v, text) def commandEntered(self): command = self.commandField.text() self.runTextCommand(command, False) def runTextCommand(self, command, printCommand=True, reloadMainWindow=False): if printCommand: self.commandField.setText(command) self.parent.textCommandLineEdit.setText(command) self.parent.runTextCommand(command) if reloadMainWindow: self.parent.reloadCurrentRecord() if config.closeControlPanelAfterRunningCommand and not self.isRefreshing: self.hide() def tabChanged(self, index): self.isRefreshing = True # refresh content if index == 3: self.historyTab.refresh() elif index == 4: self.miscellaneousTab.refresh() # set focus if index == 2: self.toolTab.searchField.setFocus() if config.contextItem: self.toolTab.searchField.setText(config.contextItem) config.contextItem = "" elif self.parent.mainView.currentWidget().selectedText(): self.toolTab.searchField.setText( self.parent.mainView.currentWidget().selectedText()) elif self.parent.studyView.currentWidget().selectedText(): self.toolTab.searchField.setText( self.parent.studyView.currentWidget().selectedText()) else: self.commandField.setFocus() self.isRefreshing = False def displayMessage(self, message="", title="UniqueBible"): reply = QMessageBox.information(self, title, message)
class GoToLineDialog(QDialog): def __init__(self, editor): QDialog.__init__(self, editor, Qt.WindowTitleHint | Qt.WindowCloseButtonHint) # 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.lineno = None self.editor = editor self.setWindowTitle(_("Editor")) self.setModal(True) label = QLabel(_("Go to line:")) self.lineedit = QLineEdit() validator = QIntValidator(self.lineedit) validator.setRange(1, editor.get_line_count()) self.lineedit.setValidator(validator) self.lineedit.textChanged.connect(self.text_has_changed) cl_label = QLabel(_("Current line:")) cl_label_v = QLabel("<b>%d</b>" % editor.get_cursor_line_number()) last_label = QLabel(_("Line count:")) last_label_v = QLabel("%d" % editor.get_line_count()) glayout = QGridLayout() glayout.addWidget(label, 0, 0, Qt.AlignVCenter | Qt.AlignRight) glayout.addWidget(self.lineedit, 0, 1, Qt.AlignVCenter) glayout.addWidget(cl_label, 1, 0, Qt.AlignVCenter | Qt.AlignRight) glayout.addWidget(cl_label_v, 1, 1, Qt.AlignVCenter) glayout.addWidget(last_label, 2, 0, Qt.AlignVCenter | Qt.AlignRight) glayout.addWidget(last_label_v, 2, 1, Qt.AlignVCenter) bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Vertical, self) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) btnlayout = QVBoxLayout() btnlayout.addWidget(bbox) btnlayout.addStretch(1) ok_button = bbox.button(QDialogButtonBox.Ok) ok_button.setEnabled(False) self.lineedit.textChanged.connect( lambda text: ok_button.setEnabled(len(text) > 0)) layout = QHBoxLayout() layout.addLayout(glayout) layout.addLayout(btnlayout) self.setLayout(layout) self.lineedit.setFocus() def text_has_changed(self, text): """Line edit's text has changed.""" text = str(text) if text: self.lineno = int(text) else: self.lineno = None def get_line_number(self): """Return line number.""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.lineno
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 LabelDialog(QDialog): def __init__(self, text="Enter object label", parent=None, listItem=None): super(LabelDialog, self).__init__(parent) self.edit = QLineEdit() self.edit.setText(text) self.edit.setValidator(self.label_validator()) self.edit.editingFinished.connect(self.post_process) model = QStringListModel() model.setStringList(listItem) completer = QCompleter() completer.setModel(model) self.edit.setCompleter(completer) layout = QVBoxLayout() layout.addWidget(self.edit) self.buttonBox = bb = BB(BB.Ok | BB.Cancel, Qt.Horizontal, self) bb.button(BB.Ok).setIcon(QIcon('icons/done.png')) bb.button(BB.Cancel).setIcon(QIcon('icons/undo.png')) bb.accepted.connect(self.validate) bb.rejected.connect(self.reject) layout.addWidget(bb) if listItem is not None and len(listItem) > 0: self.listWidget = QListWidget(self) for item in listItem: self.listWidget.addItem(item) self.listWidget.itemClicked.connect(self.listItemClick) self.listWidget.itemDoubleClicked.connect(self.listItemDoubleClick) layout.addWidget(self.listWidget) self.setLayout(layout) def label_validator(self): return QRegExpValidator(QRegExp(r'^[^ \t].+'), None) def validate(self): try: if self.edit.text().trimmed(): self.accept() except AttributeError: if self.edit.text().strip(): self.accept() def post_process(self): try: self.edit.setText(self.edit.text().trimmed()) except AttributeError: self.edit.setText(self.edit.text()) def pop_up(self, text='', move=True): self.edit.setText(text) self.edit.setSelection(0, len(text)) self.edit.setFocus(Qt.PopupFocusReason) if move: self.move(QCursor.pos()) return self.edit.text() if self.exec_() else None def listItemClick(self, tQListWidgetItem): try: text = tQListWidgetItem.text().trimmed() except AttributeError: # PyQt5: AttributeError: 'str' object has no attribute 'trimmed' text = tQListWidgetItem.text().strip() self.edit.setText(text) def listItemDoubleClick(self, tQListWidgetItem): self.listItemClick(tQListWidgetItem) self.validate()