vorta/src/vorta/views/main_window.py

304 lines
13 KiB
Python

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()