From 0bc39ada3dfe16eb1a71e78b708bb1990cdbf9de Mon Sep 17 00:00:00 2001 From: Lea Laux Date: Tue, 22 Dec 2020 15:44:16 +0100 Subject: [PATCH] Open previous files/tabs after restart Remember the open files in the editor tabs and reload them after restarting the application. This feature is configurable and described in issue #38 --- pygadmin/configurator.py | 7 +++ pygadmin/connectionstore.py | 2 +- pygadmin/file_manager.py | 83 ++++++++++++++++++++++++++ pygadmin/widgets/editor.py | 102 ++++++++++++++++++++++++-------- pygadmin/widgets/main_window.py | 10 +++- pygadmin/widgets/mdi_area.py | 24 ++++++++ 6 files changed, 201 insertions(+), 27 deletions(-) create mode 100644 pygadmin/file_manager.py diff --git a/pygadmin/configurator.py b/pygadmin/configurator.py index 6bba575..3b72b8e 100644 --- a/pygadmin/configurator.py +++ b/pygadmin/configurator.py @@ -102,6 +102,13 @@ class AppConfigurator: # Save the data in the configuration dictionary. self.save_configuration_data() + try: + self.configuration_dictionary["open_previous_files"] + + except KeyError: + self.configuration_dictionary["open_previous_files"] = True + self.save_configuration_data() + except Exception as file_error: logging.error("The file {} cannot be opened and app configuration parameter cannot be loaded with the " "following error: {}".format(self.yaml_app_configuration_file, file_error), exc_info=True) diff --git a/pygadmin/connectionstore.py b/pygadmin/connectionstore.py index 9453c83..725a236 100644 --- a/pygadmin/connectionstore.py +++ b/pygadmin/connectionstore.py @@ -102,7 +102,7 @@ class ConnectionStore: except Exception as file_error: logging.error("The file {} cannot be opened and database connection parameter cannot be saved with the " - "following error: {}".format(self.yaml_connection_parameters_file, file_error)) + "following error: {}".format(self.yaml_connection_parameters_file, file_error), exc_info=True) def check_parameter_for_duplicate(self, connection_parameter_dictionary_for_check): """ diff --git a/pygadmin/file_manager.py b/pygadmin/file_manager.py new file mode 100644 index 0000000..9c10a72 --- /dev/null +++ b/pygadmin/file_manager.py @@ -0,0 +1,83 @@ +import copy +import logging +import os +import yaml + +from pygadmin.configurator import global_app_configurator + + +class FileManager: + def __init__(self): + self.open_files = [] + # Define a path for the configuration files. + configuration_path = os.path.join(os.path.expanduser("~"), '.pygadmin') + + # If the path for the configuration files does not exist, create it. + if not os.path.exists(configuration_path): + os.mkdir(configuration_path) + + # Define a yaml file, which stores the current open files in the editor widgets, so it is independent from the + # user's operating system. + self.open_files_file = os.path.join(configuration_path, "open_files.yaml") + + # Check for the existence of the file. + if not os.path.exists(self.open_files_file): + # Create the file as an empty file for writing in it. + open(self.open_files_file, "a") + + if global_app_configurator.get_single_configuration("open_previous_files") is False: + self.delete_all_files() + self.commit_current_files_to_yaml() + + def add_new_file(self, file_name): + self.open_files.append(file_name) + + return True + + def delete_file(self, file_name): + if file_name not in self.open_files: + return False + + self.open_files.remove(file_name) + + return True + + def delete_all_files(self): + self.open_files = [] + + def commit_current_files_to_yaml(self): + try: + with open(self.open_files_file, "w") as file_data: + yaml.safe_dump(self.open_files, file_data) + + return True + + except Exception as file_error: + logging.error("The file {} cannot be opened and the open files in the editor cannot be saved with the " + "following error: {}".format(self.open_files_file, file_error), exc_info=True) + + return False + + def load_open_file_list(self): + # Use a try statement in case of a broken .yaml file. + try: + # Use the read mode for getting the content of the file. + with open(self.open_files_file, "r") as file_data: + # Use the function for a safe load, because the file can be edited manually. + self.open_files = yaml.safe_load(file_data) + + # If the list with the open files out of the .yaml file is empty, it is None after a load. But for + # preventing further errors, because a list is expected, this if branch is used. + if self.open_files is None: + # Define the list of open files as an empty list. + self.open_files = [] + + # Return a copy of the list, so there is no manipulation from the outside. + return copy.copy(self.open_files) + + except Exception as file_error: + logging.error("The file {} cannot be opened previous open files cannot be loaded with the following " + "error: {}".format(self.open_files_file, file_error), exc_info=True) + + +global_file_manager = FileManager() diff --git a/pygadmin/widgets/editor.py b/pygadmin/widgets/editor.py index 2b0952f..65d6ad3 100644 --- a/pygadmin/widgets/editor.py +++ b/pygadmin/widgets/editor.py @@ -2,6 +2,7 @@ import logging import re import datetime +from PyQt5 import QtGui from PyQt5.Qsci import QsciScintilla from PyQt5.QtCore import pyqtSlot, pyqtSignal, QEvent from PyQt5.QtGui import QKeySequence @@ -16,6 +17,7 @@ from pygadmin.database_query_executor import DatabaseQueryExecutor from pygadmin.widgets.search_replace_widget import SearchReplaceWidget from pygadmin.widgets.search_replace_parent import SearchReplaceParent from pygadmin.command_history_store import global_command_history_store +from pygadmin.file_manager import global_file_manager class MetaEditor(type(QWidget), type(SearchReplaceParent)): @@ -388,13 +390,17 @@ class EditorWidget(QWidget, SearchReplaceParent, metaclass=MetaEditor): self.stop_query_button.setEnabled(activation) self.stop_query_shortcut.setEnabled(activation) - def save_current_statement_in_file(self): + def save_current_statement_in_file(self, previous_file_name=None): """ Save the current text/statement of the lexer as query editor in for further usage. The class-wide variable for the corresponding file is used as directory with file. If this variable contains its initialized value None, use the function for opening a file dialog. """ + if previous_file_name is None and global_app_configurator.get_single_configuration("open_previous_files") is\ + True: + previous_file_name = self.corresponding_saved_file + # Check if the class-wide variable for the corresponding file is None. if self.corresponding_saved_file is None: # Open a file dialog and if the result is False, the process has been aborted. @@ -402,14 +408,33 @@ class EditorWidget(QWidget, SearchReplaceParent, metaclass=MetaEditor): # End the function with a return. return - # Open the file in the write mode, so every content is also overwritten. - with open(self.corresponding_saved_file, "w") as file_to_save: - # Define the current text of the query input editor as current text. - current_text = self.query_input_editor.text() - # Write the current text of the lexer as SQL editor in the file. - file_to_save.write(current_text) - # Save the current text in the class-wide current editor text. - self.current_editor_text = current_text + try: + # Open the file in the write mode, so every content is also overwritten. + with open(self.corresponding_saved_file, "w") as file_to_save: + # Define the current text of the query input editor as current text. + current_text = self.query_input_editor.text() + # Write the current text of the lexer as SQL editor in the file. + file_to_save.write(current_text) + + except Exception as file_error: + error_text = "The file {} cannot be written with the error: {}".format(self.corresponding_saved_file, + file_error) + + QMessageBox.critical(self, "File Reading Error", error_text) + logging.critical(error_text, exc_info=True) + + self.corresponding_saved_file = previous_file_name + + return + + if self.corresponding_saved_file != previous_file_name \ + and global_app_configurator.get_single_configuration("open_previous_files") is True: + global_file_manager.delete_file(previous_file_name) + global_file_manager.add_new_file(self.corresponding_saved_file) + global_file_manager.commit_current_files_to_yaml() + + # Save the current text in the class-wide current editor text. + self.current_editor_text = current_text # Update the current window title. self.update_window_title_and_description() @@ -455,20 +480,46 @@ class EditorWidget(QWidget, SearchReplaceParent, metaclass=MetaEditor): # Get the file name out of the tuple. file_name = file_name_and_type[0] + if file_name is False: + logging.info("The current file opening process was aborted by the user, so the content of this file is not " + "loaded.") + + return False + + return self.load_statement_with_file_name(file_name) + + def load_statement_with_file_name(self, file_name): # Check for the success in form of an existing file and not an empty string. if file_name != "": + try: + # Open the file in reading mode. + with open(file_name, "r") as file_to_load: + # Read the whole given file and save its text. + file_text = file_to_load.read() + + except Exception as file_error: + error_text = "The file {} cannot be loaded with the error: {}".format(file_name, file_error) + QMessageBox.critical(self, "File Reading Error", error_text) + logging.critical(error_text, exc_info=True) + + return False + + if global_app_configurator.get_single_configuration("open_previous_files") is True and \ + self.corresponding_saved_file is not None: + global_file_manager.delete_file(self.corresponding_saved_file) + # Save the name of the file in the class variable for the corresponding file. self.corresponding_saved_file = file_name - # Open the file in reading mode. - with open(self.corresponding_saved_file, "r") as file_to_load: - # Read the whole given file and save its text. - file_text = file_to_load.read() - # Show the content of the file as text in the lexer as SQL query editor. - self.query_input_editor.setText(file_text) - # Save the text of the file in the class-wide variable for the current text to check for changes and - # get the current state of saved/unsaved. - self.current_editor_text = file_text + if global_app_configurator.get_single_configuration("open_previous_files") is True: + global_file_manager.add_new_file(self.corresponding_saved_file) + global_file_manager.commit_current_files_to_yaml() + + # Show the content of the file as text in the lexer as SQL query editor. + self.query_input_editor.setText(file_text) + # Save the text of the file in the class-wide variable for the current text to check for changes and get the + # current state of saved/unsaved. + self.current_editor_text = file_text # Update the window title self.update_window_title_and_description() @@ -476,13 +527,6 @@ class EditorWidget(QWidget, SearchReplaceParent, metaclass=MetaEditor): # Report the success with a return value. return True - else: - logging.info("The current file opening process was aborted by the user, so the content of this file is not " - "loaded.") - - # Report the failure with a return value. - return False - def load_file_with_potential_overwrite_in_editor(self): """ Load an existing file in the editor widget. This function is a wrapper for load_statement_out_of_file with more @@ -989,3 +1033,11 @@ class EditorWidget(QWidget, SearchReplaceParent, metaclass=MetaEditor): # Write a newline at the end of a data row. file_to_save.write("\n") + def closeEvent(self, a0: QtGui.QCloseEvent) -> None: + if self.corresponding_saved_file is not None \ + and global_app_configurator.get_single_configuration("open_previous_files"): + global_file_manager.delete_file(self.corresponding_saved_file) + global_file_manager.commit_current_files_to_yaml() + + self.close() + diff --git a/pygadmin/widgets/main_window.py b/pygadmin/widgets/main_window.py index 475abe6..5cf738c 100644 --- a/pygadmin/widgets/main_window.py +++ b/pygadmin/widgets/main_window.py @@ -360,6 +360,14 @@ class MainWindow(QMainWindow): # Check, if the current editor widget exists. if current_editor_widget is not None: + if global_app_configurator.get_single_configuration("open_previous_files"): + # Get the current corresponding file name for the usage as previous file name, so an overwrite in the + # editor for the global file manager can be realized. + current_corresponding_file = current_editor_widget.corresponding_saved_file + + else: + current_corresponding_file = None + # Check the parameter for save_as. If the parameter is True, the if clause gets to the point for a new # file dialog. If the result of this file dialog is False, end the function with a return. In this case, # the process has been aborted. @@ -368,7 +376,7 @@ class MainWindow(QMainWindow): return # Save the current statement and text in the query input editor with the function of the editor widget. - current_editor_widget.save_current_statement_in_file() + current_editor_widget.save_current_statement_in_file(current_corresponding_file) # Define an else branch for error handling with a non existing current editor widget. else: diff --git a/pygadmin/widgets/mdi_area.py b/pygadmin/widgets/mdi_area.py index c484114..4782fbe 100644 --- a/pygadmin/widgets/mdi_area.py +++ b/pygadmin/widgets/mdi_area.py @@ -5,8 +5,10 @@ from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtWidgets import QMdiArea from PyQt5.QtCore import pyqtSlot, pyqtSignal +from pygadmin.configurator import global_app_configurator from pygadmin.widgets.editor import EditorWidget from pygadmin.connectionfactory import global_connection_factory +from pygadmin.file_manager import global_file_manager class MdiArea(QMdiArea): @@ -45,6 +47,28 @@ class MdiArea(QMdiArea): # Use the empty icon as window icon, so the pygadmin logo is not in the window title bar of every editor widget. self.setWindowIcon(icon) + self.init_open_files() + + def init_open_files(self): + """ + Get the list of previous opened files of the editor out of the file manager and load them in new editor widgets. + """ + + if global_app_configurator.get_single_configuration("open_previous_files") is False: + return + + # Get the files for opening. + files_to_open = global_file_manager.load_open_file_list() + # Delete all current files in the global file manager. They will be added after their load in the editor widget. + global_file_manager.delete_all_files() + + # Add a widget for every file. + for file in files_to_open: + # Get a new editor. + new_editor = self.generate_editor_tab() + # Load the file in the editor. + new_editor.load_statement_with_file_name(file) + def generate_editor_tab(self): """ Generate a new editor widget as sub window of the MdiArea and connect a signal for a change table/view for an