Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PICARD-2875: Let the user select the auto backup directory in Maintenance Options #2430

Merged
merged 11 commits into from
Apr 25, 2024
3 changes: 3 additions & 0 deletions picard/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ class UserProfileGroups():
SettingDesc('file_lookup_threshold', ['file_lookup_threshold']),
SettingDesc('cluster_lookup_threshold', ['cluster_lookup_threshold']),
SettingDesc('track_matching_threshold', ['track_matching_threshold']),

# Maintenance Options Page
SettingDesc('autobackup_directory', ['autobackup_dir']),
],
}

Expand Down
192 changes: 130 additions & 62 deletions picard/ui/options/maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@


import datetime
from os import path
import os

from PyQt6 import (
QtCore,
Expand All @@ -33,6 +33,7 @@
from picard import log
from picard.config import (
Option,
TextOption,
get_config,
load_new_config,
)
Expand Down Expand Up @@ -61,6 +62,14 @@
'write_wave_riff_info',
}

_default_autobackup_directory = os.path.normpath(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation))


def _safe_autobackup_dir(path):
if not path or not os.path.isdir(path):
return _default_autobackup_directory
return os.path.normpath(path)


class MaintenanceOptionsPage(OptionsPage):

Expand All @@ -71,7 +80,9 @@ class MaintenanceOptionsPage(OptionsPage):
ACTIVE = True
HELP_URL = "/config/options_maintenance.html"

options = []
options = [
TextOption('setting', 'autobackup_directory', _default_autobackup_directory, title=N_("Automatic backup destination directory")),
]

signal_reload = QtCore.pyqtSignal()

Expand Down Expand Up @@ -100,6 +111,7 @@ def __init__(self, parent=None):
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)
self.ui.browse_autobackup_dir.clicked.connect(self._dialog_autobackup_dir_browse)

# Set the palette of the config file QLineEdit widget to inactive.
palette_normal = self.ui.config_file.palette()
Expand All @@ -108,9 +120,22 @@ def __init__(self, parent=None):
palette_readonly.setColor(QtGui.QPalette.ColorRole.Base, disabled_color)
self.ui.config_file.setPalette(palette_readonly)

def get_current_autobackup_dir(self):
return _safe_autobackup_dir(self.ui.autobackup_dir.text())

def set_current_autobackup_dir(self, path):
self.ui.autobackup_dir.setText(_safe_autobackup_dir(path))

def _dialog_autobackup_dir_browse(self):
path = QtWidgets.QFileDialog.getExistingDirectory(self, "", self.get_current_autobackup_dir())
if path:
self.set_current_autobackup_dir(path)

def load(self):
config = get_config()

self.set_current_autobackup_dir(config.setting['autobackup_directory'])

# Show the path and file name of the currently used configuration file.
self.ui.config_file.setText(config.fileName())

Expand Down Expand Up @@ -163,7 +188,7 @@ def load(self):

def open_config_dir(self):
config = get_config()
config_dir = path.split(config.fileName())[0]
config_dir = os.path.split(config.fileName())[0]
open_local_path(config_dir)

def _get_dialog_filetypes(self, _ext='.ini'):
Expand All @@ -174,104 +199,141 @@ def _get_dialog_filetypes(self, _ext='.ini'):

def _make_backup_filename(self, auto=False):
config = get_config()
_filename = path.split(config.fileName())[1]
_root, _ext = path.splitext(_filename)
_filename = os.path.split(config.fileName())[1]
_root, _ext = os.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")
def _dialog_save_backup_error(self, filename):
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."),
_("Backup Configuration File Save Error"),
_("Failed to save the configuration file to:\n"
"%s\n"
"\n"
"Please see the logs for more details." % filename),
QtWidgets.QMessageBox.StandardButton.Ok,
self
self,
)
dialog.exec()

def _dialog_ask_backup_filename(self, default_path, ext):
filename, file_type = QtWidgets.QFileDialog.getSaveFileName(
self,
_("Backup Configuration File"),
default_path,
self._get_dialog_filetypes(ext),
)
return filename

