diff --git a/picard/config.py b/picard/config.py
index c40bdc233..e068eec94 100644
--- a/picard/config.py
+++ b/picard/config.py
@@ -12,7 +12,7 @@
# Copyright (C) 2017 Sophist-UK
# Copyright (C) 2018 Vishal Choudhary
# Copyright (C) 2020-2021 Gabriel Ferreira
-# Copyright (C) 2021 Bob Swift
+# Copyright (C) 2021-2022 Bob Swift
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -241,7 +241,8 @@ class Config(QtCore.QSettings):
self.setting = SettingConfigSection(self, "setting")
self.persist = ConfigSection(self, "persist")
- TextOption("application", "version", '0.0.0dev0')
+ if 'version' not in self.application or not self.application['version']:
+ TextOption("application", "version", '0.0.0dev0')
self._version = Version.from_string(self.application["version"])
self._upgrade_hooks = dict()
@@ -368,11 +369,16 @@ class Config(QtCore.QSettings):
def _backup_settings(self):
if Version(0, 0, 0) < self._version < PICARD_VERSION:
backup_path = self._versioned_config_filename()
- log.info('Backing up config file to %s', backup_path)
- try:
- shutil.copyfile(self.fileName(), backup_path)
- except OSError:
- log.error('Failed backing up config file to %s', backup_path)
+ self._save_backup(backup_path)
+
+ def _save_backup(self, backup_path):
+ log.info('Backing up config file to %s', backup_path)
+ try:
+ shutil.copyfile(self.fileName(), backup_path)
+ except OSError:
+ log.error('Failed backing up config file to %s', backup_path)
+ return False
+ return True
def _write_version(self):
self.application["version"] = self._version.to_string()
@@ -384,6 +390,12 @@ class Config(QtCore.QSettings):
return os.path.join(os.path.dirname(self.fileName()), '%s-%s.ini' % (
self.applicationName(), version.to_string(short=True)))
+ def save_user_backup(self, backup_path):
+ if backup_path == self.fileName():
+ log.warning("Attempt to backup configuration file to the same path.")
+ return False
+ return self._save_backup(backup_path)
+
class Option(QtCore.QObject):
@@ -479,3 +491,14 @@ def get_config():
Config objects for threads are created on demand and cached for later use.
"""
return config
+
+
+def load_new_config(filename=None):
+ config_file = get_config().fileName()
+ try:
+ shutil.copy(filename, config_file)
+ except OSError:
+ log.error('Failed restoring config file from %s', filename)
+ return False
+ setup_config(QtCore.QObject.tagger, config_file)
+ return True
diff --git a/picard/ui/options/dialog.py b/picard/ui/options/dialog.py
index 799c94f64..fb576e4c7 100644
--- a/picard/ui/options/dialog.py
+++ b/picard/ui/options/dialog.py
@@ -13,7 +13,7 @@
# Copyright (C) 2016-2017 Sambhav Kothari
# Copyright (C) 2017 Suhas
# Copyright (C) 2018 Vishal Choudhary
-# Copyright (C) 2021 Bob Swift
+# Copyright (C) 2021-2022 Bob Swift
# Copyright (C) 2021 Gabriel Ferreira
#
# This program is free software; you can redistribute it and/or
@@ -174,17 +174,15 @@ class OptionsDialog(PicardDialog, SingletonDialog):
self.restoreWindowState()
self.finished.connect(self.saveWindowState)
- for page in self.pages:
- try:
- page.load()
- except Exception:
- log.exception('Failed loading options page %r', page)
- self.disable_page(page.NAME)
+ self.load_all_pages()
self.ui.pages_tree.setCurrentItem(self.default_item)
self.profile_page = self.get_page('profiles')
self.profile_page.signal_refresh.connect(self.update_from_profile_changes)
+ self.maintenance_page = self.get_page('maintenance')
+ self.maintenance_page.signal_reload.connect(self.load_all_pages)
+
self.first_enter = True
self.installEventFilter(self)
@@ -192,6 +190,14 @@ class OptionsDialog(PicardDialog, SingletonDialog):
current_page = self.item_to_page[self.ui.pages_tree.currentItem()]
self.set_profiles_button_and_highlight(current_page)
+ def load_all_pages(self):
+ for page in self.pages:
+ try:
+ page.load()
+ except Exception:
+ log.exception('Failed loading options page %r', page)
+ self.disable_page(page.NAME)
+
def page_has_profile_options(self, page):
try:
name = page.PARENT if page.PARENT in UserProfileGroups.SETTINGS_GROUPS else page.NAME
diff --git a/picard/ui/options/maintenance.py b/picard/ui/options/maintenance.py
index f08895641..faaf18609 100644
--- a/picard/ui/options/maintenance.py
+++ b/picard/ui/options/maintenance.py
@@ -2,7 +2,7 @@
#
# Picard, the next-generation MusicBrainz tagger
#
-# Copyright (C) 2021 Bob Swift
+# Copyright (C) 2021-2022 Bob Swift
# Copyright (C) 2021 Laurent Monin
# Copyright (C) 2021-2022 Philipp Wolfer
#
@@ -21,6 +21,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+import datetime
from os import path
from PyQt5 import (
@@ -33,7 +34,9 @@ from picard import log
from picard.config import (
Option,
get_config,
+ load_new_config,
)
+from picard.config_upgrade import upgrade_config
from picard.ui.options import (
OptionsPage,
@@ -65,6 +68,8 @@ class MaintenanceOptionsPage(OptionsPage):
options = []
+ signal_reload = QtCore.pyqtSignal()
+
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_MaintenanceOptionsPage()
@@ -88,6 +93,8 @@ class MaintenanceOptionsPage(OptionsPage):
self.ui.select_all.stateChanged.connect(self.select_all_changed)
self.ui.enable_cleanup.stateChanged.connect(self.enable_cleanup_changed)
self.ui.open_folder_button.clicked.connect(self.open_config_dir)
+ self.ui.save_backup_button.clicked.connect(self.save_backup)
+ self.ui.load_backup_button.clicked.connect(self.load_backup)
# Set the palette of the config file QLineEdit widget to inactive.
palette_normal = self.ui.config_file.palette()
@@ -154,6 +161,116 @@ class MaintenanceOptionsPage(OptionsPage):
config_dir = path.split(config.fileName())[0]
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(config_dir))
+ def _get_dialog_filetypes(self, _ext='.ini'):
+ return ";;".join((
+ _("Configuration files") + " (*{0})".format(_ext,),
+ _("All files") + " (*)",
+ ))
+
+ def _make_backup_filename(self, auto=False):
+ config = get_config()
+ _filename = path.split(config.fileName())[1]
+ _root, _ext = path.splitext(_filename)
+ return "{0}_{1}_Backup_{2}{3}".format(
+ _root,
+ 'Auto' if auto else 'User',
+ datetime.datetime.now().strftime("%Y%m%d_%H%M"),
+ _ext,
+ )
+
+ def _backup_error(self, dialog_title=None):
+ if not dialog_title:
+ dialog_title = _("Backup Configuration File")
+ dialog = QtWidgets.QMessageBox(
+ QtWidgets.QMessageBox.Icon.Critical,
+ dialog_title,
+ _("There was a problem backing up the configuration file. Please see the logs for more details."),
+ QtWidgets.QMessageBox.StandardButton.Ok,
+ self
+ )
+ dialog.exec_()
+
+ def save_backup(self):
+ config = get_config()
+ directory = path.normpath(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation))
+ filename = self._make_backup_filename()
+ ext = path.splitext(filename)[1]
+ default_path = path.normpath(path.join(directory, filename))
+
+ dialog_title = _("Backup Configuration File")
+ dialog_file_types = self._get_dialog_filetypes(ext)
+ options = QtWidgets.QFileDialog.Options()
+ filename, file_type = QtWidgets.QFileDialog.getSaveFileName(self, dialog_title, default_path, dialog_file_types, options=options)
+ if not filename:
+ return
+ # Fix issue where Qt may set the extension twice
+ (name, ext) = path.splitext(filename)
+ if ext and str(name).endswith('.' + ext):
+ filename = name
+
+ if config.save_user_backup(filename):
+ dialog = QtWidgets.QMessageBox(
+ QtWidgets.QMessageBox.Icon.Information,
+ dialog_title,
+ _("Configuration successfully backed up to %s") % filename,
+ QtWidgets.QMessageBox.StandardButton.Ok,
+ self
+ )
+ dialog.exec_()
+ else:
+ self._backup_error(dialog_title)
+
+ def load_backup(self):
+ dialog_title = _("Load Backup Configuration File")
+ dialog = QtWidgets.QMessageBox(
+ QtWidgets.QMessageBox.Icon.Warning,
+ dialog_title,
+ _("Loading a backup configuration file will replace the current configuration settings. "
+ "A backup copy of the current file will be saved automatically.\n\nDo you want to continue?"),
+ QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel,
+ self
+ )
+ dialog.setDefaultButton(QtWidgets.QMessageBox.StandardButton.Cancel)
+ if not dialog.exec_() == QtWidgets.QMessageBox.StandardButton.Ok:
+ return
+
+ config = get_config()
+ directory = path.normpath(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation))
+ filename = path.join(directory, self._make_backup_filename(auto=True))
+ if not config.save_user_backup(filename):
+ self._backup_error()
+ return
+
+ ext = path.splitext(filename)[1]
+ dialog_file_types = self._get_dialog_filetypes(ext)
+ directory = path.normpath(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation))
+ options = QtWidgets.QFileDialog.Options()
+ filename, file_type = QtWidgets.QFileDialog.getOpenFileName(self, dialog_title, directory, dialog_file_types, options=options)
+ if not filename:
+ return
+ log.warning('Loading configuration from %s' % filename)
+ if load_new_config(filename):
+ config = get_config()
+ upgrade_config(config)
+ QtCore.QObject.config = get_config()
+ self.signal_reload.emit()
+ dialog = QtWidgets.QMessageBox(
+ QtWidgets.QMessageBox.Icon.Information,
+ dialog_title,
+ _("Configuration successfully loaded from %s") % filename,
+ QtWidgets.QMessageBox.StandardButton.Ok,
+ self
+ )
+ else:
+ dialog = QtWidgets.QMessageBox(
+ QtWidgets.QMessageBox.Icon.Critical,
+ dialog_title,
+ _("There was a problem restoring the configuration file. Please see the logs for more details."),
+ QtWidgets.QMessageBox.StandardButton.Ok,
+ self
+ )
+ dialog.exec_()
+
def column_items(self, column):
for idx in range(self.ui.tableWidget.rowCount()):
yield self.ui.tableWidget.item(idx, column)
diff --git a/picard/ui/ui_options_maintenance.py b/picard/ui/ui_options_maintenance.py
index 1a807398b..3dbf44402 100644
--- a/picard/ui/ui_options_maintenance.py
+++ b/picard/ui/ui_options_maintenance.py
@@ -27,6 +27,18 @@ class Ui_MaintenanceOptionsPage(object):
self.open_folder_button.setObjectName("open_folder_button")
self.horizontalLayout_3.addWidget(self.open_folder_button)
self.vboxlayout.addLayout(self.horizontalLayout_3)
+ self.horizontalLayout = QtWidgets.QHBoxLayout()
+ self.horizontalLayout.setContentsMargins(-1, -1, -1, 0)
+ self.horizontalLayout.setObjectName("horizontalLayout")
+ spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout.addItem(spacerItem)
+ self.load_backup_button = QtWidgets.QToolButton(MaintenanceOptionsPage)
+ self.load_backup_button.setObjectName("load_backup_button")
+ self.horizontalLayout.addWidget(self.load_backup_button)
+ self.save_backup_button = QtWidgets.QToolButton(MaintenanceOptionsPage)
+ self.save_backup_button.setObjectName("save_backup_button")
+ self.horizontalLayout.addWidget(self.save_backup_button)
+ self.vboxlayout.addLayout(self.horizontalLayout)
self.option_counts = QtWidgets.QLabel(MaintenanceOptionsPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
@@ -46,8 +58,8 @@ class Ui_MaintenanceOptionsPage(object):
self.description.setIndent(0)
self.description.setObjectName("description")
self.vboxlayout.addWidget(self.description)
- spacerItem = QtWidgets.QSpacerItem(20, 8, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
- self.vboxlayout.addItem(spacerItem)
+ spacerItem1 = QtWidgets.QSpacerItem(20, 8, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
+ self.vboxlayout.addItem(spacerItem1)
self.line = QtWidgets.QFrame(MaintenanceOptionsPage)
self.line.setFrameShape(QtWidgets.QFrame.HLine)
self.line.setFrameShadow(QtWidgets.QFrame.Sunken)
@@ -73,5 +85,7 @@ class Ui_MaintenanceOptionsPage(object):
_translate = QtCore.QCoreApplication.translate
self.label.setText(_("Configuration File:"))
self.open_folder_button.setText(_("Open folder"))
+ self.load_backup_button.setText(_("Load Backup"))
+ self.save_backup_button.setText(_("Save Backup"))
self.enable_cleanup.setText(_("Remove selected options"))
self.select_all.setText(_("Select all"))
diff --git a/ui/options_maintenance.ui b/ui/options_maintenance.ui
index ac4fd98c7..45b971c40 100644
--- a/ui/options_maintenance.ui
+++ b/ui/options_maintenance.ui
@@ -39,6 +39,40 @@
+ -
+
+
+ 0
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Load Backup
+
+
+
+ -
+
+
+ Save Backup
+
+
+
+
+
-