Skip to content

Commit

Permalink
Implemented a light/dark theme for GynTree. Expanded testing suite an…
Browse files Browse the repository at this point in the history
…d enhanced run_tests script. Other various fixes and modifications including new logo, new button looks, etc.
  • Loading branch information
dsj7419 committed Sep 23, 2024
1 parent 057ec5f commit e41fe1a
Show file tree
Hide file tree
Showing 54 changed files with 3,009 additions and 995 deletions.
Binary file removed assets/images/GynTree_logo 64X64.ico
Binary file not shown.
Binary file added assets/images/GynTree_logo.ico
Binary file not shown.
Binary file modified assets/images/GynTree_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/moon_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/sun_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
altgraph==0.17.4
attrs==24.2.0
colorama==0.4.6
coverage==7.6.1
execnet==2.1.1
iniconfig==2.0.0
Jinja2==3.1.4
Expand All @@ -18,6 +19,7 @@ PyQt5==5.15.11
PyQt5-Qt5==5.15.2
PyQt5_sip==12.15.0
pytest==8.3.3
pytest-cov==5.0.0
pytest-html==4.1.1
pytest-metadata==3.1.1
pytest-mock==3.14.0
Expand All @@ -27,3 +29,5 @@ referencing==0.35.1
rpds-py==0.20.0
rsa==4.9
setuptools==75.1.0
tdqm==0.0.1
tqdm==4.66.5
19 changes: 9 additions & 10 deletions scripts/build_executable.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,37 @@
import sys
from PyQt5.QtCore import QLibraryInfo

# Get the absolute path to the project root
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))

# Get PyQt5 directory
pyqt_dir = QLibraryInfo.location(QLibraryInfo.BinariesPath)

# List of QT DLLs we need
qt_dlls = ['Qt5Core.dll', 'Qt5Gui.dll', 'Qt5Widgets.dll']

# Prepare the command line arguments for PyInstaller
pyinstaller_args = [
'--name=GynTree',
'--windowed',
'--onefile',
f'--icon={os.path.join(project_root, "assets", "images", "GynTree_logo 64X64.ico")}',
f'--add-data={os.path.join(project_root, "assets")};assets',
'--clean',
f'--icon={os.path.join(project_root, "assets", "images", "GynTree_logo.ico")}', # For EXE icon
f'--add-data={os.path.join(project_root, "assets", "images", "GynTree_logo.ico")};assets/images', # For use in app
f'--add-data={os.path.join(project_root, "assets", "images", "file_icon.png")};assets/images',
f'--add-data={os.path.join(project_root, "assets", "images", "folder_icon.png")};assets/images',
f'--add-data={os.path.join(project_root, "assets", "images", "GynTree_logo.png")};assets/images',
f'--add-data={os.path.join(project_root, "src", "styles", "light_theme.qss")};styles',
f'--add-data={os.path.join(project_root, "src", "styles", "dark_theme.qss")};styles',
'--hidden-import=PyQt5.sip',
'--hidden-import=PyQt5.QtCore',
'--hidden-import=PyQt5.QtGui',
'--hidden-import=PyQt5.QtWidgets',
]

# Add Qt DLLs
for dll in qt_dlls:
dll_path = os.path.join(pyqt_dir, dll)
if os.path.exists(dll_path):
pyinstaller_args.append(f'--add-binary={dll_path};PyQt5/Qt/bin/')
else:
print(f"Warning: {dll} not found in {pyqt_dir}")

# Add the main script
pyinstaller_args.append(os.path.join(project_root, 'src', 'App.py'))

# Run PyInstaller
PyInstaller.__main__.run(pyinstaller_args)
PyInstaller.__main__.run(pyinstaller_args)
26 changes: 13 additions & 13 deletions src/App.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
"""
GynTree: Main entry point for the GynTree application. Initializes core components and starts the user interface.
The app module orchestrates the overall flow of the application, connecting various components and services.
"""

import sys
import logging
from PyQt5.QtWidgets import QApplication
from controllers.AppController import AppController
from utilities.error_handler import ErrorHandler
from utilities.theme_manager import ThemeManager

logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

def main():
# Set global exception handling
sys.excepthook = ErrorHandler.global_exception_handler

app = QApplication(sys.argv)
controller = AppController()

# Connect cleanup method to be called on application quit
theme_manager = ThemeManager.getInstance()

try:
controller = AppController()
except Exception as e:
logger.critical(f"Failed to initialize AppController: {str(e)}")
sys.exit(1)

app.aboutToQuit.connect(controller.cleanup)

# Start the application
controller.run()

# Start the event loop
theme_manager.apply_theme_to_all_windows(app)

sys.exit(app.exec_())

if __name__ == "__main__":
try:
main()
except Exception as e:
logger.critical(f"Fatal error in main: {str(e)}", exc_info=True)
sys.exit(1)
sys.exit(1)
1 change: 0 additions & 1 deletion src/components/TreeExporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ def export_as_image(self):
temp_tree.setColumnWidth(0, name_column_width + 20)
temp_tree.setColumnWidth(1, type_column_width)

# Calculate the full size of the tree
total_width = name_column_width + type_column_width + 40
total_height = 0
iterator = QTreeWidgetItemIterator(temp_tree, QTreeWidgetItemIterator.All)
Expand Down
30 changes: 20 additions & 10 deletions src/components/UI/AutoExcludeUI.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QLabel, QPushButton, QScrollArea, QWidget, QTreeWidget, QTreeWidgetItem, QMessageBox, QHeaderView, QHBoxLayout
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QLabel, QPushButton, QScrollArea, QWidget,
QTreeWidget, QTreeWidgetItem, QMessageBox, QHeaderView, QHBoxLayout)
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QIcon, QFont
import os
from utilities.resource_path import get_resource_path
from utilities.theme_manager import ThemeManager
import logging

logger = logging.getLogger(__name__)

class AutoExcludeUI(QMainWindow):
def __init__(self, auto_exclude_manager, settings_manager, formatted_recommendations, project_context):
Expand All @@ -11,18 +16,17 @@ def __init__(self, auto_exclude_manager, settings_manager, formatted_recommendat
self.settings_manager = settings_manager
self.formatted_recommendations = formatted_recommendations
self.project_context = project_context
self.theme_manager = ThemeManager.getInstance()

self.folder_icon = QIcon(get_resource_path("assets/images/folder_icon.png"))
self.file_icon = QIcon(get_resource_path("assets/images/file_icon.png"))

self.setWindowTitle('Auto-Exclude Recommendations')
self.setWindowIcon(QIcon(get_resource_path('assets/images/gyntree_logo 64x64.ico')))
self.setStyleSheet("""
QMainWindow { background-color: #f0f0f0; }
QLabel { font-size: 20px; color: #333; margin-bottom: 10px; }
QPushButton { background-color: #4caf50; color: white; padding: 8px 16px; font-size: 14px; margin: 4px 2px; border-radius: 6px; }
QPushButton:hover { background-color: #45a049; }
QTreeWidget { font-size: 14px; color: #333; background-color: #fff; border: 1px solid #ddd; }
""")
self.setWindowIcon(QIcon(get_resource_path('assets/images/GynTree_logo.ico')))

self.init_ui()

self.theme_manager.themeChanged.connect(self.apply_theme)

def init_ui(self):
central_widget = QWidget()
Expand All @@ -33,6 +37,7 @@ def init_ui(self):
header_layout = QHBoxLayout()
title_label = QLabel('Auto-Exclude Recommendations', font=QFont('Arial', 16, QFont.Bold))
header_layout.addWidget(title_label)

collapse_btn = QPushButton('Collapse All')
expand_btn = QPushButton('Expand All')
header_layout.addWidget(collapse_btn)
Expand Down Expand Up @@ -60,6 +65,8 @@ def init_ui(self):
self.setCentralWidget(central_widget)
self.setGeometry(300, 150, 800, 600)

self.apply_theme()

def populate_tree(self):
"""Populates the tree with merged exclusions from both AutoExcludeManager and project folder."""
self.tree_widget.clear()
Expand Down Expand Up @@ -106,5 +113,8 @@ def update_recommendations(self, formatted_recommendations):
self.formatted_recommendations = formatted_recommendations
self.populate_tree()

def apply_theme(self):
self.theme_manager.apply_theme(self)

def closeEvent(self, event):
super().closeEvent(event)
super().closeEvent(event)
114 changes: 76 additions & 38 deletions src/components/UI/DashboardUI.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import os
from PyQt5.QtWidgets import QMainWindow, QPushButton, QVBoxLayout, QWidget, QLabel, QStatusBar, QHBoxLayout
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QWidget, QLabel,
QStatusBar, QHBoxLayout, QPushButton, QMessageBox)
from PyQt5.QtGui import QIcon, QFont, QPixmap
from PyQt5.QtCore import Qt
from components.UI.ProjectUI import ProjectUI
from components.UI.AutoExcludeUI import AutoExcludeUI
from components.UI.ResultUI import ResultUI
from components.UI.DirectoryTreeUI import DirectoryTreeUI
from components.UI.ExclusionsManagerUI import ExclusionsManagerUI
from components.UI.animated_toggle import AnimatedToggle
from utilities.resource_path import get_resource_path
from utilities.theme_manager import ThemeManager
import logging

