diff --git a/src/OneDriveGUI.py b/src/OneDriveGUI.py index 4db11e5..8e1c360 100644 --- a/src/OneDriveGUI.py +++ b/src/OneDriveGUI.py @@ -48,15 +48,11 @@ workers = {} if __name__ == "__main__": - # gui_settings = GuiSettings(GUI_SETTINGS_FILE) - logging.info(f"Starting OneDriveGUI v{version}") if len(global_config) > 0: save_global_config(global_config) - # setup_wizard = SetupWizard(profile_settings_window=profile_settings_window) - main_window = MainWindow() main_window_start_state() diff --git a/src/main_window.py b/src/main_window.py index 1b1173c..78eab25 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -48,18 +48,28 @@ import logging # from logger import logger from global_config import DIR_PATH, PROFILES_FILE -# try: -# from ui.ui_login import Ui_LoginWindow -# except ImportError: -# logging.warning("Failed to import ui_login. This is expected if you are running AppImage version.") +try: + from ui.ui_login import Ui_LoginWindow +except ImportError: + logging.warning("Failed to import ui_login. This is expected if you are running AppImage version.") + + # Define a dummy class to avoid errors when this UI component isn't available + class Ui_LoginWindow: + def setupUi(self, window): + logging.error("LoginWindow UI is not available in this environment") + window.hide() class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): - # self.setup_wizard = setup_wizard - self.setup_wizard.show_main_window_signal.connect(self.show) + # Access setup_wizard from wizard module to avoid circular imports + import wizard + + wizard.setup_wizard.show_main_window_signal.connect(self.show) + wizard.setup_wizard.add_profile_signal.connect(self.add_profile) profile_settings_window.remove_profile_signal.connect(self.remove_profile) + profile_settings_window.rename_profile_signal.connect(self.rename_profile_in_main_window) super(MainWindow, self).__init__() self.setupUi(self) @@ -357,6 +367,11 @@ class MainWindow(QMainWindow, Ui_MainWindow): ) for profile_name in global_config: + # Check if profile exists in profile_status_pages, if not add it + if profile_name not in self.profile_status_pages: + logging.info(f"Profile {profile_name} found in global_config but not in UI, adding it now") + self.add_profile(profile_name) + profile_status_page = self.profile_status_pages[profile_name] if profile_name not in workers: @@ -693,7 +708,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): response_dialog = QMessageBox.critical( self, "Login failed", - f"Login failed! Please verify your response URL and try again." f"

{response_reason}", + f"Login failed! Please verify your response URL and try again.