def _dialog_save_backup_success(self, filename):
dialog = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Icon.Information,
_("Backup Configuration File"),
_("Configuration successfully backed up to:\n"
"%s") % filename,
QtWidgets.QMessageBox.StandardButton.Ok,
self,
)
dialog.exec()

def save_backup(self):
config = get_config()
directory = path.normpath(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation))
directory = self.get_current_autobackup_dir()
filename = self._make_backup_filename()
ext = path.splitext(filename)[1]
default_path = path.normpath(path.join(directory, filename))
ext = os.path.splitext(filename)[1]
default_path = os.path.normpath(os.path.join(directory, filename))

dialog_title = _("Backup Configuration File")
dialog_file_types = self._get_dialog_filetypes(ext)
filename, file_type = QtWidgets.QFileDialog.getSaveFileName(self, dialog_title, default_path, dialog_file_types)
filename = self._dialog_ask_backup_filename(default_path, ext)
if not filename:
return
# Fix issue where Qt may set the extension twice
(name, ext) = path.splitext(filename)
(name, ext) = os.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()
self._dialog_save_backup_success(filename)
else:
self._backup_error(dialog_title)
self._dialog_save_backup_error(filename)

def load_backup(self):
dialog_title = _("Load Backup Configuration File")
def _dialog_load_backup_confirmation(self, filename):
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?"),
_("Load Backup Configuration File"),
_("Loading a backup configuration file will replace the current configuration settings.\n"
"Before any change, current configuration will be automatically saved to:\n"
zas marked this conversation as resolved.
Show resolved Hide resolved
"%s\n"
"\n"
"Do you want to continue?") % filename,
QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel,
self
self,
)
dialog.setDefaultButton(QtWidgets.QMessageBox.StandardButton.Cancel)
if not dialog.exec() == QtWidgets.QMessageBox.StandardButton.Ok:
return dialog.exec() == QtWidgets.QMessageBox.StandardButton.Ok

def _dialog_load_backup_success(self, filename):
dialog = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Icon.Information,
_("Load Backup Configuration File"),
_("Configuration successfully loaded from:\n"
"%s") % filename,
QtWidgets.QMessageBox.StandardButton.Ok,
self,
)
dialog.exec()

def _dialog_load_backup_error(self, filename):
dialog = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Icon.Information,
_("Load Backup Configuration File"),
_("There was a problem restoring the configuration file from:\n"
"%s\n"
"\n"
"Please see the logs for more details.") % filename,
QtWidgets.QMessageBox.StandardButton.Ok,
self,
)
dialog.exec()

def _dialog_load_backup_select_filename(self, directory, ext):
filename, file_type = QtWidgets.QFileDialog.getOpenFileName(
self,
_("Select Configuration File to Load"),
directory,
self._get_dialog_filetypes(ext),
)
return filename

def load_backup(self):
directory = self.get_current_autobackup_dir()
filename = os.path.join(directory, self._make_backup_filename(auto=True))

if not self._dialog_load_backup_confirmation(filename):
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()
self._dialog_save_backup_error(filename)
return

ext = path.splitext(filename)[1]
dialog_file_types = self._get_dialog_filetypes(ext)
directory = path.normpath(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation))
filename, file_type = QtWidgets.QFileDialog.getOpenFileName(self, dialog_title, directory, dialog_file_types)
ext = os.path.splitext(filename)[1]
filename = self._dialog_load_backup_select_filename(directory, ext)
if not filename:
return

log.warning("Loading configuration from %s", filename)
if load_new_config(filename):
config = get_config()
upgrade_config(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
)
self._dialog_load_backup_success(filename)
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()
self._dialog_load_backup_error(filename)

def column_items(self, column):
for idx in range(self.ui.tableWidget.rowCount()):
Expand All @@ -287,16 +349,22 @@ def select_all_changed(self):
for item in self.column_items(0):
item.setCheckState(state)