logger = logging.getLogger(__name__)
Expand All @@ -16,29 +19,22 @@ class DashboardUI(QMainWindow):
def __init__(self, controller):
super().__init__()
self.controller = controller
self.theme_manager = ThemeManager.getInstance()
self.project_ui = None
self.result_ui = None
self.auto_exclude_ui = None
self.exclusions_ui = None
self.directory_tree_ui = None
self.theme_toggle = None
self.initUI()

# Connect controller signals
self.controller.project_created.connect(self.on_project_created)
self.controller.project_loaded.connect(self.on_project_loaded)

def initUI(self):
self.setWindowTitle('GynTree Dashboard')
self.setWindowIcon(QIcon(get_resource_path('assets/images/gyntree_logo 64x64.ico')))
self.setStyleSheet("""
QMainWindow { background-color: #f0f0f0; }
QLabel { color: #333; }
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
padding: 15px 32px;
text-align: center;
text-decoration: none;
font-size: 16px;
margin: 4px 2px;
border-radius: 8px;
}
QPushButton:hover { background-color: #45a049; }
QStatusBar { background-color: #333; color: white; }
""")
self.setWindowIcon(QIcon(get_resource_path('assets/images/GynTree_logo.ico')))

central_widget = QWidget(self)
self.setCentralWidget(central_widget)
Expand All @@ -50,7 +46,7 @@ def initUI(self):
logo_path = get_resource_path('assets/images/gyntree_logo.png')
if os.path.exists(logo_path):
logo_pixmap = QPixmap(logo_path)
logo_label.setPixmap(logo_pixmap.scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation))
logo_label.setPixmap(logo_pixmap.scaled(128, 128, Qt.KeepAspectRatio, Qt.SmoothTransformation))
else:
logger.warning(f"Logo file not found at {logo_path}")

Expand All @@ -63,13 +59,26 @@ def initUI(self):
header_layout.setAlignment(Qt.AlignCenter)
main_layout.addLayout(header_layout)

# Animated light/dark theme toggle
theme_toggle_layout = QHBoxLayout()
self.theme_toggle = AnimatedToggle(
checked_color="#FFB000",
pulse_checked_color="#44FFB000"
)
self.theme_toggle.setFixedSize(self.theme_toggle.sizeHint())
self.theme_toggle.setChecked(self.theme_manager.get_current_theme() == 'dark')
self.theme_toggle.stateChanged.connect(self.toggle_theme)
theme_toggle_layout.addWidget(self.theme_toggle)
theme_toggle_layout.setAlignment(Qt.AlignRight)
main_layout.addLayout(theme_toggle_layout)

