diff --git a/src/vorta/assets/icons/help-about.svg b/src/vorta/assets/icons/help-about.svg new file mode 100644 index 00000000..89fc594e --- /dev/null +++ b/src/vorta/assets/icons/help-about.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/src/vorta/store/connection.py b/src/vorta/store/connection.py index ff1a2b48..72a3461f 100644 --- a/src/vorta/store/connection.py +++ b/src/vorta/store/connection.py @@ -18,7 +18,7 @@ ) from .settings import get_misc_settings -SCHEMA_VERSION = 19 +SCHEMA_VERSION = 20 @signals.post_save(sender=SettingsModel) @@ -91,5 +91,7 @@ def init_db(con=None): if 'group' in setting: s.group = setting['group'] + if 'tooltip' in setting: + s.tooltip = setting['tooltip'] s.save() diff --git a/src/vorta/store/migrations.py b/src/vorta/store/migrations.py index 4603d743..0db00e9f 100644 --- a/src/vorta/store/migrations.py +++ b/src/vorta/store/migrations.py @@ -219,6 +219,13 @@ def run_migrations(current_schema, db_connection): migrator.add_column(SettingsModel._meta.table_name, 'group', pw.CharField(default='')), ) + if current_schema.version < 20: + _apply_schema_update( + current_schema, + 20, + migrator.add_column(SettingsModel._meta.table_name, 'tooltip', pw.CharField(default='')), + ) + def _apply_schema_update(current_schema, version_after, *operations): with DB.atomic(): diff --git a/src/vorta/store/models.py b/src/vorta/store/models.py index 814652b6..c893764e 100644 --- a/src/vorta/store/models.py +++ b/src/vorta/store/models.py @@ -185,6 +185,7 @@ class SettingsModel(BaseModel): str_value = pw.CharField(default='') label = pw.CharField() group = pw.CharField(default='') # Settings group name and label + tooltip = pw.CharField(default='') # optional tooltip for `checkbox` type type = pw.CharField() class Meta: diff --git a/src/vorta/store/settings.py b/src/vorta/store/settings.py index ae0b2006..3bcac865 100644 --- a/src/vorta/store/settings.py +++ b/src/vorta/store/settings.py @@ -32,7 +32,7 @@ def get_misc_settings() -> List[Dict[str, str]]: 'value': False, 'type': 'checkbox', 'group': notifications, - 'label': trans_late('settings', 'Also notify about successful background tasks'), + 'label': trans_late('settings', 'Notify about successful background tasks'), }, { 'key': 'autostart', @@ -40,6 +40,7 @@ def get_misc_settings() -> List[Dict[str, str]]: 'type': 'checkbox', 'group': startup, 'label': trans_late('settings', 'Automatically start Vorta at login'), + 'tooltip': trans_late('settings', 'Add Vorta to the systems autostart list'), }, { 'key': 'foreground', @@ -47,6 +48,7 @@ def get_misc_settings() -> List[Dict[str, str]]: 'type': 'checkbox', 'group': startup, 'label': trans_late('settings', 'Open main window on startup'), + 'tooltip': trans_late('settings', 'Open main window when the application is launched'), }, { 'key': 'get_srcpath_datasize', @@ -54,6 +56,7 @@ def get_misc_settings() -> List[Dict[str, str]]: 'type': 'checkbox', 'group': information, 'label': trans_late('settings', 'Get statistics of file/folder when added'), + 'tooltip': trans_late('settings', 'When adding a new source, calculate its size and the number of files.'), }, { 'key': 'use_system_keyring', @@ -64,6 +67,9 @@ def get_misc_settings() -> List[Dict[str, str]]: 'settings', 'Store repository passwords in system keychain, if available', ), + 'tooltip': trans_late( + 'settings', "Otherwise Vorta's configuration database stores the password in plaintext." + ), }, { 'key': 'override_mount_permissions', @@ -72,8 +78,9 @@ def get_misc_settings() -> List[Dict[str, str]]: 'group': security, 'label': trans_late( 'settings', - 'Try to replace existing permissions when mounting an archive', + 'Try to replace file permissions when mounting an archive', ), + 'tooltip': trans_late('settings', 'Set owner to current user and umask to 0277'), }, { 'key': 'previous_profile_id', diff --git a/src/vorta/views/misc_tab.py b/src/vorta/views/misc_tab.py index b3043803..a94b4d28 100644 --- a/src/vorta/views/misc_tab.py +++ b/src/vorta/views/misc_tab.py @@ -1,13 +1,15 @@ import logging from PyQt5 import uic from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QCheckBox, QFormLayout, QLabel, QSizePolicy, QSpacerItem +from PyQt5.QtWidgets import QApplication, QCheckBox, QFormLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem from vorta._version import __version__ from vorta.config import LOG_DIR from vorta.i18n import translate from vorta.store.models import BackupProfileMixin, SettingsModel from vorta.store.settings import get_misc_settings from vorta.utils import get_asset, search +from vorta.views.partials.tooltip_button import ToolTipButton +from vorta.views.utils import get_colored_icon uifile = get_asset('UI/misctab.ui') MiscTabUI, MiscTabBase = uic.loadUiType(uifile) @@ -29,10 +31,15 @@ def __init__(self, parent=None): self.checkboxLayout.setSpacing(4) self.checkboxLayout.setHorizontalSpacing(8) self.checkboxLayout.setContentsMargins(0, 0, 0, 12) + self.checkboxLayout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.FieldsStayAtSizeHint) self.checkboxLayout.setFormAlignment(Qt.AlignmentFlag.AlignHCenter) + self.tooltip_buttons = [] self.populate() + # Connect to palette change + QApplication.instance().paletteChanged.connect(lambda p: self.set_icons()) + def populate(self): """ Populate the misc tab with the settings widgets. @@ -45,6 +52,7 @@ def populate(self): self.checkboxLayout.removeItem(child) if child.widget(): child.widget().deleteLater() + self.tooltip_buttons = [] # dynamically add widgets for settings misc_settings = get_misc_settings() @@ -78,16 +86,34 @@ def populate(self): # create widget cb = QCheckBox(translate('settings', setting.label)) + cb.setToolTip(setting.tooltip) cb.setCheckState(setting.value) cb.setTristate(False) cb.stateChanged.connect(lambda v, key=setting.key: self.save_setting(key, v)) + tb = ToolTipButton() + tb.setToolTip(setting.tooltip) + + cbl = QHBoxLayout() + cbl.addWidget(cb) + if setting.tooltip: + cbl.addWidget(tb) + cbl.addItem(QSpacerItem(0, 0, hPolicy=QSizePolicy.Policy.Expanding)) + # add widget - self.checkboxLayout.setWidget(i, QFormLayout.ItemRole.FieldRole, cb) + self.checkboxLayout.setLayout(i, QFormLayout.ItemRole.FieldRole, cbl) + self.tooltip_buttons.append(tb) # increase i i += 1 + self.set_icons() + + def set_icons(self): + """Set or update the icons in this view.""" + for button in self.tooltip_buttons: + button.setIcon(get_colored_icon('help-about')) + def save_setting(self, key, new_value): setting = SettingsModel.get(key=key) setting.value = bool(new_value) diff --git a/src/vorta/views/partials/tooltip_button.py b/src/vorta/views/partials/tooltip_button.py new file mode 100644 index 00000000..bde53a81 --- /dev/null +++ b/src/vorta/views/partials/tooltip_button.py @@ -0,0 +1,127 @@ +from typing import Optional +from PyQt5.QtCore import QCoreApplication, QEvent, QSize, Qt +from PyQt5.QtGui import QHelpEvent, QIcon, QMouseEvent, QPaintEvent +from PyQt5.QtWidgets import QSizePolicy, QStyle, QStylePainter, QToolTip, QWidget + + +class ToolTipButton(QWidget): + """ + A flat button showing a tooltip when the mouse moves over it. + + The default icon is `help-about`. + + Parameters + ---------- + icon : QIcon, optional + The icon to display, by default `help-about` + parent : QWidget, optional + The parent of this widget, by default None + """ + + def __init__(self, icon: Optional[QIcon] = None, parent: Optional[QWidget] = None) -> None: + """ + Init. + """ + super().__init__(parent) + self.setCursor(Qt.CursorShape.WhatsThisCursor) + self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.setMouseTracking(True) + self._icon = icon or QIcon() + + def sizeHint(self) -> QSize: + """ + Get the recommended size for the widget. + + Returns + ------- + QSize + + See Also + -------- + https://doc.qt.io/qt-5/qwidget.html#sizeHint-prop + """ + size = self.style().pixelMetric(QStyle.PixelMetric.PM_ButtonIconSize) + return QSize(size, size) + + def paintEvent(self, event: QPaintEvent) -> None: + """ + Repaint the widget on receiving a paint event. + + A paint event is a request to repaint all or part of a widget. + It can happen for one of the following reasons: + + - repaint() or update() was invoked, + - the widget was obscured and has now been uncovered, or + - many other reasons. + + Many widgets can simply repaint their entire surface when asked to, + but some slow widgets need to optimize by painting only the + requested region: QPaintEvent::region(). + This speed optimization does not change the result, + as painting is clipped to that region during event processing. + QListView and QTableView do this, for example. + + Parameters + ---------- + event : QPaintEvent + The paint event + + See Also + -------- + https://doc.qt.io/qt-5/qwidget.html#paintEvent + """ + painter = QStylePainter(self) + if self._icon: + painter.drawPixmap( + event.rect(), + self._icon.pixmap(event.rect().size(), QIcon.Mode.Normal if self.isEnabled() else QIcon.Mode.Disabled), + ) + painter.end() + + def mouseMoveEvent(self, event: QMouseEvent) -> None: + """ + Process mouse move events for this widget. + + If mouse tracking is switched off, mouse move events only occur if a + mouse button is pressed while the mouse is being moved. + If mouse tracking is switched on, mouse move events occur even + if no mouse button is pressed. + + Parameters + ---------- + event : QMouseEvent + The corresponding mouse event. + + See Also + -------- + setMouseTracking + https://doc.qt.io/qt-5/qwidget.html#mouseMoveEvent + """ + super().mouseMoveEvent(event) + QToolTip.showText(event.globalPos(), self.toolTip(), self) + QCoreApplication.postEvent(self, QHelpEvent(QEvent.Type.ToolTip, event.pos(), event.globalPos())) + + def setIcon(self, icon: QIcon): + """ + Set the icon displayed by the widget. + + This triggers a repaint event. + + Parameters + ---------- + icon : QIcon + The new icon. + """ + self._icon = icon + self.update() + + def icon(self) -> QIcon: + """ + Get the icon displayed by the widget. + + Returns + ------- + QIcon + The current icon. + """ + return self._icon diff --git a/tests/test_misc.py b/tests/test_misc.py index 1f721f9f..e9cce1b2 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -17,7 +17,7 @@ def click_autostart(): item = tab.checkboxLayout.itemAt(x, QFormLayout.ItemRole.FieldRole) if not item: continue - checkbox = item.widget() + checkbox = item.itemAt(0).widget() checkbox.__class__ = QCheckBox if checkbox.text().startswith("Automatically"): # Have to use pos to click checkbox correctly