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 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 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 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 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)
def createEditor(self, parent: QWidget, option: QStyleOptionViewItem, index: QModelIndex) -> QWidget: editor = QLineEdit(parent=parent) editor.setAlignment(Qt.AlignCenter) editor.selectAll() return editor
class LineEditDoubleValidatorTest(unittest.TestCase): def setUp(self): self.initial_value = "0.0" self.line_edit = QLineEdit(self.initial_value) self.line_edit.show() def tearDown(self): self.assertTrue(self.line_edit.close()) def test_initialization_of_the_validator_does_not_raise(self): try: _ = self._create_validator() except Exception as ex: self.fail(f"An exception was thrown when attempting to create the validator {ex}.") def test_that_editing_the_line_edit_with_a_valid_double_will_work(self): valid_entry = "1.0" self._validator = self._create_validator() self._edit_line_edit(valid_entry) self.assertEqual(self.line_edit.text(), valid_entry) def test_that_editing_the_line_edit_with_a_valid_double_in_e_notation_will_work(self): valid_entry = "3e5" self._validator = self._create_validator() self._edit_line_edit(valid_entry) self.assertEqual(self.line_edit.text(), valid_entry) def test_that_editing_the_line_edit_with_an_empty_string_will_reset_to_the_last_valid_value(self): invalid_entry = "" self._validator = self._create_validator() self._edit_line_edit(invalid_entry) self.assertEqual(self.line_edit.text(), self.initial_value) def test_that_editing_the_line_edit_with_broken_e_notation_will_reset_to_the_last_valid_value(self): invalid_entry = "e" self._validator = self._create_validator() self._edit_line_edit(invalid_entry) self.assertEqual(self.line_edit.text(), self.initial_value) def _create_validator(self): validator = LineEditDoubleValidator(self.line_edit, self.initial_value) self.line_edit.setValidator(validator) return validator def _edit_line_edit(self, key_entry): QTest.mouseClick(self.line_edit, Qt.LeftButton) self.line_edit.selectAll() QTest.keyClick(self.line_edit, Qt.Key_Backspace) QTest.keyClicks(self.line_edit, key_entry) QTest.keyClick(self.line_edit, Qt.Key_Enter) QApplication.sendPostedEvents()