diff --git a/src/vorta/assets/UI/scheduletab.ui b/src/vorta/assets/UI/scheduletab.ui index f7cb7b34e..695391b7a 100644 --- a/src/vorta/assets/UI/scheduletab.ui +++ b/src/vorta/assets/UI/scheduletab.ui @@ -725,6 +725,20 @@ + + + + Open Pre-backup script editor + + + ... + + + + :/icons/edit.svg:/icons/edit.svg + + + @@ -764,6 +778,20 @@ + + + + Open Post-backup script editor + + + ... + + + + :/icons/edit.svg:/icons/edit.svg + + + diff --git a/src/vorta/assets/UI/scriptedit.ui b/src/vorta/assets/UI/scriptedit.ui new file mode 100644 index 000000000..bfc8edcbd --- /dev/null +++ b/src/vorta/assets/UI/scriptedit.ui @@ -0,0 +1,92 @@ + + + ScriptEdit + + + + 0 + 0 + 440 + 260 + + + + Edit Script + + + true + + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + + Pre/Post-Backup Script: + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + Monospace + + + + IBeamCursor + + + QTextEdit::NoWrap + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + + + + + diff --git a/src/vorta/utils.py b/src/vorta/utils.py index 033b486a5..7ba31f919 100644 --- a/src/vorta/utils.py +++ b/src/vorta/utils.py @@ -15,7 +15,13 @@ import psutil from PyQt6 import QtCore from PyQt6.QtCore import QFileInfo, QThread, pyqtSignal -from PyQt6.QtWidgets import QApplication, QFileDialog, QSystemTrayIcon +from PyQt6.QtWidgets import ( + QAbstractItemView, + QApplication, + QFileDialog, + QSystemTrayIcon, + QTreeView, +) from vorta.borg._compatibility import BorgCompatibility from vorta.log import logger @@ -166,12 +172,17 @@ def get_dict_from_list(dataDict, mapList): return reduce(lambda d, k: d.setdefault(k, {}), mapList, dataDict) -def choose_file_dialog(parent, title, want_folder=True): +def choose_file_dialog(parent, title, want_folder=True, file_filter=None, single_selection=False): dialog = QFileDialog(parent, title, os.path.expanduser('~')) dialog.setFileMode(QFileDialog.FileMode.Directory if want_folder else QFileDialog.FileMode.ExistingFiles) dialog.setParent(parent, QtCore.Qt.WindowType.Sheet) if want_folder: dialog.setOption(QFileDialog.Option.ShowDirsOnly) + elif file_filter: + dialog.setNameFilter(file_filter) + if single_selection: + tree_view = dialog.findChild(QTreeView) + tree_view.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) return dialog diff --git a/src/vorta/views/schedule_tab.py b/src/vorta/views/schedule_tab.py index dc97cfa47..b2c720dec 100644 --- a/src/vorta/views/schedule_tab.py +++ b/src/vorta/views/schedule_tab.py @@ -13,6 +13,7 @@ from vorta.scheduler import ScheduleStatusType from vorta.store.models import BackupProfileMixin, EventLogModel, WifiSettingModel from vorta.utils import get_asset, get_sorted_wifis +from vorta.views.script_edit_dialog import ScriptEditWindow from vorta.views.utils import get_colored_icon uifile = get_asset('UI/scheduletab.ui') @@ -81,6 +82,8 @@ def __init__(self, parent=None): self.scheduleIntervalCount.valueChanged.connect(self.on_scheduler_change) self.scheduleIntervalUnit.currentIndexChanged.connect(self.on_scheduler_change) self.scheduleFixedTime.timeChanged.connect(self.on_scheduler_change) + self.preBackupScriptEditorButton.clicked.connect(lambda: self.launch_script_editor(context="pre")) + self.postBackupScriptEditorButton.clicked.connect(lambda: self.launch_script_editor(context="post")) # Network and shell commands events self.meteredNetworksCheckBox.stateChanged.connect( @@ -135,6 +138,8 @@ def set_icons(self): self.toolBox.setItemIcon(1, get_colored_icon('wifi')) self.toolBox.setItemIcon(2, get_colored_icon('tasks')) self.toolBox.setItemIcon(3, get_colored_icon('terminal')) + self.preBackupScriptEditorButton.setIcon(get_colored_icon('edit')) + self.postBackupScriptEditorButton.setIcon(get_colored_icon('edit')) def populate_from_profile(self): """Populate current view with data from selected profile.""" @@ -237,3 +242,8 @@ def populate_logs(self): QTableWidgetItem(str(log_line.returncode)), ) self.logTableWidget.setSortingEnabled(sorting) # restore sorting now that modifications are done + + def launch_script_editor(self, context: str) -> None: + edit_window = ScriptEditWindow(context, profile=self.profile()) + edit_window.exec() + self.populate_from_profile() # Refresh the view after the script has been edited. diff --git a/src/vorta/views/script_edit_dialog.py b/src/vorta/views/script_edit_dialog.py new file mode 100644 index 000000000..3bbed7baf --- /dev/null +++ b/src/vorta/views/script_edit_dialog.py @@ -0,0 +1,56 @@ +from PyQt6 import QtCore, uic +from PyQt6.QtWidgets import QDialogButtonBox + +from vorta.utils import get_asset + +uifile = get_asset("UI/scriptedit.ui") +ScriptEditUI, ScriptEditBase = uic.loadUiType(uifile) + + +class ScriptEditWindow(ScriptEditUI, ScriptEditBase): + def __init__(self, context: str, profile, parent=None) -> None: + super().__init__(parent) + self.setupUi(self) + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) + + self.context = context + if context == "pre": + self.setWindowTitle(self.tr("Edit Pre-Backup Script")) + self.label.setText(self.tr("Pre-Backup Script:")) + elif context == "post": + self.setWindowTitle(self.tr("Edit Post-Backup Script")) + self.label.setText(self.tr("Post-Backup Script:")) + + self.buttonBox.button(QDialogButtonBox.StandardButton.Save).setText(self.tr("Save")) + self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText(self.tr("Cancel")) + + self.profile = profile + + self.buttonBox.button(QDialogButtonBox.StandardButton.Save).clicked.connect(self.save_script) + self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).clicked.connect(self.close) + + # Populate data from profile + self.populate_from_profile() + + def populate_from_profile(self): + """Populate the script editor with the current profile's script.""" + profile = self.profile + if self.context == "pre": + self.scriptEdit.setPlainText(profile.pre_backup_cmd) + elif self.context == "post": + self.scriptEdit.setPlainText(profile.post_backup_cmd) + + def save_profile_attr(self, attr, new_value): + profile = self.profile + setattr(profile, attr, new_value) + profile.save() + + def save_script(self): + script = self.scriptEdit.toPlainText() + profile = self.profile + if self.context == "pre": + self.save_profile_attr("pre_backup_cmd", script) + elif self.context == "post": + self.save_profile_attr("post_backup_cmd", script) + profile.save() + self.close()