{response_reason}", buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok, ) @@ -715,11 +730,97 @@ class MainWindow(QMainWindow, Ui_MainWindow): workers.pop(profile_name, None) logging.info(f"[GUI] Remaining running workers: {workers}") + def add_profile(self, profile_name): + """ + Adds a new profile to the main window UI. + Called when a new profile is created through the wizard. + """ + logging.info(f"[MAIN_WINDOW] Adding profile {profile_name} to main window UI") + + # Check if profile already exists in UI + if profile_name in self.profile_status_pages: + logging.info(f"[MAIN_WINDOW] Profile {profile_name} already exists in UI, skipping add") + return + + # Create a profile status page and add it to the UI + self.comboBox.addItem(profile_name) + self.profile_status_pages[profile_name] = ProfileStatusPage(profile_name) + self.profile_status_pages[profile_name].start_sync_signal.connect(self.start_onedrive_monitor) + self.profile_status_pages[profile_name].stop_sync_signal.connect(self.stop_onedrive_monitor) + self.profile_status_pages[profile_name].quit_gui_signal.connect(self.graceful_shutdown) + self.stackedLayout.addWidget(self.profile_status_pages[profile_name]) + + # Select the newly added profile + index = self.comboBox.findText(profile_name) + if index >= 0: + self.comboBox.setCurrentIndex(index) + self.stackedLayout.setCurrentIndex(index) + logging.info(f"[MAIN_WINDOW] Selected profile {profile_name} at index {index}") + + # Show comboBox if more than one profile exists + if len(self.profile_status_pages) > 1: + self.comboBox.show() + + # Force update the UI + self.comboBox.update() + self.update() + + # Make the profile visible and force a layout refresh + self.comboBox.show() + self.stackedLayout.update() + self.verticalLayout_2.update() + + # Process events to ensure UI updates immediately + from PySide6.QtCore import QCoreApplication + + QCoreApplication.processEvents() + + @Slot(str, str) + def rename_profile_in_main_window(self, old_name, new_name): + """ + Updates the profile name in the main window UI elements. + """ + logging.info(f"[MAIN_WINDOW] Renaming profile '{old_name}' to '{new_name}' in main window UI.") + + # 1. Update the item text in the comboBox. + index = self.comboBox.findText(old_name) + if index >= 0: + self.comboBox.setItemText(index, new_name) + logging.debug(f"[MAIN_WINDOW] Updated comboBox item at index {index} from '{old_name}' to '{new_name}'.") + else: + logging.warning(f"[MAIN_WINDOW] Profile '{old_name}' not found in comboBox.") + + # 2. Update the key in the self.profile_status_pages dictionary. + if old_name in self.profile_status_pages: + self.profile_status_pages[new_name] = self.profile_status_pages.pop(old_name) + # Also update the profile_name attribute within the ProfileStatusPage instance + self.profile_status_pages[new_name].profile_name = new_name + logging.debug(f"[MAIN_WINDOW] Updated profile_status_pages key from '{old_name}' to '{new_name}'.") + else: + logging.warning(f"[MAIN_WINDOW] Profile '{old_name}' not found in profile_status_pages.") + + # 3. Update the key in the workers dictionary if a worker exists. + if old_name in workers: + workers[new_name] = workers.pop(old_name) + # Also update the profile_name attribute within the WorkerThread instance + workers[new_name].profile_name = new_name + logging.debug(f"[MAIN_WINDOW] Updated workers key from '{old_name}' to '{new_name}'.") + else: + logging.debug(f"[MAIN_WINDOW] No worker found for profile '{old_name}'.") + + # Force update the UI + self.comboBox.update() + self.update() + + # Process events to ensure UI updates immediately + from PySide6.QtCore import QCoreApplication + + QCoreApplication.processEvents() + def remove_profile(self, profile_name): """ Removes onedrive profile from the GUI. """ - print("AAAAA") combo_box_index = self.comboBox.findText(profile_name) self.comboBox.removeItem(combo_box_index) self.stackedLayout.setCurrentIndex(0) diff --git a/src/options.py b/src/options.py index bf883db..0dd2cd5 100644 --- a/src/options.py +++ b/src/options.py @@ -7,7 +7,7 @@ from settings.gui_settings import gui_settings client_bin_path = gui_settings.get("frameless_window") -version = "1.1.1" +version = "1.2.0" client_bin_path = config_client_bin_path() client_version = get_installed_client_version(client_bin_path) global_config = create_global_config() diff --git a/src/profile_settings_window.py b/src/profile_settings_window.py index dd06c47..a44a468 100644 --- a/src/profile_settings_window.py +++ b/src/profile_settings_window.py @@ -8,6 +8,7 @@ from PySide6.QtWidgets import ( QMessageBox, QStyledItemDelegate, QStyleOptionViewItem, + QInputDialog, ) # Imports for Profiles windows. @@ -19,13 +20,16 @@ import os import copy from configparser import ConfigParser -from wizard import setup_wizard +# Import setup_wizard after importing wizard, keeping it at the bottom to avoid circular imports +import wizard from global_config import save_global_config -# from main_window import main_window +# Import main_window (lazy import to avoid circular references) +import main_window from options import client_bin_path, global_config -from options import temp_global_config # , profile_settings_window +from options import temp_global_config +from workers import workers import logging @@ -35,13 +39,14 @@ from global_config import DIR_PATH, PROFILES_FILE class ProfileSettingsWindow(QWidget, Ui_profile_settings_window): remove_profile_signal = Signal(str) + rename_profile_signal = Signal(str, str) # Signal to emit old_name, new_name def __init__(self): super(ProfileSettingsWindow, self).__init__() # Listen for Signals from wizard - # self.setup_wizard = setup_wizard - setup_wizard.add_profile_signal.connect(self.add_profile) + # Access setup_wizard through the wizard module to avoid circular imports + wizard.setup_wizard.add_profile_signal.connect(self.add_profile) self.unsaved_profiles = [] @@ -64,6 +69,7 @@ class ProfileSettingsWindow(QWidget, Ui_profile_settings_window): self.listWidget_profiles.itemSelectionChanged.connect(self.switch_account_settings_page) self.pushButton_remove.clicked.connect(self.remove_profile) + self.pushButton_rename.clicked.connect(self.rename_profile) self.pushButton_create_import.clicked.connect(self.show_setup_wizard) def add_profile(self, profile): @@ -120,36 +126,124 @@ class ProfileSettingsWindow(QWidget, Ui_profile_settings_window): self.stackedLayout.setCurrentIndex(self.listWidget_profiles.currentRow()) def show_setup_wizard(self): - # self.setup_wizard = SetupWizard() - setup_wizard.setStartId(3) - setup_wizard.show() + # Access setup_wizard through the wizard module to avoid circular imports + wizard.setup_wizard.setStartId(3) + wizard.setup_wizard.show() def remove_profile(self): # Stop checking for unsaved changes while new profile is being removed. self.stop_unsaved_changes_timer() - # Remove profile from settings window. selected_profile_name = self.listWidget_profiles.currentItem().text() - selected_profile_index = self.listWidget_profiles.currentRow() - selected_profile_widget = self.stackedLayout.currentWidget() - self.listWidget_profiles.takeItem(selected_profile_index) - self.stackedLayout.removeWidget(selected_profile_widget) - # Remove profile from main window. - print(f"emit removal of profile {selected_profile_name}") - self.remove_profile_signal.emit(selected_profile_name) + # Add confirmation dialog + confirm_removal = QMessageBox.question( + self, + "Confirm Profile Removal", + f"Are you sure you want to remove the profile {selected_profile_name}?", + buttons=QMessageBox.Yes | QMessageBox.No, + defaultButton=QMessageBox.No, + ) - # Load existing user profiles and remove the new profile. - _profiles = ConfigParser() - _profiles.read(PROFILES_FILE) - _profiles.remove_section(selected_profile_name) + if confirm_removal == QMessageBox.Yes: + logging.info(f"[GUI] Removing profile: {selected_profile_name}") + # Remove profile from settings window. + selected_profile_index = self.listWidget_profiles.currentRow() + selected_profile_widget = self.stackedLayout.currentWidget() + self.listWidget_profiles.takeItem(selected_profile_index) + self.stackedLayout.removeWidget(selected_profile_widget) - # Save the new profiles file. - with open(PROFILES_FILE, "w") as profilefile: - _profiles.write(profilefile) + # Remove profile from main window. + print(f"emit removal of profile {selected_profile_name}") + self.remove_profile_signal.emit(selected_profile_name) - # Start checking for unsaved changes again after profile has been removed. - self.start_unsaved_changes_timer() + # Load existing user profiles and remove the new profile. + _profiles = ConfigParser() + _profiles.read(PROFILES_FILE) + _profiles.remove_section(selected_profile_name) + + # Save the new profiles file. + with open(PROFILES_FILE, "w") as profilefile: + _profiles.write(profilefile) + + # Start checking for unsaved changes again after profile has been removed. + self.start_unsaved_changes_timer() + else: + logging.info(f"[GUI] Profile removal cancelled for: {selected_profile_name}") + # If removal is cancelled, restart the timer as no changes were made + self.start_unsaved_changes_timer() + + def rename_profile(self): + selected_item = self.listWidget_profiles.currentItem() + if not selected_item: + QMessageBox.warning(self, "Rename Profile", "Please select a profile to rename.") + return + + old_name = selected_item.text() + + new_name, ok = QInputDialog.getText( + self, "Rename Profile", f"Enter new name for profile '{old_name}':", text=old_name + ) + + if ok and new_name: + new_name = new_name.strip() + if not new_name: + QMessageBox.warning(self, "Rename Profile", "Profile name cannot be empty.") + return + if new_name == old_name: + logging.info("[GUI] Rename cancelled: New name is the same as the old name.") + return + if new_name in global_config: + QMessageBox.warning(self, "Rename Profile", f"Profile '{new_name}' already exists.") + return + + logging.info(f"[GUI] Renaming profile '{old_name}' to '{new_name}'.") + + # Stop checking for unsaved changes during rename + self.stop_unsaved_changes_timer() + + # 4. Update global_config and temp_global_config. + global_config[new_name] = global_config.pop(old_name) + temp_global_config[new_name] = temp_global_config.pop(old_name) + + # 5. Rename section in PROFILES_FILE. + _profiles = ConfigParser() + _profiles.read(PROFILES_FILE) + if _profiles.has_section(old_name): + items = _profiles.items(old_name) + _profiles.add_section(new_name) + for key, value in items: + _profiles.set(new_name, key, value) + _profiles.remove_section(old_name) + with open(PROFILES_FILE, "w") as profilefile: + _profiles.write(profilefile) + else: + logging.warning(f"[GUI] Section '{old_name}' not found in {PROFILES_FILE}.") + + # 6. Update listWidget_profiles item. + selected_item.setText(new_name) + + # 7. Update label_profile_name in the corresponding ProfileSettingsPage. + current_index = self.listWidget_profiles.currentRow() + page_widget = self.stackedLayout.widget(current_index) + if isinstance(page_widget, ProfileSettingsPage): + page_widget.profile = new_name # Update the profile name stored in the page widget + page_widget.label_profile_name.setText(new_name) + # Update the temp_profile_config reference in the page widget + page_widget.temp_profile_config = temp_global_config[new_name] + + # 8. Signal main_window to update profile name display. + self.rename_profile_signal.emit(old_name, new_name) + + # Save the updated global config + save_global_config(global_config) + + # Restart checking for unsaved changes + self.start_unsaved_changes_timer() + + elif not ok: + logging.info("[GUI] Rename profile dialog cancelled.") + # If ok is True but new_name is empty, the validation message box is shown. class ListItemDelegate(QStyledItemDelegate): @@ -203,7 +297,19 @@ class ProfileSettingsPage(QWidget, Ui_profile_settings): config_changed = False sync_list_changed = False pixmap_warning = QPixmap(DIR_PATH + "/resources/images/warning.png").scaled(20, 20, Qt.KeepAspectRatio) - unsaved_profile = profile_settings_window.listWidget_profiles.findItems(self.profile, Qt.MatchExactly)[0] + + # Check if profile exists in listWidget + matching_items = profile_settings_window.listWidget_profiles.findItems(self.profile, Qt.MatchExactly) + if not matching_items: + logging.warning(f"[PROFILE_SETTINGS] Profile {self.profile} not found in UI list, skipping check") + return + + unsaved_profile = matching_items[0] + + # Check if profile exists in global_config + if self.profile not in global_config: + logging.warning(f"[PROFILE_SETTINGS] Profile {self.profile} not found in global_config, skipping check") + return # Unsaved changes to config file? if global_config[self.profile] != self.temp_profile_config: @@ -328,7 +434,7 @@ class ProfileSettingsPage(QWidget, Ui_profile_settings): self.checkBox_disable_notifications.stateChanged.connect(self.validate_checkbox_input) # Account tab - self.pushButton_login.clicked.connect(lambda: main_window.show_login(self.profile)) + self.pushButton_login.clicked.connect(lambda: main_window.main_window_instance.show_login(self.profile)) self.pushButton_logout.clicked.connect(self.logout) self.checkBox_auto_sync.stateChanged.connect(self.set_check_box_state_profile) @@ -516,17 +622,19 @@ class ProfileSettingsPage(QWidget, Ui_profile_settings): os.system(f"{client_bin_path} --confdir='{self.config_dir}' --logout") logging.info(f"Profile {self.profile} has been logged out.") - main_window.profile_status_pages[self.profile].stop_monitor() - if self.profile in main_window.workers: - main_window.workers[self.profile].stop_worker() - main_window.profile_status_pages[self.profile].label_onedrive_status.setText( + main_window.main_window_instance.profile_status_pages[self.profile].stop_monitor() + if self.profile in workers: + workers[self.profile].stop_worker() + main_window.main_window_instance.profile_status_pages[self.profile].label_onedrive_status.setText( "OneDrive sync has been stopped" ) logging.info(f"OneDrive sync for profile {self.profile} has been stopped.") else: logging.info(f"OneDrive for profile {self.profile} is not running.") - main_window.profile_status_pages[self.profile].label_onedrive_status.setText("You have been logged out") + main_window.main_window_instance.profile_status_pages[self.profile].label_onedrive_status.setText( + "You have been logged out" + ) def get_sync_dir_name(self): self.file_dialog = QFileDialog.getExistingDirectory(dir=os.path.expanduser("~/")) diff --git a/src/wizard.py b/src/wizard.py index 762c0e2..13b5777 100644 --- a/src/wizard.py +++ b/src/wizard.py @@ -14,9 +14,6 @@ from PySide6.QtWidgets import ( QComboBox, ) -# from profile_settings_window import profile_settings_page -# from main_window import ProfileStatusPage - import re import os import sys @@ -25,11 +22,7 @@ import requests import subprocess from configparser import ConfigParser -# from wizard import SetupWizard from global_config import save_global_config, read_config - -# from profile_settings_window import profile_settings_window, ProfileSettingsPage -# from main_window import main_window from options import ( global_config, temp_global_config, @@ -38,26 +31,17 @@ from options import ( version, ) - from workers import MaintenanceWorker - - import logging - -# from logger import logger from global_config import DIR_PATH, PROFILES_FILE class SetupWizard(QWizard): show_main_window_signal = Signal() + add_profile_signal = Signal(str) def __init__(self, parent=None): super(SetupWizard, self).__init__(parent) - # Lazy import - from profile_settings_window import ProfileSettingsWindow - self.profile_settings_window = ProfileSettingsWindow() - - self.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-clouds-80-dark-edge.png")) self.setPage(1, WizardPage_welcome(self)) @@ -74,6 +58,45 @@ class SetupWizard(QWizard): self.currentIdChanged.connect(self.on_page_change) # self.profile_settings_window = profile_settings_window + def showEvent(self, event): + """Reset form fields when wizard is shown""" + super(SetupWizard, self).showEvent(event) + + # Always restart at the first page (Welcome) + self.restart() + + # Reset the create profile page + create_page = self.page(4) + if hasattr(create_page, "lineEdit_new_profile_name"): + create_page.lineEdit_new_profile_name.setText("") + create_page.lineEdit_sync_dir.setText("") + create_page.pushButton_create.setText("Create new profile") + create_page.pushButton_create.setEnabled(False) + create_page.lineEdit_new_profile_name.setEnabled(True) + create_page.lineEdit_sync_dir.setEnabled(True) + create_page.pushButton_browse.setEnabled(True) + + # Reset the import profile page + import_page = self.page(5) + if hasattr(import_page, "lineEdit_profile_name"): + import_page.lineEdit_profile_name.setText("") + import_page.lineEdit_config_path.setText("") + import_page.pushButton_import.setText("Import") + import_page.pushButton_import.setEnabled(False) + import_page.lineEdit_profile_name.setEnabled(True) + import_page.lineEdit_config_path.setEnabled(True) + import_page.pushButton_browse.setEnabled(True) + + # Reset the create/import selection page + selection_page = self.page(3) + if hasattr(selection_page, "checkBox_create"): + selection_page.checkBox_create.setChecked(False) + selection_page.checkBox_import.setChecked(False) + selection_page.checkBox_sharepoint_library.setChecked(False) + selection_page.checkBox_create.setEnabled(True) + selection_page.checkBox_import.setEnabled(True) + selection_page.checkBox_sharepoint_library.setEnabled(True) + def on_page_change(self): if self.currentId() == 2: logging.info("Checking installed OneDrive version") @@ -348,12 +371,22 @@ class wizardPage_create_shared_library(QWizardPage): logging.info(f"[GUI] Starting maintenance worker to obtain SharePoint Library List for profile {profile_name}.") + # Create and configure the worker before connecting signals self.obtain_sharepoint_site_list = MaintenanceWorker(profile_name, options) - self.obtain_sharepoint_site_list.start() + + # Connect signals before starting the worker self.obtain_sharepoint_site_list.update_sharepoint_site_list.connect( self.populate_comboBox_sharepoint_site_list ) + # Process events to ensure UI stays responsive + from PySide6.QtCore import QCoreApplication + + QCoreApplication.processEvents() + + # Start the worker after all connections are set up + self.obtain_sharepoint_site_list.start() + def populate_comboBox_sharepoint_site_list(self, sharepoint_site_list): """ Populates comboBox_sharepoint_site_list with a list of emitted SharePoint Sites. @@ -443,7 +476,7 @@ class wizardPage_create_shared_library(QWizardPage): _site_name = self.comboBox_sharepoint_site_list.currentText() _library_name = self.comboBox_sharepoint_library_list.currentText() _library_id = self.library_dict[_library_name] - profile_name = f'SharePoint_{_site_name.replace(" ", "_")}_{_library_name.replace(" ", "_")}' + profile_name = f"SharePoint_{_site_name.replace(' ', '_')}_{_library_name.replace(' ', '_')}" sync_dir = os.path.expanduser(f"~/{profile_name}") config_path = os.path.expanduser(f"~/.config/onedrive/accounts/{profile_name}/config") @@ -517,6 +550,16 @@ class wizardPage_create_shared_library(QWizardPage): self.pushButton_create_profile.setDisabled(True) self.comboBox_sharepoint_library_list.setDisabled(True) + # Emit signal to add the profile to profile settings window and main window + logging.info(f"[WIZARD] Emitting add_profile_signal for {profile_name}") + wizard_instance = self.wizard() + logging.info(f"[WIZARD] wizard_instance: {wizard_instance}") + if wizard_instance: + wizard_instance.add_profile_signal.emit(profile_name) + logging.info(f"[WIZARD] Signal emitted successfully") + else: + logging.error(f"[WIZARD] Failed to get wizard instance!") + self.completeChanged.emit() @@ -658,7 +701,15 @@ class wizardPage_create(QWizardPage): save_global_config(global_config) # Add Setting page widget for new profile - self.add_profile_signal.emit(profile_name) + # Emit signal to add the profile to profile settings window and main window + logging.info(f"[WIZARD] Emitting add_profile_signal for {profile_name}") + wizard_instance = self.wizard() + logging.info(f"[WIZARD] wizard_instance: {wizard_instance}") + if wizard_instance: + wizard_instance.add_profile_signal.emit(profile_name) + logging.info(f"[WIZARD] Signal emitted successfully") + else: + logging.error(f"[WIZARD] Failed to get wizard instance!") # self.profile_settings_window.listWidget_profiles.addItem(profile_name) # # self.setting_page = ProfileSettingsPage(profile_name) # self.profile_settings_window.stackedLayout.addWidget(self.setting_page) @@ -684,7 +735,16 @@ class wizardPage_create(QWizardPage): self.lineEdit_new_profile_name.setDisabled(True) self.lineEdit_sync_dir.setDisabled(True) self.pushButton_browse.setDisabled(True) + + # Force UI update immediately + from PySide6.QtCore import QCoreApplication + + QCoreApplication.processEvents() + + # Signal that this page is complete so the wizard can move to the next page self.completeChanged.emit() + # Force the wizard to advance to the next page + self.wizard().next() class wizardPage_import(QWizardPage): @@ -837,6 +897,15 @@ class wizardPage_import(QWizardPage): # Automatically save global config to prevent loss if user does not press 'Save' button. save_global_config(global_config) + # Emit signal to add the profile to main window and profile settings window + logging.info(f"[WIZARD] Emitting add_profile_signal for imported profile {profile_name}") + wizard_instance = self.wizard() + if wizard_instance: + wizard_instance.add_profile_signal.emit(profile_name) + logging.info(f"[WIZARD] Signal emitted successfully for imported profile") + else: + logging.error(f"[WIZARD] Failed to get wizard instance!") + # Start checking for unsaved changes again after new profile has being created. # self.profile_settings_window.start_unsaved_changes_timer() @@ -867,4 +936,5 @@ class wizardPage_finish(QWizardPage): self.setLayout(layout) +# Create a single instance of the wizard setup_wizard = SetupWizard()