def _dialog_ask_remove_confirmation(self):
return QtWidgets.QMessageBox.question(
self,
_("Confirm Remove"),
_("Are you sure you want to remove the selected option settings?"),
) == QtWidgets.QMessageBox.StandardButton.Yes

def save(self):
config = get_config()

config.setting['autobackup_directory'] = self.get_current_autobackup_dir()

if not self.ui.enable_cleanup.checkState() == QtCore.Qt.CheckState.Checked:
return
to_remove = set(self.selected_options())
if to_remove and QtWidgets.QMessageBox.question(
self,
_("Confirm Remove"),
_("Are you sure you want to remove the selected option settings?"),
) == QtWidgets.QMessageBox.StandardButton.Yes:
config = get_config()
if to_remove and self._dialog_ask_remove_confirmation():
for item in to_remove:
Option.add_if_missing('setting', item, None)
log.warning("Removing option setting '%s' from the INI file.", item)
Expand Down
34 changes: 34 additions & 0 deletions picard/ui/ui_options_maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ def setupUi(self, MaintenanceOptionsPage):
self.config_file.setObjectName("config_file")
self.horizontalLayout_3.addWidget(self.config_file)
self.open_folder_button = QtWidgets.QToolButton(parent=MaintenanceOptionsPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.open_folder_button.sizePolicy().hasHeightForWidth())
self.open_folder_button.setSizePolicy(sizePolicy)
self.open_folder_button.setObjectName("open_folder_button")
self.horizontalLayout_3.addWidget(self.open_folder_button)
self.vboxlayout.addLayout(self.horizontalLayout_3)
Expand All @@ -40,12 +45,39 @@ def setupUi(self, MaintenanceOptionsPage):
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.load_backup_button = QtWidgets.QToolButton(parent=MaintenanceOptionsPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.load_backup_button.sizePolicy().hasHeightForWidth())
self.load_backup_button.setSizePolicy(sizePolicy)
self.load_backup_button.setObjectName("load_backup_button")
self.horizontalLayout.addWidget(self.load_backup_button)
self.save_backup_button = QtWidgets.QToolButton(parent=MaintenanceOptionsPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.save_backup_button.sizePolicy().hasHeightForWidth())
self.save_backup_button.setSizePolicy(sizePolicy)
self.save_backup_button.setObjectName("save_backup_button")
self.horizontalLayout.addWidget(self.save_backup_button)
self.vboxlayout.addLayout(self.horizontalLayout)
self.label_2 = QtWidgets.QLabel(parent=MaintenanceOptionsPage)
self.label_2.setObjectName("label_2")
self.vboxlayout.addWidget(self.label_2)
self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
self.autobackup_dir = QtWidgets.QLineEdit(parent=MaintenanceOptionsPage)
self.autobackup_dir.setObjectName("autobackup_dir")
self.horizontalLayout_6.addWidget(self.autobackup_dir)
self.browse_autobackup_dir = QtWidgets.QToolButton(parent=MaintenanceOptionsPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.browse_autobackup_dir.sizePolicy().hasHeightForWidth())
self.browse_autobackup_dir.setSizePolicy(sizePolicy)
self.browse_autobackup_dir.setObjectName("browse_autobackup_dir")
self.horizontalLayout_6.addWidget(self.browse_autobackup_dir)
self.vboxlayout.addLayout(self.horizontalLayout_6)
self.option_counts = QtWidgets.QLabel(parent=MaintenanceOptionsPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
Expand Down Expand Up @@ -93,5 +125,7 @@ def retranslateUi(self, MaintenanceOptionsPage):
self.open_folder_button.setText(_("Open folder"))
self.load_backup_button.setText(_("Load Backup"))
self.save_backup_button.setText(_("Save Backup"))
self.label_2.setText(_("Automatic backups directory"))
zas marked this conversation as resolved.
Show resolved Hide resolved
self.browse_autobackup_dir.setText(_("Browse…"))
self.enable_cleanup.setText(_("Remove selected options"))
self.select_all.setText(_("Select all"))
Loading
Loading