diff --git a/.gitignore b/.gitignore index 0c67d955c..049271a30 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ nghdl* tags build/ dist/ +venv311/ diff --git a/images/sun.png b/images/sun.png new file mode 100644 index 000000000..956ed9319 Binary files /dev/null and b/images/sun.png differ diff --git a/library/browser/User-Manual/eSim.html b/library/browser/User-Manual/eSim.html index db749c5ae..712ecbaf6 100644 --- a/library/browser/User-Manual/eSim.html +++ b/library/browser/User-Manual/eSim.html @@ -28,7 +28,7 @@ eSim User Manual
version 2.5.0
+class="cmr-10">version 2.4.0
Prepared By:
- - + -

About eSim

-
eSim logo
-
-

- eSim is an open source EDA tool for circuit design, simulation, analysis and PCB design. It is an integrated tool built using open source softwares such as KiCad (https://www.kicad.org/), Ngspice (https://ngspice.sourceforge.io/), GHDL (http://ghdl.free.fr), Verilator (https://www.veripool.org/verilator/), Makerchip IDE (https://www.makerchip.com/), and SkyWater SKY130 PDK (https://skywater-pdk.rtfd.io/). eSim source is released under GNU General Public License. -

-
-

- This tool is developed by the eSim Team at FOSSEE, IIT Bombay. To know more about eSim, please visit: https://esim.fossee.in/. -

-
-

- To discuss more about eSim, please visit: https://forums.fossee.in/ -

-
+
+

About eSim

+ +

+ eSim is an open source EDA tool for circuit design, simulation, analysis and PCB design. It is an integrated tool built using open source softwares such as + KiCad, + Ngspice, + GHDL, + Verilator, + Makerchip IDE, and + SkyWater SKY130 PDK. + eSim source is released under GNU General Public License. +

+

+ This tool is developed by the eSim Team at FOSSEE, IIT Bombay.
+ To know more about eSim, please visit: + https://esim.fossee.in/. +

+

+ To discuss more about eSim, please visit: + https://forums.fossee.in/ +

+
- - + \ No newline at end of file diff --git a/src/browser/HTMLUserManual.py b/src/browser/HTMLUserManual.py new file mode 100644 index 000000000..aa773f945 --- /dev/null +++ b/src/browser/HTMLUserManual.py @@ -0,0 +1,602 @@ +from PyQt5 import QtWidgets, QtCore +from PyQt5.QtGui import QPalette, QColor +import os +import re + +class HTMLUserManual(QtWidgets.QWidget): + """ + This class displays the user manual in a widget with proper theme switching. + """ + + def __init__(self, is_dark_theme=False): + super().__init__() + self.is_dark_theme = is_dark_theme + self.original_html_content = None + self.vlayout = QtWidgets.QVBoxLayout() + self.browser = QtWidgets.QTextBrowser() + self.vlayout.addWidget(self.browser) + self.setLayout(self.vlayout) + + # Set margins for a more professional look + self.vlayout.setContentsMargins(0, 0, 0, 0) + + self.load_original_html() + self.set_manual_html() + self.show() + + def load_original_html(self): + """Load the original HTML content once and store it.""" + path_from_script = os.path.join(os.path.dirname(__file__), '..', '..', 'library', 'browser', 'User-Manual', 'eSim.html') + try: + with open(path_from_script, 'r', encoding='utf-8') as f: + self.original_html_content = f.read() + # Set search paths for images + self.browser.setSearchPaths([os.path.dirname(path_from_script)]) + except FileNotFoundError: + self.original_html_content = f"

Error: User manual file not found

Path: {os.path.realpath(path_from_script)}

" + + def get_base_styles(self): + """Get the base styles that work for both themes.""" + return ''' + + ''' + + def get_light_theme_styles(self): + """Get styles specific to light theme.""" + return ''' + + ''' + + def get_dark_theme_styles(self): + """Get styles specific to dark theme.""" + return ''' + + ''' + + def clean_existing_styles(self, html_content): + """Remove any existing injected styles more thoroughly.""" + # Remove styles with specific IDs + html_content = re.sub(r']*id="esim-[^"]*"[^>]*>.*?', '', html_content, flags=re.DOTALL | re.IGNORECASE) + + # Remove old style comments + html_content = re.sub(r' + + + Terminal Simulation Console + + + Qt::AlignCenter + + + - + + - 6 + 16 0 - - - - 0 - 0 - + + + + 8 - - - 16777215 - 35 - - - - QProgressBar::chunk { - background-color: rgb(54,158,225); -} - - - 0 - - - -1 - - - - - + + + + Simulation Progress + + + + + + + + 0 + 0 + + + + + 300 + 40 + + + + + 16777215 + 40 + + + + 0 + + + -1 + + + + + + + - - + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + - 16777215 - 35 + 20 + 20 - - Resimulate - - + - - - - 16777215 - 35 - + + + + 12 + + + + + 140 + 56 + + + + + 16777215 + 56 + + + + ↻ Resimulate + + + Start a new simulation + + + + + + + + 140 + 56 + + + + + 16777215 + 56 + + + + ✕ Cancel + + + Stop current simulation + + + + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + Toggle light/dark mode + + + + + + + + + + + + 12 + + + - Cancel Simulation + Console Output - + - + 0 - 0 + 1 - + - 35 - 35 + 0 + 350 - - + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + Simulation output will appear here... - - - - - 0 - 0 - - - - - 0 - 400 - - - - QTextEdit { - background-color: rgb(36, 31, 49); - color: white; -} - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - + \ No newline at end of file diff --git a/src/frontEnd/color_science.py b/src/frontEnd/color_science.py new file mode 100644 index 000000000..993266e47 --- /dev/null +++ b/src/frontEnd/color_science.py @@ -0,0 +1,126 @@ +""" +Modern Color Science Module for eSim +References: +- Elliot & Maier (2007): https://doi.org/10.1037/0003-066X.62.4.313 +- Singh (2006): https://doi.org/10.1080/0013188042000277323 +- Whitfield & Wiltshire (1990): https://doi.org/10.1016/0272-4944(90)90047-3 +- Robins & Holmes (2008): https://doi.org/10.1007/s10799-007-0037-4 +- Bottomley & Doyle (2006): https://doi.org/10.1002/bdm.515 +- W3C WCAG 2.1: https://www.w3.org/TR/WCAG21/ +- Rigden (1999): https://doi.org/10.1109/38.768554 +""" +import colorsys +from typing import Tuple, List, Dict + +class ColorScience: + # Color psychology mapping (simplified) + PSYCHOLOGY = { + 'blue': {'emotion': 'trust', 'hex': '#2563EB'}, + 'green': {'emotion': 'success', 'hex': '#059669'}, + 'orange': {'emotion': 'energy', 'hex': '#D97706'}, + 'red': {'emotion': 'error', 'hex': '#DC2626'}, + 'purple': {'emotion': 'creativity', 'hex': '#7C3AED'}, + 'gray': {'emotion': 'neutral', 'hex': '#64748B'}, + } + # WCAG 2.1 contrast ratios + WCAG_AA = 4.5 + WCAG_AAA = 7.0 + # Colorblind simulation matrices (protanopia, deuteranopia, tritanopia) + COLORBLIND_MATRICES = { + 'protanopia': (0.56667, 0.43333, 0, 0.55833, 0.44167, 0, 0, 0.24167, 0.75833), + 'deuteranopia':(0.625, 0.375, 0, 0.7, 0.3, 0, 0, 0.3, 0.7), + 'tritanopia': (0.95, 0.05, 0, 0, 0.43333, 0.56667, 0, 0.475, 0.525), + } + # Semantic color system + SEMANTIC = { + 'primary': '#2563EB', + 'secondary': '#7C3AED', + 'success': '#059669', + 'warning': '#D97706', + 'error': '#DC2626', + 'info': '#0891B2', + 'background_light': '#FFFFFF', + 'background_dark': '#181b24', + 'text_light': '#2c3e50', + 'text_dark': '#e8eaed', + } + + @staticmethod + def hex_to_rgb(hex_color: str) -> Tuple[float, float, float]: + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i+2], 16)/255 for i in (0, 2, 4)) + + @staticmethod + def rgb_to_hex(r: float, g: float, b: float) -> str: + return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}" + + @staticmethod + def contrast_ratio(hex1: str, hex2: str) -> float: + def luminance(rgb): + r, g, b = [x/12.92 if x <= 0.03928 else ((x+0.055)/1.055)**2.4 for x in rgb] + return 0.2126*r + 0.7152*g + 0.0722*b + l1 = luminance(ColorScience.hex_to_rgb(hex1)) + l2 = luminance(ColorScience.hex_to_rgb(hex2)) + lighter, darker = max(l1, l2), min(l1, l2) + return (lighter+0.05)/(darker+0.05) + + @staticmethod + def ensure_wcag(fg: str, bg: str, level: str = 'AA') -> bool: + ratio = ColorScience.contrast_ratio(fg, bg) + return ratio >= (ColorScience.WCAG_AAA if level == 'AAA' else ColorScience.WCAG_AA) + + @staticmethod + def simulate_colorblind(hex_color: str, mode: str = 'deuteranopia') -> str: + r, g, b = ColorScience.hex_to_rgb(hex_color) + m = ColorScience.COLORBLIND_MATRICES.get(mode) + if not m: + return hex_color + r2 = r*m[0] + g*m[1] + b*m[2] + g2 = r*m[3] + g*m[4] + b*m[5] + b2 = r*m[6] + g*m[7] + b*m[8] + return ColorScience.rgb_to_hex(r2, g2, b2) + + @staticmethod + def harmonious_palette(base: str, mode: str = 'analogous', n: int = 5) -> List[str]: + # Generate harmonious palette using HSL + r, g, b = ColorScience.hex_to_rgb(base) + h, l, s = colorsys.rgb_to_hls(r, g, b) + palette = [] + if mode == 'analogous': + for i in range(n): + h2 = (h + (i - n//2)*0.08) % 1.0 + rgb = colorsys.hls_to_rgb(h2, l, s) + palette.append(ColorScience.rgb_to_hex(*rgb)) + elif mode == 'complementary': + palette = [base, ColorScience.rgb_to_hex(*colorsys.hls_to_rgb((h+0.5)%1.0, l, s))] + elif mode == 'triadic': + palette = [ColorScience.rgb_to_hex(*colorsys.hls_to_rgb((h+shift)%1.0, l, s)) for shift in (0, 1/3, 2/3)] + else: + palette = [base] + return palette + + @staticmethod + def adaptive_theme(is_dark: bool) -> Dict[str, str]: + # Adaptive color scheme + if is_dark: + return { + 'background': ColorScience.SEMANTIC['background_dark'], + 'text': ColorScience.SEMANTIC['text_dark'], + 'primary': ColorScience.SEMANTIC['primary'], + 'secondary': ColorScience.SEMANTIC['secondary'], + 'success': ColorScience.SEMANTIC['success'], + 'warning': ColorScience.SEMANTIC['warning'], + 'error': ColorScience.SEMANTIC['error'], + 'info': ColorScience.SEMANTIC['info'], + } + else: + return { + 'background': ColorScience.SEMANTIC['background_light'], + 'text': ColorScience.SEMANTIC['text_light'], + 'primary': ColorScience.SEMANTIC['primary'], + 'secondary': ColorScience.SEMANTIC['secondary'], + 'success': ColorScience.SEMANTIC['success'], + 'warning': ColorScience.SEMANTIC['warning'], + 'error': ColorScience.SEMANTIC['error'], + 'info': ColorScience.SEMANTIC['info'], + } \ No newline at end of file diff --git a/src/frontEnd/sim_setup_fix.py b/src/frontEnd/sim_setup_fix.py new file mode 100644 index 000000000..30da5be7f --- /dev/null +++ b/src/frontEnd/sim_setup_fix.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +eSim Setup and Configuration Fix +This script fixes common setup issues with eSim application including: +1. Creating missing .esim directory +2. Setting up workspace configuration +3. Creating default workspace directory +4. Fixing file permissions +""" + +import os +import sys +from pathlib import Path + +def create_esim_config(): + """Create the .esim configuration directory and files""" + try: + # Get user home directory + if os.name == 'nt': + # Windows path handling (as per the original code) + user_home = os.path.join('library', 'config') + else: + # Unix/Linux path handling + user_home = os.path.expanduser('~') + + esim_config_dir = os.path.join(user_home, '.esim') + + # Create .esim directory if it doesn't exist + if not os.path.exists(esim_config_dir): + os.makedirs(esim_config_dir, exist_ok=True) + print(f"✓ Created .esim configuration directory: {esim_config_dir}") + else: + print(f"✓ .esim directory already exists: {esim_config_dir}") + + # Create workspace.txt file if it doesn't exist + workspace_file = os.path.join(esim_config_dir, 'workspace.txt') + if not os.path.exists(workspace_file): + with open(workspace_file, 'w') as f: + f.write('1') # Default value indicating workspace is set + print(f"✓ Created workspace configuration: {workspace_file}") + else: + print(f"✓ Workspace configuration already exists: {workspace_file}") + + # Create default eSim workspace directory + default_workspace = os.path.join(user_home, 'eSim-Workspace') + if not os.path.exists(default_workspace): + os.makedirs(default_workspace, exist_ok=True) + print(f"✓ Created default workspace directory: {default_workspace}") + else: + print(f"✓ Default workspace directory already exists: {default_workspace}") + + # Set proper permissions (Unix/Linux only) + if os.name != 'nt': + os.chmod(esim_config_dir, 0o755) + os.chmod(workspace_file, 0o644) + if os.path.exists(default_workspace): + os.chmod(default_workspace, 0o755) + print("✓ Set proper file permissions") + + return True + + except Exception as e: + print(f"✗ Error creating eSim configuration: {e}") + return False + +def check_dependencies(): + """Check if required Python packages are available""" + missing_packages = [] + + try: + import PyQt5 + print("✓ PyQt5 is available") + except ImportError: + missing_packages.append("PyQt5") + print("✗ PyQt5 is missing") + + try: + from PyQt5 import QtGui, QtCore, QtWidgets + print("✓ PyQt5 components are available") + except ImportError: + print("✗ PyQt5 components are missing or incomplete") + + if missing_packages: + print(f"\n⚠️ Missing packages: {', '.join(missing_packages)}") + print("Install them using: pip install " + " ".join(missing_packages)) + return False + + return True + +def fix_application_py(): + """Provide suggested fix for the Application.py file""" + fix_code = ''' +# Add this function to your Workspace.py file in the createWorkspace method +# Before trying to open the workspace.txt file for writing: + +def ensure_config_directory(): + """Ensure .esim configuration directory exists""" + if os.name == 'nt': + user_home = os.path.join('library', 'config') + else: + user_home = os.path.expanduser('~') + + esim_config_dir = os.path.join(user_home, '.esim') + + # Create directory if it doesn't exist + if not os.path.exists(esim_config_dir): + os.makedirs(esim_config_dir, exist_ok=True) + + return user_home + +# Then modify your createWorkspace method to call this function first: +# user_home = ensure_config_directory() +# file = open(os.path.join(user_home, ".esim/workspace.txt"), 'w') +''' + + print("\n" + "="*60) + print("SUGGESTED CODE FIX FOR WORKSPACE.PY:") + print("="*60) + print(fix_code) + +def main(): + """Main function to run all fixes""" + print("eSim Configuration Fix Tool") + print("="*30) + + # Check Python version + if sys.version_info < (3, 6): + print("⚠️ Warning: Python 3.6+ recommended for eSim") + + # Check dependencies + print("\n1. Checking dependencies...") + deps_ok = check_dependencies() + + # Create configuration + print("\n2. Setting up eSim configuration...") + config_ok = create_esim_config() + + # Provide code fix suggestions + print("\n3. Code fix suggestions...") + fix_application_py() + + # Summary + print("\n" + "="*60) + print("SETUP SUMMARY:") + print("="*60) + + if config_ok: + print("✓ Configuration setup completed successfully") + print("✓ You can now try running eSim again") + + if not deps_ok: + print("⚠️ Some dependencies are missing - install them first") + + print("\nNext steps:") + print("1. Apply the suggested code fix to Workspace.py") + print("2. Run: python Application.py") + + else: + print("✗ Configuration setup failed") + print("Please check file permissions and try again") + + print("\nIf you still encounter issues:") + print("- Check file permissions in your home directory") + print("- Make sure you have write access to ~/.esim/") + print("- Verify all Python dependencies are installed") + +if __name__ == "__main__": + main() diff --git a/src/frontEnd/simple_app.py b/src/frontEnd/simple_app.py new file mode 100644 index 000000000..22cd2828b --- /dev/null +++ b/src/frontEnd/simple_app.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +import sys +from PyQt5 import QtWidgets + +def main(): + app = QtWidgets.QApplication(sys.argv) + window = QtWidgets.QMainWindow() + window.setWindowTitle("eSim Simple App") + window.setGeometry(100, 100, 800, 600) + + # Create a central widget + central_widget = QtWidgets.QWidget() + window.setCentralWidget(central_widget) + + # Create a layout + layout = QtWidgets.QVBoxLayout() + central_widget.setLayout(layout) + + # Add a label + label = QtWidgets.QLabel("eSim is running!") + label.setStyleSheet("font-size: 24px; color: #1976d2;") + layout.addWidget(label) + + # Add a button + button = QtWidgets.QPushButton("Click Me") + button.clicked.connect(lambda: label.setText("Button clicked!")) + layout.addWidget(button) + + window.show() + sys.exit(app.exec_()) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/frontEnd/tab_colors_config.py b/src/frontEnd/tab_colors_config.py new file mode 100644 index 000000000..634201ef5 --- /dev/null +++ b/src/frontEnd/tab_colors_config.py @@ -0,0 +1,161 @@ +""" +Tab Color Configuration for eSim +This file allows easy customization of tab colors for both dark and light themes. +""" + +# Dark Theme Tab Colors +DARK_TAB_COLORS = { + # Normal tab state + 'background': { + 'start': '#2d3748', # Top gradient color + 'end': '#1a202c' # Bottom gradient color + }, + 'text': '#e2e8f0', # Tab text color + 'border': '#4a5568', # Tab border color + + # Selected tab state + 'selected_background': '#667eea', # Selected tab background (purple) + 'selected_text': '#1a202c', # Selected tab text + 'selected_border': '#667eea', # Selected tab border + + # Hover state + 'hover_background': '#4a5568', # Hover background + 'hover_text': '#f7fafc' # Hover text color +} + +# Light Theme Tab Colors +LIGHT_TAB_COLORS = { + # Normal tab state + 'background': { + 'start': '#ffffff', # Top gradient color + 'end': '#f8f9fa' # Bottom gradient color + }, + 'text': '#2c3e50', # Tab text color + 'border': '#e1e4e8', # Tab border color + + # Selected tab state + 'selected_background': '#1976d2', # Selected tab background (blue) + 'selected_text': '#ffffff', # Selected tab text + 'selected_border': '#1976d2', # Selected tab border + + # Hover state + 'hover_background': '#f1f4f9', # Hover background + 'hover_text': '#1976d2' # Hover text color +} + +# Alternative Color Schemes (you can uncomment and use these) + +# Purple Theme +PURPLE_TAB_COLORS = { + 'background': {'start': '#553c9a', 'end': '#b794f4'}, + 'text': '#e9d8fd', + 'border': '#9f7aea', + 'selected_background': '#d53f8c', + 'selected_text': '#fed7e2', + 'selected_border': '#d53f8c', + 'hover_background': '#9f7aea', + 'hover_text': '#faf5ff' +} + +# Green Theme +GREEN_TAB_COLORS = { + 'background': {'start': '#22543d', 'end': '#38a169'}, + 'text': '#c6f6d5', + 'border': '#48bb78', + 'selected_background': '#38a169', + 'selected_text': '#f0fff4', + 'selected_border': '#38a169', + 'hover_background': '#48bb78', + 'hover_text': '#f0fff4' +} + +# Orange Theme +ORANGE_TAB_COLORS = { + 'background': {'start': '#744210', 'end': '#ed8936'}, + 'text': '#fed7aa', + 'border': '#f6ad55', + 'selected_background': '#dd6b20', + 'selected_text': '#fffaf0', + 'selected_border': '#dd6b20', + 'hover_background': '#f6ad55', + 'hover_text': '#fffaf0' +} + +def get_tab_stylesheet(colors, theme_type='dark'): + """ + Generate CSS stylesheet for tabs based on color configuration. + + Args: + colors (dict): Color configuration dictionary + theme_type (str): 'dark' or 'light' theme + + Returns: + str: CSS stylesheet string + """ + return f""" + QTabBar::tab {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {colors['background']['start']}, stop:1 {colors['background']['end']}); + color: {colors['text']}; + border: 1px solid {colors['border']}; + border-bottom: none; + border-top-left-radius: 12px; + border-top-right-radius: 12px; + padding: 12px 28px; + margin-right: 4px; + font-weight: 600; + font-size: 13px; + letter-spacing: 0.3px; + }} + QTabBar::tab:selected {{ + background: {colors['selected_background']}; + color: {colors['selected_text']}; + border: 1px solid {colors['selected_border']}; + border-bottom: 3px solid {colors['selected_border']}; + font-weight: 700; + }} + QTabBar::tab:hover:!selected {{ + background: {colors['hover_background']}; + color: {colors['hover_text']}; + }} + QTabWidget::pane {{ + border: 1px solid {colors['border']}; + border-radius: 0 12px 12px 12px; + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {colors['background']['start']}, stop:1 {colors['background']['end']}); + }} + """ + +def apply_custom_tab_colors(application, dark_colors=None, light_colors=None): + """ + Apply custom tab colors to the application. + + Args: + application: The main application instance + dark_colors (dict): Custom dark theme colors (optional) + light_colors (dict): Custom light theme colors (optional) + """ + if dark_colors is None: + dark_colors = DARK_TAB_COLORS + if light_colors is None: + light_colors = LIGHT_TAB_COLORS + + # Store the custom colors in the application for later use + application.custom_dark_tab_colors = dark_colors + application.custom_light_tab_colors = light_colors + + # Apply the current theme + if hasattr(application, 'is_dark_theme') and application.is_dark_theme: + application.apply_dark_theme() + else: + application.apply_light_theme() + +# Example usage: +# To use purple theme for dark mode: +# apply_custom_tab_colors(app, dark_colors=PURPLE_TAB_COLORS) + +# To use green theme for light mode: +# apply_custom_tab_colors(app, light_colors=GREEN_TAB_COLORS) + +# To use both: +# apply_custom_tab_colors(app, dark_colors=PURPLE_TAB_COLORS, light_colors=GREEN_TAB_COLORS) \ No newline at end of file diff --git a/src/kicadtoNgspice/KicadtoNgspice.py b/src/kicadtoNgspice/KicadtoNgspice.py index e018143fe..4ebc4271f 100644 --- a/src/kicadtoNgspice/KicadtoNgspice.py +++ b/src/kicadtoNgspice/KicadtoNgspice.py @@ -230,7 +230,41 @@ def createcreateConvertWidget(self): self.microcontrollerTab.setWidgetResizable(True) self.tabWidget = QtWidgets.QTabWidget() - # self.tabWidget.TabShape(QtWidgets.QTabWidget.Rounded) + # Apply custom stylesheet to ensure tab text is not cropped and matches Makerchip/NgVeri style + self.tabWidget.setStyleSheet(''' + QTabWidget::pane { + border: 2px solid #23273a; + border-radius: 14px; + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #23273a, stop:1 #181b24); + } + QTabBar::tab { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #23273a, stop:1 #181b24); + color: #e8eaed; + border: 1px solid #40c4ff; + border-bottom: none; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + padding: 12px 28px; + margin-right: 2px; + font-size: 14px; + font-weight: bold; + min-width: 150px; + max-width: 300px; + } + QTabBar::tab:selected { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #40c4ff, stop:1 #1976d2); + color: #181b24; + border: 1px solid #40c4ff; + border-bottom: none; + } + QTabBar::tab:hover:!selected { + background: #1976d2; + color: #fff; + } + ''') self.tabWidget.addTab(self.analysisTab, "Analysis") self.tabWidget.addTab(self.sourceTab, "Source Details") self.tabWidget.addTab(self.modelTab, "Ngspice Model") diff --git a/src/maker/Appconfig.py b/src/maker/Appconfig.py index 315ecff81..93289eed7 100755 --- a/src/maker/Appconfig.py +++ b/src/maker/Appconfig.py @@ -1,52 +1,198 @@ -import os.path -from configparser import ConfigParser +#!/usr/bin/env python3 +""" +eSim Configuration File Fix +This script fixes the configuration file format issue in Appconfig.py +The error occurs when the workspace configuration file doesn't have the expected format. +""" +import os +import sys +from pathlib import Path -class Appconfig: +def diagnose_config_files(): + """Diagnose the current state of eSim configuration files""" + print("Diagnosing eSim Configuration Files") + print("=" * 40) + + # Determine user home directory if os.name == 'nt': - home = os.path.join('library', 'config') + user_home = os.path.join('library', 'config') else: - home = os.path.expanduser('~') + user_home = os.path.expanduser('~') + + esim_config_dir = os.path.join(user_home, '.esim') + workspace_file = os.path.join(esim_config_dir, 'workspace.txt') + + print(f"User home: {user_home}") + print(f"Config directory: {esim_config_dir}") + print(f"Workspace file: {workspace_file}") + + # Check if directory exists + if not os.path.exists(esim_config_dir): + print("✗ .esim directory does not exist") + return False, esim_config_dir, workspace_file + else: + print("✓ .esim directory exists") + + # Check if workspace.txt exists + if not os.path.exists(workspace_file): + print("✗ workspace.txt does not exist") + return False, esim_config_dir, workspace_file + else: + print("✓ workspace.txt exists") + + # Read and analyze workspace.txt content + print("\nAnalyzing workspace.txt content:") + try: + with open(workspace_file, 'r') as f: + content = f.read() + lines = content.strip().split('\n') + + print(f"File content: '{content}'") + print(f"Number of lines: {len(lines)}") + + if lines: + first_line = lines[0].strip() + print(f"First line: '{first_line}'") + + # Check if first line has space-separated values + parts = first_line.split(' ') + print(f"Parts when split by space: {parts} (count: {len(parts)})") + + if len(parts) < 2: + print("✗ First line doesn't have expected format (workspace_check home_path)") + return False, esim_config_dir, workspace_file + else: + print("✓ First line has correct format") + + except Exception as e: + print(f"✗ Error reading workspace.txt: {e}") + return False, esim_config_dir, workspace_file + + return True, esim_config_dir, workspace_file - # Reading all variables from eSim config.ini - parser_esim = ConfigParser() - parser_esim.read(os.path.join(home, os.path.join('.esim', 'config.ini'))) +def create_proper_workspace_config(esim_config_dir, workspace_file): + """Create a properly formatted workspace configuration""" try: - src_home = parser_esim.get('eSim', 'eSim_HOME') - xml_loc = os.path.join(src_home, 'library/modelParamXML') - lib_loc = os.path.expanduser('~') - except BaseException: - pass - esimFlag = 0 - - # Reading all variables from ngveri config.ini - # parser_ngveri = ConfigParser() - # parser_ngveri.read(os.path.join(home, - # os.path.join('.ngveri', 'config.ini'))) - - # KiCad v6 Library Template - kicad_sym_template = { - "start_def": "(symbol \"comp_name\" (pin_names (offset 1.016)) " + - "(in_bom yes) (on_board yes)", - "U_field": "(property \"Reference\" \"U\" (id 0) (at 12 15 0)" + - "(effects (font (size 1.524 1.524))))", - "comp_name_field": "(property \"Value\" \"comp_name\" (id 1) " + - "(at 12 18 0)(effects (font (size 1.524 1.524))))", - "blank_field": [ - "(property \"Footprint\" blank_quotes (id 2) " + - "(at 72.39 49.53 0)(effects (font (size 1.524 1.524))))", - "(property \"Datasheet\" blank_quotes (id 3) " + - "(at 72.39 49.53 0)(effects (font (size 1.524 1.524))))" - ], - "draw_pos": "(symbol \"comp_name\"(rectangle (start 0 0 ) " + - "(end 25.40 3.6 )(stroke (width 0) (type default) " + - "(color 0 0 0 0))(fill (type none))))", - "start_draw": "(symbol", - "input_port": "(pin input line(at -5.15 0.54 0 )(length 5.08 )" + - "(name \"in\" (effects(font(size 1.27 1.27))))" + - "(number \"1\" (effects (font (size 1.27 1.27)))))", - "output_port": "(pin output line(at 30.52 0.54 180 )(length 5.08 )" + - "(name \"out\" (effects(font(size 1.27 1.27))))" + - "(number \"2\" (effects (font (size 1.27 1.27)))))", - "end_draw": "))" - } + # Ensure directory exists + os.makedirs(esim_config_dir, exist_ok=True) + + # Determine default workspace path + if os.name == 'nt': + user_home = os.path.join('library', 'config') + else: + user_home = os.path.expanduser('~') + + default_workspace = os.path.join(user_home, 'eSim-Workspace') + + # Create workspace directory if it doesn't exist + os.makedirs(default_workspace, exist_ok=True) + + # Create properly formatted workspace.txt + # Format expected by Appconfig.py: "workspace_check home_path" + # workspace_check: 1 (workspace is set) or 0 (not set) + # home_path: path to the workspace directory + config_content = f"1 {default_workspace}\n" + + with open(workspace_file, 'w') as f: + f.write(config_content) + + print(f"✓ Created proper workspace configuration:") + print(f" Content: '{config_content.strip()}'") + print(f" Workspace directory: {default_workspace}") + + return True + + except Exception as e: + print(f"✗ Error creating workspace configuration: {e}") + return False + +def fix_appconfig_robustness(): + """Provide suggestions for making Appconfig.py more robust""" + + robust_code = ''' +# Suggested improvement for Appconfig.py around line 46: +# Replace the problematic line with more robust parsing: + +# ORIGINAL (problematic): +# workspace_check, home = file.readline().split(' ', 1) + +# IMPROVED (robust): +try: + line = file.readline().strip() + if line: + parts = line.split(' ', 1) + if len(parts) >= 2: + workspace_check, home = parts[0], parts[1] + elif len(parts) == 1: + # Handle case where only workspace_check exists + workspace_check = parts[0] + home = os.path.join(os.path.expanduser('~'), 'eSim-Workspace') + print(f"Warning: Using default workspace path: {home}") + else: + # Handle empty line + workspace_check = '0' + home = os.path.join(os.path.expanduser('~'), 'eSim-Workspace') + print(f"Warning: Empty config, using defaults") + else: + # Handle empty file + workspace_check = '0' + home = os.path.join(os.path.expanduser('~'), 'eSim-Workspace') + print(f"Warning: Empty config file, using defaults") +except Exception as e: + # Handle any other parsing errors + print(f"Error reading config: {e}") + workspace_check = '0' + home = os.path.join(os.path.expanduser('~'), 'eSim-Workspace') + print(f"Using default configuration") +''' + + print("\n" + "=" * 60) + print("SUGGESTED ROBUST CODE FIX FOR APPCONFIG.PY:") + print("=" * 60) + print(robust_code) + +def main(): + """Main function to diagnose and fix configuration issues""" + print("eSim Configuration Diagnostic and Fix Tool") + print("=" * 45) + + # Step 1: Diagnose current state + print("\n1. Diagnosing current configuration...") + is_valid, config_dir, workspace_file = diagnose_config_files() + + # Step 2: Fix configuration if needed + if not is_valid: + print("\n2. Fixing configuration...") + success = create_proper_workspace_config(config_dir, workspace_file) + if success: + print("✓ Configuration fixed successfully") + else: + print("✗ Failed to fix configuration") + return + else: + print("\n2. Configuration appears to be valid") + + # Step 3: Provide code improvement suggestions + print("\n3. Providing robustness improvements...") + fix_appconfig_robustness() + + # Step 4: Final verification + print("\n4. Final verification...") + is_valid_final, _, _ = diagnose_config_files() + + if is_valid_final: + print("\n" + "=" * 50) + print("SUCCESS: Configuration is now properly set up!") + print("=" * 50) + print("You can now try running: python Application.py") + print("\nIf you still get errors, consider applying the") + print("robust code fix to Appconfig.py as shown above.") + else: + print("\n" + "=" * 50) + print("ISSUE: Configuration still has problems") + print("=" * 50) + print("Please check file permissions and try again.") + +if __name__ == "__main__": + main() diff --git a/src/maker/Maker.py b/src/maker/Maker.py index b9895f21d..ad32761ac 100755 --- a/src/maker/Maker.py +++ b/src/maker/Maker.py @@ -26,520 +26,570 @@ # REVISION: Tuesday 25, January 2022 # ========================================================================= -# importing the files and libraries -import hdlparse.verilog_parser as vlog -from PyQt5 import QtCore, QtWidgets -from PyQt5.QtCore import QThread -from configuration.Appconfig import Appconfig +# Import required libraries import os +import hdlparse.verilog_parser as vlog import watchdog.events import watchdog.observers from os.path import expanduser -home = expanduser("~") -# import inotify.adapters +from PyQt5 import QtCore, QtWidgets +from PyQt5.QtCore import QThread +from configuration.Appconfig import Appconfig -# declaring the global variables -# verilogfile stores the name of the file -# toggle flag stores the object of the toggling button +# Global variables +home = expanduser("~") verilogFile = [] toggle_flag = [] -# This function is called to accept TOS of makerchip def makerchipTOSAccepted(display=True): + """ + Function to accept Terms of Service of Makerchip + + Args: + display (bool): Whether to display the dialog + + Returns: + bool: True if TOS accepted, False otherwise + """ if not os.path.isfile(home + "/.makerchip_accepted"): if display: reply = QtWidgets.QMessageBox.warning( - None, "Terms of Service", "Please review the Makerchip \ - Terms of Service \ - (\ - https://www.makerchip.com/terms/). \ - Have you read and do you \ - accept these Terms of Service?", + None, + "Terms of Service", + "Please review the Makerchip Terms of Service " + "(" + "https://www.makerchip.com/terms/). " + "Have you read and do you accept these Terms of Service?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No ) - + if reply == QtWidgets.QMessageBox.Yes: - f = open(home + "/.makerchip_accepted", "w") - f.close() + with open(home + "/.makerchip_accepted", "w") as f: + f.close() return True - return False return True -# beginning class Maker. This class create the Maker Tab class Maker(QtWidgets.QWidget): - - # initailising the varaibles - def __init__(self, filecount): + """ + Main class for creating the Makerchip Tab widget + """ + + def __init__(self, filecount, is_dark_theme=False): + """ + Initialize the Maker widget + + Args: + filecount (int): File counter + is_dark_theme (bool): Whether to use dark theme + """ print(self) - + QtWidgets.QWidget.__init__(self) self.count = 0 self.text = "" self.filecount = filecount self.entry_var = {} - self.createMakerWidget() self.obj_Appconfig = Appconfig() + self.is_dark_theme = is_dark_theme + + # Initialize components + self.createMakerWidget() verilogFile.append("") - - # Creating the various components of the Widget(Maker Tab) + def createMakerWidget(self): - + """Create the main widget layout""" self.grid = QtWidgets.QGridLayout() self.setLayout(self.grid) - - self.grid.addWidget(self.createoptionsBox(), 0, 0, QtCore.Qt.AlignTop) - self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) - # self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) + + # Add spacing between widgets + self.grid.setVerticalSpacing(20) + self.grid.setContentsMargins(10, 10, 10, 10) + + # Add options box at the top + self.grid.addWidget(self.createoptionsBox(), 0, 0) + + # Add tlv file group below with proper spacing + self.grid.addWidget(self.creategroup(), 1, 0) + + # Apply initial theme styling + self.apply_theme_styling() + self.show() - - # This function is to Add new verilog file + + def apply_theme_styling(self): + """Apply theme styling to the Maker widget.""" + self.setObjectName("maker_widget") + if self.is_dark_theme: + self.setStyleSheet(""" + QWidget { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #23273a, stop:1 #181b24); color: #e8eaed; } + QGroupBox { border: 2px solid #40c4ff; border-radius: 14px; margin-top: 1em; padding: 15px; background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #23273a, stop:1 #181b24); color: #e8eaed; } + QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 5px; color: #40c4ff; font-weight: bold; font-size: 14px; } + QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #40c4ff, stop:1 #1976d2); color: #181b24; border: 1px solid #40c4ff; min-height: 35px; min-width: 120px; padding: 8px 15px; border-radius: 10px; font-weight: 700; font-size: 12px; } + QPushButton:hover { background: #1976d2; color: #fff; border: 1.5px solid #1976d2; } + QPushButton:pressed { background: #23273a; color: #40c4ff; border: 1.5px solid #40c4ff; } + QPushButton:disabled { background: #23273a; color: #888; border: 1px solid #23273a; } + QTextEdit { background: #23273a; color: #e8eaed; border: 1px solid #40c4ff; border-radius: 8px; padding: 10px; font-size: 12px; font-family: 'Consolas', 'Monaco', monospace; } + QLineEdit { background: #23273a; color: #e8eaed; border: 1px solid #40c4ff; border-radius: 8px; padding: 8px 12px; min-height: 30px; font-size: 12px; } + QLineEdit:focus { border: 1.5px solid #1976d2; } + QLabel { color: #e8eaed; } + """) + else: + self.setStyleSheet(""" + QWidget { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #f8f9fa); color: #2c3e50; } + QGroupBox { border: 2px solid #1976d2; border-radius: 14px; margin-top: 1em; padding: 15px; background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #f8f9fa); color: #2c3e50; } + QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 5px; color: #1976d2; font-weight: bold; font-size: 14px; } + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #f5f7fa, stop:1 #e3e8ee); + color: #1976d2; + border: 1px solid #b0bec5; + min-height: 35px; + min-width: 120px; + padding: 8px 15px; + border-radius: 10px; + font-weight: 700; + font-size: 12px; + } + QPushButton:hover { + background: #e3e8ee; + color: #1565c0; + border: 1.5px solid #1976d2; + } + QPushButton:pressed { + background: #cfd8dc; + color: #1976d2; + border: 1.5px solid #1976d2; + } + QPushButton:disabled { + background: #e1e4e8; + color: #7f8c8d; + border: 1px solid #e1e4e8; + } + QTextEdit { background: #ffffff; color: #2c3e50; border: 1px solid #1976d2; border-radius: 8px; padding: 10px; font-size: 12px; font-family: 'Consolas', 'Monaco', monospace; } + QLineEdit { background: #ffffff; color: #2c3e50; border: 1px solid #1976d2; border-radius: 8px; padding: 8px 12px; min-height: 30px; font-size: 12px; } + QLineEdit:focus { border: 1.5px solid #1565c0; } + QLabel { color: #2c3e50; } + """) + def addverilog(self): - - init_path = '../../' - if os.name == 'nt': - init_path = '' + """Add new Verilog file to the widget""" + init_path = '../../' if os.name != 'nt' else '' + self.verilogfile = QtCore.QDir.toNativeSeparators( QtWidgets.QFileDialog.getOpenFileName( - self, "Open Verilog Directory", - init_path + "home", "*v" + self, + "Open Verilog Directory", + init_path + "home", + "*v" )[0] ) + if self.verilogfile == "": self.verilogfile = self.entry_var[0].text() - + if self.verilogfile == "": reply = QtWidgets.QMessageBox.critical( None, "Error Message", - "No Verilog File Chosen. \ - Please choose a verilog file.", - QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) - + "No Verilog File Chosen. Please choose a verilog file.", + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel + ) + if reply == QtWidgets.QMessageBox.Ok: self.addverilog() - if self.verilogfile == "": return - self.obj_Appconfig.print_info('Add Verilog File Called') - + elif reply == QtWidgets.QMessageBox.Cancel: self.obj_Appconfig.print_info('No Verilog File Chosen') return - + + # Read and set file content self.text = open(self.verilogfile).read() self.entry_var[0].setText(self.verilogfile) self.entry_var[1].setText(self.text) + global verilogFile - verilogFile[self.filecount] = self.verilogfile + + # Setup file watching + self._setup_file_watcher() + + def _setup_file_watcher(self): + """Setup file watcher for automatic refresh""" if self.refreshoption in toggle_flag: toggle_flag.remove(self.refreshoption) - + self.observer = watchdog.observers.Observer() self.event_handler = Handler( self.verilogfile, self.refreshoption, - self.observer) - + self.observer + ) + self.observer.schedule( self.event_handler, path=self.verilogfile, - recursive=True) + recursive=True + ) self.observer.start() - # self.notify=notify(self.verilogfile,self.refreshoption) - # self.notify.start() - # open("filepath.txt","w").write(self.verilogfile) - - # This function is used to call refresh while - # running Ngspice to Verilog Converter - # (as the original one gets destroyed) + def refresh_change(self): + """ + Call refresh while running Ngspice to Verilog Converter + (as the original one gets destroyed) + """ if self.refreshoption in toggle_flag: self.toggle = toggle(self.refreshoption) self.toggle.start() - - # It is used to refresh the file in eSim if its edited anywhere else + def refresh(self): + """Refresh the file content if edited elsewhere""" if not hasattr(self, 'verilogfile'): return + self.text = open(self.verilogfile).read() self.entry_var[1].setText(self.text) + print("NgVeri File: " + self.verilogfile + " Refreshed") self.obj_Appconfig.print_info( - "NgVeri File: " + self.verilogfile + " Refreshed") - self.observer = watchdog.observers.Observer() - self.event_handler = Handler( - self.verilogfile, - self.refreshoption, - self.observer) - - self.observer.schedule( - self.event_handler, - path=self.verilogfile, - recursive=True) - self.observer.start() - # self.notify.start() + "NgVeri File: " + self.verilogfile + " Refreshed" + ) + + # Restart file watcher + self._setup_file_watcher() + global toggle_flag if self.refreshoption in toggle_flag: toggle_flag.remove(self.refreshoption) - - # This function is used to save the edited file in eSim + def save(self): + """Save the edited file""" try: wr = self.entry_var[1].toPlainText() - open(self.verilogfile, "w+").write(wr) + with open(self.verilogfile, "w+") as f: + f.write(wr) except BaseException as err: - self.msg = QtWidgets.QErrorMessage(self) - self.msg.setModal(True) - self.msg.setWindowTitle("Error Message") - self.msg.showMessage( + self._show_error_message( "Error in saving verilog file. Please check if it is chosen." ) - self.msg.exec_() print("Error in saving verilog file: " + str(err)) - - # This is used to run the makerchip-app + + def _show_error_message(self, message): + """Show error message dialog""" + self.msg = QtWidgets.QErrorMessage(self) + self.msg.setModal(True) + self.msg.setWindowTitle("Error Message") + self.msg.showMessage(message) + self.msg.exec_() + def runmakerchip(self): - init_path = '../../' - if os.name == 'nt': - init_path = '' + """Run the Makerchip IDE""" + init_path = '../../' if os.name != 'nt' else '' + try: if not makerchipTOSAccepted(True): return - + print("Running Makerchip IDE...........................") - # self.file = open(self.verilogfile,"w") - # self.file.write(self.entry_var[1].toPlainText()) - # self.file.close() filename = self.verilogfile + if self.verilogfile.split('.')[-1] != "tlv": - reply = QtWidgets.QMessageBox.warning( - None, - "Do you want to automate the top module? ", - "Click on YES button if you want the top module \ - to be added automatically. A .tlv file will be created \ - in the directory of current verilog file \ - and the Makerchip IDE will be running on \ - this file. Otherwise click on NO button. \ - To not open Makerchip IDE, click on CANCEL button. \ -

NOTE: Makerchip IDE requires an active \ - internet connection and a browser.", - QtWidgets.QMessageBox.Yes - | QtWidgets.QMessageBox.No - | QtWidgets.QMessageBox.Cancel) + reply = self._show_automation_dialog() + if reply == QtWidgets.QMessageBox.Cancel: return + if reply == QtWidgets.QMessageBox.Yes: - code = open(self.verilogfile).read() - text = code - filename = '.'.join( - self.verilogfile.split('.')[:-1]) + ".tlv" - file = os.path.basename('.'.join( - self.verilogfile.split('.')[:-1])) - f = open(filename, 'w') - code = code.replace(" wire ", " ") - code = code.replace(" reg ", " ") - vlog_ex = vlog.VerilogExtractor() - vlog_mods = vlog_ex.extract_objects_from_source(code) - lint_off = open( - init_path + "library/tlv/lint_off.txt" - ).readlines() - string = '''\\TLV_version 1d: tl-x.org\n\\SV\n''' - for item in lint_off: - string += "/* verilator lint_off " + \ - item.strip("\n") + "*/ " - string += '''\n\n//Your Verilog/System \ -Verilog Code Starts Here:\n''' + \ - text + '''\n\n//Top Module Code \ -Starts here:\n\tmodule top(input \ -logic clk, input logic reset, input logic [31:0] cyc_cnt, \ -output logic passed, output logic failed);\n''' - print(file) - for m in vlog_mods: - if m.name.lower() == file.lower(): - for p in m.ports: - if str( - p.name) != "clk" and str( - p.name) != "reset" and str( - p.name) != "cyc_cnt" and str( - p.name) != "passed" and str( - p.name) != "failed": - string += '\t\tlogic ' + p.data_type\ - + " " + p.name + ";//" + p.mode + "\n" - if m.name.lower() != file.lower(): - QtWidgets.QMessageBox.critical( - None, - "Error Message", - "Error: File name and module \ - name are not same. Please \ - ensure that they are same.", - QtWidgets.QMessageBox.Ok) - - self.obj_Appconfig.print_info( - 'NgVeri stopped due to file \ -name and module name not matching error') + filename = self._process_verilog_to_tlv(init_path) + if filename is None: return - string += "//The $random() can be replaced \ -if user wants to assign values\n" - for m in vlog_mods: - if m.name.lower() == file.lower(): - for p in m.ports: - if str( - p.mode) == "input" or str( - p.mode) == "inout": - if str( - p.name) != "clk" and str( - p.name) != "reset" and str( - p.name) != "cyc_cnt" and str( - p.name) != "passed" and str( - p.name) != "failed": - string += '\t\tassign ' + p.name\ - + " = " + "$random();\n" - - for m in vlog_mods: - if m.name.lower() == file.lower(): - string += '\t\t' + m.name + " " + m.name + '(' - i = 0 - for p in m.ports: - i = i + 1 - string += "."+p.name+"("+p.name+")" - if i == len(m.ports): - string += ");\n\t\n\\TLV\n//\ -Add \\TLV here if desired\ - \n\\SV\nendmodule\n\n" - else: - string += ", " - f.write(string) - + + # Start Makerchip process self.process = QtCore.QProcess(self) cmd = 'makerchip ' + filename print("File: " + filename) self.process.start(cmd) - print( - "Makerchip IDE command process pid ---------->", - self.process.pid()) + print("Makerchip IDE command process pid ---------->", self.process.pid()) + except BaseException as e: print(e) - self.msg = QtWidgets.QErrorMessage(self) - self.msg.setModal(True) - self.msg.setWindowTitle("Error Message") - self.msg.showMessage( - "Error in running Makerchip IDE. \ -Please check if verilog file is chosen.") - self.msg.exec_() - print("Error in running Makerchip IDE. \ -Please check if verilog file is chosen.") - # initial = self.read_file() - - # while True: - # current = self.read_file() - # if initial != current: - # for line in current: - # if line not in initial: - # print(line) - # initial = current - # self.processfile = QtCore.QProcess(self) - # self.processfile.start("python3 notify.py") - # print(self.processfile.readChannel()) - - # This creates the buttons/options - + self._show_error_message( + "Error in running Makerchip IDE. Please check if verilog file is chosen." + ) + print("Error in running Makerchip IDE. Please check if verilog file is chosen.") + + def _show_automation_dialog(self): + """Show automation confirmation dialog""" + return QtWidgets.QMessageBox.warning( + None, + "Do you want to automate the top module? ", + "Click on YES button if you want the top module to be added automatically. " + "A .tlv file will be created in the directory of current verilog file " + "and the Makerchip IDE will be running on this file. Otherwise click on NO button. " + "To not open Makerchip IDE, click on CANCEL button.

" + "NOTE: Makerchip IDE requires an active internet connection and a browser.", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel + ) + + def _process_verilog_to_tlv(self, init_path): + """Process Verilog file to TLV format""" + code = open(self.verilogfile).read() + text = code + filename = '.'.join(self.verilogfile.split('.')[:-1]) + ".tlv" + file = os.path.basename('.'.join(self.verilogfile.split('.')[:-1])) + + # Process Verilog code + code = code.replace(" wire ", " ") + code = code.replace(" reg ", " ") + + vlog_ex = vlog.VerilogExtractor() + vlog_mods = vlog_ex.extract_objects_from_source(code) + + # Read lint_off file + lint_off = open(init_path + "library/tlv/lint_off.txt").readlines() + + # Generate TLV content + string = self._generate_tlv_content(lint_off, text, file, vlog_mods) + + # Validate module name + if not self._validate_module_name(file, vlog_mods): + return None + + # Write TLV file + with open(filename, 'w') as f: + f.write(string) + + return filename + + def _generate_tlv_content(self, lint_off, text, file, vlog_mods): + """Generate TLV file content""" + string = '''\\TLV_version 1d: tl-x.org\n\\SV\n''' + + # Add lint_off directives + for item in lint_off: + string += "/* verilator lint_off " + item.strip("\n") + "*/ " + + string += '''\n\n//Your Verilog/System Verilog Code Starts Here:\n''' + text + string += '''\n\n//Top Module Code Starts here:\n\tmodule top(input logic clk, ''' + string += '''input logic reset, input logic [31:0] cyc_cnt, ''' + string += '''output logic passed, output logic failed);\n''' + + print(file) + + # Add port declarations + for m in vlog_mods: + if m.name.lower() == file.lower(): + for p in m.ports: + if str(p.name) not in ["clk", "reset", "cyc_cnt", "passed", "failed"]: + string += '\t\tlogic ' + p.data_type + " " + p.name + ";//" + p.mode + "\n" + + # Add random assignments + string += "//The $random() can be replaced if user wants to assign values\n" + for m in vlog_mods: + if m.name.lower() == file.lower(): + for p in m.ports: + if str(p.mode) in ["input", "inout"]: + if str(p.name) not in ["clk", "reset", "cyc_cnt", "passed", "failed"]: + string += '\t\tassign ' + p.name + " = " + "$random();\n" + + # Add module instantiation + for m in vlog_mods: + if m.name.lower() == file.lower(): + string += '\t\t' + m.name + " " + m.name + '(' + i = 0 + for p in m.ports: + i = i + 1 + string += "." + p.name + "(" + p.name + ")" + if i == len(m.ports): + string += ");\n\t\n\\TLV\n//Add \\TLV here if desired\n\\SV\nendmodule\n\n" + else: + string += ", " + + return string + + def _validate_module_name(self, file, vlog_mods): + """Validate that file name matches module name""" + for m in vlog_mods: + if m.name.lower() != file.lower(): + QtWidgets.QMessageBox.critical( + None, + "Error Message", + "Error: File name and module name are not same. " + "Please ensure that they are same.", + QtWidgets.QMessageBox.Ok + ) + self.obj_Appconfig.print_info( + 'NgVeri stopped due to file name and module name not matching error' + ) + return False + return True + def createoptionsBox(self): - + """Create the options/buttons box""" self.optionsbox = QtWidgets.QGroupBox() self.optionsbox.setTitle("Select Options") self.optionsgrid = QtWidgets.QGridLayout() - # self.optionsbox2 = QtWidgets.QGroupBox() - # self.optionsbox2.setTitle("Note: Please save the file once edited") - # self.optionsgrid2 = QtWidgets.QGridLayout() self.optionsgroupbtn = QtWidgets.QButtonGroup() + + # Set margins and spacing for the options grid + self.optionsgrid.setContentsMargins(15, 20, 15, 15) + self.optionsgrid.setSpacing(15) + + # Add Top Level Verilog Model button self.addoptions = QtWidgets.QPushButton("Add Top Level Verilog Model") self.optionsgroupbtn.addButton(self.addoptions) self.addoptions.clicked.connect(self.addverilog) - self.optionsgrid.addWidget(self.addoptions, 0, 1) - # self.optionsbox.setLayout(self.optionsgrid) - # self.grid.addWidget(self.creategroup(), 1, 0, 5, 0 + self.optionsgrid.addWidget(self.addoptions, 0, 0) + + # Refresh button self.refreshoption = QtWidgets.QPushButton("Refresh") self.optionsgroupbtn.addButton(self.refreshoption) self.refreshoption.clicked.connect(self.refresh) - self.optionsgrid.addWidget(self.refreshoption, 0, 2) - # self.optionsbox.setLayout(self.optionsgrid) - # self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) + self.optionsgrid.addWidget(self.refreshoption, 0, 1) + + # Save button self.saveoption = QtWidgets.QPushButton("Save") self.optionsgroupbtn.addButton(self.saveoption) self.saveoption.clicked.connect(self.save) - self.optionsgrid.addWidget(self.saveoption, 0, 3) - # self.optionsbox.setLayout(self.optionsgrid) - # self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) + self.optionsgrid.addWidget(self.saveoption, 0, 2) + + # Edit in Makerchip IDE button self.runoptions = QtWidgets.QPushButton("Edit in Makerchip IDE") - self.runoptions.setToolTip( - "Requires internet connection and a browser" - ) + self.runoptions.setToolTip("Requires internet connection and a browser") self.runoptions.setToolTipDuration(5000) self.optionsgroupbtn.addButton(self.runoptions) self.runoptions.clicked.connect(self.runmakerchip) - self.optionsgrid.addWidget(self.runoptions, 0, 4) - # self.optionsbox.setLayout(self.optionsgrid) - # self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) + self.optionsgrid.addWidget(self.runoptions, 0, 3) + + # Accept TOS button (if needed) if not makerchipTOSAccepted(False): self.acceptTOS = QtWidgets.QPushButton("Accept Makerchip TOS") self.optionsgroupbtn.addButton(self.acceptTOS) self.acceptTOS.clicked.connect(lambda: makerchipTOSAccepted(True)) - self.optionsgrid.addWidget(self.acceptTOS, 0, 5) - # self.optionsbox.setLayout(self.optionsgrid) - # self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) + self.optionsgrid.addWidget(self.acceptTOS, 0, 4) + self.optionsbox.setLayout(self.optionsgrid) return self.optionsbox - - # This function adds the other parts of widget like text box + def creategroup(self): + """Create the text editor group""" self.trbox = QtWidgets.QGroupBox() self.trbox.setTitle(".tlv file") - # self.trbox.setDisabled(True) - # self.trbox.setVisible(False) self.trgrid = QtWidgets.QGridLayout() - self.trbox.setLayout(self.trgrid) - + + # Set margins and spacing for the tlv grid + self.trgrid.setContentsMargins(10, 15, 10, 10) + self.trgrid.setSpacing(10) + + # Path label and field self.start = QtWidgets.QLabel("Path to .tlv file") - self.trgrid.addWidget(self.start, 1, 0) + self.trgrid.addWidget(self.start, 0, 0) + self.count = 0 self.entry_var[self.count] = QtWidgets.QLabel() - self.trgrid.addWidget(self.entry_var[self.count], 1, 1) + self.trgrid.addWidget(self.entry_var[self.count], 0, 1) self.entry_var[self.count].setMaximumWidth(1000) self.count += 1 - - # CSS - self.trbox.setStyleSheet(" \ - QGroupBox { border: 1px solid gray; border-radius: \ - 9px; margin-top: 0.5em; } \ - QGroupBox::title { subcontrol-origin: margin; left: \ - 10px; padding: 0 3px 0 3px; } \ - ") - + + # Code editor self.start = QtWidgets.QLabel(".tlv code") - # self.start2 = QtWidgets.QLabel("Note: \ - # Please save the file once edited") - # self.start2.setStyleSheet("background-color: red") - self.trgrid.addWidget(self.start, 2, 0) - # self.trgrid.addWidget(self.start2, 3,0) + self.trgrid.addWidget(self.start, 1, 0) + self.entry_var[self.count] = QtWidgets.QTextEdit() - self.trgrid.addWidget(self.entry_var[self.count], 2, 1) + self.trgrid.addWidget(self.entry_var[self.count], 1, 1) self.entry_var[self.count].setMaximumWidth(1000) - self.entry_var[self.count].setMaximumHeight(1000) - # self.entry_var[self.count].textChanged.connect(self.save) + self.entry_var[self.count].setMinimumHeight(300) # Set minimum height self.count += 1 - - # CSS - self.trbox.setStyleSheet(" \ - QGroupBox { border: 1px solid gray; border-radius: \ - 9px; margin-top: 0.5em; } \ - QGroupBox::title { subcontrol-origin: margin; left: \ - 10px; padding: 0 3px 0 3px; } \ - ") - + + self.trbox.setLayout(self.trgrid) return self.trbox + def set_theme(self, is_dark_theme): + """Update the theme and re-apply styling.""" + self.is_dark_theme = is_dark_theme + self.apply_theme_styling() + -# The Handler class is used to create a watch on the files using WatchDog class Handler(watchdog.events.PatternMatchingEventHandler): - # this function initialisses the variable and the objects of watchdog + """ + Handler class for file watching using WatchDog + """ + def __init__(self, verilogfile, refreshoption, observer): - # Set the patterns for PatternMatchingEventHandler + """ + Initialize the file handler + + Args: + verilogfile (str): Path to the Verilog file + refreshoption (QPushButton): Refresh button reference + observer (Observer): File observer instance + """ watchdog.events.PatternMatchingEventHandler.__init__( - self, ignore_directories=True, case_sensitive=False) + self, + ignore_directories=True, + case_sensitive=False + ) self.verilogfile = verilogfile self.refreshoption = refreshoption self.obj_Appconfig = Appconfig() self.observer = observer self.toggle = toggle(self.refreshoption) - - # if a file is modified, toggle starts to toggle the refresh button + def on_modified(self, event): - print("Watchdog received modified event - % s." % event.src_path) + """Handle file modification events""" + print("Watchdog received modified event - %s." % event.src_path) + msg = QtWidgets.QErrorMessage() msg.setWindowTitle("eSim Message") msg.showMessage( - "NgVeri File: " + - self.verilogfile + - " modified. Please click on Refresh") + "NgVeri File: " + self.verilogfile + " modified. Please click on Refresh" + ) msg.exec_() - print("NgVeri File: " + self.verilogfile + - " modified. Please click on Refresh") - # self.obj_Appconfig.print_info("NgVeri File:\ - # "+self.verilogfile+" modified. Please click on Refresh") + + print("NgVeri File: " + self.verilogfile + " modified. Please click on Refresh") + global toggle_flag if self.refreshoption not in toggle_flag: toggle_flag.append(self.refreshoption) - # i.rm_watch() + self.observer.stop() self.toggle.start() -# class notify(QThread): -# def __init__(self,verilogfile,refreshoption):#,obj_Appconfig): -# QThread.__init__(self) -# self.verilogfile=verilogfile -# self.refreshoption=refreshoption -# self.obj_Appconfig = Appconfig() -# self.toggle=toggle(self.refreshoption) - - -# def __del__(self): -# self.wait() - -# def run(self): -# i = inotify.adapters.Inotify() - -# i.add_watch(self.verilogfile) - -# for event in i.event_gen(): -# if not self.refreshoption.isVisible(): -# break -# if event!=None: -# print(event) -# if "IN_CLOSE_WRITE" in event[1] : -# msg = QtWidgets.QErrorMessage() -# msg.setModal(True) -# msg.setWindowTitle("eSim Message") -# msg.showMessage( -# "NgVeri File: "+self.verilogfile+"\ -# modified. Please click on Refresh") -# msg.exec_() -# print("NgVeri File: "+self.verilogfile+"\ -# modified. Please click on Refresh") -# # self.obj_Appconfig.print_info("NgVeri File: \ -# "+self.verilogfile+" modified. Please click on Refresh") -# global toggle_flag -# toggle_flag.append(self.refreshoption) -# #i.rm_watch() -# self.toggle.start() -# break - - -# This class is used to toggle a button(change colour by toggling) class toggle(QThread): - # initialising the threads + """ + Class to toggle button appearance (change color by toggling) + """ + def __init__(self, option): + """ + Initialize the toggle thread + + Args: + option (QPushButton): Button to toggle + """ QThread.__init__(self) self.option = option - + def __del__(self): + """Clean up the thread""" self.wait() - - # running the thread to toggle + def run(self): - + """Run the toggle thread""" while True: self.option.setStyleSheet("background-color: red") self.sleep(1) self.option.setStyleSheet("background-color: none") self.sleep(1) + print(toggle_flag) + if not self.option.isVisible(): break if self.option not in toggle_flag: - break + break \ No newline at end of file diff --git a/src/maker/ModelGeneration.py b/src/maker/ModelGeneration.py index f6afd5c00..7dce1de70 100755 --- a/src/maker/ModelGeneration.py +++ b/src/maker/ModelGeneration.py @@ -167,12 +167,6 @@ def verilogParse(self): code = code.replace("wire", " ") code = code.replace("reg", " ") - - header_re = re.compile(r'module\s+\w+\s*\((.*?)\)\s*;', re.S) - def _split_ports(match): - # add a newline after every comma that is inside the header - return match.group(0).replace(',', ',\n') - code = header_re.sub(_split_ports, code) vlog_ex = vlog.VerilogExtractor() vlog_mods = vlog_ex.extract_objects_from_source(code) f = open(self.modelpath + "connection_info.txt", 'w') @@ -724,7 +718,7 @@ def sim_main(self): int foo_''' + self.fname.split('.')[0] + '''(int init,int count) { int argc=1; - const char* argv[]={"fullverbose"}; + char* argv[]={"fullverbose"}; Verilated::commandArgs(argc, argv); static VerilatedContext* contextp = new VerilatedContext; static V''' + self.fname.split('.')[0] + "* " + \ diff --git a/src/maker/NgVeri.py b/src/maker/NgVeri.py index 1678248fe..8cbd17e94 100755 --- a/src/maker/NgVeri.py +++ b/src/maker/NgVeri.py @@ -41,10 +41,11 @@ class NgVeri(QtWidgets.QWidget): ''' This class create the NgVeri Tab ''' - def __init__(self, filecount): + def __init__(self, filecount, is_dark_theme=False): QtWidgets.QWidget.__init__(self) # Maker.addverilog(self) self.obj_Appconfig = Appconfig() + self.is_dark_theme = is_dark_theme if os.name == 'nt': self.home = os.path.join('library', 'config') @@ -77,8 +78,74 @@ def createNgveriWidget(self): self.grid.addWidget(self.createoptionsBox(), 0, 0, QtCore.Qt.AlignTop) self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) + # Apply initial theme styling + self.apply_theme_styling() + self.show() + def apply_theme_styling(self): + """Apply theme styling to the NgVeri widget.""" + self.setObjectName("ngveri_widget") + + if self.is_dark_theme: + self.setStyleSheet(""" + QWidget { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #23273a, stop:1 #181b24); color: #e8eaed; } + QGroupBox { border: 2px solid #40c4ff; border-radius: 14px; margin-top: 1em; padding: 15px; background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #23273a, stop:1 #181b24); color: #e8eaed; } + QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 5px; color: #40c4ff; font-weight: bold; font-size: 14px; } + QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #40c4ff, stop:1 #1976d2); color: #181b24; border: 1px solid #40c4ff; min-height: 35px; min-width: 120px; padding: 8px 15px; border-radius: 10px; font-weight: 700; font-size: 12px; } + QPushButton:hover { background: #1976d2; color: #fff; border: 1.5px solid #1976d2; } + QPushButton:pressed { background: #23273a; color: #40c4ff; border: 1.5px solid #40c4ff; } + QPushButton:disabled { background: #23273a; color: #888; border: 1px solid #23273a; } + QTextEdit { background: #23273a; color: #e8eaed; border: 1px solid #40c4ff; border-radius: 8px; padding: 10px; font-size: 12px; font-family: 'Consolas', 'Monaco', monospace; } + QComboBox { background: #23273a; color: #e8eaed; border: 1px solid #40c4ff; border-radius: 8px; padding: 5px 10px; min-height: 30px; font-size: 12px; } + QComboBox:hover { border: 1.5px solid #1976d2; } + QComboBox::drop-down { border: none; width: 20px; } + QComboBox::down-arrow { width: 12px; height: 12px; } + QLineEdit { background: #23273a; color: #e8eaed; border: 1px solid #40c4ff; border-radius: 8px; padding: 8px 12px; min-height: 30px; font-size: 12px; } + QLineEdit:focus { border: 1.5px solid #1976d2; } + QLabel { color: #e8eaed; } + """) + else: + self.setStyleSheet(""" + QWidget { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #f8f9fa); color: #2c3e50; } + QGroupBox { border: 2px solid #1976d2; border-radius: 14px; margin-top: 1em; padding: 15px; background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #f8f9fa); color: #2c3e50; } + QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 5px; color: #1976d2; font-weight: bold; font-size: 14px; } + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #f5f7fa, stop:1 #e3e8ee); + color: #1976d2; + border: 1px solid #b0bec5; + min-height: 35px; + min-width: 120px; + padding: 8px 15px; + border-radius: 10px; + font-weight: 700; + font-size: 12px; + } + QPushButton:hover { + background: #e3e8ee; + color: #1565c0; + border: 1.5px solid #1976d2; + } + QPushButton:pressed { + background: #cfd8dc; + color: #1976d2; + border: 1.5px solid #1976d2; + } + QPushButton:disabled { + background: #e1e4e8; + color: #7f8c8d; + border: 1px solid #e1e4e8; + } + QTextEdit { background: #ffffff; color: #2c3e50; border: 1px solid #1976d2; border-radius: 8px; padding: 10px; font-size: 12px; font-family: 'Consolas', 'Monaco', monospace; } + QComboBox { background: #ffffff; color: #2c3e50; border: 1px solid #1976d2; border-radius: 8px; padding: 5px 10px; min-height: 30px; font-size: 12px; } + QComboBox:hover { border: 1.5px solid #1565c0; } + QComboBox::drop-down { border: none; width: 20px; } + QComboBox::down-arrow { width: 12px; height: 12px; } + QLineEdit { background: #ffffff; color: #2c3e50; border: 1px solid #1976d2; border-radius: 8px; padding: 8px 12px; min-height: 30px; font-size: 12px; } + QLineEdit:focus { border: 1.5px solid #1565c0; } + QLabel { color: #2c3e50; } + """) + def addverilog(self): ''' Adding the verilog file in Maker tab to Ngveri Tab automatically @@ -231,9 +298,12 @@ def createoptionsBox(self): self.optionsbox = QtWidgets.QGroupBox() self.optionsbox.setTitle("Select Options") self.optionsgrid = QtWidgets.QGridLayout() - self.optionsgroupbtn = QtWidgets.QButtonGroup() + # Set margins and spacing for better layout + self.optionsgrid.setContentsMargins(15, 20, 15, 15) + self.optionsgrid.setSpacing(15) + self.addverilogbutton = QtWidgets.QPushButton( "Convert Verilog to Ngspice") self.addverilogbutton.setToolTip( @@ -243,30 +313,23 @@ def createoptionsBox(self): self.optionsgroupbtn.addButton(self.addverilogbutton) self.addverilogbutton.clicked.connect(self.addverilog) self.optionsgrid.addWidget(self.addverilogbutton, 0, 1) - # self.optionsbox.setLayout(self.optionsgrid) - # self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) self.addfilebutton = QtWidgets.QPushButton("Add dependency files") self.optionsgroupbtn.addButton(self.addfilebutton) self.addfilebutton.clicked.connect(self.addfile) self.optionsgrid.addWidget(self.addfilebutton, 0, 2) - # self.optionsbox.setLayout(self.optionsgrid) - # self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) self.addfolderbutton = QtWidgets.QPushButton("Add dependency folder") self.optionsgroupbtn.addButton(self.addfolderbutton) self.addfolderbutton.clicked.connect(self.addfolder) self.optionsgrid.addWidget(self.addfolderbutton, 0, 3) - # self.optionsbox.setLayout(self.optionsgrid) - # self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) self.clearTerminalBtn = QtWidgets.QPushButton("Clear Terminal") self.optionsgroupbtn.addButton(self.clearTerminalBtn) self.clearTerminalBtn.clicked.connect(self.clearTerminal) self.optionsgrid.addWidget(self.clearTerminalBtn, 0, 4) - self.optionsbox.setLayout(self.optionsgrid) - # self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) + self.optionsbox.setLayout(self.optionsgrid) return self.optionsbox def edit_modlst(self, text): @@ -338,21 +401,14 @@ def lint_off_edit(self, text): QtWidgets.QMessageBox.Cancel) if ret == QtWidgets.QMessageBox.Ok: - try: - file_path = os.path.join(init_path, "library/tlv/lint_off.txt") - with open(file_path, 'r') as file: - data = file.readlines() - data = [line for line in data if line.strip() != text] - with open(file_path, 'w') as file: - file.writelines(data) - - except Exception as e: - QtWidgets.QMessageBox.warning( - None, - "Warning", - f"Could not remove lint_off entry '{text}'", - QtWidgets.QMessageBox.Ok - ) + file = open(init_path + "library/tlv/lint_off.txt", 'r') + data = file.readlines() + file.close() + + data.remove(text + "\n") + file = open(init_path + "library/tlv/lint_off.txt", 'w') + for item in data: + file.write(item) def add_lint_off(self): ''' @@ -379,14 +435,15 @@ def creategroup(self): ''' self.trbox = QtWidgets.QGroupBox() self.trbox.setTitle("Terminal") - # self.trbox.setDisabled(True) - # self.trbox.setVisible(False) self.trgrid = QtWidgets.QGridLayout() self.trbox.setLayout(self.trgrid) self.count = 0 + # Set margins and spacing for better layout + self.trgrid.setContentsMargins(15, 20, 15, 15) + self.trgrid.setSpacing(15) + self.start = QtWidgets.QLabel("Terminal") - # self.trgrid.addWidget(self.start, 2,0) self.entry_var[self.count] = QtWidgets.QTextEdit() self.entry_var[self.count].setReadOnly(1) self.trgrid.addWidget(self.entry_var[self.count], 1, 1, 5, 3) @@ -405,6 +462,7 @@ def creategroup(self): self.entry_var[self.count].activated[str].connect(self.edit_modlst) self.trgrid.addWidget(self.entry_var[self.count], 1, 4, 1, 2) self.count += 1 + self.entry_var[self.count] = QtWidgets.QComboBox() self.entry_var[self.count].addItem("Remove lint_off") @@ -421,23 +479,23 @@ def creategroup(self): self.entry_var[self.count].activated[str].connect(self.lint_off_edit) self.trgrid.addWidget(self.entry_var[self.count], 2, 4, 1, 2) self.count += 1 + self.entry_var[self.count] = QtWidgets.QLineEdit(self) self.trgrid.addWidget(self.entry_var[self.count], 3, 4) - self.entry_var[self.count].setMaximumWidth(100) + self.entry_var[self.count].setMaximumWidth(200) self.count += 1 + self.entry_var[self.count] = QtWidgets.QPushButton("Add lint_off") - self.entry_var[self.count].setMaximumWidth(100) + self.entry_var[self.count].setMaximumWidth(150) self.trgrid.addWidget(self.entry_var[self.count], 3, 5) self.entry_var[self.count].clicked.connect(self.add_lint_off) self.count += 1 - # CSS - self.trbox.setStyleSheet(" \ - QGroupBox { border: 1px solid gray; border-radius: \ - 9px; margin-top: 0.5em; } \ - QGroupBox::title { subcontrol-origin: margin; left: \ - 10px; padding: 0 3px 0 3px; } \ - ") - + self.trbox.setLayout(self.trgrid) return self.trbox + + def set_theme(self, is_dark_theme): + """Update the theme and re-apply styling.""" + self.is_dark_theme = is_dark_theme + self.apply_theme_styling() diff --git a/src/maker/createkicad.py b/src/maker/createkicad.py index ccc197198..3f30e8359 100644 --- a/src/maker/createkicad.py +++ b/src/maker/createkicad.py @@ -189,14 +189,9 @@ def removeOldLibrary(self): def createSym(self): ''' creating the symbol - (pins snapped to KiCad-6 grid) ''' - self.grid = 0.635 - self.dist_port = 4 * self.grid # Distance between two ports # 100 mil (= 2.54 mm) - self.inc_size = self.dist_port # Increment size of a block (mil) - def snap(val): - snapped = round(float(val) / self.grid) * self.grid - return f"{snapped:.3f}" + self.dist_port = 2.54 # Distance between two ports (mil) + self.inc_size = 2.54 # Increment size of a block (mil) cwd = os.getcwd() os.chdir(self.lib_loc) print("Changing directory to ", self.lib_loc) @@ -255,7 +250,7 @@ def snap(val): draw_pos = \ [w.replace('comp_name', f"{self.modelname}_0_1") for w in draw_pos] - draw_pos[8] = snap(float(draw_pos[8]) + # previously it is (-) + draw_pos[8] = str(float(draw_pos[8]) + # previously it is (-) float(self.findBlockSize() * self.inc_size)) draw_pos_rec = draw_pos[8] @@ -270,8 +265,6 @@ def snap(val): input_port = input_port.split() output_port = self.template["output_port"] output_port = output_port.split() - input_port[3] = snap(float(input_port[3])) - output_port[3] = snap(float(output_port[3])) inputs = self.portInfo[0: self.input_length] outputs = self.portInfo[self.input_length:] inputName = [] @@ -305,7 +298,7 @@ def snap(val): input_port[9] = f"\"{inputName[i]}\"" input_port[13] = f"\"{str(i + 1)}\"" input_port[4] = \ - snap(float(input_port[4]) - float(self.dist_port)) + str(float(input_port[4]) - float(self.dist_port)) input_list = ' '.join(input_port) port_list.append(input_list) j = j + 1 @@ -314,7 +307,7 @@ def snap(val): output_port[9] = f"\"{outputName[i - inputs]}\"" output_port[13] = f"\"{str(i + 1)}\"" output_port[4] = \ - snap(float(output_port[4]) - float(self.dist_port)) + str(float(output_port[4]) - float(self.dist_port)) output_list = ' '.join(output_port) port_list.append(output_list) diff --git a/src/maker/makerchip.py b/src/maker/makerchip.py index 152c6cbb7..0127e2aa6 100755 --- a/src/maker/makerchip.py +++ b/src/maker/makerchip.py @@ -41,13 +41,8 @@ class makerchip(QtWidgets.QWidget): # initialising the variables def __init__(self, parent=None): QtWidgets.QWidget.__init__(self) - - # filecount=int(open("a.txt",'r').read()) - print(filecount) - # self.splitter.setOrientation(QtCore.Qt.Vertical) - print("==================================") - print("Makerchip and Verilog to Ngspice Converter") - print("==================================") + self.maker_widget = None + self.ngveri_widget = None self.createMainWindow() # Creating the main Window(Main tab) @@ -65,31 +60,57 @@ def createMainWindow(self): # Creating the maker and ngveri widgets def createWidget(self): - global obj_Maker global filecount self.convertWindow = QtWidgets.QWidget() + # Get current theme from parent Application if possible + is_dark_theme = False + parent = self.parent() + if parent and hasattr(parent, 'is_dark_theme'): + is_dark_theme = parent.is_dark_theme + self.MakerTab = QtWidgets.QScrollArea() - obj_Maker = Maker.Maker(filecount) - self.MakerTab.setWidget(obj_Maker) + self.maker_widget = Maker.Maker(filecount, is_dark_theme=is_dark_theme) + self.MakerTab.setWidget(self.maker_widget) self.MakerTab.setWidgetResizable(True) - global obj_NgVeri self.NgVeriTab = QtWidgets.QScrollArea() - obj_NgVeri = NgVeri.NgVeri(filecount) - self.NgVeriTab.setWidget(obj_NgVeri) + self.ngveri_widget = NgVeri.NgVeri(filecount, is_dark_theme=is_dark_theme) + self.NgVeriTab.setWidget(self.ngveri_widget) self.NgVeriTab.setWidgetResizable(True) + self.tabWidget = QtWidgets.QTabWidget() self.tabWidget.addTab(self.MakerTab, "Makerchip") self.tabWidget.addTab(self.NgVeriTab, "NgVeri") - # The object refresh gets destroyed when Ngspice\ - # to verilog converter is called - # so calling refresh_change to start toggling of refresh again - self.tabWidget.currentChanged.connect(obj_Maker.refresh_change) + + # Re-apply theme on tab change + self.tabWidget.currentChanged.connect(self._on_tab_changed) + self.mainLayout = QtWidgets.QVBoxLayout() + self.mainLayout.setContentsMargins(15, 15, 15, 15) # Add margins + self.mainLayout.setSpacing(15) # Add spacing self.mainLayout.addWidget(self.tabWidget) + self.convertWindow.setLayout(self.mainLayout) self.convertWindow.show() - # incrementing filecount for every new window + filecount = filecount + 1 return self.convertWindow + + def set_theme(self, is_dark_theme): + """Update the theme for both Maker and NgVeri widgets.""" + if self.maker_widget: + self.maker_widget.set_theme(is_dark_theme) + if self.ngveri_widget: + self.ngveri_widget.set_theme(is_dark_theme) + + def _on_tab_changed(self, index): + # Ensure the correct theme is always applied to the active tab + is_dark_theme = False + parent = self.parent() + if parent and hasattr(parent, 'is_dark_theme'): + is_dark_theme = parent.is_dark_theme + if index == 0 and self.maker_widget: + self.maker_widget.set_theme(is_dark_theme) + elif index == 1 and self.ngveri_widget: + self.ngveri_widget.set_theme(is_dark_theme) diff --git a/src/modelEditor/ModelEditor.py b/src/modelEditor/ModelEditor.py index cddcc78f5..2b59c275b 100644 --- a/src/modelEditor/ModelEditor.py +++ b/src/modelEditor/ModelEditor.py @@ -1,3 +1,9 @@ +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from configuration.Appconfig import Appconfig + from PyQt5 import QtWidgets, QtCore from PyQt5.Qt import QTableWidgetItem import xml.etree.ElementTree as ET @@ -28,9 +34,11 @@ class ModelEditorclass(QtWidgets.QWidget): - Magnetic Core magnetic_click ''' - def __init__(self): + def __init__(self, is_dark_theme=False): QtWidgets.QWidget.__init__(self) + self.is_dark_theme = is_dark_theme + self.init_path = '../../' if os.name == 'nt': self.init_path = '' @@ -112,8 +120,157 @@ def __init__(self): self.grid.addWidget(self.igbt, 7, 1) self.grid.addWidget(self.magnetic, 8, 1) self.setLayout(self.grid) + + # Apply initial theme styling + self.apply_theme_styling() + self.show() + def apply_theme_styling(self): + """Apply theme styling to the model editor widgets.""" + self.setObjectName("model_editor") + if self.is_dark_theme: + self.setStyleSheet(""" + QWidget { background: transparent; } + QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #40c4ff, stop:1 #1976d2); color: #181b24; border: 1px solid #40c4ff; min-height: 35px; min-width: 120px; padding: 8px 15px; border-radius: 10px; font-weight: 700; font-size: 12px; } + QPushButton:hover { background: #1976d2; color: #fff; border: 1.5px solid #1976d2; } + QPushButton:pressed { background: #23273a; color: #40c4ff; border: 1.5px solid #40c4ff; } + QPushButton:disabled { background: #23273a; color: #888; border: 1px solid #23273a; } + QRadioButton { color: #e8eaed; font-weight: 600; font-size: 13px; } + QRadioButton::indicator { width: 16px; height: 16px; border: 2px solid #40c4ff; border-radius: 8px; background: #23273a; } + QRadioButton::indicator:checked { background: #40c4ff; border: 2px solid #40c4ff; } + QComboBox { background: #23273a; color: #e8eaed; border: 1px solid #40c4ff; border-radius: 8px; padding: 5px 10px; min-height: 30px; font-size: 12px; } + QComboBox:hover { border: 1.5px solid #1976d2; } + QComboBox::drop-down { border: none; width: 20px; } + QComboBox::down-arrow { width: 12px; height: 12px; } + QTableWidget { background: #23273a; color: #e8eaed; border: 1px solid #40c4ff; border-radius: 8px; gridline-color: #40c4ff; font-size: 12px; } + QTableWidget::item { padding: 8px; border-bottom: 1px solid #181b24; } + QTableWidget::item:selected { background: #40c4ff; color: #181b24; } + QHeaderView::section { background: #181b24; color: #40c4ff; border: 1px solid #40c4ff; padding: 8px; font-weight: 700; } + QLabel { color: #e8eaed; } + """) + else: + self.setStyleSheet(""" + QGroupBox { + border-radius: 14px; + border: 2px solid #1976d2; + margin-top: 1em; + padding: 15px; + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #ffffff, stop:1 #f8f9fa); + color: #2c3e50; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 15px; + padding: 0 5px; + color: #1976d2; + font-weight: 600; + font-size: 14px; + } + QPushButton { + background: #ffffff; + color: #000000; + border: 1px solid #cccccc; + min-height: 25px; + min-width: 80px; + padding: 5px 10px; + border-radius: 6px; + font-weight: 600; + font-size: 12px; + } + QPushButton:hover { + background: #1976d2; + color: #ffffff; + border: 1px solid #1976d2; + } + QPushButton:pressed { + background: #1565c0; + color: #ffffff; + border: 1px solid #1565c0; + } + QPushButton:disabled { + background: #f5f5f5; + color: #999999; + border: 1px solid #e0e0e0; + } + QRadioButton { + color: #2c3e50; + font-weight: 200; + font-size: 13px; + spacing: 10px; + } + + QRadioButton::indicator { + width: 16px; + height: 16px; + # border: 2px solid #1976d2; + border-radius: 8px; + background-color: #ffffff; + margin-right: 6px; + } + + QRadioButton::indicator:checked { + background-color: #ffffff; + # border: 2px solid #1976d2; + } + + QRadioButton::indicator:checked:after { + content: ""; + background-color: #1976d2; + border-radius: 4px; + width: 8px; + height: 8px; + margin: 4px; + display: block; + } + QComboBox { + background: #ffffff; + color: #2c3e50; + border: 1px solid #1976d2; + border-radius: 8px; + padding: 5px 10px; + min-height: 30px; + font-size: 12px; + } + QComboBox:hover { + border: 1.5px solid #1565c0; + } + QComboBox::drop-down { + border: none; + width: 20px; + } + QComboBox::down-arrow { + width: 12px; + height: 12px; + } + QTableWidget { + background: #ffffff; + color: #2c3e50; + border: 1px solid #1976d2; + border-radius: 8px; + gridline-color: #1976d2; + font-size: 12px; + } + QTableWidget::item { + padding: 8px; + border-bottom: 1px solid #f8f9fa; + } + QTableWidget::item:selected { + background: #1976d2; + color: #ffffff; + } + QHeaderView::section { + background: #f8f9fa; + color: #1976d2; + border: 1px solid #1976d2; + padding: 8px; + font-weight: 700; + } + QLabel { + color: #2c3e50; +} +""") def opennew(self): ''' - To create New Model file @@ -869,3 +1026,8 @@ def converttoxml(self): os.chdir(defaultcwd) libopen.close() libopen1.close() + + def set_theme(self, is_dark_theme): + """Update the theme and re-apply styling.""" + self.is_dark_theme = is_dark_theme + self.apply_theme_styling() diff --git a/src/modelEditor/ModelicaMapping.json b/src/modelEditor/ModelicaMapping.json new file mode 100644 index 000000000..1cd3982e1 --- /dev/null +++ b/src/modelEditor/ModelicaMapping.json @@ -0,0 +1 @@ +{"Modelica": {"Basic": {}, "Electrical": {}, "Analog": {}}} diff --git a/src/ngspiceSimulation/NgspiceWidget.py b/src/ngspiceSimulation/NgspiceWidget.py index 6d8a9d742..cec54b7ad 100644 --- a/src/ngspiceSimulation/NgspiceWidget.py +++ b/src/ngspiceSimulation/NgspiceWidget.py @@ -2,7 +2,7 @@ from PyQt5 import QtWidgets, QtCore from configuration.Appconfig import Appconfig from frontEnd import TerminalUi -from configparser import ConfigParser + # This Class creates NgSpice Window class NgspiceWidget(QtWidgets.QWidget): @@ -34,6 +34,17 @@ def __init__(self, netlist, simEndSignal, plotFlag): self.layout = QtWidgets.QVBoxLayout(self) self.layout.addWidget(self.terminalUi) + # --- Ensure the correct theme is applied immediately --- + app_parent = self.parent() + is_dark_theme = False + while app_parent is not None: + if hasattr(app_parent, 'is_dark_theme'): + is_dark_theme = app_parent.is_dark_theme + break + app_parent = app_parent.parent() if hasattr(app_parent, 'parent') else None + if hasattr(self, 'terminalUi') and hasattr(self.terminalUi, 'set_theme'): + self.terminalUi.set_theme(is_dark_theme) + # Receiving the plotFlag self.plotFlag = plotFlag print("Value of plotFlag: ", self.plotFlag) @@ -188,30 +199,30 @@ def finishSimulation(self, exitCode, exitStatus, def plotFlagFunc(self,projPath,command): if self.plotFlag == True: + print("reached here too") if os.name == 'nt': parser_nghdl = ConfigParser() - config_path = os.path.join('library', 'config', '.nghdl', 'config.ini') - parser_nghdl.read(config_path) + parser_nghdl.read( + os.path.join('library', 'config', '.nghdl', 'config.ini') + ) + msys_home = parser_nghdl.get('COMPILER', 'MSYS_HOME') + tempdir = os.getcwd() projPath = self.obj_appconfig.current_project["ProjectName"] os.chdir(projPath) - - self.command = ( - 'cmd /c "start /min ' + - msys_home + '/usr/bin/mintty.exe ngspice -p ' + command + '"' - ) - - # Create a new QProcess for mintty - self.minttyProcess = QtCore.QProcess(self) - self.minttyProcess.start(self.command) + self.command = 'cmd /c ' + '"start /min ' + \ + msys_home + "/usr/bin/mintty.exe ngspice -p " + command + '"' + self.process.start(self.command) os.chdir(tempdir) else: + print("reached .. 4") self.commandi = "cd " + projPath + \ ";ngspice -r " + command.replace(".cir.out", ".raw") + \ " " + command self.xtermArgs = ['-hold', '-e', self.commandi] + print("xTerm") self.xtermProcess = QtCore.QProcess(self) self.xtermProcess.start('xterm', self.xtermArgs) diff --git a/src/ngspiceSimulation/__init__.py b/src/ngspiceSimulation/__init__.py index e69de29bb..a9bb73b20 100644 --- a/src/ngspiceSimulation/__init__.py +++ b/src/ngspiceSimulation/__init__.py @@ -0,0 +1,4 @@ +from .pythonPlotting import plotWindow +from .NgspiceWidget import NgspiceWidget + +__all__ = ['plotWindow', 'NgspiceWidget'] diff --git a/src/ngspiceSimulation/pythonPlotting.py b/src/ngspiceSimulation/pythonPlotting.py index 615ad02b5..7571319a2 100644 --- a/src/ngspiceSimulation/pythonPlotting.py +++ b/src/ngspiceSimulation/pythonPlotting.py @@ -10,7 +10,325 @@ from matplotlib.figure import Figure from configuration.Appconfig import Appconfig import numpy as np - +import re +from cycler import cycler + +# Dark theme colors - Modern GitHub Dark inspired theme +DARK_BLUE = "#0d1117" # Main background +LIGHTER_BLUE = "#161b22" # Secondary background +ACCENT_BLUE = "#1f6feb" # Primary accent +ACCENT_HOVER = "#388bfd" # Hover state +TEXT_COLOR = "#f0f6fc" # Main text +SECONDARY_TEXT = "#8b949e" # Secondary text +BORDER_COLOR = "#30363d" # Borders +GRADIENT_START = "#1f2937" # Background gradient start +GRADIENT_END = "#111827" # Background gradient end + +# Light theme colors - Modern GitHub Light inspired theme +LIGHT_BG = "#ffffff" # Main background +LIGHT_SECONDARY = "#f6f8fa" # Secondary background +LIGHT_ACCENT = "#0969da" # Primary accent +LIGHT_ACCENT_HOVER = "#1a7f37" # Hover state +LIGHT_TEXT = "#24292f" # Main text +LIGHT_SECONDARY_TEXT = "#57606a" # Secondary text +LIGHT_BORDER = "#d0d7de" # Borders +LIGHT_GRADIENT_START = "#f6f8fa" # Background gradient start +LIGHT_GRADIENT_END = "#ffffff" # Background gradient end + +# Toolbar icon size +TOOLBAR_ICON_SIZE = 24 # Size for toolbar icons in pixels + +# Define the stylesheets +DARK_STYLESHEET = f""" + /* Main window and widget styling */ + QMainWindow {{ + background: {DARK_BLUE}; + }} + + QWidget {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {GRADIENT_START}, stop:1 {GRADIENT_END}); + color: {TEXT_COLOR}; + font-size: 14px; + }} + QMainWindow, QWidget {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {GRADIENT_START}, stop:1 {GRADIENT_END}); + color: {TEXT_COLOR}; + font-size: 14px; + }} + + QPushButton {{ + background-color: {ACCENT_BLUE}; + color: {TEXT_COLOR}; + border: 2px solid {ACCENT_HOVER}; + padding: 6px 12px; + border-radius: 4px; + min-width: 80px; + font-size: 13px; + font-weight: bold; + margin: 1px; + }} + QPushButton:hover {{ + background-color: {ACCENT_HOVER}; + border-color: {TEXT_COLOR}; + color: {TEXT_COLOR}; + }} + QPushButton:pressed {{ + background-color: {GRADIENT_START}; + border-color: {ACCENT_HOVER}; + color: {TEXT_COLOR}; + }} + + QLabel {{ + color: {TEXT_COLOR}; + font-size: 14px; + font-weight: bold; + padding: 4px; + }} + + QLineEdit {{ + background-color: {LIGHTER_BLUE}; + color: {TEXT_COLOR}; + border: 2px solid {BORDER_COLOR}; + padding: 10px; + border-radius: 6px; + font-size: 14px; + }} + QLineEdit:focus {{ + border-color: {ACCENT_BLUE}; + }} + + QCheckBox {{ + color: {TEXT_COLOR}; + spacing: 8px; + font-size: 14px; + padding: 4px; + }} + QCheckBox::indicator {{ + width: 20px; + height: 20px; + border-radius: 4px; + }} + QCheckBox::indicator:unchecked {{ + border: 2px solid {BORDER_COLOR}; + background-color: {LIGHTER_BLUE}; + }} + QCheckBox::indicator:checked {{ + border: 2px solid {ACCENT_HOVER}; + background-color: {ACCENT_BLUE}; + }} + QCheckBox::indicator:hover {{ + border-color: {TEXT_COLOR}; + }} + + QScrollArea {{ + border: 2px solid {BORDER_COLOR}; + border-radius: 8px; + background-color: transparent; + }} + + QToolBar {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {GRADIENT_START}, stop:1 {GRADIENT_END}); + border-bottom: 2px solid {ACCENT_BLUE}; + padding: 8px; + spacing: 8px; + min-height: 48px; + }} + + QToolButton {{ + background-color: {LIGHTER_BLUE}; + border: 2px solid {BORDER_COLOR}; + border-radius: 6px; + padding: 8px; + margin: 2px; + min-width: 36px; + min-height: 36px; + font-size: 14px; + color: {TEXT_COLOR}; + }} + QToolButton:hover {{ + background-color: {ACCENT_BLUE}; + border-color: {ACCENT_HOVER}; + color: {TEXT_COLOR}; + }} + QToolButton:pressed {{ + background-color: {GRADIENT_START}; + border-color: {TEXT_COLOR}; + color: {TEXT_COLOR}; + }} +""" + +# Light theme stylesheet +LIGHT_STYLESHEET = f""" + /* Main window and widget styling */ + QMainWindow {{ + background: {LIGHT_BG}; + }} + + QWidget {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {LIGHT_GRADIENT_START}, stop:1 {LIGHT_GRADIENT_END}); + color: {LIGHT_TEXT}; + font-size: 14px; + }} + QMainWindow, QWidget {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {LIGHT_GRADIENT_START}, stop:1 {LIGHT_GRADIENT_END}); + color: {LIGHT_TEXT}; + font-size: 14px; + }} + + QPushButton {{ + background-color: {LIGHT_ACCENT}; + color: #24292f; + border: 2px solid {LIGHT_ACCENT_HOVER}; + padding: 6px 12px; + border-radius: 4px; + min-width: 80px; + font-size: 13px; + font-weight: bold; + margin: 1px; + }} + QPushButton:hover {{ + background-color: {LIGHT_ACCENT_HOVER}; + color: #24292f; + border-color: {LIGHT_TEXT}; + }} + QPushButton:pressed {{ + background-color: {LIGHT_GRADIENT_START}; + color: #24292f; + border-color: {LIGHT_ACCENT_HOVER}; + }} + + QLabel {{ + color: {LIGHT_TEXT}; + font-size: 14px; + font-weight: bold; + padding: 4px; + }} + + QLineEdit {{ + background-color: {LIGHT_SECONDARY}; + color: {LIGHT_TEXT}; + border: 2px solid {LIGHT_BORDER}; + padding: 10px; + border-radius: 6px; + font-size: 14px; + }} + QLineEdit:focus {{ + border-color: {LIGHT_ACCENT}; + }} + + QCheckBox {{ + color: {LIGHT_TEXT}; + spacing: 8px; + font-size: 14px; + padding: 4px; + }} + QCheckBox::indicator {{ + width: 20px; + height: 20px; + border-radius: 4px; + }} + QCheckBox::indicator:unchecked {{ + border: 2px solid {LIGHT_BORDER}; + background-color: {LIGHT_SECONDARY}; + }} + QCheckBox::indicator:checked {{ + border: 2px solid {LIGHT_ACCENT_HOVER}; + background-color: {LIGHT_ACCENT}; + }} + QCheckBox::indicator:hover {{ + border-color: {LIGHT_TEXT}; + }} + + QScrollArea {{ + border: 2px solid {LIGHT_BORDER}; + border-radius: 8px; + background-color: transparent; + }} + + QToolBar {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {LIGHT_GRADIENT_START}, stop:1 {LIGHT_GRADIENT_END}); + border-bottom: 2px solid {LIGHT_ACCENT}; + padding: 8px; + spacing: 8px; + min-height: 48px; + }} + + QToolButton {{ + background-color: {LIGHT_SECONDARY}; + border: 2px solid {LIGHT_BORDER}; + border-radius: 6px; + padding: 8px; + margin: 2px; + min-width: 36px; + min-height: 36px; + font-size: 14px; + color: {LIGHT_TEXT}; + }} + QToolButton:hover {{ + background-color: {LIGHT_ACCENT}; + border-color: {LIGHT_ACCENT_HOVER}; + color: {LIGHT_TEXT}; + }} + QToolButton:pressed {{ + background-color: {LIGHT_GRADIENT_START}; + border-color: {LIGHT_TEXT}; + }} +""" + +# Multimeter widget styles +DARK_MULTIMETER_STYLE = f""" + QWidget {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {GRADIENT_START}, stop:1 {GRADIENT_END}); + color: {TEXT_COLOR}; + border: 2px solid {BORDER_COLOR}; + border-radius: 8px; + padding: 10px; + }} + QLabel {{ + color: {TEXT_COLOR}; + padding: 8px; + font-size: 13px; + font-weight: bold; + background: transparent; + border: none; + }} + QLabel[class="value"] {{ + color: {ACCENT_BLUE}; + font-size: 16px; + font-weight: bold; + }} +""" + +LIGHT_MULTIMETER_STYLE = f""" + QWidget {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {LIGHT_GRADIENT_START}, stop:1 {LIGHT_GRADIENT_END}); + color: {LIGHT_TEXT}; + border: 2px solid {LIGHT_BORDER}; + border-radius: 8px; + padding: 10px; + }} + QLabel {{ + color: {LIGHT_TEXT}; + padding: 8px; + font-size: 13px; + font-weight: bold; + background: transparent; + border: none; + }} + QLabel[class="value"] {{ + color: {LIGHT_ACCENT}; + font-size: 16px; + font-weight: bold; + }} +""" # This class creates Python Plotting window class plotWindow(QtWidgets.QMainWindow): @@ -18,8 +336,35 @@ class plotWindow(QtWidgets.QMainWindow): This class defines python plotting window, its features, buttons, colors, AC and DC analysis, plotting etc. """ - - def __init__(self, fpath, projectName): + + # Class variable to store the single instance + instance = None + + @classmethod + def add_output(cls, fpath, projectName, is_dark_theme=True): + """Static method to manage plot window instances. + + Args: + fpath (str): Path to the project directory + projectName (str): Name of the project + is_dark_theme (bool): Whether to use dark theme (default: True) + """ + if cls.instance is None: + cls.instance = cls(fpath, projectName, is_dark_theme) + else: + # Update existing instance with new data + cls.instance.fpath = fpath + cls.instance.projectName = projectName + cls.instance.is_dark_theme = is_dark_theme + cls.instance.obj_dataext = DataExtraction() + cls.instance.plotType = cls.instance.obj_dataext.openFile(fpath) + cls.instance.obj_dataext.computeAxes() + cls.instance.a = cls.instance.obj_dataext.numVals() + cls.instance.createMainFrame() + + return cls.instance + + def __init__(self, fpath, projectName, is_dark_theme=True): """This create constructor for plotWindow class.""" QtWidgets.QMainWindow.__init__(self) self.fpath = fpath @@ -34,42 +379,275 @@ def __init__(self, fpath, projectName): self.combo = [] self.combo1 = [] self.combo1_rev = [] - # Creating Frame + self.is_dark_theme = is_dark_theme + # Only apply stylesheet for dark mode + if self.is_dark_theme: + self.setStyleSheet(DARK_STYLESHEET) + # Set global tooltip style for dark mode + app = QtWidgets.QApplication.instance() + if app: + app.setStyleSheet(''' + QToolTip { + background-color: #23272e; + color: #fff; + border: 1px solid #388bfd; + border-radius: 6px; + font-size: 13px; + padding: 6px; + } + ''') + else: + self.setStyleSheet(LIGHT_STYLESHEET) # Use light stylesheet for light mode + # Reset tooltip style for light mode + app = QtWidgets.QApplication.instance() + if app: + app.setStyleSheet('') self.createMainFrame() + def toggle_theme(self): + """Toggle between light and dark themes.""" + self.is_dark_theme = not self.is_dark_theme + self.setStyleSheet(DARK_STYLESHEET if self.is_dark_theme else LIGHT_STYLESHEET) + + # Update tooltip styling based on theme + app = QtWidgets.QApplication.instance() + if app: + if self.is_dark_theme: + app.setStyleSheet(''' + QToolTip { + background-color: #23272e; + color: #fff; + border: 1px solid #388bfd; + border-radius: 6px; + font-size: 13px; + padding: 6px; + } + ''') + else: + app.setStyleSheet('') + + self.update_plot_theme() + + def update_plot_theme(self): + """Update plot colors based on current theme.""" + if self.is_dark_theme: + # Dark theme colors + bg_color = DARK_BLUE + text_color = TEXT_COLOR # Use white for all text + accent_color = ACCENT_BLUE + grid_color = BORDER_COLOR + function_color = TEXT_COLOR # White for dark theme + # Set a bright color cycle for plot lines in dark mode + self.axes.set_prop_cycle(cycler('color', ['#00eaff', '#ff6b6b', '#ffe156', '#6bffb4', '#a55eea', '#fd79a8', '#ffb347', '#f9ca24', '#4ecdc4', '#45b7d1'])) + else: + # Light theme colors + bg_color = LIGHT_BG + text_color = LIGHT_TEXT + accent_color = LIGHT_ACCENT + grid_color = LIGHT_BORDER + function_color = LIGHT_TEXT # Black for light theme + self.axes.set_prop_cycle(cycler('color', ['#00eaff', '#ff6b6b', '#ffe156', '#6bffb4', '#a55eea', '#fd79a8', '#ffb347', '#f9ca24', '#4ecdc4', '#45b7d1'])) + + # Update figure and axes colors + self.fig.patch.set_facecolor(bg_color) + self.axes.set_facecolor(bg_color) + + # Update text colors + self.axes.tick_params(colors=text_color, labelsize=12) + self.axes.xaxis.label.set_color(text_color) + self.axes.yaxis.label.set_color(text_color) + self.axes.title.set_color(text_color) + + # Update spines + for spine in self.axes.spines.values(): + spine.set_color(accent_color) + spine.set_linewidth(2) + + # Update grid + self.axes.grid(True, color=grid_color, alpha=0.3) + + # Update function text colors + for text in self.axes.texts: + text.set_color(function_color) + + # Update annotation colors + for annotation in self.axes.annotations if hasattr(self.axes, 'annotations') else []: + annotation.set_color(function_color) + # Update all children that are Text (for annotations, etc.) + for child in self.axes.get_children(): + if hasattr(child, 'set_color') and hasattr(child, 'get_text') and child.get_text() != '': + child.set_color(function_color) + + # Update legend colors if it exists + if self.axes.get_legend(): + legend = self.axes.get_legend() + legend.get_frame().set_facecolor(bg_color) + legend.get_frame().set_edgecolor(accent_color) + for text in legend.get_texts(): + text.set_color(text_color) + + # Redraw the canvas + self.canvas.draw() + + # Update coordinates label color for dark mode + if hasattr(self, 'coord_label') and self.coord_label: + if self.is_dark_theme: + self.coord_label.setStyleSheet('font-size: 12px; padding-left: 8px; color: #f0f6fc;') + else: + self.coord_label.setStyleSheet('font-size: 12px; padding-left: 8px; color: #24292f;') + + # Update multimeter themes if they exist + for widget in self.findChildren(MultimeterWidgetClass): + widget.toggle_theme() + + # Update right panel label and checkbox colors for theme + if self.is_dark_theme: + self.analysisType.setStyleSheet('color: #f0f6fc;') + self.listNode.setStyleSheet('color: #f0f6fc;') + self.listBranch.setStyleSheet('color: #f0f6fc;') + self.funcLabel.setStyleSheet('color: #f0f6fc;') + self.funcName.setStyleSheet('color: #f0f6fc;') + self.funcExample.setStyleSheet('color: #f0f6fc;') + for cb in self.chkbox: + cb.setStyleSheet('color: #f0f6fc;') + else: + self.analysisType.setStyleSheet('color: #24292f;') + self.listNode.setStyleSheet('color: #24292f;') + self.listBranch.setStyleSheet('color: #24292f;') + self.funcLabel.setStyleSheet('color: #24292f;') + self.funcName.setStyleSheet('color: #24292f;') + self.funcExample.setStyleSheet('color: #24292f;') + for cb in self.chkbox: + cb.setStyleSheet('color: #24292f;') + def createMainFrame(self): self.mainFrame = QtWidgets.QWidget() self.dpi = 100 - self.fig = Figure((7.0, 7.0), dpi=self.dpi) - # Creating Canvas which will figure + if self.is_dark_theme: + self.fig = Figure((7.0, 7.0), dpi=self.dpi, facecolor="#000000") # Black canvas + else: + self.fig = Figure((7.0, 7.0), dpi=self.dpi) # Default white bg self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self.mainFrame) self.axes = self.fig.add_subplot(111) + # Set axes and tick colors for dark mode + if self.is_dark_theme: + self.axes.set_facecolor("#000000") + self.axes.tick_params(colors="#f0f6fc", labelsize=12) + self.axes.xaxis.label.set_color("#f0f6fc") + self.axes.yaxis.label.set_color("#f0f6fc") + self.axes.title.set_color("#f0f6fc") + for spine in self.axes.spines.values(): + spine.set_color("#1f6feb") + spine.set_linewidth(2) self.navToolBar = NavigationToolbar(self.canvas, self.mainFrame) - - # LeftVbox hold navigation tool bar and canvas self.left_vbox = QtWidgets.QVBoxLayout() - self.left_vbox.addWidget(self.navToolBar) + # Custom toolbar for both dark and light mode + self.navToolBar.hide() + custom_toolbar = QtWidgets.QWidget() + custom_toolbar_layout = QtWidgets.QHBoxLayout() + custom_toolbar_layout.setContentsMargins(0, 0, 0, 0) + custom_toolbar_layout.setSpacing(0) # Minimum horizontal spacing + for action in self.navToolBar.actions(): + if action.isSeparator() or action.icon().isNull(): + continue # Skip separators and actions without icons + btn = QtWidgets.QToolButton() + btn.setDefaultAction(action) + btn.setIcon(action.icon()) + btn.setToolTip(action.toolTip()) + # Modern styling for both modes + if self.is_dark_theme: + btn.setStyleSheet(''' + QToolButton { + background-color: #23272e; + border: 2px solid #30363d; + border-radius: 8px; + padding: 4px; + margin: 0px; + min-width: 28px; + min-height: 28px; + font-size: 13px; + color: #f0f6fc; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + transition: background 0.2s, border 0.2s; + } + QToolButton:hover { + background-color: #1f6feb; + color: #f0f6fc; + border-color: #388bfd; + } + QToolButton:pressed { + background-color: #161b22; + color: #f0f6fc; + border-color: #f0f6fc; + } + QToolButton:checked { + background-color: #388bfd; + color: #f0f6fc; + border-color: #f0f6fc; + } + ''') + else: + btn.setStyleSheet(''' + QToolButton { + background-color: #fff; + border: 2px solid #d0d7de; + border-radius: 8px; + padding: 4px; + margin: 0px; + min-width: 28px; + min-height: 28px; + font-size: 13px; + color: #24292f; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); + transition: background 0.2s, border 0.2s; + } + QToolButton:hover { + background-color: #f6f8fa; + border-color: #0969da; + color: #24292f; + } + QToolButton:pressed { + background-color: #eaeef2; + border-color: #24292f; + } + ''') + vbox = QtWidgets.QVBoxLayout() + vbox.setAlignment(QtCore.Qt.AlignHCenter) + vbox.setContentsMargins(0, 0, 0, 0) # Remove margins between button+label + vbox.addWidget(btn, alignment=QtCore.Qt.AlignHCenter) + label = QtWidgets.QLabel() + label.setAlignment(QtCore.Qt.AlignHCenter) + tooltip_plain = re.sub('<[^<]+?>', '', action.toolTip()) + label.setText(tooltip_plain) + label.setStyleSheet('font-size: 9px; color: gray; margin-top: 0px;') + vbox.addWidget(label) + custom_toolbar_layout.addLayout(vbox) + # Add a custom QLabel for coordinates display to the far right + self.coord_label = QtWidgets.QLabel() + self.coord_label.setVisible(True) + if self.is_dark_theme: + self.coord_label.setStyleSheet('font-size: 12px; padding-left: 8px; color: #f0f6fc;') + custom_toolbar_layout.addStretch(1) + custom_toolbar_layout.addWidget(self.coord_label, alignment=QtCore.Qt.AlignVCenter) + custom_toolbar.setLayout(custom_toolbar_layout) + self.left_vbox.addWidget(custom_toolbar) self.left_vbox.addWidget(self.canvas) - - # right VBOX is main Layout which hold right grid(bottom part) and top - # grid(top part) + # Explicitly connect mpl_connect to update coordinates in the custom label + self.canvas.mpl_connect('motion_notify_event', self.update_coordinates) self.right_vbox = QtWidgets.QVBoxLayout() self.right_grid = QtWidgets.QGridLayout() self.top_grid = QtWidgets.QGridLayout() - - # Get DataExtraction Details self.obj_dataext = DataExtraction() self.plotType = self.obj_dataext.openFile(self.fpath) - self.obj_dataext.computeAxes() self.a = self.obj_dataext.numVals() - self.chkbox = [] - - # Generating list of colors : - # ,(0.4,0.5,0.2),(0.1,0.4,0.9),(0.4,0.9,0.2),(0.9,0.4,0.9)] - self.full_colors = ['r', 'b', 'g', 'y', 'c', 'm', 'k'] + # Color palette + if self.is_dark_theme: + self.full_colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#a55eea', '#fd79a8', '#00d2d3'] + else: + self.full_colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#a55eea', '#fd79a8', '#00d2d3'] self.color = [] for i in range(0, self.a[0] - 1): if i % 7 == 0: @@ -86,10 +664,6 @@ def createMainFrame(self): self.color.append(self.full_colors[5]) elif (i - 6) % 7 == 0: self.color.append(self.full_colors[6]) - - # Color generation ends here - - # Total number of voltage source self.volts_length = self.a[1] self.analysisType = QtWidgets.QLabel() self.top_grid.addWidget(self.analysisType, 0, 0) @@ -99,7 +673,10 @@ def createMainFrame(self): self.top_grid.addWidget(self.listBranch, self.a[1] + 2, 0) for i in range(0, self.a[1]): # a[0]-1 self.chkbox.append(QtWidgets.QCheckBox(self.obj_dataext.NBList[i])) - self.chkbox[i].setStyleSheet('color') + if self.is_dark_theme: + self.chkbox[i].setStyleSheet('color: #f0f6fc;') + else: + self.chkbox[i].setStyleSheet('color: #24292f;') self.chkbox[i].setToolTip('Check To Plot') self.top_grid.addWidget(self.chkbox[i], i + 2, 0) self.colorLab = QtWidgets.QLabel() @@ -112,6 +689,10 @@ def createMainFrame(self): for i in range(self.a[1], self.a[0] - 1): # a[0]-1 self.chkbox.append(QtWidgets.QCheckBox(self.obj_dataext.NBList[i])) + if self.is_dark_theme: + self.chkbox[i].setStyleSheet('color: #f0f6fc;') + else: + self.chkbox[i].setStyleSheet('color: #24292f;') self.chkbox[i].setToolTip('Check To Plot') self.top_grid.addWidget(self.chkbox[i], i + 3, 0) self.colorLab = QtWidgets.QLabel() @@ -140,6 +721,12 @@ def createMainFrame(self): self.plotfuncbtn = QtWidgets.QPushButton("Plot Function") self.plotfuncbtn.setToolTip('Press to Plot the function') + # Set button text color explicitly for dark/light mode + self.plotbtn.setStyleSheet(f'color: {"#f0f6fc" if self.is_dark_theme else "#24292f"};') + self.clear.setStyleSheet(f'color: {"#f0f6fc" if self.is_dark_theme else "#24292f"};') + self.multimeterbtn.setStyleSheet(f'color: {"#f0f6fc" if self.is_dark_theme else "#24292f"};') + self.plotfuncbtn.setStyleSheet(f'color: {"#f0f6fc" if self.is_dark_theme else "#24292f"};') + self.palette1.setColor(QtGui.QPalette.Foreground, QtCore.Qt.blue) self.palette2.setColor(QtGui.QPalette.Foreground, QtCore.Qt.red) self.funcName.setPalette(self.palette1) @@ -158,37 +745,54 @@ def createMainFrame(self): self.right_grid.addWidget(self.funcExample, 4, 1) self.right_vbox.addLayout(self.right_grid) + # Set background colors for full window, right panel, and scroll area + bg_color = DARK_BLUE if self.is_dark_theme else LIGHT_BG + self.mainFrame.setStyleSheet(f"background-color: {bg_color};") + self.right_vbox.setContentsMargins(0, 0, 0, 0) + self.right_vbox.setSpacing(4) + self.right_grid.setContentsMargins(0, 0, 0, 0) + self.right_grid.setSpacing(4) + self.top_grid.setContentsMargins(0, 0, 0, 0) + self.top_grid.setSpacing(4) + # Set right panel background + right_panel_widget = QtWidgets.QWidget() + right_panel_widget.setLayout(self.right_vbox) + right_panel_widget.setStyleSheet(f"background-color: {bg_color};") + # Replace right_vbox in hbox with right_panel_widget self.hbox = QtWidgets.QHBoxLayout() - self.hbox.addLayout(self.left_vbox) - self.hbox.addLayout(self.right_vbox) - + self.hbox.addLayout(self.left_vbox, stretch=4) + self.hbox.addWidget(right_panel_widget, stretch=1) self.widget = QtWidgets.QWidget() - self.widget.setLayout(self.hbox) # finalvbox + self.widget.setLayout(self.hbox) self.scrollArea = QtWidgets.QScrollArea() self.scrollArea.setWidgetResizable(True) self.scrollArea.setWidget(self.widget) - ''' - Right side box containing checkbox for different inputs and - options of plot, multimeter and plot function. - ''' + self.scrollArea.setStyleSheet(f"background-color: {bg_color};") self.finalhbox = QtWidgets.QHBoxLayout() self.finalhbox.addWidget(self.scrollArea) - # Right side window frame showing list of nodes and branches. self.mainFrame.setLayout(self.finalhbox) self.showMaximized() - self.listNode.setText("List of Nodes:") + self.listNode.setText(f"

List of Nodes:

") self.listBranch.setText( - "List of Branches:") - self.funcLabel.setText("Function:") + f"

List of Branches:

") + self.funcLabel.setText(f"

Function:

") self.funcName.setText( - "Standard functions\ -

Addition:
Subtraction:
\ - Multiplication:
Division:
Comparison:" + f"

Standard functions

\ +

\ + Addition:
\ + Subtraction:
\ + Multiplication:
\ + Division:
\ + Comparison:

" ) self.funcExample.setText( - "\n\nNode1 + Node2\nNode1 - Node2\nNode1 * Node2\nNode1 / Node2\ - \nNode1 vs Node2") + f"

\ + Node1 + Node2
\ + Node1 - Node2
\ + Node1 * Node2
\ + Node1 / Node2
\ + Node1 vs Node2

") # Connecting to plot and clear function self.clear.clicked.connect(self.pushedClear) @@ -217,6 +821,8 @@ def createMainFrame(self): def pushedClear(self): self.text.clear() self.axes.cla() + # Reapply theme after clearing + self.update_plot_theme() self.canvas.draw() def pushedPlotFunc(self): @@ -296,11 +902,11 @@ def pushedPlotFunc(self): label=str(2)) # _rev if max(a) < self.volts_length: - self.axes.set_ylabel('Voltage(V)-->') - self.axes.set_xlabel('Voltage(V)-->') + self.axes.set_ylabel('Voltage(V)-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) + self.axes.set_xlabel('Voltage(V)-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) else: - self.axes.set_ylabel('Current(I)-->') - self.axes.set_ylabel('Current(I)-->') + self.axes.set_ylabel('Current(I)-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) + self.axes.set_xlabel('Current(I)-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) elif max(a) >= self.volts_length and min(a) < self.volts_length: QtWidgets.QMessageBox.about( @@ -337,12 +943,12 @@ def pushedPlotFunc(self): c=self.color[0], label=str(1)) - self.axes.set_xlabel('freq-->') + self.axes.set_xlabel('freq-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) if max(a) < self.volts_length: - self.axes.set_ylabel('Voltage(V)-->') + self.axes.set_ylabel('Voltage(V)-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) else: - self.axes.set_ylabel('Current(I)-->') + self.axes.set_ylabel('Current(I)-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) elif self.plotType2[0] == 1: # self.setWindowTitle('Transient Analysis') @@ -351,11 +957,11 @@ def pushedPlotFunc(self): finalResult, c=self.color[0], label=str(1)) - self.axes.set_xlabel('time-->') + self.axes.set_xlabel('time-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) if max(a) < self.volts_length: - self.axes.set_ylabel('Voltage(V)-->') + self.axes.set_ylabel('Voltage(V)-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) else: - self.axes.set_ylabel('Current(I)-->') + self.axes.set_ylabel('Current(I)-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) else: # self.setWindowTitle('DC Analysis') @@ -364,13 +970,15 @@ def pushedPlotFunc(self): finalResult, c=self.color[0], label=str(1)) - self.axes.set_xlabel('I/P Voltage-->') + self.axes.set_xlabel('I/P Voltage-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) if max(a) < self.volts_length: - self.axes.set_ylabel('Voltage(V)-->') + self.axes.set_ylabel('Voltage(V)-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) else: - self.axes.set_ylabel('Current(I)-->') + self.axes.set_ylabel('Current(I)-->', fontsize=14, fontweight='bold', color=ACCENT_HOVER) self.axes.grid(True) + # Reapply theme after plotting + self.update_plot_theme() self.canvas.draw() self.combo = [] self.combo1 = [] @@ -391,11 +999,11 @@ def onPush_decade(self): c=self.color[j], label=str( j + 1)) - self.axes.set_xlabel('freq-->') + self.axes.set_xlabel('Frequency', fontsize=14, fontweight='bold', color=ACCENT_HOVER) if j < self.volts_length: - self.axes.set_ylabel('Voltage(V)-->') + self.axes.set_ylabel('Voltage (V)', fontsize=14, fontweight='bold', color=ACCENT_HOVER) else: - self.axes.set_ylabel('Current(I)-->') + self.axes.set_ylabel('Current (A)', fontsize=14, fontweight='bold', color=ACCENT_HOVER) self.axes.grid(True) if boxCheck == 0: @@ -403,7 +1011,9 @@ def onPush_decade(self): self, "Warning!!", "Please select at least one Node OR Branch" ) return - + + # Reapply theme after plotting + self.update_plot_theme() self.canvas.draw() def onPush_ac(self): @@ -418,11 +1028,11 @@ def onPush_ac(self): c=self.color[j], label=str( j + 1)) - self.axes.set_xlabel('freq-->') + self.axes.set_xlabel('Frequency', fontsize=14, fontweight='bold', color=ACCENT_HOVER) if j < self.volts_length: - self.axes.set_ylabel('Voltage(V)-->') + self.axes.set_ylabel('Voltage (V)', fontsize=14, fontweight='bold', color=ACCENT_HOVER) else: - self.axes.set_ylabel('Current(I)-->') + self.axes.set_ylabel('Current (A)', fontsize=14, fontweight='bold', color=ACCENT_HOVER) self.axes.grid(True) if boxCheck == 0: QtWidgets.QMessageBox.about( @@ -430,6 +1040,8 @@ def onPush_ac(self): ) return + # Reapply theme after plotting + self.update_plot_theme() self.canvas.draw() def onPush_trans(self): @@ -444,17 +1056,20 @@ def onPush_trans(self): c=self.color[j], label=str( j + 1)) - self.axes.set_xlabel('time-->') + self.axes.set_xlabel('Time', fontsize=14, fontweight='bold', color=ACCENT_HOVER) if j < self.volts_length: - self.axes.set_ylabel('Voltage(V)-->') + self.axes.set_ylabel('Voltage (V)', fontsize=14, fontweight='bold', color=ACCENT_HOVER) else: - self.axes.set_ylabel('Current(I)-->') + self.axes.set_ylabel('Current (A)', fontsize=14, fontweight='bold', color=ACCENT_HOVER) self.axes.grid(True) if boxCheck == 0: QtWidgets.QMessageBox.about( self, "Warning!!", "Please select at least one Node OR Branch" ) return + + # Reapply theme after plotting + self.update_plot_theme() self.canvas.draw() def onPush_dc(self): @@ -469,12 +1084,11 @@ def onPush_dc(self): c=self.color[j], label=str( j + 1)) - self.axes.set_xlabel('Voltage Sweep(V)-->') - + self.axes.set_xlabel('Voltage Sweep (V)', fontsize=14, fontweight='bold', color=ACCENT_HOVER) if j < self.volts_length: - self.axes.set_ylabel('Voltage(V)-->') + self.axes.set_ylabel('Voltage (V)', fontsize=14, fontweight='bold', color=ACCENT_HOVER) else: - self.axes.set_ylabel('Current(I)-->') + self.axes.set_ylabel('Current (A)', fontsize=14, fontweight='bold', color=ACCENT_HOVER) self.axes.grid(True) if boxCheck == 0: QtWidgets.QMessageBox.about( @@ -482,18 +1096,12 @@ def onPush_dc(self): ) return + # Reapply theme after plotting + self.update_plot_theme() self.canvas.draw() - def colorName(self, letter): - return { - 'r': 'color:red', - 'b': 'color:blue', - 'g': 'color:green', - 'y': 'color:yellow', - 'c': 'color:cyan', - 'm': 'color:magenta', - 'k': 'color:black' - }[letter] + def colorName(self, color): + return f'color:{color}' def multiMeter(self): print("Function : MultiMeter") @@ -533,11 +1141,33 @@ def getRMSValue(self, dataPoints): getcontext().prec = 5 return np.sqrt(np.mean(np.square(dataPoints))) + def eventFilter(self, obj, event): + # Forward mouse move events from the canvas to the NavigationToolbar for coordinate updates + if obj == self.canvas and event.type() == QtCore.QEvent.MouseMove: + QtWidgets.QApplication.sendEvent(self.navToolBar, event) + return super().eventFilter(obj, event) + + def update_coordinates(self, event): + # Directly update the custom coordinates label + if hasattr(self, 'coord_label') and self.coord_label: + if event.inaxes: + x, y = event.xdata, event.ydata + msg = f"x={x:.3f}, y={y:.3f}" + self.coord_label.setText(msg) + else: + self.coord_label.setText("") + class MultimeterWidgetClass(QtWidgets.QWidget): def __init__(self, node_branch, rmsValue, loc_x, loc_y, voltFlag): QtWidgets.QWidget.__init__(self) - + + # Get the current theme from the plot window + self.is_dark_theme = plotWindow.instance.is_dark_theme if plotWindow.instance else True + + # Apply theme + self.setStyleSheet(DARK_MULTIMETER_STYLE if self.is_dark_theme else LIGHT_MULTIMETER_STYLE) + self.multimeter = QtWidgets.QWidget(self) if voltFlag: self.node_branchLabel = QtWidgets.QLabel("Node") @@ -549,6 +1179,10 @@ def __init__(self, node_branch, rmsValue, loc_x, loc_y, voltFlag): self.rmsLabel = QtWidgets.QLabel("RMS Value") self.nodeBranchValue = QtWidgets.QLabel(str(node_branch)) + # Set value label class for special styling + self.rmsValue.setProperty("class", "value") + self.nodeBranchValue.setProperty("class", "value") + self.layout = QtWidgets.QGridLayout(self) self.layout.addWidget(self.node_branchLabel, 0, 0) self.layout.addWidget(self.rmsLabel, 0, 1) @@ -562,6 +1196,73 @@ def __init__(self, node_branch, rmsValue, loc_x, loc_y, voltFlag): self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) self.show() + def toggle_theme(self): + """Toggle between light and dark themes.""" + self.is_dark_theme = not self.is_dark_theme + self.setStyleSheet(DARK_MULTIMETER_STYLE if self.is_dark_theme else LIGHT_MULTIMETER_STYLE) + + def update_plot_theme(self): + """Update plot colors based on current theme.""" + if self.is_dark_theme: + # Dark theme colors + bg_color = DARK_BLUE + text_color = TEXT_COLOR # Use white for all text + accent_color = ACCENT_BLUE + grid_color = BORDER_COLOR + function_color = TEXT_COLOR # White for dark theme + else: + # Light theme colors + bg_color = LIGHT_BG + text_color = LIGHT_TEXT + accent_color = LIGHT_ACCENT + grid_color = LIGHT_BORDER + function_color = LIGHT_TEXT # Black for light theme + + # Update figure and axes colors + self.fig.patch.set_facecolor(bg_color) + self.axes.set_facecolor(bg_color) + + # Update text colors + self.axes.tick_params(colors=text_color, labelsize=12) + self.axes.xaxis.label.set_color(text_color) + self.axes.yaxis.label.set_color(text_color) + self.axes.title.set_color(text_color) + + # Update spines + for spine in self.axes.spines.values(): + spine.set_color(accent_color) + spine.set_linewidth(2) + + # Update grid + self.axes.grid(True, color=grid_color, alpha=0.3) + + # Update function text colors + for text in self.axes.texts: + text.set_color(function_color) + + # Update annotation colors + for annotation in self.axes.annotations if hasattr(self.axes, 'annotations') else []: + annotation.set_color(function_color) + # Update all children that are Text (for annotations, etc.) + for child in self.axes.get_children(): + if hasattr(child, 'set_color') and hasattr(child, 'get_text') and child.get_text() != '': + child.set_color(function_color) + + # Update legend colors if it exists + if self.axes.get_legend(): + legend = self.axes.get_legend() + legend.get_frame().set_facecolor(bg_color) + legend.get_frame().set_edgecolor(accent_color) + for text in legend.get_texts(): + text.set_color(text_color) + + # Redraw the canvas + self.canvas.draw() + + # Update multimeter themes if they exist + for widget in self.findChildren(MultimeterWidgetClass): + widget.toggle_theme() + class DataExtraction: def __init__(self): @@ -656,23 +1357,39 @@ def numberFinder(self, fpath): def openFile(self, fpath): try: + vfile = os.path.join(fpath, "plot_data_v.txt") + if not os.path.exists(vfile): + raise FileNotFoundError(f"Missing file: {vfile}") + if os.path.getsize(vfile) == 0: + raise ValueError(f"File is empty: {vfile}") + with open(vfile) as f1: + allv = f1.read() + + if not allv.strip(): + raise ValueError(f"File is empty: {vfile}") + + if len(allv.splitlines()) < 6: + raise ValueError(f"File {vfile} does not have enough lines for plotting.") + + if not os.path.exists(os.path.join(fpath, "plot_data_i.txt")): + raise FileNotFoundError(f"Missing file: {os.path.join(fpath, 'plot_data_i.txt')}") with open(os.path.join(fpath, "plot_data_i.txt")) as f2: alli = f2.read() + if not alli.strip(): + raise ValueError(f"File is empty: {os.path.join(fpath, 'plot_data_i.txt')}") + alli = alli.split("\n") self.NBIList = [] - - with open(os.path.join(fpath, "plot_data_v.txt")) as f1: - allv = f1.read() - except Exception as e: print("Exception Message : ", str(e)) self.obj_appconfig.print_error('Exception Message :' + str(e)) self.msg = QtWidgets.QErrorMessage() self.msg.setModal(True) self.msg.setWindowTitle("Error Message") - self.msg.showMessage('Unable to open plot data files.') + self.msg.showMessage(f'Unable to open plot data files.\n{str(e)}') self.msg.exec_() + raise # Reraise so caller knows it failed try: for l in alli[3].split(" "): @@ -686,8 +1403,9 @@ def openFile(self, fpath): self.msg = QtWidgets.QErrorMessage() self.msg.setModal(True) self.msg.setWindowTitle("Error Message") - self.msg.showMessage('Unable to read Analysis File.') + self.msg.showMessage(f'Unable to read Analysis File.\n{str(e)}') self.msg.exec_() + raise d = self.numberFinder(fpath) d1 = int(d[0] + 1) diff --git a/src/ngspicetoModelica/ModelicaUI.py b/src/ngspicetoModelica/ModelicaUI.py index a687bb931..25d440d1a 100755 --- a/src/ngspicetoModelica/ModelicaUI.py +++ b/src/ngspicetoModelica/ModelicaUI.py @@ -21,7 +21,13 @@ def __init__(self, dir=None): self.ngspiceNetlist = os.path.join( self.projDir, self.projName + ".cir.out") self.modelicaNetlist = os.path.join(self.projDir, "*.mo") - self.map_json = Appconfig.modelica_map_json + + # Handle the case where modelica_map_json might be None + try: + self.map_json = Appconfig.modelica_map_json + except AttributeError: + self.map_json = None + print("Warning: Modelica map JSON not available") self.grid = QtWidgets.QGridLayout() self.FileEdit = QtWidgets.QLineEdit() diff --git a/src/ngspicetoModelica/NgspicetoModelica.py b/src/ngspicetoModelica/NgspicetoModelica.py index 9a3d504fb..43d2ff8e4 100755 --- a/src/ngspicetoModelica/NgspicetoModelica.py +++ b/src/ngspicetoModelica/NgspicetoModelica.py @@ -8,8 +8,17 @@ class NgMoConverter: def __init__(self, map_json): # Loading JSON file which hold the mapping information between ngspice # and Modelica. - with open(map_json) as mappingFile: - self.mappingData = json.load(mappingFile) + if map_json is None: + # Use default mapping if no JSON file is available + self.mappingData = {} + print("Warning: No mapping JSON file available, using default mapping") + else: + try: + with open(map_json) as mappingFile: + self.mappingData = json.load(mappingFile) + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Warning: Could not load mapping JSON file: {e}") + self.mappingData = {} self.ifMOS = False self.sourceDetail = [] diff --git a/src/projManagement/Kicad.py b/src/projManagement/Kicad.py index 833f075e0..b045585d9 100644 --- a/src/projManagement/Kicad.py +++ b/src/projManagement/Kicad.py @@ -20,7 +20,7 @@ from . import Validation from configuration.Appconfig import Appconfig from . import Worker -from PyQt5 import QtWidgets +from PyQt5 import QtWidgets, QtGui, QtCore class Kicad: @@ -70,6 +70,7 @@ def openSchematic(self): @params + @return """ print("Function : Open Kicad Schematic") @@ -235,3 +236,71 @@ def openKicadToNgspice(self): self.obj_appconfig.print_warning( 'Please select the project first. You can either ' + 'create new project or open an existing project') + + +class KicadWidget(QtWidgets.QWidget): + """ + A modern, themed QWidget for Kicad actions, matching the Application.py style. + This does not affect any backend logic. + """ + def __init__(self, parent=None): + super(KicadWidget, self).__init__(parent) + self.setObjectName("KicadWidget") + self.setStyleSheet(""" + QWidget#KicadWidget { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #23273a, stop:1 #181b24); + border-radius: 16px; + border: 1.5px solid #23273a; + } + QPushButton { + background-color: #23273a; + color: #e8eaed; + border: 1.5px solid #353b48; + border-radius: 10px; + padding: 10px 24px; + font-family: 'Inter', 'Segoe UI', 'Roboto', 'Arial', sans-serif; + font-size: 15px; + font-weight: 500; + letter-spacing: 0.1px; + } + QPushButton:hover { + background-color: #353b48; + color: #40c4ff; + } + QPushButton:pressed { + background-color: #181b24; + color: #40c4ff; + } + QLabel { + color: #e8eaed; + font-size: 18px; + font-weight: bold; + padding-bottom: 12px; + } + """) + layout = QtWidgets.QVBoxLayout() + layout.setSpacing(18) + layout.setContentsMargins(32, 32, 32, 32) + + title = QtWidgets.QLabel("Kicad Project Actions") + title.setAlignment(QtCore.Qt.AlignCenter) + layout.addWidget(title) + + btn_open_schematic = QtWidgets.QPushButton("Open Schematic") + btn_open_layout = QtWidgets.QPushButton("Open Layout") + btn_open_footprint = QtWidgets.QPushButton("Open Footprint Editor") + btn_convert_ngspice = QtWidgets.QPushButton("Convert to Ngspice") + + # These are placeholders for connecting to actual logic + btn_open_schematic.clicked.connect(lambda: print("Open Schematic clicked")) + btn_open_layout.clicked.connect(lambda: print("Open Layout clicked")) + btn_open_footprint.clicked.connect(lambda: print("Open Footprint Editor clicked")) + btn_convert_ngspice.clicked.connect(lambda: print("Convert to Ngspice clicked")) + + layout.addWidget(btn_open_schematic) + layout.addWidget(btn_open_layout) + layout.addWidget(btn_open_footprint) + layout.addWidget(btn_convert_ngspice) + layout.addStretch(1) + self.setLayout(layout) diff --git a/src/projManagement/Validation.py b/src/projManagement/Validation.py index 5f239163e..e4f582c5a 100644 --- a/src/projManagement/Validation.py +++ b/src/projManagement/Validation.py @@ -61,8 +61,8 @@ def validateNewproj(self, projDir): :projDir => Contains path of the new projDir created @return - :"CHECKEXIST" => If smae project name folder exists - :"CHECKNAME" => If space is there in name + :"CHECKEXIST" => If same project name folder exists + :"CHECKNAME" => If space is there in project name :"VALID" => If valid project name given """ print("Function: Validating New Project Information") @@ -72,7 +72,9 @@ def validateNewproj(self, projDir): return "CHECKEXIST" # Project with name already exist else: # Check Proper name for project. It should not have space - if re.search(r"\s", projDir): + # Extract only the project name (basename) from the full path + projName = os.path.basename(projDir) + if re.search(r"\s", projName): return "CHECKNAME" else: return "VALID" @@ -219,4 +221,4 @@ def validateSubcir(self, projDir, fileName): return True print("Last line not found:", last_line) - return False + return false \ No newline at end of file diff --git a/src/projManagement/newProject.py b/src/projManagement/newProject.py index 10fb0cb5a..fd2973153 100644 --- a/src/projManagement/newProject.py +++ b/src/projManagement/newProject.py @@ -63,8 +63,8 @@ def createProject(self, projName): self.projName = projName self.workspace = self.obj_appconfig.default_workspace['workspace'] # self.projName = self.projEdit.text() - # Remove leading and trailing space - self.projName = str(self.projName).rstrip().lstrip() + # Remove leading and trailing spaces AND replace internal spaces with underscores + self.projName = str(self.projName).strip().replace(" ", "_") self.projDir = os.path.join(self.workspace, str(self.projName)) @@ -144,5 +144,5 @@ def createProject(self, projName): self.msg.exec_() return None, None - def cancelProject(self): - self.close() +def cancelProject(self): + self.close() \ No newline at end of file diff --git a/src/subcircuit/Subcircuit.py b/src/subcircuit/Subcircuit.py index eb06e145f..0961f36f0 100644 --- a/src/subcircuit/Subcircuit.py +++ b/src/subcircuit/Subcircuit.py @@ -29,26 +29,61 @@ def __init__(self, parent=None): self.splitter = QtWidgets.QSplitter() self.splitter.setOrientation(QtCore.Qt.Vertical) + # Create buttons with proper sizing and styling self.newbtn = QtWidgets.QPushButton('New Subcircuit Schematic') self.newbtn.setToolTip('To create new Subcircuit Schematic') - self.newbtn.setFixedSize(200, 40) + self.newbtn.setMinimumWidth(250) # Increased width + self.newbtn.setMinimumHeight(45) # Increased height + self.newbtn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #40c4ff, stop:1 #1976d2); + color: #181b24; + border: 1px solid #40c4ff; + border-radius: 10px; + padding: 10px 20px; + font-size: 14px; + font-weight: bold; + text-align: center; + } + QPushButton:hover { + background: #1976d2; + color: #fff; + border: 1.5px solid #1976d2; + } + QPushButton:pressed { + background: #23273a; + color: #40c4ff; + border: 1.5px solid #40c4ff; + } + """) self.newbtn.clicked.connect(self.newsch) + self.editbtn = QtWidgets.QPushButton('Edit Subcircuit Schematic') self.editbtn.setToolTip('To edit existing Subcircuit Schematic') - self.editbtn.setFixedSize(200, 40) + self.editbtn.setMinimumWidth(250) # Increased width + self.editbtn.setMinimumHeight(45) # Increased height + self.editbtn.setStyleSheet(self.newbtn.styleSheet()) self.editbtn.clicked.connect(self.editsch) + self.convertbtn = QtWidgets.QPushButton('Convert Kicad to Ngspice') - self.convertbtn.setToolTip( - 'To convert Subcircuit Kicad Netlist to Ngspice Netlist') - self.convertbtn.setFixedSize(200, 40) + self.convertbtn.setToolTip('To convert Subcircuit Kicad Netlist to Ngspice Netlist') + self.convertbtn.setMinimumWidth(250) # Increased width + self.convertbtn.setMinimumHeight(45) # Increased height + self.convertbtn.setStyleSheet(self.newbtn.styleSheet()) self.convertbtn.clicked.connect(self.convertsch) + self.uploadbtn = QtWidgets.QPushButton('Upload a Subcircuit') - self.uploadbtn.setToolTip( - 'To Upload a subcircuit') - self.uploadbtn.setFixedSize(180, 38) + self.uploadbtn.setToolTip('To Upload a subcircuit') + self.uploadbtn.setMinimumWidth(250) # Increased width + self.uploadbtn.setMinimumHeight(45) # Increased height + self.uploadbtn.setStyleSheet(self.newbtn.styleSheet()) self.uploadbtn.clicked.connect(self.uploadSub) + # Create layout with proper spacing self.hbox = QtWidgets.QHBoxLayout() + self.hbox.setSpacing(15) # Add spacing between buttons + self.hbox.setContentsMargins(15, 15, 15, 15) # Add margins around buttons self.hbox.addWidget(self.newbtn) self.hbox.addWidget(self.editbtn) self.hbox.addWidget(self.convertbtn) @@ -56,6 +91,8 @@ def __init__(self, parent=None): self.hbox.addStretch(1) self.vbox = QtWidgets.QVBoxLayout() + self.vbox.setSpacing(15) # Add vertical spacing + self.vbox.setContentsMargins(15, 15, 15, 15) # Add margins self.vbox.addLayout(self.hbox) self.vbox.addStretch(1)