From 11c614c9485ce7fbe52030c98f5e5ddcd6e439d8 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 24 Aug 2024 19:03:48 -0500 Subject: [PATCH 1/6] App: Set title and icon for About dialog --- spyder/plugins/application/widgets/about.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spyder/plugins/application/widgets/about.py b/spyder/plugins/application/widgets/about.py index 62be57e26d7..9c643042740 100644 --- a/spyder/plugins/application/widgets/about.py +++ b/spyder/plugins/application/widgets/about.py @@ -48,6 +48,8 @@ def __init__(self, parent): self.setWindowFlags( self.windowFlags() & ~Qt.WindowContextHelpButtonHint ) + self.setWindowTitle(_("About Spyder")) + self.setWindowIcon(ima.icon("MessageBoxInformation")) versions = get_versions() # -- Show Git revision for development version From b96bf6255a885eaa57bd4aaff949ad4f0266f0e8 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 26 Aug 2024 19:38:56 -0500 Subject: [PATCH 2/6] Application: Add status bar widget to show in-app appeal message --- spyder/plugins/application/container.py | 3 +- spyder/plugins/application/plugin.py | 13 +++- .../plugins/application/widgets/__init__.py | 1 + spyder/plugins/application/widgets/status.py | 70 +++++++++++++++++++ spyder/plugins/statusbar/plugin.py | 8 ++- spyder/utils/icon_manager.py | 1 + spyder/utils/palette.py | 6 ++ 7 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 spyder/plugins/application/widgets/status.py diff --git a/spyder/plugins/application/container.py b/spyder/plugins/application/container.py index e773e7e3ff0..39654bb5c30 100644 --- a/spyder/plugins/application/container.py +++ b/spyder/plugins/application/container.py @@ -27,7 +27,7 @@ from spyder.api.widgets.main_container import PluginMainContainer from spyder.utils.installers import InstallerMissingDependencies from spyder.config.base import get_conf_path, get_debug_level -from spyder.plugins.application.widgets import AboutDialog +from spyder.plugins.application.widgets import AboutDialog, InAppAppealStatus from spyder.plugins.console.api import ConsoleActions from spyder.utils.environ import UserEnvDialog from spyder.utils.qthelpers import start_file, DialogManager @@ -96,6 +96,7 @@ def setup(self): # Attributes self.dialog_manager = DialogManager() + self.inapp_appeal_status = InAppAppealStatus(self) # Actions # Documentation actions diff --git a/spyder/plugins/application/plugin.py b/spyder/plugins/application/plugin.py index d6915189ab1..d7b83492d82 100644 --- a/spyder/plugins/application/plugin.py +++ b/spyder/plugins/application/plugin.py @@ -98,8 +98,13 @@ def on_editor_available(self): editor = self.get_plugin(Plugins.Editor) self.get_container().sig_load_log_file.connect(editor.load) - # -------------------------- PLUGIN TEARDOWN ------------------------------ + @on_plugin_available(plugin=Plugins.StatusBar) + def on_statusbar_available(self): + statusbar = self.get_plugin(Plugins.StatusBar) + inapp_appeal_status = self.get_container().inapp_appeal_status + statusbar.add_status_widget(inapp_appeal_status) + # -------------------------- PLUGIN TEARDOWN ------------------------------ @on_plugin_teardown(plugin=Plugins.Preferences) def on_preferences_teardown(self): preferences = self.get_plugin(Plugins.Preferences) @@ -122,6 +127,12 @@ def on_main_menu_teardown(self): self._depopulate_help_menu() self.report_action.setVisible(False) + @on_plugin_teardown(plugin=Plugins.StatusBar) + def on_statusbar_teardown(self): + statusbar = self.get_plugin(Plugins.StatusBar) + inapp_appeal_status = self.get_container().inapp_appeal_status + statusbar.remove_status_widget(inapp_appeal_status.ID) + def on_close(self, _unused=True): self.get_container().on_close() diff --git a/spyder/plugins/application/widgets/__init__.py b/spyder/plugins/application/widgets/__init__.py index a45d6b823ca..3516092c053 100644 --- a/spyder/plugins/application/widgets/__init__.py +++ b/spyder/plugins/application/widgets/__init__.py @@ -8,3 +8,4 @@ """Widgets for the Application plugin.""" from .about import AboutDialog # noqa +from .status import InAppAppealStatus # noqa diff --git a/spyder/plugins/application/widgets/status.py b/spyder/plugins/application/widgets/status.py new file mode 100644 index 00000000000..11b7b7f1183 --- /dev/null +++ b/spyder/plugins/application/widgets/status.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Status bar widgets.""" + +# Standard library imports +import datetime + +# Local imports +from spyder.api.widgets.status import BaseTimerStatus +from spyder.api.translations import _ + + +class InAppAppealStatus(BaseTimerStatus): + """Status bar widget for current file read/write mode.""" + + ID = "inapp_appeal_status" + CONF_SECTION = "main" + INTERACT_ON_CLICK = True + + DAYS_TO_SHOW_AGAIN = 15 + + def __init__(self, parent=None): + super().__init__(parent) + + self._is_shown = False + + # We don't need to show a label for this widget + self.label_value.setVisible(False) + + # Update status every hour + self.set_interval(60 * 60 * 1000) + + # ---- StatusBarWidget API + # ------------------------------------------------------------------------- + def get_icon(self): + return self.create_icon("inapp_appeal") + + def update_status(self): + """ + Show widget for a day after a certain number of days, then hide it. + """ + today = datetime.date.today() + last_date = self.get_conf("last_inapp_appeal", default="") + + if last_date: + delta = today - datetime.date.fromisoformat(last_date) + if 0 < delta.days < self.DAYS_TO_SHOW_AGAIN: + self.setVisible(False) + else: + self.setVisible(True) + self.set_conf("last_inapp_appeal", str(today)) + else: + self.set_conf("last_inapp_appeal", str(today)) + + def get_tooltip(self): + return _("Help Spyder!") + + # ---- Qt methods + # ------------------------------------------------------------------------- + def showEvent(self, event): + super().showEvent(event) + + # Hide widget if necessary at startup + if not self._is_shown: + self.update_status() + self._is_shown = True diff --git a/spyder/plugins/statusbar/plugin.py b/spyder/plugins/statusbar/plugin.py index 72954766756..8e08e21a7ed 100644 --- a/spyder/plugins/statusbar/plugin.py +++ b/spyder/plugins/statusbar/plugin.py @@ -55,6 +55,7 @@ class StatusBar(SpyderPluginV2): "pythonenv_status", "matplotlib_status", "update_manager_status", + "inapp_appeal_status", } # ---- SpyderPluginV2 API @@ -234,6 +235,7 @@ def _organize_status_widgets(self): "pythonenv_status", "matplotlib_status", "update_manager_status", + "inapp_appeal_status", ] external_left = list(self.EXTERNAL_LEFT_WIDGETS.keys()) @@ -249,13 +251,15 @@ def _organize_status_widgets(self): # This is needed in the case kite is installed but not enabled if id_ in self.INTERNAL_WIDGETS: self._statusbar.insertPermanentWidget( - StatusBarWidgetPosition.Left, self.INTERNAL_WIDGETS[id_]) + StatusBarWidgetPosition.Left, self.INTERNAL_WIDGETS[id_] + ) self.INTERNAL_WIDGETS[id_].setVisible(True) # Add the external left widgets for id_ in external_left: self._statusbar.insertPermanentWidget( - StatusBarWidgetPosition.Left, self.EXTERNAL_LEFT_WIDGETS[id_]) + StatusBarWidgetPosition.Left, self.EXTERNAL_LEFT_WIDGETS[id_] + ) self.EXTERNAL_LEFT_WIDGETS[id_].setVisible(True) def before_mainwindow_visible(self): diff --git a/spyder/utils/icon_manager.py b/spyder/utils/icon_manager.py index 0e50f6821f3..9289d879a48 100644 --- a/spyder/utils/icon_manager.py +++ b/spyder/utils/icon_manager.py @@ -291,6 +291,7 @@ def __init__(self): 'undock': [('mdi.open-in-new',), {'color': self.MAIN_FG_COLOR}], 'close_pane': [('mdi.window-close',), {'color': self.MAIN_FG_COLOR}], 'toolbar_ext_button': [('mdi.dots-horizontal',), {'color': self.MAIN_FG_COLOR}], + 'inapp_appeal': [('mdi6.heart',), {'color': SpyderPalette.COLOR_HEART}], # --- Autocompletion/document symbol type icons -------------- 'completions': [('mdi.code-tags-check',), {'color': self.MAIN_FG_COLOR}], 'keyword': [('mdi.alpha-k-box',), {'color': SpyderPalette.GROUP_9, 'scale_factor': self.BIG_ATTR_FACTOR}], diff --git a/spyder/utils/palette.py b/spyder/utils/palette.py index 65b9e09041d..3c9fd56d97c 100644 --- a/spyder/utils/palette.py +++ b/spyder/utils/palette.py @@ -84,6 +84,9 @@ class SpyderPaletteDark(DarkPalette): SPECIAL_TABS_SEPARATOR = Gray.B70 SPECIAL_TABS_SELECTED = DarkPalette.COLOR_ACCENT_2 + # For the heart used to ask for donations + COLOR_HEART = Blue.B80 + # For editor tooltips TIP_TITLE_COLOR = Green.B80 TIP_CHAR_HIGHLIGHT_COLOR = Orange.B90 @@ -152,6 +155,9 @@ class SpyderPaletteLight(LightPalette): SPECIAL_TABS_SEPARATOR = Gray.B70 SPECIAL_TABS_SELECTED = LightPalette.COLOR_ACCENT_5 + # For the heart used to ask for donations + COLOR_HEART = Red.B70 + # For editor tooltips TIP_TITLE_COLOR = Green.B20 TIP_CHAR_HIGHLIGHT_COLOR = Orange.B30 From d82fb6dacd1620974f30f0bd48822f083a84947a Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 26 Aug 2024 19:39:59 -0500 Subject: [PATCH 3/6] Testing: Add tests for the new InAppAppealStatus widget --- .../application/widgets/tests/__init__.py | 0 .../application/widgets/tests/test_status.py | 55 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 spyder/plugins/application/widgets/tests/__init__.py create mode 100644 spyder/plugins/application/widgets/tests/test_status.py diff --git a/spyder/plugins/application/widgets/tests/__init__.py b/spyder/plugins/application/widgets/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spyder/plugins/application/widgets/tests/test_status.py b/spyder/plugins/application/widgets/tests/test_status.py new file mode 100644 index 00000000000..34355d257d7 --- /dev/null +++ b/spyder/plugins/application/widgets/tests/test_status.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright © Spyder Project Contributors +# +# Licensed under the terms of the MIT License +# ---------------------------------------------------------------------------- + +"""Tests for status bar widget.""" + +# Standard library imports +import datetime + +# Third-party imports +from flaky import flaky + +# Local imports +from spyder.plugins.statusbar.widgets.tests.test_status import status_bar # noqa +from spyder.plugins.application.widgets.status import InAppAppealStatus + + +@flaky(max_runs=5) +def test_inapp_appeal_status_bar(status_bar, qtbot): # noqa + """Test in-app appeal status bar widget.""" + plugin, window = status_bar + widget = InAppAppealStatus(window) + plugin.add_status_widget(widget) + + # The widget should be visible on a clean config + assert widget.isVisible() + + # The last day the widget is shown should be today + today = datetime.date.today() + widget.get_conf("last_inapp_appeal") == str(today) + + # The widget should be visible during a day + qtbot.wait(1000) + widget.update_status() + assert widget.isVisible() + + # The widget should be hidden if it was shown less than DAYS_TO_SHOW_AGAIN + # ago + some_days_ago = today - datetime.timedelta( + days=widget.DAYS_TO_SHOW_AGAIN - 5 + ) + widget.set_conf("last_inapp_appeal", str(some_days_ago)) + widget.update_status() + assert not widget.isVisible() + + # But it should be visible again if it was shown DAYS_TO_SHOW_AGAIN ago + days_to_show_again = today - datetime.timedelta( + days=widget.DAYS_TO_SHOW_AGAIN + ) + widget.set_conf("last_inapp_appeal", str(days_to_show_again)) + widget.update_status() + assert widget.isVisible() From c07fc5fb63a53d8c8a0f2622310af45699b3eb00 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 26 Aug 2024 19:57:32 -0500 Subject: [PATCH 4/6] API: Some small improvements to BaseTimerStatus --- spyder/api/widgets/status.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spyder/api/widgets/status.py b/spyder/api/widgets/status.py index f49ecce9012..72836bfdaf0 100644 --- a/spyder/api/widgets/status.py +++ b/spyder/api/widgets/status.py @@ -292,13 +292,13 @@ def closeEvent(self, event): super().closeEvent(event) def setVisible(self, value): - """Override Qt method to stops timers if widget is not visible.""" + """Stop timer if widget is not visible.""" if self.timer is not None: if value: self.timer.start(self._interval) else: self.timer.stop() - super(BaseTimerStatus, self).setVisible(value) + super().setVisible(value) # ---- Public API # ------------------------------------------------------------------------- From 70f0fecec6aa92fcb368aaa14e7ce580df26ca8d Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 28 Aug 2024 12:15:02 -0500 Subject: [PATCH 5/6] Widgets: Allow to set a delta when setting size font in WebView --- spyder/widgets/browser.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spyder/widgets/browser.py b/spyder/widgets/browser.py index c3f674dbb3d..e8fe4cdcf2e 100644 --- a/spyder/widgets/browser.py +++ b/spyder/widgets/browser.py @@ -296,20 +296,23 @@ def get_number_matches(self, pattern, source_text='', case=False, return number_matches - def set_font(self, font, fixed_font=None): + def set_font(self, font, fixed_font=None, size_delta=0): font = QFontInfo(font) settings = self.page().settings() + for fontfamily in (QWebEngineSettings.FontFamily.StandardFont, QWebEngineSettings.FontFamily.SerifFont, QWebEngineSettings.FontFamily.SansSerifFont, QWebEngineSettings.FontFamily.CursiveFont, QWebEngineSettings.FontFamily.FantasyFont): settings.setFontFamily(fontfamily, font.family()) + if fixed_font is not None: settings.setFontFamily( QWebEngineSettings.FontFamily.FixedFont, fixed_font.family() ) - size = font.pixelSize() + + size = font.pixelSize() + size_delta settings.setFontSize(QWebEngineSettings.FontSize.DefaultFontSize, size) settings.setFontSize( QWebEngineSettings.FontSize.DefaultFixedFontSize, size From 4eedb04e28827492162bdad7ea851f020eed8031 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 28 Aug 2024 16:54:57 -0500 Subject: [PATCH 6/6] Application: Add in-app appeal dialog Also, show it when clicking on its corresponding status bar widget --- .../appeal_page/dark/assets/appeal.css | 1 + .../appeal_page/dark/header_donations.svg | 18 ++ .../appeal_page/dark/icon_donations.svg | 16 ++ .../widgets/appeal_page/dark/index.html | 52 +++++ .../widgets/appeal_page/dark/spyder-logo.svg | 4 + .../appeal_page/light/assets/appeal.css | 1 + .../appeal_page/light/header_donations.svg | 199 ++++++++++++++++++ .../appeal_page/light/icon_donations.svg | 95 +++++++++ .../widgets/appeal_page/light/index.html | 52 +++++ .../widgets/appeal_page/light/spyder-logo.svg | 4 + spyder/plugins/application/widgets/status.py | 79 +++++++ 11 files changed, 521 insertions(+) create mode 100644 spyder/plugins/application/widgets/appeal_page/dark/assets/appeal.css create mode 100644 spyder/plugins/application/widgets/appeal_page/dark/header_donations.svg create mode 100644 spyder/plugins/application/widgets/appeal_page/dark/icon_donations.svg create mode 100644 spyder/plugins/application/widgets/appeal_page/dark/index.html create mode 100644 spyder/plugins/application/widgets/appeal_page/dark/spyder-logo.svg create mode 100644 spyder/plugins/application/widgets/appeal_page/light/assets/appeal.css create mode 100644 spyder/plugins/application/widgets/appeal_page/light/header_donations.svg create mode 100644 spyder/plugins/application/widgets/appeal_page/light/icon_donations.svg create mode 100644 spyder/plugins/application/widgets/appeal_page/light/index.html create mode 100644 spyder/plugins/application/widgets/appeal_page/light/spyder-logo.svg diff --git a/spyder/plugins/application/widgets/appeal_page/dark/assets/appeal.css b/spyder/plugins/application/widgets/appeal_page/dark/assets/appeal.css new file mode 100644 index 00000000000..ffecf147a19 --- /dev/null +++ b/spyder/plugins/application/widgets/appeal_page/dark/assets/appeal.css @@ -0,0 +1 @@ +*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.-mx-5{margin-left:-1.25rem;margin-right:-1.25rem}.flex{display:flex}.grid{display:grid}.w-full{width:100%}.max-w-screen-sm{max-width:640px}.items-center{align-items:center}.justify-center{justify-content:center}.p-10{padding:2.5rem}:root{--dark: #19232d;--med: #54687a;--light: #dfe1e2;--highlight: #259ae9;--action: #5d7c97;line-height:1.6;font-weight:400;background-color:var(--dark);color:var(--light);font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:var(--highlight);text-decoration:inherit}a:hover{color:var(--action)}body{margin:0;min-height:100vh}h1{font-size:3.2em;line-height:1.1}p{text-align:left}.center{text-align:center}.bold{font-weight:900}.underline{-webkit-text-decoration:underline wavy var(--highlight);text-decoration:underline wavy var(--highlight);text-underline-offset:.3em}.logo{width:250px;padding:0 30px 30px}.button{width:240px;height:100px;border-radius:8px;padding:15px;margin:20px auto;background-color:var(--highlight);cursor:pointer;box-shadow:0 0 5px #00000040;display:flex;align-items:center;justify-content:center}.button:hover{background-color:var(--action)}.button img{width:100%;height:auto}.heart-container{position:absolute;top:28%;right:25%;display:grid;align-items:center;justify-content:center;z-index:-1}.heart{background-color:var(--highlight);position:relative;transform:rotate(45deg);display:block}.heart:before,.heart:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background-color:inherit;border-radius:50%;display:block}.heart:before{transform:translateY(-50%)}.heart:after{transform:translate(-50%)}.heart-beat{height:6vw;width:6vw;animation:beat 1.4s infinite linear}.grid-center{display:grid;position:relative;align-items:center;justify-content:center;z-index:2}.content{padding-top:20px}.content p{text-align:center;margin-bottom:0}@keyframes beat{0%{transform:rotate(45deg) scale(1)}25%{transform:rotate(45deg) scale(1)}30%{transform:rotate(45deg) scale(1.4)}50%{transform:rotate(45deg) scale(1.2)}70%{transform:rotate(45deg) scale(1.4)}to{transform:rotate(45deg) scale(1)}} diff --git a/spyder/plugins/application/widgets/appeal_page/dark/header_donations.svg b/spyder/plugins/application/widgets/appeal_page/dark/header_donations.svg new file mode 100644 index 00000000000..a8c82a7cd7d --- /dev/null +++ b/spyder/plugins/application/widgets/appeal_page/dark/header_donations.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/spyder/plugins/application/widgets/appeal_page/dark/icon_donations.svg b/spyder/plugins/application/widgets/appeal_page/dark/icon_donations.svg new file mode 100644 index 00000000000..592dae5ec68 --- /dev/null +++ b/spyder/plugins/application/widgets/appeal_page/dark/icon_donations.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/spyder/plugins/application/widgets/appeal_page/dark/index.html b/spyder/plugins/application/widgets/appeal_page/dark/index.html new file mode 100644 index 00000000000..f042322d49a --- /dev/null +++ b/spyder/plugins/application/widgets/appeal_page/dark/index.html @@ -0,0 +1,52 @@ + + + + + + Donate today! + + + + +
+ + + + +
+ Help Keep Spyder Strong! +
+ +
+
+ + + + +
+

