# -*- coding: utf-8 -*- # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """Kite installation widget.""" # Standard library imports import sys # Third-party imports from qtpy.QtCore import QEvent, QObject, Qt, Signal from qtpy.QtGui import QPixmap from qtpy.QtWidgets import (QApplication, QDialog, QHBoxLayout, QMessageBox, QLabel, QProgressBar, QPushButton, QVBoxLayout, QWidget) # Local imports from spyder.config.base import _ from spyder.utils.image_path_manager import get_image_path from spyder.utils.icon_manager import ima from spyder.utils.palette import QStylePalette from spyder.plugins.completion.providers.kite.utils.install import ( ERRORED, INSTALLING, FINISHED, CANCELLED) from spyder.utils.stylesheet import DialogStyle KITE_SPYDER_URL = "https://kite.com/integrations/spyder" KITE_CONTACT_URL = "https://kite.com/contact/" MAC = sys.platform == 'darwin' class KiteIntegrationInfo(QWidget): """Initial Widget with info about the integration with Kite.""" # Signal triggered for the 'Install Kite' button sig_install_button_clicked = Signal() # Signal triggered for the 'Dismiss' button sig_dismiss_button_clicked = Signal() def __init__(self, parent): super(KiteIntegrationInfo, self).__init__(parent) # Images images_layout = QHBoxLayout() icon_filename = 'kite_completions' image_path = get_image_path(icon_filename) image = QPixmap(image_path) image_label = QLabel() image_label = QLabel() image_height = int(image.height() * DialogStyle.IconScaleFactor) image_width = int(image.width() * DialogStyle.IconScaleFactor) image = image.scaled(image_width, image_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) image_label.setPixmap(image) images_layout.addStretch() images_layout.addWidget(image_label) images_layout.addStretch() ilayout = QHBoxLayout() ilayout.addLayout(images_layout) # Label integration_label_title = QLabel( "Get better code completions in Spyder") integration_label_title.setStyleSheet( f"font-size: {DialogStyle.TitleFontSize}") integration_label_title.setWordWrap(True) integration_label = QLabel( _("Now Spyder can use Kite to provide better code " "completions for key packages in the scientific Python " "Ecosystem. Install Kite for a better editor experience in " "Spyder.

