From cd3c57c2253daae1bb9a967f89911e5fa13198c7 Mon Sep 17 00:00:00 2001 From: Tony XT Yan Date: Wed, 11 Dec 2024 18:29:05 +1100 Subject: [PATCH 01/67] UI tweaks and bug fixes --- HeLab.spec | 4 +- README.md | 17 +- helab/main.py | 19 +- helab/models/helabFileSystemModel.py | 10 +- helab/resources/icons.py | 5 + helab/views/folderExplorer.py | 4 +- helab/views/folderTabsWidget.py | 49 +++- helab/views/helabMainWindow.py | 343 ++++++++++++++++++++------- helab/views/settingsDialog.py | 15 +- helab/workers/statusDeepWorker.py | 4 +- requirements.txt | 1 + setup.py | 2 +- tests/test_helabFileSystemModel.py | 15 -- tests/test_main_pt.py | 2 +- tests/test_main_ut.py | 3 +- 15 files changed, 360 insertions(+), 133 deletions(-) diff --git a/HeLab.spec b/HeLab.spec index 2ffae43..6cbcf55 100644 --- a/HeLab.spec +++ b/HeLab.spec @@ -33,7 +33,7 @@ def get_git_commit_hash(): ['git', 'rev-parse', 'HEAD'], stderr=subprocess.STDOUT ).decode('utf-8').strip() - return commit_hash[:8] # Return only the first 8 characters + return commit_hash[:6].upper() # Return only the first 6 characters except Exception: return 'unknown' @@ -69,7 +69,7 @@ exe = EXE( upx=True, upx_exclude=[], runtime_tmpdir=None, - console=False, + console=True, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, diff --git a/README.md b/README.md index 51a4d4b..9cbc328 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,13 @@ here are some more random badges because they look cool +--- + +For 'downstairs' and 'upstairs' lab deployment (most stable version) use: https://github.com/HeBECANU/HeLab + +Development branch: (all sorts of bugs and features) see: https://github.com/TonyXTYan/HeLab + + --- I'm targeting Python3.12 for its new typing features. As of now this builds on python 3.11, 3.12 and 3.13. I'll try to maintain compatibility with 3.12 and 3.13 but might use newer features and drop support for lower python versions. For more information about which python version are supported, see the CI runs [here](https://github.com/TonyXTYan/HeLab/actions) @@ -69,9 +76,13 @@ I'm targeting Python3.12 for its new typing features. As of now this builds on p -Comparison with +Comparison with existing software: - https://pypi.org/project/argos/ + - wants datafiles to be in a single file (e.g. HDF5) + - doesn't allow different analysis plots, so not general enough for multiple purposes in one app. - https://github.com/adareau/HAL + - data need to be arranged in a particular format + - difficult to do multiple analysis in one app. --- @@ -95,7 +106,7 @@ Na, apparently github action is free for public repos, also much easier to setup -## Other random scripts +## Notes of other random scripts ```bash @@ -134,7 +145,7 @@ pytest pytest ``` - +/Users/tonyyan/Library/Preferences/com.anu.HeLab.plist --- diff --git a/helab/main.py b/helab/main.py index 8e2d3be..9f06606 100644 --- a/helab/main.py +++ b/helab/main.py @@ -1,25 +1,34 @@ # This Python file uses the following encoding: utf-8 +import logging +import re +import subprocess import sys from PyQt6.QtCore import Qt -from PyQt6.QtGui import QFont +from PyQt6.QtGui import QFont, QFontDatabase from PyQt6.QtWidgets import QApplication from helab.resources.icons import StatusIcons, ToolIcons, IconsInitUtil from helab.utils.loggingSetup import setup_logging -from helab.views.helabMainWindow import MainWindow - +from helab.views.helabMainWindow import MainWindow, APP_VERSION, APP_COMMIT_HASH if __name__ == "__main__": setup_logging() + logging.info(f"Starting HeLab v{APP_VERSION} ({APP_COMMIT_HASH})") - font = QFont("SF Mono") + # font_db = QFontDatabase() app = QApplication(sys.argv) app.setStyle("Fusion") - app.setFont(font) # app.setStyleSheet("QWidget { background-color: #fafafa; }") + if "SF Mono" in QFontDatabase.families(): + font = QFont("SF Mono", 12) + logging.debug("Using SF Mono font.") + else: + font = QFont("Monospace", 12) + logging.warning("SF Mono font not found. Using Monospace font instead.") + app.setFont(font) IconsInitUtil.initialise_icons() diff --git a/helab/models/helabFileSystemModel.py b/helab/models/helabFileSystemModel.py index 46e3962..96ea4fe 100644 --- a/helab/models/helabFileSystemModel.py +++ b/helab/models/helabFileSystemModel.py @@ -290,16 +290,17 @@ def start_deep_status_worker(self, root_path: str, max_depth: int = sys.maxsize) self.running_workers_deep[root_path] = worker logging.debug(f"Started StatusDeepWorker for: {root_path}") - def process_deep_status(self, directory_list: List[str]) -> None: + def process_deep_status(self, root_path: str, directory_list: List[str]) -> None: logging.debug(f"Processing {len(directory_list)} directories for status recalculation") self.directory_iterator = iter(directory_list) - self.process_next_directories() + self.process_next_directories(root_path) # After processing is done, remove all workers # Note: StatusDeepWorker does not have a direct reference here # So, we assume it's already removed from running_workers_deep when canceled - def process_next_directories(self) -> None: + + def process_next_directories(self, root_path: str) -> None: # Process a batch of directories batch_size = 100 # Adjust as needed count = 0 @@ -312,10 +313,11 @@ def process_next_directories(self) -> None: count += 1 except StopIteration: # No more directories + del self.running_workers_deep[root_path] logging.debug("Finished processing directories for status recalculation") return # Schedule next batch - QTimer.singleShot(0, self.process_next_directories) + QTimer.singleShot(0, lambda: self.process_next_directories(root_path)) # THIS IS BUGGY, MIGHT NEED REWRITE?! def context_menu_action_deep_calc_status(self, folder_info: QModelIndex) -> None: diff --git a/helab/resources/icons.py b/helab/resources/icons.py index 0df08e1..d261625 100644 --- a/helab/resources/icons.py +++ b/helab/resources/icons.py @@ -92,6 +92,8 @@ class ToolIcons: ICON_FOLDER_UP = QIcon() ICON_LEFT_COLLAPSE = QIcon() ICON_LEFT_EXPAND = QIcon() + ICON_RIGHT_COLLAPSE = QIcon() + ICON_RIGHT_EXPAND = QIcon() ICON_BOTTOM_COLLAPSE = QIcon() ICON_BOTTOM_EXPAND = QIcon() ICON_BOTTOM_INACTIVE = QIcon() @@ -118,6 +120,9 @@ def initialise_icons() -> None: ToolIcons.ICON_LEFT_COLLAPSE = tablerIcon(OutlineIcon.LAYOUT_SIDEBAR_LEFT_COLLAPSE, '#000000') ToolIcons.ICON_LEFT_EXPAND = tablerIcon(OutlineIcon.LAYOUT_SIDEBAR_LEFT_EXPAND, '#000000') + ToolIcons.ICON_RIGHT_COLLAPSE = tablerIcon(OutlineIcon.LAYOUT_SIDEBAR_RIGHT_COLLAPSE, '#000000') + ToolIcons.ICON_RIGHT_EXPAND = tablerIcon(OutlineIcon.LAYOUT_SIDEBAR_RIGHT_EXPAND, '#000000') + ToolIcons.ICON_BOTTOM_COLLAPSE = tablerIcon(OutlineIcon.LAYOUT_BOTTOMBAR_COLLAPSE, '#000000') ToolIcons.ICON_BOTTOM_EXPAND = tablerIcon(OutlineIcon.LAYOUT_BOTTOMBAR_EXPAND, '#000000') diff --git a/helab/views/folderExplorer.py b/helab/views/folderExplorer.py index ddd793c..469b884 100644 --- a/helab/views/folderExplorer.py +++ b/helab/views/folderExplorer.py @@ -371,10 +371,10 @@ def refresh(self) -> None: self.model.setRootPath(self.dir_path) self.tree.setRootIndex(self.model.index(self.view_path)) self.expand_to_path(self.target_path) - logging.debug("FolderExplorer refreshed successfully.") + logging.debug("FolderExplorer.refresh() finished.") def rescan(self) -> None: - + logging.debug(f"FolderExplorer.rescan() at path: {self.view_path}") # TODO later return diff --git a/helab/views/folderTabsWidget.py b/helab/views/folderTabsWidget.py index cc2bc93..419a1da 100644 --- a/helab/views/folderTabsWidget.py +++ b/helab/views/folderTabsWidget.py @@ -5,8 +5,8 @@ import sys from typing import Tuple, List, Dict -from PyQt6.QtCore import QDir, QThreadPool, Qt -from PyQt6.QtWidgets import QTabWidget, QWidget, QMessageBox +from PyQt6.QtCore import QDir, QThreadPool, Qt, QSize +from PyQt6.QtWidgets import QTabWidget, QWidget, QMessageBox, QTabBar from cachetools import LRUCache from helab.models.helabFileSystemModel import helabFileSystemModel @@ -21,6 +21,13 @@ def __init__(self, parent: QWidget | None = None): self.setTabsClosable(True) self.setMovable(True) self.tabCloseRequested.connect(self.removeTab) + self._tab_switching_enabled = True + + self.setStyleSheet(""" + QTabBar::tab { + height: 30px; /* Set the desired height */ + } + """) # Initialize shared resources self.status_cache: LRUCache[str, Tuple[str, int, List[str]]] = LRUCache(maxsize=10000) @@ -35,6 +42,33 @@ def __init__(self, parent: QWidget | None = None): self.currentChanged.connect(self.on_current_tab_changed) + + def set_tab_switching_enable(self) -> None: + self._tab_switching_enabled = True + # self.setTabsClosable(True) + self.setMovable(True) + # self.setTabEnabled() + tab_bar = self.tabBar() + if tab_bar: + for index in range(self.count()): + if tab_bar: + close_button = tab_bar.tabButton(index, QTabBar.ButtonPosition.RightSide) + if close_button: + close_button.setEnabled(True) # Enable the close button + tab_bar.setEnabled(True) + + def set_tab_switching_disable(self) -> None: + self._tab_switching_enabled = False + # self.setTabsClosable(False) + self.setMovable(False) + tab_bar = self.tabBar() + if tab_bar: + for index in range(self.count()): + close_button = tab_bar.tabButton(index, QTabBar.ButtonPosition.RightSide) + if close_button: + close_button.setEnabled(False) # Disable the close button + tab_bar.setEnabled(False) + def add_new_folder_explorer_tab(self) -> None: # Create a new FolderExplorer instance dirPath = QDir.rootPath() @@ -50,7 +84,7 @@ def add_new_folder_explorer_tab(self) -> None: ] target_path = next((path for path in target_paths if os.path.exists(path)), '') - + try: if not os.path.commonpath([dirPath, target_path]) == os.path.abspath(dirPath): logging.warning( @@ -108,7 +142,7 @@ def refresh_current_folder_explorer(self) -> None: current_folder_explorer = self.currentWidget() if isinstance(current_folder_explorer, FolderExplorer): current_folder_explorer.refresh() - logging.info("Current FolderExplorer tab has been refreshed.") + logging.debug("FolderTabWidget.refresh_current_folder_explorer() completed.") else: logging.warning("Current tab is not a FolderExplorer instance.") @@ -116,12 +150,17 @@ def rescan_current_folder_explorer(self) -> None: current_folder_explorer = self.currentWidget() if isinstance(current_folder_explorer, FolderExplorer): current_folder_explorer.rescan() - logging.info("Current FolderExplorer tab has been rescanned.") + logging.info("Current FolderExplorer tab rescan completed.") else: logging.warning("Current tab is not a FolderExplorer instance.") def on_current_tab_changed(self, index: int) -> None: logging.debug(f"(TabWidget) Current tab changed to index {index}") + current_folder_explorer = self.currentWidget() + if isinstance(current_folder_explorer, FolderExplorer): + logging.debug(f"Current tab dir_path: {current_folder_explorer.dir_path}, view_path: {current_folder_explorer.view_path}, target_path: {current_folder_explorer.target_path}") + else: + logging.warning("Current tab is not a FolderExplorer instance.") # current_folder_explorer = self.currentWidget() # if isinstance(current_folder_explorer, FolderExplorer): # current_folder_explorer.update_back_button_state() diff --git a/helab/views/helabMainWindow.py b/helab/views/helabMainWindow.py index 32b6eb7..c01c04b 100644 --- a/helab/views/helabMainWindow.py +++ b/helab/views/helabMainWindow.py @@ -1,7 +1,10 @@ import logging import os +import re +import subprocess import sys +import psutil from PyQt6.QtCore import Qt, QDir, QSize, QTimer, QThreadPool, QSettings, QEvent from PyQt6.QtGui import QAction, QIcon, QPalette, QColor, QPixmap, QTransform, QCloseEvent from PyQt6.QtWidgets import QMainWindow, QDockWidget, QStatusBar, QMenuBar, QWidget, QVBoxLayout, QTabWidget, QSplitter, \ @@ -14,15 +17,62 @@ from helab.views.folderTabsWidget import FolderTabWidget from helab.views.settingsDialog import SettingsDialog +# TOOLBAR_STYLESHEET_LR = """ +# QToolBar { +# background: none; +# border: none; +# spacing: 5px; +# } +# """ +TOOLBAR_STYLESHEET_LR = """ +QToolBar { + spacing: 5px; + padding: 2px; +} +""" + + +# Function to extract version from setup.py +def get_version() -> str: + with open('setup.py', 'r', encoding='utf-8') as f: + content = f.read() + match = re.search(r'version\s*=\s*[\'"]([^\'"]+)[\'"]', content) + if match: + return match.group(1) + return '0.0.0' # Default version if not found + +def get_git_commit_hash() -> str: + try: + commit_hash = subprocess.check_output( + ['git', 'rev-parse', 'HEAD'], + stderr=subprocess.STDOUT + ).decode('utf-8').strip() + return commit_hash[:6].upper() # Return only the first 6 characters + except Exception: + return 'unknown' + + + +APP_VERSION = get_version() +APP_COMMIT_HASH = get_git_commit_hash() + + class MainWindow(QMainWindow): - DEFAULT_WIDTH = 1800 - DEFAULT_HEIGHT = 1000 + DEFAULT_WIDTH = 1600 + DEFAULT_HEIGHT = 900 def __init__(self) -> None: super().__init__() - self.setWindowTitle('HeLab') + + # current directory is + logging.debug(f"Current directory is {os.getcwd()}") + + self.process = psutil.Process() + self.setWindowIcon(QIcon("./helab/resources/icons/ai-icon.icns")) # this doesn't do anything on macos (?) + + self.setWindowTitle(f"HeLab \t v{APP_VERSION} ({APP_COMMIT_HASH})") self.resize(self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT) self.setMinimumSize(600, 340) @@ -40,7 +90,12 @@ def __init__(self) -> None: # Create Status Bar self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) - + self.status_bar_message_left = QLabel("Left Message") + self.status_bar.addWidget(self.status_bar_message_left) + # self.status_bar_message_right = QLabel(f"v{APP_VERSION} ({APP_COMMIT_HASH})") + self.status_bar_message_right = QLabel("Right Message") + self.status_bar.addPermanentWidget(self.status_bar_message_right) + self.status_bar_message_right.setToolTip("(App resource usage) / (system total resource usage)") # Create Left Panel (File Tree View) # self.setup_file_tree_view() @@ -49,16 +104,22 @@ def __init__(self) -> None: self.tab_widget.currentChanged.connect(self.on_current_tab_changed) # Setup a timer to update the status bar with thread status - self.thread_status_timer = QTimer(self) - self.thread_status_timer.timeout.connect(self.update_thread_status) - self.thread_status_timer.start(100) # Update every second + self.status_timer_threadpool = QTimer(self) + self.status_timer_threadpool.timeout.connect(self.update_status_bar_left) + self.status_timer_threadpool.start(200) # Update every 300ms + + + self.status_timer_cpu_ram = QTimer(self) + self.status_timer_cpu_ram.timeout.connect(self.update_status_bar_right) + self.status_timer_cpu_ram.start(1000) # Update every 1000ms + # self.status_icon_loading = QPixmap(TablerIcons.load(OutlineIcon.LOADER_2,color='000000').toqpixmap().scaled(128, 128, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) # self.status_icon_loading_angle = 0 # self.status_icon_checked = QPixmap(TablerIcons.load(OutlineIcon.CIRCLE_CHECK, color='005500').toqpixmap().scaled(128, 128, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) # self.status_icon = QLabel() # self.status_bar.addPermanentWidget(self.status_icon) - def update_thread_status(self) -> None: + def update_status_bar_left(self) -> None: thread_pool = QThreadPool.globalInstance() if thread_pool is None: logging.error("QThreadPool.globalInstance() returned None.") @@ -73,16 +134,35 @@ def update_thread_status(self) -> None: # rotated_pixmap = self.status_icon_loading.transformed(transform) # self.status_icon.setPixmap(rotated_pixmap) # self.status_icon_loading_angle = (self.status_icon_loading_angle + 10) % 360 - self.status_bar.showMessage(f"Working: {active_threads}, Queued: {queue_depth}") + self.status_bar_message_left.setText(f"Working: {active_threads}, Queued: {queue_depth}") self.action_tab_cancel.setEnabled(True) + self.tab_widget.set_tab_switching_disable() + self.set_tools_and_tabs_disable() else: - self.status_bar.showMessage(f"Threads Ready") + self.status_bar_message_left.setText(f"Threads Pool Standby") # self.status_bar.setPixmap(self.status_icon_checked) self.action_tab_cancel.setEnabled(False) + self.tab_widget.set_tab_switching_enable() + self.set_tools_and_tabs_enable() + + def update_status_bar_right(self) -> None: + # Update the right message with CPU and RAM usage + cpu_usage_app = self.process.cpu_percent() + cpu_usage_total = sum(psutil.cpu_percent(percpu=True)) + ram_usage_app = self.process.memory_info().rss # in bytes + ram_usage_total = psutil.virtual_memory().used # total used RAM in bytes + ram_usage_app_gb = ram_usage_app / (1024 ** 3) + ram_usage_total_gb = ram_usage_total / (1024 ** 3) + + # Update the status bar message + self.status_bar_message_right.setText( + f"CPU {cpu_usage_app:.1f}% / {cpu_usage_total:.1f}% RAM {ram_usage_app_gb:.1f}GB / {ram_usage_total_gb:.1f}GB") + def create_menus(self) -> None: # Add menus and actions + # file_menu = self.menu_bar.addMenu('File') if file_menu is not None: # Create actions @@ -101,12 +181,12 @@ def create_menus(self) -> None: else : logging.error("file_menu is None") - + debug_menu = self.menu_bar.addMenu('Debug') if debug_menu is not None: debug_action_1 = QAction('Clear Status Cache', self) debug_action_1.triggered.connect(self.tab_widget.clear_status_cache) - debug_action_2 = QAction('Draw a line', self) + debug_action_2 = QAction('Draw a line in debug console', self) debug_action_2.triggered.connect(lambda: logging.debug("="*80)) debug_menu.addAction(debug_action_1) @@ -114,6 +194,7 @@ def create_menus(self) -> None: debug_menu.addAction(debug_action_2) else: logging.error("debug_menu is None") + def show_settings_dialog(self) -> None: # pass @@ -138,13 +219,13 @@ def setup_central_widgets(self) -> None: self.setCentralWidget(self.splitter) self.splitter.setStyleSheet(""" QSplitter::handle { - background: #cccccc; /* Light gray background for the handle */ + background: #d8d8d8; /* Light gray background for the handle */ } QSplitter::handle:horizontal { - width: 5px; + width: 4px; } QSplitter::handle:vertical { - height: 5px; + height: 4px; } QSplitter::handle:hover { background: #000000; /* Darker gray when hovered */ @@ -153,22 +234,28 @@ def setup_central_widgets(self) -> None: # Add a toolbar with a toggle button for the left panel - self.sidebar_toolbar = QToolBar("Sidebar Toolbar", self) - self.sidebar_toolbar.setIconSize(QSize(24, 24)) + self.sidebar_toolbar_left = QToolBar("Sidebar Toolbar Left", self) + self.sidebar_toolbar_left.setMovable(False) + self.sidebar_toolbar_left.setIconSize(QSize(24, 24)) + self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.sidebar_toolbar_left) + self.sidebar_toolbar_right = QToolBar("Sidebar Toolbar Right", self) + self.sidebar_toolbar_right.setIconSize(QSize(24, 24)) + self.sidebar_toolbar_right.setMovable(False) + self.addToolBar(Qt.ToolBarArea.RightToolBarArea, self.sidebar_toolbar_right) + + toggle_left_panel_action = QAction(ToolIcons.ICON_LEFT_COLLAPSE, "Toggle Left Panel", self) toggle_left_panel_action.setCheckable(True) toggle_left_panel_action.setChecked(True) toggle_left_panel_action.triggered.connect(self.toggle_left_panel) - self.sidebar_toolbar.addAction(toggle_left_panel_action) - self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.sidebar_toolbar) - self.sidebar_toolbar.setMovable(False) + self.sidebar_toolbar_left.addAction(toggle_left_panel_action) # Left panel left_panel = QWidget() self.left_panel_width = 700 # Store the width of the left panel left_layout = QVBoxLayout(left_panel) left_layout.setContentsMargins(0, 0, 0, 0) - # Create a splitter to hold the tab_widget and bottom_panel + # Create a splitter to hold the tab_widget and panel_left_bottom self.left_splitter = QSplitter(Qt.Orientation.Vertical) left_layout.addWidget(self.left_splitter) left_panel.setLayout(left_layout) @@ -192,7 +279,7 @@ def setup_central_widgets(self) -> None: self.action_tab_new.setToolTip("New Tab") self.action_tab_new.setWhatsThis("New Tab??? plz let me know if you see this text") # literally don't know where this will show up memm self.action_tab_refresh.setToolTip("Refresh file list view") - self.action_tab_folder_up.setToolTip("Going back up a directory level") + self.action_tab_folder_up.setToolTip("Navigate up one directory") self.action_tab_rescan.setToolTip("Rescan the current directory") self.action_tab_cancel.setToolTip("Cancel background tasks") @@ -204,36 +291,17 @@ def setup_central_widgets(self) -> None: # toolbar.addAction(action_tab_new) # toolbar.addAction(action_tab_refresh) # toolbar.addAction(action_tab_folder_up) - self.sidebar_toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) + self.sidebar_toolbar_left.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) - self.sidebar_toolbar.setStyleSheet(""" - QToolBar { - background: none; - border: none; - spacing: 5px; - } - QToolButton:checked { - background-color: #000000; # Color when checked - } - # QToolButton { - # background: none; - # border: none; - # padding: 5px; - # } - QToolButton:hover { - background: #d4d4d4; - } - QToolButton:pressed { - background: #999999; - } - """) - # left_layout.addWidget(self.sidebar_toolbar) + self.sidebar_toolbar_left.setStyleSheet(TOOLBAR_STYLESHEET_LR) + self.sidebar_toolbar_right.setStyleSheet(TOOLBAR_STYLESHEET_LR) + # left_layout.addWidget(self.sidebar_toolbar_left) - self.sidebar_toolbar.addAction(self.action_tab_new) - self.sidebar_toolbar.addAction(self.action_tab_folder_up) - self.sidebar_toolbar.addAction(self.action_tab_refresh) - self.sidebar_toolbar.addAction(self.action_tab_rescan) - self.sidebar_toolbar.addAction(self.action_tab_cancel) + self.sidebar_toolbar_left.addAction(self.action_tab_new) + self.sidebar_toolbar_left.addAction(self.action_tab_folder_up) + self.sidebar_toolbar_left.addAction(self.action_tab_refresh) + self.sidebar_toolbar_left.addAction(self.action_tab_rescan) + self.sidebar_toolbar_left.addAction(self.action_tab_cancel) # Create the tab widget and add it to the left panel self.tab_widget = FolderTabWidget() @@ -251,41 +319,54 @@ def setup_central_widgets(self) -> None: self.action_tab_folder_up.setEnabled(self.tab_widget.tab_back_button_enabled) self.action_tab_rescan.triggered.connect(self.tab_widget.rescan_current_folder_explorer) - self.sidebar_toolbar.addSeparator() + self.sidebar_toolbar_left.addSeparator() - # Add a spacer widget to push the toggle button to the bottom - spacer = QWidget() - spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - self.sidebar_toolbar.addWidget(spacer) + # Add a spacer_left widget to push the toggle button to the bottom + spacer_left = QWidget() + spacer_left.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.sidebar_toolbar_left.addWidget(spacer_left) - self.sidebar_toolbar.addSeparator() + self.sidebar_toolbar_left.addSeparator() action_settings = QAction(ToolIcons.ICON_SETTINGS, "Settings", self) action_settings.triggered.connect(self.show_settings_dialog) - self.sidebar_toolbar.addAction(action_settings) + self.sidebar_toolbar_left.addAction(action_settings) + + self.sidebar_toolbar_left.addSeparator() + + # Add a button to the bottom of sidebar_toolbar_left + toggle_panel_left_bottom_action = QAction(ToolIcons.ICON_BOTTOM_EXPAND, "Toggle Panel", self) + toggle_panel_left_bottom_action.setCheckable(True) + toggle_panel_left_bottom_action.setChecked(True) + self.sidebar_toolbar_left.addAction(toggle_panel_left_bottom_action) - self.sidebar_toolbar.addSeparator() - # Add a button to the bottom of sidebar_toolbar - toggle_panel_action = QAction(ToolIcons.ICON_BOTTOM_EXPAND, "Toggle Panel", self) - toggle_panel_action.setCheckable(True) - toggle_panel_action.setChecked(False) - self.sidebar_toolbar.addAction(toggle_panel_action) + + toggle_right_panel_action = QAction(ToolIcons.ICON_RIGHT_COLLAPSE, "Toggle Right Panel", self) + toggle_right_panel_action.setCheckable(True) + toggle_right_panel_action.setChecked(True) + toggle_right_panel_action.triggered.connect(self.toggle_right_panel) + self.sidebar_toolbar_right.addAction(toggle_right_panel_action) + spacer_right = QWidget() + spacer_right.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.sidebar_toolbar_right.addWidget(spacer_right) + # Create a panel below the tab_widget - self.bottom_panel = QWidget() - self.bottom_panel_layout = QVBoxLayout(self.bottom_panel) - self.bottom_panel_layout.setContentsMargins(0, 0, 0, 0) - self.bottom_panel_label = QLabel("Placeholder") - self.bottom_panel_layout.addWidget(self.bottom_panel_label) - self.bottom_panel.setVisible(False) + self.panel_left_bottom = QWidget() + self.panel_layout_left_bottom = QVBoxLayout(self.panel_left_bottom) + self.panel_layout_left_bottom.setContentsMargins(0, 0, 0, 0) + self.panel_label_left_bottom = QLabel("Placeholder") + self.panel_label_left_bottom.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.panel_layout_left_bottom.addWidget(self.panel_label_left_bottom) + self.panel_left_bottom.setVisible(True) # Add the bottom panel to the left_layout - # left_layout.addWidget(self.bottom_panel) - self.left_splitter.addWidget(self.bottom_panel) + # left_layout.addWidget(self.panel_left_bottom) + self.left_splitter.addWidget(self.panel_left_bottom) # Connect the button to a function that toggles the visibility of the panel - toggle_panel_action.triggered.connect(self.toggle_bottom_panel) + toggle_panel_left_bottom_action.triggered.connect(self.toggle_left_bottom_panel) self.left_splitter.setSizes([400, 100]) @@ -302,15 +383,21 @@ def setup_central_widgets(self) -> None: # Add dock widgets to the middle main window dock_widget1 = QDockWidget("Dock Widget 1", self) - dock_widget1.setWidget(QLabel("Content of Dock Widget 1")) + dock_widget_placeholder_label_1 = QLabel("Content of Dock Widget 1") + dock_widget_placeholder_label_1.setAlignment(Qt.AlignmentFlag.AlignCenter) + dock_widget1.setWidget(dock_widget_placeholder_label_1) middle_mainwindow.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock_widget1) dock_widget2 = QDockWidget("Dock Widget 2", self) - dock_widget2.setWidget(QLabel("Content of Dock Widget 2")) + dock_widget_placeholder_label_2 = QLabel("Content of Dock Widget 2") + dock_widget_placeholder_label_2.setAlignment(Qt.AlignmentFlag.AlignCenter) + dock_widget2.setWidget(dock_widget_placeholder_label_2) middle_mainwindow.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock_widget2) dock_widget3 = QDockWidget("Dock Widget 3", self) - dock_widget3.setWidget(QLabel("Content of Dock Widget 3")) + dock_widget_placeholder_label_3 = QLabel("Content of Dock Widget 3") + dock_widget_placeholder_label_3.setAlignment(Qt.AlignmentFlag.AlignCenter) + dock_widget3.setWidget(dock_widget_placeholder_label_3) middle_mainwindow.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock_widget3) # Add the middle main window to the splitter @@ -322,7 +409,37 @@ def setup_central_widgets(self) -> None: right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(0, 0, 0, 0) # Placeholder content - right_layout.addWidget(QLabel("Right Panel Placeholder")) + # right_layout.addWidget(QLabel("Right Panel Placeholder")) + self.right_splitter = QSplitter(Qt.Orientation.Vertical) + right_layout.addWidget(self.right_splitter) + right_panel.setLayout(right_layout) + + right_top_widget = QLabel("Right Top Placeholder") + right_top_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + + # Right bottom panel + self.panel_right_bottom = QWidget() + self.panel_layout_right_bottom = QVBoxLayout(self.panel_right_bottom) + self.panel_layout_right_bottom.setContentsMargins(0, 0, 0, 0) + self.panel_label_right_bottom = QLabel("Right Bottom Placeholder") + self.panel_label_right_bottom.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.panel_layout_right_bottom.addWidget(self.panel_label_right_bottom) + self.panel_right_bottom.setVisible(True) + # Add the bottom panel to the right_layout + # Add a button to the bottom of sidebar_toolbar_right + toggle_right_bottom_panel_action = QAction(ToolIcons.ICON_BOTTOM_EXPAND, "Toggle Right Bottom Panel", self) + toggle_right_bottom_panel_action.setCheckable(True) + toggle_right_bottom_panel_action.setChecked(True) + toggle_right_bottom_panel_action.triggered.connect(self.toggle_right_bottom_panel) + self.sidebar_toolbar_right.addAction(toggle_right_bottom_panel_action) + + + + + self.right_splitter.addWidget(right_top_widget) + self.right_splitter.addWidget(self.panel_right_bottom) + self.right_splitter.setSizes([400, 300]) + # Add the right panel to the splitter self.splitter.addWidget(right_panel) @@ -398,41 +515,66 @@ def toggle_left_panel(self, checked: bool) -> None: self.width()-self.left_panel_width-self.right_panel_width, self.right_panel_width ]) - self.sidebar_toolbar.actions()[0].setIcon(ToolIcons.ICON_LEFT_COLLAPSE) + self.sidebar_toolbar_left.actions()[0].setIcon(ToolIcons.ICON_LEFT_COLLAPSE) else: self.left_panel_width = self.left_panel.width() if self.left_panel.width() > 600 else self.left_panel_width self.right_panel_width = self.right_panel.width() if self.right_panel.width() > 300 else self.right_panel_width self.splitter.setSizes([0, self.splitter.sizes()[1], self.splitter.sizes()[2]]) - self.sidebar_toolbar.actions()[0].setIcon(ToolIcons.ICON_LEFT_EXPAND) + self.sidebar_toolbar_left.actions()[0].setIcon(ToolIcons.ICON_LEFT_EXPAND) # Update the bottom panel icon based on its visibility and the state of the left panel - # if self.bottom_panel.isVisible(): - # self.sidebar_toolbar.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_COLLAPSE) + # if self.panel_left_bottom.isVisible(): + # self.sidebar_toolbar_left.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_COLLAPSE) # else: if self.splitter.sizes()[0] == 0: # Check if the left panel is hidden - self.sidebar_toolbar.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_INACTIVE) + self.sidebar_toolbar_left.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_INACTIVE) else: - if self.bottom_panel.isVisible(): - self.sidebar_toolbar.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_COLLAPSE) + if self.panel_left_bottom.isVisible(): + self.sidebar_toolbar_left.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_COLLAPSE) else: - self.sidebar_toolbar.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_EXPAND) + self.sidebar_toolbar_left.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_EXPAND) - def toggle_bottom_panel(self, checked: bool) -> None: + def toggle_left_bottom_panel(self, checked: bool) -> None: if checked: - self.bottom_panel.setVisible(True) - self.sidebar_toolbar.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_COLLAPSE) + self.panel_left_bottom.setVisible(True) + self.sidebar_toolbar_left.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_COLLAPSE) else: - self.bottom_panel.setVisible(False) + self.panel_left_bottom.setVisible(False) if self.splitter.sizes()[0] == 0: # Check if the left panel is hidden - self.sidebar_toolbar.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_INACTIVE) + self.sidebar_toolbar_left.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_INACTIVE) else: - self.sidebar_toolbar.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_EXPAND) + self.sidebar_toolbar_left.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_EXPAND) # def enterEvent(self, a0): # # Display the tooltip immediately when mouse enters the widget area # QToolTip.showText(a0.globalPosition().toPoint(), self.toolTip(), self, self.rect()) # super().enterEvent(a0) + def toggle_right_panel(self, checked: bool) -> None: + if checked: + self.splitter.setSizes([ + self.splitter.sizes()[0], + self.width()-self.left_panel_width-self.right_panel_width, + self.right_panel_width + ]) + self.sidebar_toolbar_right.actions()[0].setIcon(ToolIcons.ICON_RIGHT_COLLAPSE) + else: + self.right_panel_width = self.right_panel.width() if self.right_panel.width() > 300 else self.right_panel_width + self.splitter.setSizes([ + self.splitter.sizes()[0], + self.width()-self.left_panel_width, + 0 + ]) + self.sidebar_toolbar_right.actions()[0].setIcon(ToolIcons.ICON_RIGHT_EXPAND) + + def toggle_right_bottom_panel(self, checked: bool) -> None: + if checked: + self.panel_right_bottom.setVisible(True) + self.sidebar_toolbar_right.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_COLLAPSE) + else: + self.panel_right_bottom.setVisible(False) + self.sidebar_toolbar_right.actions()[-1].setIcon(ToolIcons.ICON_BOTTOM_EXPAND) + def on_back_button_clicked(self) -> None: self.tab_widget.on_back_button_clicked() self.action_tab_folder_up.setEnabled(self.tab_widget.tab_back_button_enabled) @@ -458,6 +600,20 @@ def update_tool_enabled_state(self) -> None: else: self.action_tab_folder_up.setEnabled(False) + def set_tools_and_tabs_enable(self) -> None: + self.action_tab_new.setEnabled(True) + self.action_tab_folder_up.setEnabled(True) + self.action_tab_refresh.setEnabled(True) + self.action_tab_rescan.setEnabled(True) + + def set_tools_and_tabs_disable(self) -> None: + self.action_tab_new.setEnabled(False) + self.action_tab_folder_up.setEnabled(False) + self.action_tab_refresh.setEnabled(False) + self.action_tab_rescan.setEnabled(False) + + + def closeEvent(self, a0: QCloseEvent | None) -> None: logging.debug("MainWindow closeEvent") @@ -468,3 +624,10 @@ def closeEvent(self, a0: QCloseEvent | None) -> None: # settings.setValue("windowState", self.saveState()) # super().closeEvent(a0) pass + + + + + + + diff --git a/helab/views/settingsDialog.py b/helab/views/settingsDialog.py index d18e2df..e313368 100644 --- a/helab/views/settingsDialog.py +++ b/helab/views/settingsDialog.py @@ -9,8 +9,10 @@ class SettingsDialog(QDialog): def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.setWindowTitle("Settings") - self.setMinimumSize(400, 300) + self.setMinimumSize(800, 400) self.main_layout = QVBoxLayout(self) + MLCM = 8 # Main Layout Contents Margin + self.main_layout.setContentsMargins(MLCM,MLCM,MLCM,MLCM) # Create tab widget self.tabs = QTabWidget() @@ -30,13 +32,22 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: self.tabs.addTab(self.general_tab, "General") - # Second tab + + # Scripts Tab + self.scripts_tab = QWidget() + self.scripts_layout = QVBoxLayout(self.scripts_tab) + self.scripts_layout.addWidget(QLabel("Scripts Settings")) + self.tabs.addTab(self.scripts_tab, "Scripts") + + + # Placeholder tab self.placeholder_tab = QWidget() self.placeholder_layout = QVBoxLayout(self.placeholder_tab) self.placeholder_layout.addWidget(QLabel("Placeholder text for the second tab")) self.tabs.addTab(self.placeholder_tab, "Placeholder") + # Buttons self.button_layout = QHBoxLayout() self.save_button = QPushButton("Save") diff --git a/helab/workers/statusDeepWorker.py b/helab/workers/statusDeepWorker.py index 92145ec..1e68eca 100644 --- a/helab/workers/statusDeepWorker.py +++ b/helab/workers/statusDeepWorker.py @@ -4,7 +4,7 @@ from PyQt6.QtCore import QObject, pyqtSignal, QRunnable class StatusDeepWorkerSignals(QObject): - finished = pyqtSignal(list) # list of directory paths + finished = pyqtSignal(str, list) # list of directory paths class StatusDeepWorker(QRunnable): def __init__(self, root_path: str, max_depth: int = sys.maxsize): @@ -45,7 +45,7 @@ def run(self) -> None: except Exception as e: logging.error(f"Error in StatusDeepWorker: {e}") return - self.signals.finished.emit(directory_list) + self.signals.finished.emit(self.root_path, directory_list) logging.debug(f"Deep worker finished for: {self.root_path}") def cancel(self) -> None: diff --git a/requirements.txt b/requirements.txt index b956b5b..6bef402 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ pytest-qt pytest-xdist pympler pyinstaller +psutil \ No newline at end of file diff --git a/setup.py b/setup.py index a0b6072..be92201 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="helab", - version="0.0.1", + version="0.0.3", author="Tony Yan", author_email="tony.yan@anu.edu.au", description="HELIUM Experiment Lab Information Unified Manager", diff --git a/tests/test_helabFileSystemModel.py b/tests/test_helabFileSystemModel.py index 786cb3e..63658c0 100644 --- a/tests/test_helabFileSystemModel.py +++ b/tests/test_helabFileSystemModel.py @@ -118,21 +118,6 @@ def test_start_deep_status_worker(self, mock_StatusDeepWorker: MagicMock) -> Non self.assertIn(root_path, self.model.running_workers_deep) self.assertEqual(self.model.running_workers_deep[root_path], mock_worker_instance) - @patch('helab.models.helabFileSystemModel.logging') - def test_process_deep_status(self, mock_logging: MagicMock) -> None: - directory_list = ['/dir1', '/dir2', '/dir3'] - - with patch.object(self.model, 'process_next_directories') as mock_process_next_directories: - self.model.process_deep_status(directory_list) - - # Assert that directory_iterator is set correctly - self.assertEqual(list(self.model.directory_iterator), directory_list) - - # Assert that process_next_directories is called - mock_process_next_directories.assert_called_once() - - # Assert logging message - mock_logging.debug.assert_called_with(f"Processing {len(directory_list)} directories for status recalculation") if __name__ == '__main__': diff --git a/tests/test_main_pt.py b/tests/test_main_pt.py index 1ac1216..72114a6 100644 --- a/tests/test_main_pt.py +++ b/tests/test_main_pt.py @@ -72,7 +72,7 @@ def test_main_window_title(app: QApplication) -> None: # QThreadPool.globalInstance().waitForDone() # QTest.qWait(QTEST_WAIT_MS_SHORT) wait_thread_pool_complete(thread_pool) - assert main_window.windowTitle() == "HeLab" + assert "HeLab" in main_window.windowTitle() def test_main_window_size(app: QApplication) -> None: thread_pool = setup_thread_pool() diff --git a/tests/test_main_ut.py b/tests/test_main_ut.py index 1fe88f5..9312b07 100644 --- a/tests/test_main_ut.py +++ b/tests/test_main_ut.py @@ -58,8 +58,9 @@ def tearDown(self) -> None: pass def test_window_title(self) -> None: - self.assertEqual(self.main_window.windowTitle(), 'HeLab') + # self.assertEqual(self.main_window.windowTitle(), 'HeLab') # QThreadPool.globalInstance().waitForDone() + self.assertTrue("HeLab" in self.main_window.windowTitle()) def test_default_size(self) -> None: self.assertEqual(self.main_window.width(), MainWindow.DEFAULT_WIDTH) From 7ea608723bc0f3b9062ff9a141dcc209bd1fad23 Mon Sep 17 00:00:00 2001 From: Tony Yan Date: Sat, 14 Dec 2024 03:58:26 +1100 Subject: [PATCH 02/67] UI tweaks and stability --- .idea/runConfigurations/main.xml | 2 +- coverage.xml | 1231 +++++++++++++++++------------- helab/main.py | 6 + helab/utils/loggingSetup.py | 7 +- helab/views/folderExplorer.py | 31 +- helab/views/folderTabsWidget.py | 8 +- helab/views/helabMainWindow.py | 279 ++++--- helab/views/settingsDialog.py | 26 + requirements.txt | 3 +- 9 files changed, 958 insertions(+), 635 deletions(-) diff --git a/.idea/runConfigurations/main.xml b/.idea/runConfigurations/main.xml index 9a4e90f..8a6c645 100644 --- a/.idea/runConfigurations/main.xml +++ b/.idea/runConfigurations/main.xml @@ -16,7 +16,7 @@