Spyder is a fully independent project, unaffiliated with Anaconda or + other companies. As it’s developed by and for + you, our beloved community, we need + your support to help keep it moving forward! + Your donation goes directly to funding Spyder development through NumFOCUS, a recognized charitable + organization.

+
+
+ + diff --git a/spyder/plugins/application/widgets/appeal_page/dark/spyder-logo.svg b/spyder/plugins/application/widgets/appeal_page/dark/spyder-logo.svg new file mode 100644 index 00000000000..f1aea8207f8 --- /dev/null +++ b/spyder/plugins/application/widgets/appeal_page/dark/spyder-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/spyder/plugins/application/widgets/appeal_page/light/assets/appeal.css b/spyder/plugins/application/widgets/appeal_page/light/assets/appeal.css new file mode 100644 index 00000000000..f50c3d2ba72 --- /dev/null +++ b/spyder/plugins/application/widgets/appeal_page/light/assets/appeal.css @@ -0,0 +1 @@ +*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.-mx-5{margin-left:-1.25rem;margin-right:-1.25rem}.flex{display:flex}.grid{display:grid}.w-full{width:100%}.max-w-screen-sm{max-width:640px}.items-center{align-items:center}.justify-center{justify-content:center}.p-10{padding:2.5rem}:root{--dark: #1b1f24;--med: #26486b;--light: #fafafa;--highlight: #8c0000;--action: #ac0000;line-height:1.6;font-weight:400;background-color:var(--light);color:var(--dark);font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:var(--action);text-decoration:inherit}a:hover{color:var(--action)}body{margin:0;min-height:100vh}h1{font-size:3.2em;line-height:1.1}p{text-align:left}.center{text-align:center}.bold{font-weight:900}.underline{-webkit-text-decoration:underline wavy var(--highlight);text-decoration:underline wavy var(--highlight);text-underline-offset:.3em}.logo{width:250px;padding:0 30px 30px}.button{width:240px;height:100px;border-radius:8px;padding:15px;margin:20px auto;background-color:var(--highlight);cursor:pointer;box-shadow:0 0 5px #00000040;display:flex;align-items:center;justify-content:center}.button:hover{background-color:var(--action)}.button img{width:100%;height:auto}.heart-container{position:absolute;top:28%;right:25%;display:grid;align-items:center;justify-content:center;z-index:-1}.heart{background-color:var(--highlight);position:relative;transform:rotate(45deg);display:block}.heart:before,.heart:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background-color:inherit;border-radius:50%;display:block}.heart:before{transform:translateY(-50%)}.heart:after{transform:translate(-50%)}.heart-beat{height:6vw;width:6vw;animation:beat 1.4s infinite linear}.grid-center{display:grid;position:relative;align-items:center;justify-content:center;z-index:2}.content{padding-top:20px}.content p{text-align:center;margin-bottom:0}@keyframes beat{0%{transform:rotate(45deg) scale(1)}25%{transform:rotate(45deg) scale(1)}30%{transform:rotate(45deg) scale(1.4)}50%{transform:rotate(45deg) scale(1.2)}70%{transform:rotate(45deg) scale(1.4)}to{transform:rotate(45deg) scale(1)}} diff --git a/spyder/plugins/application/widgets/appeal_page/light/header_donations.svg b/spyder/plugins/application/widgets/appeal_page/light/header_donations.svg new file mode 100644 index 00000000000..ddb9f5b1c01 --- /dev/null +++ b/spyder/plugins/application/widgets/appeal_page/light/header_donations.svg @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spyder/plugins/application/widgets/appeal_page/light/icon_donations.svg b/spyder/plugins/application/widgets/appeal_page/light/icon_donations.svg new file mode 100644 index 00000000000..90360209f51 --- /dev/null +++ b/spyder/plugins/application/widgets/appeal_page/light/icon_donations.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + diff --git a/spyder/plugins/application/widgets/appeal_page/light/index.html b/spyder/plugins/application/widgets/appeal_page/light/index.html new file mode 100644 index 00000000000..f042322d49a --- /dev/null +++ b/spyder/plugins/application/widgets/appeal_page/light/index.html @@ -0,0 +1,52 @@ + + + + + + Donate today! + + + + +
+ + + + +
+ Help Keep Spyder Strong! +
+ +
+
+ + + + +
+

Spyder is a fully independent project, unaffiliated with Anaconda or + other companies. As it’s developed by and for + you, our beloved community, we need + your support to help keep it moving forward! + Your donation goes directly to funding Spyder development through NumFOCUS, a recognized charitable + organization.

+
+
+ + diff --git a/spyder/plugins/application/widgets/appeal_page/light/spyder-logo.svg b/spyder/plugins/application/widgets/appeal_page/light/spyder-logo.svg new file mode 100644 index 00000000000..43957b8dd3c --- /dev/null +++ b/spyder/plugins/application/widgets/appeal_page/light/spyder-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/spyder/plugins/application/widgets/status.py b/spyder/plugins/application/widgets/status.py index 11b7b7f1183..8c9f50ee2ff 100644 --- a/spyder/plugins/application/widgets/status.py +++ b/spyder/plugins/application/widgets/status.py @@ -7,11 +7,74 @@ """Status bar widgets.""" # Standard library imports +import os.path as osp import datetime +# Third-party imports +from qtpy.QtCore import Qt, QUrl +from qtpy.QtWidgets import QDialog, QVBoxLayout + # Local imports +from spyder.api.fonts import SpyderFontType, SpyderFontsMixin from spyder.api.widgets.status import BaseTimerStatus from spyder.api.translations import _ +from spyder.config.base import get_module_source_path +from spyder.config.gui import is_dark_interface +from spyder.utils.icon_manager import ima +from spyder.utils.qthelpers import start_file +from spyder.utils.stylesheet import WIN +from spyder.widgets.browser import WebView + + +class InAppAppealDialog(QDialog, SpyderFontsMixin): + + CONF_SECTION = "main" + WIDTH = 530 + HEIGHT = 620 if WIN else 665 + + def __init__(self, parent=None): + super().__init__(parent) + + # Attributes + self.setWindowFlags( + self.windowFlags() & ~Qt.WindowContextHelpButtonHint + ) + self.setFixedWidth(self.WIDTH) + self.setFixedHeight(self.HEIGHT) + self.setWindowTitle(_("Help Spyder")) + self.setWindowIcon(ima.icon("inapp_appeal")) + + # Path to the appeal page + appeal_page_path = osp.join( + get_module_source_path("spyder.plugins.application.widgets"), + "appeal_page", + "dark" if is_dark_interface() else "light", + "index.html", + ) + + # Create webview to render the appeal message + webview = WebView(self, handle_links=True) + + # Set font used in the view + app_font = self.get_font(SpyderFontType.Interface) + webview.set_font(app_font, size_delta=2) + + # Load page + webview.load(QUrl.fromLocalFile(appeal_page_path)) + + # Open links in external browser + webview.page().linkClicked.connect(self._handle_link_clicks) + + # Layout + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(webview) + self.setLayout(layout) + + def _handle_link_clicks(self, url): + url = str(url.toString()) + if url.startswith('http'): + start_file(url) class InAppAppealStatus(BaseTimerStatus): @@ -27,6 +90,7 @@ def __init__(self, parent=None): super().__init__(parent) self._is_shown = False + self._appeal_dialog = None # We don't need to show a label for this widget self.label_value.setVisible(False) @@ -34,6 +98,21 @@ def __init__(self, parent=None): # Update status every hour self.set_interval(60 * 60 * 1000) + # Show appeal on click + self.sig_clicked.connect(self._on_click) + + # ---- Private API + # ------------------------------------------------------------------------- + def _on_click(self): + """Handle widget clicks.""" + if self._appeal_dialog is None: + self._appeal_dialog = InAppAppealDialog(self) + + if self._appeal_dialog.isVisible(): + self._appeal_dialog.hide() + else: + self._appeal_dialog.show() + # ---- StatusBarWidget API # ------------------------------------------------------------------------- def get_icon(self):