self.create_project_btn = self.create_styled_button('Create Project')
self.load_project_btn = self.create_styled_button('Load Project')
self.manage_exclusions_btn = self.create_styled_button('Manage Exclusions')
self.analyze_directory_btn = self.create_styled_button('Analyze Directory')
self.view_directory_tree_btn = self.create_styled_button('View Directory Tree')

for btn in [self.create_project_btn, self.load_project_btn, self.manage_exclusions_btn,
for btn in [self.create_project_btn, self.load_project_btn, self.manage_exclusions_btn,
self.analyze_directory_btn, self.view_directory_tree_btn]:
main_layout.addWidget(btn)

Expand All @@ -85,11 +94,16 @@ def initUI(self):

self.setGeometry(300, 300, 800, 600)

self.theme_manager.apply_theme(self)

def create_styled_button(self, text):
btn = QPushButton(text)
btn.setFont(QFont('Arial', 14))
return btn

def toggle_theme(self):
self.controller.toggle_theme()

def show_dashboard(self):
self.show()

Expand All @@ -100,29 +114,54 @@ def show_project_ui(self):
self.project_ui.show()
return self.project_ui

def update_project_info(self, project):
self.setWindowTitle(f"GynTree - {project.name}")
self.status_bar.showMessage(f"Current project: {project.name}, Start directory: {project.start_directory}")
def on_project_created(self, project):
logger.info(f"Project created: {project.name}")
self.update_project_info(project)
self.enable_project_actions()

def on_project_loaded(self, project):
logger.info(f"Project loaded: {project.name}")
self.update_project_info(project)
self.enable_project_actions()

def enable_project_actions(self):
self.manage_exclusions_btn.setEnabled(True)
self.analyze_directory_btn.setEnabled(True)
self.view_directory_tree_btn.setEnabled(True)

def show_auto_exclude_ui(self, auto_exclude_manager, settings_manager, formatted_recommendations, project_context):
auto_exclude_ui = AutoExcludeUI(auto_exclude_manager, settings_manager, formatted_recommendations, project_context)
auto_exclude_ui.show()
return auto_exclude_ui
if not self.auto_exclude_ui:
self.auto_exclude_ui = AutoExcludeUI(auto_exclude_manager, settings_manager, formatted_recommendations, project_context)
self.auto_exclude_ui.show()

def show_result(self, directory_analyzer):
result_ui = ResultUI(directory_analyzer)
result_ui.show()
return result_ui
if self.controller.project_controller.project_context:
self.result_ui = ResultUI(self.controller, self.theme_manager, directory_analyzer)
self.result_ui.show()
return self.result_ui
else:
return None

def manage_exclusions(self, settings_manager):
exclusions_ui = ExclusionsManagerUI(settings_manager)
exclusions_ui.show()
return exclusions_ui
if self.controller.project_controller.project_context:
self.exclusions_ui = ExclusionsManagerUI(self.controller, self.theme_manager, settings_manager)
self.exclusions_ui.show()
return self.exclusions_ui
else:
QMessageBox.warning(self, "No Project", "Please load or create a project before managing exclusions.")
return None

def view_directory_tree_ui(self, result):
if not self.directory_tree_ui:
self.directory_tree_ui = DirectoryTreeUI(self.controller, self.theme_manager)
self.directory_tree_ui.update_tree(result)
self.directory_tree_ui.show()


def view_directory_tree(self, result):
tree_ui = DirectoryTreeUI(result)
tree_ui.show()
return tree_ui

def update_project_info(self, project):
self.setWindowTitle(f"GynTree - {project.name}")
self.status_bar.showMessage(f"Current project: {project.name}, Start directory: {project.start_directory}")

def clear_directory_tree(self):
if hasattr(self, 'directory_tree_view'):
Expand All @@ -140,5 +179,4 @@ def clear_exclusions(self):
logger.debug("Exclusions list cleared")

def show_error_message(self, title, message):
from PyQt5.QtWidgets import QMessageBox
QMessageBox.critical(self, title, message)
Loading

0 comments on commit e41fe1a

Please sign in to comment.