From 9bfc2916be7d5276a163b6a02eb92a6323c3b85a Mon Sep 17 00:00:00 2001 From: Abhishek-Soni-25 Date: Mon, 7 Jul 2025 13:25:32 +0530 Subject: [PATCH 1/3] Resolved issue #209 --- src/frontEnd/ProjectExplorer.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/frontEnd/ProjectExplorer.py b/src/frontEnd/ProjectExplorer.py index 997723787..1942538fa 100755 --- a/src/frontEnd/ProjectExplorer.py +++ b/src/frontEnd/ProjectExplorer.py @@ -28,6 +28,7 @@ def __init__(self): self.obj_validation = Validation() self.treewidget = QtWidgets.QTreeWidget() self.window = QtWidgets.QVBoxLayout() + self.fs_watcher = QtCore.QFileSystemWatcher() header = QtWidgets.QTreeWidgetItem(["Projects", "path"]) self.treewidget.setHeaderItem(header) self.treewidget.setColumnHidden(1, True) @@ -68,13 +69,22 @@ def __init__(self): QtWidgets.QTreeWidgetItem( parentnode, [files, os.path.join(parents, files)] ) + self.fs_watcher.addPath(parents) self.window.addWidget(self.treewidget) + self.fs_watcher.directoryChanged.connect(self.handleDirectoryChanged) self.treewidget.expanded.connect(self.refreshInstant) self.treewidget.doubleClicked.connect(self.openProject) self.treewidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.treewidget.customContextMenuRequested.connect(self.openMenu) self.setLayout(self.window) self.show() + + def handleDirectoryChanged(self, path): + for i in range(self.treewidget.topLevelItemCount()): + item = self.treewidget.topLevelItem(i) + if item.text(1) == path and item.isExpanded(): + index = self.treewidget.indexFromItem(item) + self.refreshProject(indexItem=index) def refreshInstant(self): for i in range(self.treewidget.topLevelItemCount()): From ead7fdd83d2095cad778bff782642d256f96ff6f Mon Sep 17 00:00:00 2001 From: Abhishek-Soni-25 Date: Tue, 8 Jul 2025 14:25:24 +0530 Subject: [PATCH 2/3] Fixes project reopening issue --- src/configuration/Appconfig.py | 23 +++++++++++++++++++++++ src/frontEnd/Application.py | 13 +++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/configuration/Appconfig.py b/src/configuration/Appconfig.py index a2f18ab61..a7f4faff6 100644 --- a/src/configuration/Appconfig.py +++ b/src/configuration/Appconfig.py @@ -110,3 +110,26 @@ def print_warning(self, warning): def print_error(self, error): self.noteArea['Note'].append('[ERROR]: ' + error) + + def save_current_project(self): + try: + path = os.path.join(self.user_home, ".esim", "last_project.json") + with open(path, "w") as f: + json.dump(self.current_project, f) + except Exception as e: + print("Failed to save current project:", str(e)) + + def load_last_project(self): + try: + path = os.path.join(self.user_home, ".esim", "last_project.json") + with open(path, "r") as f: + data = json.load(f) + project_path = data.get("ProjectName", None) + if project_path and os.path.exists(project_path): + self.current_project["ProjectName"] = project_path + return project_path + else: + print("Project path does not exist: ", project_path) + except Exception as e: + print("Error: ", str(e)) + return None \ No newline at end of file diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 73c626013..301a60743 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -349,6 +349,8 @@ def new_project(self): self.obj_Mainview.obj_projectExplorer.addTreeNode( directory, filelist ) + self.obj_appconfig.current_project["ProjectName"] = directory + self.obj_appconfig.save_current_project() updated = True if not updated: @@ -370,6 +372,8 @@ def open_project(self): directory, filelist = self.project.body() self.obj_Mainview.obj_projectExplorer.addTreeNode( directory, filelist) + self.obj_appconfig.current_project["ProjectName"] = directory + self.obj_appconfig.save_current_project() except BaseException: pass @@ -398,6 +402,7 @@ def close_project(self): pass self.obj_Mainview.obj_dockarea.closeDock() self.obj_appconfig.current_project['ProjectName'] = None + self.obj_appconfig.save_current_project() self.systemTrayIcon.showMessage( 'Close', 'Current project ' + os.path.basename(current_project) + ' is Closed.' @@ -871,6 +876,14 @@ def main(args): app.setApplicationName("eSim") appView = Application() + last_project_path = appView.obj_appconfig.load_last_project() + if last_project_path: + try: + open_proj = OpenProjectInfo() + directory, filelist = open_proj.body(last_project_path) + appView.obj_Mainview.obj_projectExplorer.addTreeNode(directory, filelist) + except Exception as e: + print("Could not restore last project:", str(e)) appView.hide() splash_pix = QtGui.QPixmap(init_path + 'images/splash_screen_esim.png') From 0a05defe22572c391fa02d8363656583003464c6 Mon Sep 17 00:00:00 2001 From: Abhishek-Soni-25 Date: Wed, 9 Jul 2025 13:37:51 +0530 Subject: [PATCH 3/3] Added Timeline Feature --- src/frontEnd/Application.py | 17 ++- src/frontEnd/ProjectExplorer.py | 34 ++++++ src/frontEnd/TimeExplorer.py | 188 ++++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 src/frontEnd/TimeExplorer.py diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 301a60743..1c420597a 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -34,6 +34,7 @@ from PyQt5.Qt import QSize from configuration.Appconfig import Appconfig from frontEnd import ProjectExplorer +from frontEnd import TimeExplorer from frontEnd import Workspace from frontEnd import DockArea from projManagement.openProject import OpenProjectInfo @@ -350,6 +351,9 @@ def new_project(self): directory, filelist ) self.obj_appconfig.current_project["ProjectName"] = directory + project_path = self.obj_appconfig.current_project["ProjectName"] + project_name = os.path.basename(project_path) + self.obj_Mainview.obj_timeExplorer.load_snapshots(project_name) self.obj_appconfig.save_current_project() updated = True @@ -373,6 +377,9 @@ def open_project(self): self.obj_Mainview.obj_projectExplorer.addTreeNode( directory, filelist) self.obj_appconfig.current_project["ProjectName"] = directory + project_path = self.obj_appconfig.current_project["ProjectName"] + project_name = os.path.basename(project_path) + self.obj_Mainview.obj_timeExplorer.load_snapshots(project_name) self.obj_appconfig.save_current_project() except BaseException: pass @@ -844,6 +851,8 @@ def __init__(self, *args): self.obj_dockarea = DockArea.DockArea() self.obj_projectExplorer = ProjectExplorer.ProjectExplorer() + self.obj_timeExplorer = TimeExplorer.TimeExplorer() + self.obj_projectExplorer.set_time_explorer(self.obj_timeExplorer) # Adding content to vertical middle Split. self.middleSplit.setOrientation(QtCore.Qt.Vertical) @@ -855,7 +864,12 @@ def __init__(self, *args): self.middleContainer.setLayout(self.middleContainerLayout) # Adding content of left split - self.leftSplit.addWidget(self.obj_projectExplorer) + self.leftPanel = QtWidgets.QVBoxLayout() + self.leftPanelWidget = QtWidgets.QWidget() + self.leftPanel.addWidget(self.obj_projectExplorer) + self.leftPanel.addWidget(self.obj_timeExplorer) + self.leftPanelWidget.setLayout(self.leftPanel) + self.leftSplit.addWidget(self.leftPanelWidget) self.leftSplit.addWidget(self.middleContainer) # Adding to main Layout @@ -884,6 +898,7 @@ def main(args): appView.obj_Mainview.obj_projectExplorer.addTreeNode(directory, filelist) except Exception as e: print("Could not restore last project:", str(e)) + appView.obj_Mainview.obj_timeExplorer.load_last_snapshots() appView.hide() splash_pix = QtGui.QPixmap(init_path + 'images/splash_screen_esim.png') diff --git a/src/frontEnd/ProjectExplorer.py b/src/frontEnd/ProjectExplorer.py index 1942538fa..5cb5791c7 100755 --- a/src/frontEnd/ProjectExplorer.py +++ b/src/frontEnd/ProjectExplorer.py @@ -1,6 +1,9 @@ from PyQt5 import QtCore, QtWidgets import os import json +import shutil +from datetime import datetime +from pathlib import Path from configuration.Appconfig import Appconfig from projManagement.Validation import Validation @@ -133,6 +136,8 @@ def openMenu(self, position): elif level == 1: openfile = menu.addAction(self.tr("Open")) openfile.triggered.connect(self.openProject) + snapshot = menu.addAction(self.tr("Snapshot")) + snapshot.triggered.connect(self.takeSnapshot) menu.exec_(self.treewidget.viewport().mapToGlobal(position)) @@ -440,3 +445,32 @@ def renameProject(self): 'contain space between them' ) msg.exec_() + + def set_time_explorer(self, time_explorer_widget): + self.time_explorer = time_explorer_widget + + def takeSnapshot(self): + index = self.treewidget.currentIndex() + file_path = str(index.sibling(index.row(), 1).data()) + file_name = os.path.basename(file_path) + + if not os.path.isfile(file_path): + QtWidgets.QMessageBox.warning(self, "Snapshot Failed", "Selected item is not a file.") + return + + project_path = self.obj_appconfig.current_project["ProjectName"] + project_name = os.path.basename(project_path) + + snapshot_dir = os.path.join(Path.home(), ".esim", "history", project_name) + os.makedirs(snapshot_dir, exist_ok=True) + + formatted_time = datetime.now().strftime("%I.%M %p %d-%m-%Y") + snapshot_name = f"{file_name}({formatted_time})" + snapshot_path = os.path.join(snapshot_dir, snapshot_name) + + shutil.copy2(file_path, snapshot_path) + + if hasattr(self, 'time_explorer'): + self.time_explorer.add_snapshot(file_name, formatted_time) + else: + print(f"Snapshot taken: {snapshot_path}") \ No newline at end of file diff --git a/src/frontEnd/TimeExplorer.py b/src/frontEnd/TimeExplorer.py new file mode 100644 index 000000000..1c38d8023 --- /dev/null +++ b/src/frontEnd/TimeExplorer.py @@ -0,0 +1,188 @@ +import os +import re +import shutil +import json +from PyQt5 import QtWidgets + +class TimeExplorer(QtWidgets.QWidget): + + if os.name == 'nt': + user_home = os.path.join('library', 'config') + else: + user_home = os.path.expanduser('~') + + current_project = {"ProjectName": None} + current_project_path = {"ProjectPath": None} + + def __init__(self): + super(TimeExplorer, self).__init__() + + self.setFixedHeight(200) + + self.layout = QtWidgets.QVBoxLayout() + self.setLayout(self.layout) + + self.treewidget = QtWidgets.QTreeWidget() + self.treewidget.setHeaderLabels(["Timeline", ""]) + self.treewidget.setColumnWidth(0, 150) + + self.treewidget.setStyleSheet(" \ + QTreeView { border-radius: 15px; border: 1px \ + solid gray; padding: 5px; width: 200px; height: 150px; }\ + ") + + self.layout.addWidget(self.treewidget) + + self.button_layout = QtWidgets.QHBoxLayout() + self.restore = QtWidgets.QPushButton("Restore") + self.clear = QtWidgets.QPushButton("Clear") + self.button_layout.addWidget(self.restore) + self.button_layout.addWidget(self.clear) + + self.layout.addLayout(self.button_layout) + + self.restore.clicked.connect(self.restore_snapshots) + self.clear.clicked.connect(self.clear_snapshots) + + def add_snapshot(self, file_name, timestamp): + item = QtWidgets.QTreeWidgetItem([file_name, timestamp]) + self.treewidget.addTopLevelItem(item) + + def load_snapshots(self, project_name): + self.treewidget.clear() + snapshot_dir = os.path.join(self.user_home, ".esim", "history", project_name) + self.current_project["ProjectName"] = project_name + if not os.path.exists(snapshot_dir): + return + pattern = re.compile(r"(.+)\((\d{1,2}\.\d{2} [APM]{2} \d{2}-\d{2}-\d{4})\)$") + for filename in os.listdir(snapshot_dir): + match = pattern.match(filename) + if match: + file_name = match.group(1) + timestamp = match.group(2) + self.add_snapshot(file_name, timestamp) + else: + print(f"Skipping unmatched snapshot file: {filename}") + + def load_last_snapshots(self): + try: + path = os.path.join(self.user_home, ".esim", "last_project.json") + with open(path, "r") as f: + data = json.load(f) + project_path = data.get("ProjectName", None) + self.current_project_path["ProjectPath"] = project_path + if project_path and os.path.exists(project_path): + project_name = os.path.basename(project_path) + self.current_project["ProjectName"] = project_name + self.load_snapshots(project_name) + except Exception as e: + print(f"Error loading last snapshots: {e}") + + def clear_snapshots(self): + selected = self.treewidget.selectedItems() + project_name = self.current_project["ProjectName"] + snapshot_dir = os.path.join(self.user_home, ".esim", "history", project_name) + + if selected: + item = selected[0] + file_name = item.text(0) + timestamp = item.text(1) + + snapshot_filename = f"{file_name}({timestamp})" + snapshot_path = os.path.join(snapshot_dir, snapshot_filename) + + confirm = QtWidgets.QMessageBox.question( + self, "Confirm Deletion", + f"Are you sure you want to delete this snapshot?\n\n{file_name}", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No + ) + + if confirm == QtWidgets.QMessageBox.Yes: + try: + os.remove(snapshot_path) + self.treewidget.takeTopLevelItem(self.treewidget.indexOfTopLevelItem(item)) + except Exception as e: + QtWidgets.QMessageBox.warning(self, "Error", f"Could not delete snapshot:\n{e}") + else: + confirm = QtWidgets.QMessageBox.question( + self, "Clear All Snapshots", + f"No file selected.\nDo you want to delete ALL snapshots for '{project_name}'?", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No + ) + if confirm == QtWidgets.QMessageBox.Yes: + deleted = 0 + for filename in os.listdir(snapshot_dir): + path = os.path.join(snapshot_dir, filename) + try: + os.remove(path) + deleted += 1 + except Exception as e: + print(f"Error deleting {filename}: {e}") + self.treewidget.clear() + QtWidgets.QMessageBox.information(self, "Deleted", f"{deleted} snapshots deleted.") + + def restore_snapshots(self): + selected_items = self.treewidget.selectedItems() + + project_name = self.current_project["ProjectName"] + snapshot_dir = os.path.join(self.user_home, ".esim", "history", project_name) + + if not os.path.exists(snapshot_dir): + QtWidgets.QMessageBox.warning(self, "No Snapshots", "No snapshots found for this project.") + return + + if selected_items: + item = selected_items[0] + file_name = item.text(0) + timestamp = item.text(1) + + snapshot_filename = f"{file_name}({timestamp})" + snapshot_path = os.path.join(snapshot_dir, snapshot_filename) + destination_path = os.path.join(self.current_project_path["ProjectPath"], file_name) + + confirm = QtWidgets.QMessageBox.question( + self, "Confirm Restore", + f"Do you want to restore this snapshot?\n\n{file_name}", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No + ) + + if confirm == QtWidgets.QMessageBox.Yes: + try: + if os.path.exists(destination_path): + os.remove(destination_path) + shutil.copy2(snapshot_path, destination_path) + if os.path.exists(snapshot_path): + os.remove(snapshot_path) + self.treewidget.takeTopLevelItem(self.treewidget.indexOfTopLevelItem(item)) + QtWidgets.QMessageBox.information(self, "Restored", f"{file_name} has been restored.") + except Exception as e: + QtWidgets.QMessageBox.warning(self, "Error", f"Could not restore:\n{e}") + + else: + confirm = QtWidgets.QMessageBox.question( + self, "Restore All Snapshots", + "No file selected.\nDo you want to restore ALL snapshot files?", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No + ) + if confirm == QtWidgets.QMessageBox.Yes: + restored = 0 + for filename in os.listdir(snapshot_dir): + match = re.match(r"(.+)\((\d{1,2}\.\d{2} [APM]{2} \d{2}-\d{2}-\d{4})\)$", filename) + if match: + file_base = match.group(1) + snapshot_path = os.path.join(snapshot_dir, filename) + destination_path = os.path.join(self.current_project_path["ProjectPath"], file_base) + + try: + if os.path.exists(destination_path): + os.remove(destination_path) + shutil.copy2(snapshot_path, destination_path) + if os.path.exists(snapshot_path): + os.remove(snapshot_path) + restored += 1 + except Exception as e: + print(f"Could not restore {file_base}: {e}") + + self.treewidget.clear() + + QtWidgets.QMessageBox.information(self, "Restored", f"{restored} snapshot(s) restored.") \ No newline at end of file