diff --git a/.gitignore b/.gitignore index e98ac33c9..38761bb61 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ common/doc-dev/_build/html # Linter configuration .flake8 + +# Preferences files +qt/main_preferences \ No newline at end of file diff --git a/common/test/test_app.py b/common/test/test_app.py new file mode 100644 index 000000000..7f4091839 --- /dev/null +++ b/common/test/test_app.py @@ -0,0 +1,66 @@ +# TODO: add copyright text + + +import unittest +import os +import sys +import itertools +#from test import generic +import json + +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) +from qt import app + +from qt import qttools_path +qttools_path.registerBackintimePath('common') + +# Workaround until the codebase is rectified/equalized. +from common import tools +tools.initiate_translation(None) + +from common import logger +from qt import qttools +from common import backintime +from common import guiapplicationinstance + + +class TestSavePreferences(unittest.TestCase): + def setUp(self): + cfg = backintime.startApp('backintime-qt') + + raiseCmd = '' + if len(sys.argv) > 1: + raiseCmd = '\n'.join(sys.argv[1:]) + + appInstance = guiapplicationinstance.GUIApplicationInstance(cfg.appInstanceFile(), raiseCmd) + cfg.PLUGIN_MANAGER.load(cfg=cfg) + cfg.PLUGIN_MANAGER.appStart() + + logger.openlog() + qapp = qttools.createQApplication(cfg.APP_NAME) + translator = qttools.initiate_translator(cfg.language()) + qapp.installTranslator(translator) + + self.mainWindow = app.MainWindow(cfg, appInstance, qapp) + + def tearDown(self): + #cfg.PLUGIN_MANAGER.appExit() + #appInstance.exitApplication() + #logger.closelog() + pass + + def test_save_preferences_writes_file(self): + self.mainWindow.save_preferences() + self.assertTrue(os.path.exists('main_preferences')) + + def test_save_preferences_writes_correct_data(self): + test_preferences = {'key1': 'value1', 'key2': 'value2'} + self.mainWindow.main_preferences = test_preferences + self.mainWindow.save_preferences() + with open('main_preferences', 'r') as file: + saved_preferences = json.load(file) + self.assertEqual(saved_preferences, test_preferences) + + +if __name__ == '__main__': + unittest.main() diff --git a/qt/app.py b/qt/app.py index b4ce7e34b..ebc7c03e2 100644 --- a/qt/app.py +++ b/qt/app.py @@ -28,6 +28,7 @@ import signal from contextlib import contextmanager from tempfile import TemporaryDirectory +import json # We need to import common/tools.py import qttools_path @@ -133,6 +134,17 @@ def __init__(self, config, appInstance, qapp): # shortcuts without buttons self._create_shortcuts_without_actions() + # get user preferences + self.main_preferences = self.get_preferences() + if self.main_preferences is None: + self.main_preferences = {'show_toolbar_text': False} + self.save_preferences() + + # GUI elements to use throughout class + self.actions_for_toolbar = None + self.icon_text_actions = [] + self.toolbar = None + self._create_actions() self._create_menubar() self._create_main_toolbar() @@ -241,7 +253,7 @@ def __init__(self, config, appInstance, qapp): self.filesView.header().sortIndicatorSection(), self.filesView.header().sortIndicatorOrder()) self.filesView.header() \ - .sortIndicatorChanged.connect(self.filesViewModel.sort) + .sortIndicatorChanged.connect(self.filesViewModel.sort) self.stackFilesView.setCurrentWidget(self.filesView) @@ -251,7 +263,7 @@ def __init__(self, config, appInstance, qapp): # context menu for Files View self.filesView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.filesView.customContextMenuRequested \ - .connect(self.contextMenuClicked) + .connect(self.contextMenuClicked) self.contextMenu = QMenu(self) self.contextMenu.addAction(self.act_restore) self.contextMenu.addAction(self.act_restore_to) @@ -393,7 +405,7 @@ def __init__(self, config, appInstance, qapp): # populate lists self.updateProfiles() self.comboProfiles.currentIndexChanged \ - .connect(self.comboProfileChanged) + .connect(self.comboProfileChanged) self.filesView.setFocus() @@ -593,6 +605,11 @@ def _create_actions(self): 'act_snapshots_dialog': ( icon.SNAPSHOTS, _('Compare snapshots…'), self.btnSnapshotsClicked, None, None), + + # Could be moved into dedicated preferences window in the future + 'act_show_toolbar_text': ( + None, _('Show toolbar text'), + self.btnShowToolbarTextClicked, None, None), } for attr in action_dict: @@ -605,6 +622,14 @@ def _create_actions(self): action = QAction(ico, txt, self) if ico else \ QAction(txt, self) + # Save action to use elsewhere in class + self.icon_text_actions.append([attr, action, ico, txt]) + + # Make items checkboxes + if attr == 'act_show_toolbar_text': + action.setCheckable(True) + action.setChecked(self.main_preferences['show_toolbar_text']) + # Connect handler function if slot: action.triggered.connect(slot) @@ -677,6 +702,9 @@ def _create_menubar(self): self.act_restore_parent, self.act_restore_parent_to, ), + _('&Preferences'): ( + self.act_show_toolbar_text + ), _('&Help'): ( self.act_help_help, self.act_help_configfile, @@ -692,9 +720,10 @@ def _create_menubar(self): # Filter out 'None' from 'Back In &Time' menu_dict['Back In &Time'] = tuple(a for a in menu_dict['Back In &Time'] if a is not None) - for key in menu_dict: + for key, actions in menu_dict.items(): menu = self.menuBar().addMenu(key) - menu.addActions(menu_dict[key]) + menu.addActions(actions) if isinstance(actions, tuple) else \ + menu.addAction(actions) menu.setToolTipsVisible(True) # The action of the restore menu. It is used by the menuBar and by the @@ -719,14 +748,14 @@ def _create_menubar(self): def _create_main_toolbar(self): """Create the main toolbar and connect it to actions.""" - toolbar = self.addToolBar('main') - toolbar.setFloatable(False) + self.toolbar = self.addToolBar('main') + self.toolbar.setFloatable(False) # Drop-Down: Profiles self.comboProfiles = qttools.ProfileCombo(self) - self.comboProfilesAction = toolbar.addWidget(self.comboProfiles) + self.comboProfilesAction = self.toolbar.addWidget(self.comboProfiles) - actions_for_toolbar = [ + self.actions_for_toolbar = [ self.act_take_snapshot, self.act_pause_take_snapshot, self.act_resume_take_snapshot, @@ -743,21 +772,22 @@ def _create_main_toolbar(self): actions_for_toolbar.append(self.act_suspend) # Add each action to toolbar - for act in actions_for_toolbar: - toolbar.addAction(act) + for act in self.actions_for_toolbar: + self.toolbar.addAction(act) # Assume an explicit tooltip if it is different from "text()". # Note that Qt use "text()" as "toolTip()" by default. - if act.toolTip() != act.text(): + if act.toolTip() == act.text(): + continue - if QApplication.instance().isRightToLeft(): - # RTL/BIDI language like Hebrew - button_tip = f'{act.toolTip()} :{act.text()}' - else: - # (default) LTR language (e.g. English) - button_tip = f'{act.text()}: {act.toolTip()}' + if QApplication.instance().isRightToLeft(): + # RTL/BIDI language like Hebrew + button_tip = f'{act.toolTip()} :{act.text()}' + else: + # (default) LTR language (e.g. English) + button_tip = f'{act.text()}: {act.toolTip()}' - toolbar.widgetForAction(act).setToolTip(button_tip) + self.toolbar.widgetForAction(act).setToolTip(button_tip) # toolbar sub menu: take snapshot submenu_take_snapshot = QMenu(self) @@ -766,7 +796,7 @@ def _create_main_toolbar(self): submenu_take_snapshot.setToolTipsVisible(True) # get the toolbar buttons widget... - button_take_snapshot = toolbar.widgetForAction(self.act_take_snapshot) + button_take_snapshot = self.toolbar.widgetForAction(self.act_take_snapshot) # ...and add the menu to it button_take_snapshot.setMenu(submenu_take_snapshot) button_take_snapshot.setPopupMode( @@ -777,6 +807,8 @@ def _create_main_toolbar(self): toolbar.insertSeparator(self.act_shutdown) if self.shutdown.canSuspend(): toolbar.insertSeparator(self.act_suspend) + + self.set_toolbar_icon_text() def _create_and_get_filesview_toolbar(self): """Create the filesview toolbar object, connect it to actions and @@ -994,8 +1026,8 @@ def updateTakeSnapshot(self, force_wait_lock=False): if not self.act_stop_take_snapshot.isVisible(): for action in (self.act_pause_take_snapshot, - self.act_resume_take_snapshot, - self.act_stop_take_snapshot): + self.act_resume_take_snapshot, + self.act_stop_take_snapshot): action.setEnabled(True) self.act_take_snapshot.setVisible(False) self.act_pause_take_snapshot.setVisible(not paused) @@ -1186,6 +1218,9 @@ def updateSnapshotActions(self, item = None): self.act_remove_snapshot.setEnabled(enabled) self.act_snapshot_logview.setEnabled(enabled) + # setEnabled returns icon, which is not ideal if buttons should only show text + self.set_toolbar_icon_text() + def timeLineChanged(self): item = self.timeLine.currentItem() self.updateSnapshotActions(item) @@ -1394,17 +1429,17 @@ def backupOnRestore(self): cb = QCheckBox(_( 'Create backup copies with trailing {suffix}\n' 'before overwriting or removing local elements.').format( - suffix=self.snapshots.backupSuffix())) + suffix=self.snapshots.backupSuffix())) cb.setChecked(self.config.backupOnRestore()) cb.setToolTip(_( "Newer versions of files will be renamed with trailing " "{suffix} before restoring.\n" "If you don't need them anymore you can remove them with {cmd}") - .format(suffix=self.snapshots.backupSuffix(), - cmd='find ./ -name "*{suffix}" -delete' - .format(suffix=self.snapshots.backupSuffix())) - ) + .format(suffix=self.snapshots.backupSuffix(), + cmd='find ./ -name "*{suffix}" -delete' + .format(suffix=self.snapshots.backupSuffix())) + ) return { 'widget': cb, 'retFunc': cb.isChecked, @@ -1685,8 +1720,8 @@ def openPath(self, rel_path): # The class "GenericNonSnapshot" indicates that "Now" is selected # in the snapshots timeline widget. if (os.path.exists(full_path) - and (isinstance(self.sid, snapshots.GenericNonSnapshot) # "Now" - or self.sid.isExistingPathInsideSnapshotFolder(rel_path))): + and (isinstance(self.sid, snapshots.GenericNonSnapshot) # "Now" + or self.sid.isExistingPathInsideSnapshotFolder(rel_path))): if os.path.isdir(full_path): self.path = rel_path @@ -1928,6 +1963,38 @@ def slot_setup_language(self): def slot_help_translation(self): self._open_approach_translator_dialog() + def btnShowToolbarTextClicked(self, checked): + self.main_preferences['show_toolbar_text'] = checked + self.save_preferences() + self.set_toolbar_icon_text() + + def set_toolbar_icon_text(self): + """Based on user preference, this sets the toolbar buttons to display either text or icons.""" + for action in self.actions_for_toolbar: + attr, act, ico, txt = [a for a in self.icon_text_actions if a[1] == action][0] + widget = self.toolbar.widgetForAction(act) + + if self.main_preferences['show_toolbar_text'] or not ico: + widget.setIcon(QIcon()) + else: + widget.setIcon(ico) + + setattr(self, attr, act) + + def get_preferences(self): + """Returns a dictionary of the main user-preferences from a json-formatted text file.""" + file = 'main_preferences' + if not os.path.exists(file) or os.path.getsize(file) == 0: + return + + with open('main_preferences', 'r') as file: + return json.load(file) + + def save_preferences(self): + """Writes user-preferences to a text file.""" + with open('main_preferences', 'w') as file: + json.dump(self.main_preferences, file, indent=4) + class ExtraMouseButtonEventFilter(QObject): """