vorta/src/vorta/views/main_window.py

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