import logging from pathlib import Path from PyQt6 import QtCore, uic from PyQt6.QtCore import QPoint from PyQt6.QtGui import QFontMetrics, QKeySequence, QShortcut from PyQt6.QtWidgets import ( QApplication, QCheckBox, QFileDialog, QMenu, QMessageBox, QToolTip, ) from vorta.profile_export import ImportFailedException, ProfileExport from vorta.store.models import BackupProfileModel, SettingsModel from vorta.utils import ( borg_compat, get_asset, get_network_status_monitor, is_system_tray_available, ) from vorta.views.partials.loading_button import LoadingButton from vorta.views.utils import get_colored_icon from .archive_tab import ArchiveTab from .export_window import ExportWindow from .import_window import ImportWindow from .misc_tab import MiscTab from .profile_add_edit_dialog import AddProfileWindow, EditProfileWindow from .repo_tab import RepoTab from .schedule_tab import ScheduleTab from .source_tab import SourceTab uifile = get_asset('UI/mainwindow.ui') MainWindowUI, MainWindowBase = uic.loadUiType(uifile) logger = logging.getLogger(__name__) class MainWindow(MainWindowBase, MainWindowUI): def __init__(self, parent=None): super().__init__() self.setupUi(self) self.setWindowTitle('Vorta for Borg Backup') self.app = parent self.setWindowIcon(get_colored_icon("icon")) self.setWindowFlags(QtCore.Qt.WindowType.WindowCloseButtonHint | QtCore.Qt.WindowType.WindowMinimizeButtonHint) self.createStartBtn = LoadingButton(self.tr("Start Backup")) self.gridLayout.addWidget(self.createStartBtn, 0, 0, 1, 1) self.createStartBtn.setGif(get_asset("icons/loading")) # set log label height to two lines fontmetrics: QFontMetrics = self.logText.fontMetrics() self.logText.setMinimumHeight(fontmetrics.lineSpacing() * 2 + fontmetrics.leading()) # Use previous window state previous_window_width = SettingsModel.get(key='previous_window_width') previous_window_height = SettingsModel.get(key='previous_window_height') self.resize(int(previous_window_width.str_value), int(previous_window_height.str_value)) # Select previously used profile, if available prev_profile_id = SettingsModel.get(key='previous_profile_id') self.current_profile = BackupProfileModel.get_or_none(id=prev_profile_id.str_value) if self.current_profile is None: self.current_profile = BackupProfileModel.select().order_by('name').first() # Load tab models self.repoTab = RepoTab(self.repoTabSlot) self.sourceTab = SourceTab(self.sourceTabSlot) self.archiveTab = ArchiveTab(self.archiveTabSlot, app=self.app) self.scheduleTab = ScheduleTab(self.scheduleTabSlot) self.miscTab = MiscTab(self.miscTabSlot) self.miscTab.set_borg_details(borg_compat.version, borg_compat.path) self.tabWidget.setCurrentIndex(0) self.repoTab.repo_changed.connect(self.archiveTab.populate_from_profile) self.repoTab.repo_changed.connect(self.scheduleTab.populate_from_profile) self.repoTab.repo_added.connect(self.archiveTab.refresh_archive_list) self.createStartBtn.clicked.connect(self.app.create_backup_action) self.cancelButton.clicked.connect(self.app.backup_cancelled_event.emit) QShortcut(QKeySequence("Ctrl+W"), self).activated.connect(self.on_close_window) QShortcut(QKeySequence("Ctrl+Q"), self).activated.connect(self.on_close_window) self.app.backup_started_event.connect(self.backup_started_event) self.app.backup_finished_event.connect(self.backup_finished_event) self.app.backup_log_event.connect(self.set_log) self.app.backup_progress_event.connect(self.set_progress) self.app.backup_cancelled_event.connect(self.backup_cancelled_event) # Init profile list self.populate_profile_selector() self.profileSelector.currentIndexChanged.connect(self.profile_select_action) self.profileRenameButton.clicked.connect(self.profile_rename_action) self.profileExportButton.clicked.connect(self.profile_export_action) self.profileDeleteButton.clicked.connect(self.profile_delete_action) profile_add_menu = QMenu() profile_add_menu.addAction(self.tr('Import from fileā€¦'), self.profile_import_action) self.profileAddButton.setMenu(profile_add_menu) self.profileAddButton.clicked.connect(self.profile_add_action) # OS-specific startup options: if not get_network_status_monitor().is_network_status_available(): # Hide Wifi-rule section in schedule tab. self.scheduleTab.wifiListLabel.hide() self.scheduleTab.wifiListWidget.hide() self.scheduleTab.page_2.hide() self.scheduleTab.toolBox.removeItem(1) # Connect to existing thread. if self.app.jobs_manager.is_worker_running(): self.createStartBtn.setEnabled(False) self.createStartBtn.start() self.cancelButton.setEnabled(True) # Connect to palette change QApplication.instance().paletteChanged.connect(lambda p: self.set_icons()) self.set_icons() def on_close_window(self): self.close() def set_icons(self): self.profileAddButton.setIcon(get_colored_icon('plus')) self.profileRenameButton.setIcon(get_colored_icon('edit')) self.profileExportButton.setIcon(get_colored_icon('file-import-solid')) self.profileDeleteButton.setIcon(get_colored_icon('trash')) def set_progress(self, text=''): self.progressText.setText(text) self.progressText.repaint() def set_log(self, text=''): self.logText.setText(text) self.logText.repaint() def _toggle_buttons(self, create_enabled=True): if create_enabled: self.createStartBtn.stop() else: self.createStartBtn.start() self.createStartBtn.setEnabled(create_enabled) self.createStartBtn.repaint() self.cancelButton.setEnabled(not create_enabled) self.cancelButton.repaint() def populate_profile_selector(self): self.profileSelector.clear() for profile in BackupProfileModel.select().order_by(BackupProfileModel.name): self.profileSelector.addItem(profile.name, profile.id) current_profile_index = self.profileSelector.findData(self.current_profile.id) self.profileSelector.setCurrentIndex(current_profile_index) def profile_select_action(self, index): backup_profile_id = self.profileSelector.currentData() if not backup_profile_id: return self.current_profile = BackupProfileModel.get(id=backup_profile_id) self.archiveTab.populate_from_profile() self.repoTab.populate_from_profile() self.sourceTab.populate_from_profile() self.scheduleTab.populate_from_profile() SettingsModel.update({SettingsModel.str_value: self.current_profile.id}).where( SettingsModel.key == 'previous_profile_id' ).execute() def profile_rename_action(self): window = EditProfileWindow(rename_existing_id=self.profileSelector.currentData()) self.window = window # For tests window.setParent(self, QtCore.Qt.WindowType.Sheet) window.open() window.profile_changed.connect(self.profile_add_edit_result) window.rejected.connect(lambda: self.profileSelector.setCurrentIndex(self.profileSelector.currentIndex())) def profile_delete_action(self): if self.profileSelector.count() > 1: to_delete_id = self.profileSelector.currentData() to_delete = BackupProfileModel.get(id=to_delete_id) msg = self.tr("Are you sure you want to delete profile '{}'?".format(to_delete.name)) reply = QMessageBox.question( self, self.tr("Confirm deletion"), msg, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: to_delete.delete_instance(recursive=True) self.app.scheduler.remove_job(to_delete_id) # Remove pending jobs self.profileSelector.removeItem(self.profileSelector.currentIndex()) self.profile_select_action(0) else: warn = self.tr("Cannot delete the last profile.") point = QPoint(0, int(self.profileDeleteButton.size().height() / 2)) QToolTip.showText(self.profileDeleteButton.mapToGlobal(point), warn) def profile_add_action(self): window = AddProfileWindow() self.window = window # For tests window.setParent(self, QtCore.Qt.WindowType.Sheet) window.open() window.profile_changed.connect(self.profile_add_edit_result) window.rejected.connect(lambda: self.profileSelector.setCurrentIndex(self.profileSelector.currentIndex())) def profile_export_action(self): """ React to pressing "Export Profile" button and save current profile as .json file. """ window = ExportWindow(profile=self.current_profile.refresh()) self.window = window window.setParent(self, QtCore.Qt.WindowType.Sheet) window.show() def profile_import_action(self): """ React to "Import Profile". Ask to select a .json file and import it as new profile. """ def profile_imported_event(profile): QMessageBox.information( None, self.tr('Profile import successful!'), self.tr('Profile {} imported.').format(profile.name), ) self.repoTab.populate_from_profile() self.scheduleTab.populate_logs() self.scheduleTab.populate_wifi() self.miscTab.populate() self.populate_profile_selector() filename = QFileDialog.getOpenFileName( self, self.tr("Load profile"), str(Path.home()), self.tr("JSON (*.json);;All files (*)"), )[0] if filename: try: profile_export = ProfileExport.from_json(filename) except ImportFailedException as exception: QMessageBox.critical(None, self.tr('Failed to import profile'), str(exception)) return window = ImportWindow(profile_export=profile_export) self.window = window window.setParent(self, QtCore.Qt.WindowType.Sheet) window.profile_imported.connect(profile_imported_event) window.show() def profile_add_edit_result(self, profile_name, profile_id): # Profile is renamed if self.profileSelector.currentData() == profile_id: self.profileSelector.setItemText(self.profileSelector.currentIndex(), profile_name) # Profile is added else: self.profileSelector.addItem(profile_name, profile_id) self.profileSelector.setCurrentIndex(self.profileSelector.count() - 1) def backup_started_event(self): self._toggle_buttons(create_enabled=False) self.archiveTab._toggle_all_buttons(enabled=False) self.set_log('') def backup_finished_event(self): self.archiveTab.populate_from_profile() self.repoTab.init_repo_stats() self.scheduleTab.populate_logs() if not self.app.jobs_manager.is_worker_running(): self._toggle_buttons(create_enabled=True) self.archiveTab._toggle_all_buttons(enabled=True) def backup_cancelled_event(self): self._toggle_buttons(create_enabled=True) self.set_log(self.tr('Task cancelled')) self.archiveTab.cancel_action() def closeEvent(self, event): # Save window state in SettingsModel SettingsModel.update({SettingsModel.str_value: str(self.width())}).where( SettingsModel.key == 'previous_window_width' ).execute() SettingsModel.update({SettingsModel.str_value: str(self.height())}).where( SettingsModel.key == 'previous_window_height' ).execute() if not is_system_tray_available(): if SettingsModel.get(key="enable_background_question").value: msg = QMessageBox() msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) msg.setParent(self, QtCore.Qt.WindowType.Sheet) msg.setText(self.tr("Should Vorta continue to run in the background?")) msg.button(QMessageBox.StandardButton.Yes).clicked.connect( lambda: self.miscTab.save_setting("disable_background_state", True) ) msg.button(QMessageBox.StandardButton.No).clicked.connect( lambda: ( self.miscTab.save_setting("disable_background_state", False), self.app.quit(), ) ) msg.setWindowTitle(self.tr("Quit")) dont_show_box = QCheckBox(self.tr("Don't show this again")) dont_show_box.clicked.connect(lambda x: self.miscTab.save_setting("enable_background_question", not x)) dont_show_box.setTristate(False) msg.setCheckBox(dont_show_box) msg.exec() elif not SettingsModel.get(key="disable_background_state").value: self.app.quit() event.accept()