Kite is free to use but is not open " "source. Learn more about Kite ") .format(kite_url=KITE_SPYDER_URL)) integration_label.setStyleSheet( f"font-size: {DialogStyle.ContentFontSize}") integration_label.setOpenExternalLinks(True) integration_label.setWordWrap(True) integration_label.setFixedWidth(360) label_layout = QVBoxLayout() label_layout.addWidget(integration_label_title) label_layout.addWidget(integration_label) # Buttons install_button_color = QStylePalette.COLOR_ACCENT_2 install_button_hover = QStylePalette.COLOR_ACCENT_3 install_button_pressed = QStylePalette.COLOR_ACCENT_4 dismiss_button_color = QStylePalette.COLOR_BACKGROUND_4 dismiss_button_hover = QStylePalette.COLOR_BACKGROUND_5 dismiss_button_pressed = QStylePalette.COLOR_BACKGROUND_6 font_color = QStylePalette.COLOR_TEXT_1 buttons_layout = QHBoxLayout() install_button = QPushButton(_('Install Kite')) install_button.setAutoDefault(False) install_button.setStyleSheet(( "QPushButton {{ " "background-color: {background_color};" "border-color: {border_color};" "font-size: {font_size};" "color: {font_color};" "padding: {padding}}}" "QPushButton:hover:!pressed {{ " "background-color: {color_hover}}}" "QPushButton:pressed {{ " "background-color: {color_pressed}}}" ).format(background_color=install_button_color, border_color=install_button_color, font_size=DialogStyle.ButtonsFontSize, font_color=font_color, padding=DialogStyle.ButtonsPadding, color_hover=install_button_hover, color_pressed=install_button_pressed)) dismiss_button = QPushButton(_('Dismiss')) dismiss_button.setAutoDefault(False) dismiss_button.setStyleSheet(( "QPushButton {{ " "background-color: {background_color};" "border-color: {border_color};" "font-size: {font_size};" "color: {font_color};" "padding: {padding}}}" "QPushButton:hover:!pressed {{ " "background-color: {color_hover}}}" "QPushButton:pressed {{ " "background-color: {color_pressed}}}" ).format(background_color=dismiss_button_color, border_color=dismiss_button_color, font_size=DialogStyle.ButtonsFontSize, font_color=font_color, padding=DialogStyle.ButtonsPadding, color_hover=dismiss_button_hover, color_pressed=dismiss_button_pressed)) buttons_layout.addStretch() buttons_layout.addWidget(install_button) if not MAC: buttons_layout.addSpacing(10) buttons_layout.addWidget(dismiss_button) # Buttons with label vertical_layout = QVBoxLayout() if not MAC: vertical_layout.addStretch() vertical_layout.addLayout(label_layout) vertical_layout.addSpacing(20) vertical_layout.addLayout(buttons_layout) vertical_layout.addStretch() else: vertical_layout.addLayout(label_layout) vertical_layout.addLayout(buttons_layout) general_layout = QHBoxLayout() general_layout.addStretch() general_layout.addLayout(ilayout) general_layout.addSpacing(15) general_layout.addLayout(vertical_layout) general_layout.addStretch() self.setLayout(general_layout) # Signals install_button.clicked.connect(self.sig_install_button_clicked) dismiss_button.clicked.connect(self.sig_dismiss_button_clicked) self.setStyleSheet( f"background-color: {QStylePalette.COLOR_BACKGROUND_2}") self.setContentsMargins(18, 40, 18, 40) if not MAC: self.setFixedSize(800, 350) class HoverEventFilter(QObject): """QObject to handle event filtering.""" # Signal to trigger on a HoverEnter event sig_hover_enter = Signal() # Signal to trigger on a HoverLeave event sig_hover_leave = Signal() def eventFilter(self, widget, event): """Reimplemented Qt method.""" if event.type() == QEvent.HoverEnter: self.sig_hover_enter.emit() elif event.type() == QEvent.HoverLeave: self.sig_hover_leave.emit() return super(HoverEventFilter, self).eventFilter(widget, event) class KiteInstallation(QWidget): """Kite progress installation widget.""" def __init__(self, parent): super(KiteInstallation, self).__init__(parent) # Left side action_layout = QVBoxLayout() progress_layout = QHBoxLayout() self._progress_widget = QWidget(self) self._progress_widget.setFixedHeight(50) self._progress_filter = HoverEventFilter() self._progress_bar = QProgressBar(self) self._progress_bar.setFixedWidth(180) self._progress_widget.installEventFilter(self._progress_filter) self.cancel_button = QPushButton() self.cancel_button.setIcon(ima.icon('DialogCloseButton')) self.cancel_button.hide() progress_layout.addWidget(self._progress_bar, alignment=Qt.AlignLeft) progress_layout.addWidget(self.cancel_button) self._progress_widget.setLayout(progress_layout) self._progress_label = QLabel(_('Downloading')) install_info = QLabel( _("Kite comes with a native app called the Copilot
" "which provides you with real time
" "documentation as you code.

" "When Kite is done installing, the Copilot will
" "launch automatically and guide you throught the
" "rest of the setup process.")) button_layout = QHBoxLayout() self.ok_button = QPushButton(_('OK')) button_layout.addStretch() button_layout.addWidget(self.ok_button) button_layout.addStretch() action_layout.addStretch() action_layout.addWidget(self._progress_label) action_layout.addWidget(self._progress_widget) action_layout.addWidget(install_info) action_layout.addSpacing(10) action_layout.addLayout(button_layout) action_layout.addStretch() # Right side copilot_image_source = get_image_path('kite_copilot') copilot_image = QPixmap(copilot_image_source) copilot_label = QLabel() screen = QApplication.primaryScreen() device_pixel_ratio = screen.devicePixelRatio() if device_pixel_ratio > 1: copilot_image.setDevicePixelRatio(device_pixel_ratio) copilot_label.setPixmap(copilot_image) else: image_height = int(copilot_image.height() * 0.4) image_width = int(copilot_image.width() * 0.4) copilot_label.setPixmap( copilot_image.scaled(image_width, image_height, Qt.KeepAspectRatio, Qt.SmoothTransformation)) # Layout general_layout = QHBoxLayout() general_layout.addLayout(action_layout) general_layout.addWidget(copilot_label) self.setLayout(general_layout) # Signals self._progress_filter.sig_hover_enter.connect( lambda: self.cancel_button.show()) self._progress_filter.sig_hover_leave.connect( lambda: self.cancel_button.hide()) def update_installation_status(self, status): """Update installation status (downloading, installing, finished).""" self._progress_label.setText(status) if status == INSTALLING: self._progress_bar.setRange(0, 0) def update_installation_progress(self, current_value, total): """Update installation progress bar.""" self._progress_bar.setMaximum(total) self._progress_bar.setValue(current_value) class KiteInstallerDialog(QDialog): """Kite installer.""" def __init__(self, parent, installation_thread): super(KiteInstallerDialog, self).__init__(parent) self.setStyleSheet( f"background-color: {QStylePalette.COLOR_BACKGROUND_2}") if sys.platform == 'darwin': self.setWindowFlags(Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint | Qt.Tool) else: self.setWindowFlags(Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint) self._parent = parent self._installation_thread = installation_thread self._integration_widget = KiteIntegrationInfo(self) self._installation_widget = KiteInstallation(self) # Layout installer_layout = QVBoxLayout() installer_layout.addWidget(self._integration_widget) installer_layout.addWidget(self._installation_widget) self.setLayout(installer_layout) # Signals self._installation_thread.sig_download_progress.connect( self._installation_widget.update_installation_progress) self._installation_thread.sig_installation_status.connect( self._installation_widget.update_installation_status) self._installation_thread.sig_installation_status.connect( self.finished_installation) self._installation_thread.sig_error_msg.connect(self._handle_error_msg) self._integration_widget.sig_install_button_clicked.connect( self.install) self._integration_widget.sig_dismiss_button_clicked.connect( self.reject) self._installation_widget.ok_button.clicked.connect( self.close_installer) self._installation_widget.cancel_button.clicked.connect( self.cancel_install) # Show integration widget self.setup() def _handle_error_msg(self, msg): """Handle error message with an error dialog.""" QMessageBox.critical( self._parent, _('Kite installation error'), _("An error ocurred while installing Kite!

" "Please try to " "install it manually or " "contact Kite for help") .format(kite_url=KITE_SPYDER_URL, kite_contact=KITE_CONTACT_URL)) self.accept() def setup(self, integration=True, installation=False): """Setup visibility of widgets.""" self._integration_widget.setVisible(integration) self._installation_widget.setVisible(installation) self.adjustSize() def install(self): """Initialize installation process and show install widget.""" self.setup(integration=False, installation=True) self._installation_thread.cancelled = False self._installation_thread.install() def cancel_install(self): """Cancel the installation in progress.""" reply = QMessageBox.critical( self._parent, 'Spyder', _('Do you really want to cancel Kite installation?'), QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes and self._installation_thread.isRunning(): self._installation_thread.cancelled = True self._installation_thread.quit() self.setup() self.accept() return True return False def finished_installation(self, status): """Handle finished installation.""" if status == FINISHED: self.setup() self.accept() def close_installer(self): """Close the installation dialog.""" if (self._installation_thread.status == ERRORED or self._installation_thread.status == FINISHED or self._installation_thread.status == CANCELLED): self.setup() self.accept() else: self.hide() def reject(self): """Reimplement Qt method.""" on_installation_widget = self._installation_widget.isVisible() if on_installation_widget: self.close_installer() else: super(KiteInstallerDialog, self).reject() if __name__ == "__main__": from spyder.utils.qthelpers import qapplication app = qapplication() install_progress = KiteInstallation(None) install_progress.show() app.exec_()