Skip to content

FOSSEE Summer Fellowship 2025: Bug Fixing and Feature Enhancement of eSim #390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/configuration/Appconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 29 additions & 1 deletion src/frontEnd/Application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -349,6 +350,11 @@ def new_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()
updated = True

if not updated:
Expand All @@ -370,6 +376,11 @@ def open_project(self):
directory, filelist = self.project.body()
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

Expand Down Expand Up @@ -398,6 +409,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.'
Expand Down Expand Up @@ -839,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)
Expand All @@ -850,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
Expand All @@ -871,6 +890,15 @@ 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.obj_Mainview.obj_timeExplorer.load_last_snapshots()
appView.hide()

splash_pix = QtGui.QPixmap(init_path + 'images/splash_screen_esim.png')
Expand Down
44 changes: 44 additions & 0 deletions src/frontEnd/ProjectExplorer.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -28,6 +31,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)
Expand Down Expand Up @@ -68,13 +72,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()):
Expand Down Expand Up @@ -123,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))

Expand Down Expand Up @@ -430,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}")
188 changes: 188 additions & 0 deletions src/frontEnd/TimeExplorer.py
Original file line number Diff line number Diff line change
@@ -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.")