├── .gitignore ├── pcm ├── icon.png └── metadata.template.json ├── easyeda_lib_loader.png ├── __init__.py ├── easyeda_lib_loader_64.png ├── decryptor.py ├── create_pcm_archive.sh ├── README.md ├── easyeda_lib_loader.svg ├── config_manager.py ├── easyeda_lib_loader_dialog.py ├── component_loader.py ├── easyeda_lib_loader.py └── easyeda_lib_loader_dialog.fbp /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .vscode/ 3 | .out/archive/ 4 | -------------------------------------------------------------------------------- /pcm/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsa-t/jlc-kicad-lib-loader/HEAD/pcm/icon.png -------------------------------------------------------------------------------- /easyeda_lib_loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsa-t/jlc-kicad-lib-loader/HEAD/easyeda_lib_loader.png -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .easyeda_lib_loader import EasyEDALibLoaderPlugin 2 | 3 | EasyEDALibLoaderPlugin().register() -------------------------------------------------------------------------------- /easyeda_lib_loader_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsa-t/jlc-kicad-lib-loader/HEAD/easyeda_lib_loader_64.png -------------------------------------------------------------------------------- /decryptor.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import io 3 | import platform 4 | 5 | try: 6 | from Crypto.Cipher import AES 7 | except ImportError: 8 | if platform.system() == "Windows": 9 | raise Exception("Please install PyCryptodome in KiCad Command Prompt using: pip install pycryptodome") 10 | else: 11 | raise Exception("Please install PyCryptodome using: pip install pycryptodome") 12 | 13 | def decryptDataStrIdData(encoded_data, key_hex, iv_hex): 14 | # Convert hex strings to bytes 15 | key = bytes.fromhex(key_hex) 16 | iv = bytes.fromhex(iv_hex) 17 | 18 | # Create AES-GCM cipher 19 | cipher = AES.new(key, AES.MODE_GCM, nonce=iv) 20 | 21 | # In AES-GCM, typically the last 16 bytes are the authentication tag 22 | tag = encoded_data[-16:] 23 | actual_ciphertext = encoded_data[:-16] 24 | 25 | # Decrypt data 26 | plaintext = cipher.decrypt_and_verify(actual_ciphertext, tag) 27 | 28 | # Decompress with gzip 29 | with io.BytesIO(plaintext) as compressed_data: 30 | with gzip.GzipFile(fileobj=compressed_data, mode='rb') as f: 31 | decompressed_data = f.read() 32 | 33 | return decompressed_data.decode('utf-8') -------------------------------------------------------------------------------- /pcm/metadata.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://go.kicad.org/pcm/schemas/v1", 3 | "name": "JLCPCB/LCSC Library Loader", 4 | "description": "Browse and download parts from JLCPCB/LCSC.", 5 | "description_full": "Find symbols/footprints with 3D models on JLCPCB/LCSC and use them in your KiCad project. Uses native KiCad importer.", 6 | "identifier": "com.github.dsa-t.jlc-kicad-lib-loader", 7 | "type": "plugin", 8 | "author": { 9 | "name": "dsa-t", 10 | "contact": { 11 | } 12 | }, 13 | "license": "MIT", 14 | "resources": { 15 | "homepage": "https://github.com/dsa-t/jlc-kicad-lib-loader" 16 | }, 17 | "tags": [ 18 | "pcbnew", 19 | "models", 20 | "easyeda", 21 | "jlc" 22 | ], 23 | "versions": [ 24 | { 25 | "version": "VERSION_HERE", 26 | "status": "stable", 27 | "kicad_version": "8.0.7", 28 | "kicad_version_max": "9.0", 29 | "download_sha256": "SHA256_HERE", 30 | "download_size": DOWNLOAD_SIZE_HERE, 31 | "download_url": "DOWNLOAD_URL_HERE", 32 | "install_size": INSTALL_SIZE_HERE 33 | } 34 | ] 35 | } 36 | 37 | -------------------------------------------------------------------------------- /create_pcm_archive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VERSION="1.0.6" 4 | 5 | echo "Clean up old files" 6 | rm -rf .out/archive 7 | rm -f .out/* 8 | 9 | 10 | echo "Create folder structure for ZIP" 11 | mkdir -p .out/archive/plugins 12 | mkdir -p .out/archive/resources 13 | 14 | echo "Copy files to destination" 15 | cp *.py .out/archive/plugins 16 | cp *.png .out/archive/plugins 17 | cp pcm/icon.png .out/archive/resources 18 | cp pcm/metadata.template.json .out/archive/metadata.json 19 | 20 | echo "Write version info to file" 21 | echo $VERSION > .out/archive/plugins/VERSION 22 | 23 | echo "Modify archive metadata.json" 24 | sed -i "s/VERSION_HERE/$VERSION/g" .out/archive/metadata.json 25 | sed -i "s/\"kicad_version_max\": \"9.0\",/\"kicad_version_max\": \"9.0\"/g" .out/archive/metadata.json 26 | sed -i "/SHA256_HERE/d" .out/archive/metadata.json 27 | sed -i "/DOWNLOAD_SIZE_HERE/d" .out/archive/metadata.json 28 | sed -i "/DOWNLOAD_URL_HERE/d" .out/archive/metadata.json 29 | sed -i "/INSTALL_SIZE_HERE/d" .out/archive/metadata.json 30 | 31 | echo "Zip PCM archive" 32 | cd .out/archive 33 | zip -r ../jlc-kicad-lib-loader-$VERSION-pcm.zip . 34 | cd ../.. 35 | 36 | echo "Gather data for repo rebuild" 37 | CI_ENV="${CI_ENV:-.out/env}" 38 | 39 | echo VERSION=$VERSION >> $CI_ENV 40 | echo DOWNLOAD_SHA256=$(shasum --algorithm 256 .out/jlc-kicad-lib-loader-$VERSION-pcm.zip | xargs | cut -d' ' -f1) >> $CI_ENV 41 | echo DOWNLOAD_SIZE=$(ls -l .out/jlc-kicad-lib-loader-$VERSION-pcm.zip | xargs | cut -d' ' -f5) >> $CI_ENV 42 | echo DOWNLOAD_URL="https://github.com/dsa-t/jlc-kicad-lib-loader/releases/download/$VERSION/jlc-kicad-lib-loader-$VERSION-pcm.zip" >> $CI_ENV 43 | echo INSTALL_SIZE=$(unzip -l .out/jlc-kicad-lib-loader-$VERSION-pcm.zip | tail -1 | xargs | cut -d' ' -f1) >> $CI_ENV 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JLCPCB/LCSC Library Loader 2 | 3 | This KiCad plugin allows you to search and download symbols/footprints with 3D models to a local .elibz library that can be read by KiCad. 4 | 5 | ![image](https://github.com/user-attachments/assets/37e16749-94ea-46e8-88c9-e85164eaf495) 6 | 7 | # System support 8 | 9 | - **KiCad**: version 8.0.7 or newer. 10 | - **Windows**: version 10 or newer with normal KiCad installation. 11 | - **Ubuntu**: install KiCad from PPA. To make the preview work, install `python3-wxgtk-webview4.0`. 12 | - **Flatpak**: works but preview is not available due to missing webkitgtk2. 13 | - **macOS**: not tested 14 | 15 | # Installation 16 | 17 | 1. Download the latest `jlc-kicad-lib-loader-*-pcm.zip` archive from [Releases page](https://github.com/dsa-t/jlc-kicad-lib-loader/releases). 18 | 19 | 2. Open PCM in KiCad, click "Install from File...", then choose the downloaded `-pcm` archive: 20 | 21 | ![image](https://github.com/user-attachments/assets/debae118-1292-498a-81f2-29fdc2cf455d) 22 | 23 | ## To support importing encrypted data 24 | 25 | ### Windows 26 | 27 | 3. Open "KiCad x.x Command Prompt": 28 | 29 | ![image](https://github.com/user-attachments/assets/9975de9a-d1cc-4ee7-94b8-11fb492b8b77) 30 | 31 | 4. Execute `pip install pycryptodome` 32 | 33 | ![image](https://github.com/user-attachments/assets/1abcd9ed-7358-4508-a9fb-75d2bc9bb2a1) 34 | 35 | ### Debian/Ubuntu 36 | 37 | 3. ``` 38 | sudo apt install python3-pycryptodome 39 | ``` 40 | 41 | ### Flatpak 42 | 43 | 3. ``` 44 | flatpak run --command=pip org.kicad.KiCad install pycryptodome 45 | ``` 46 | 47 | ### Other OSes 48 | 49 | 3. ``` 50 | pip install pycryptodome 51 | ``` 52 | 53 | # Library setup 54 | 55 | The plugin now automatically manages library configuration: 56 | 57 | - **Library Name Storage**: The library name is saved in a `jlc-kicad-lib-loader.ini` file in your project directory and will be remembered for future use. 58 | - **Automatic Library Table Addition**: When downloading components, if the library is not found in your project-specific Symbol/Footprint library tables, the plugin will prompt you to add it automatically. 59 | 60 | ## Manual Library Setup (if needed) 61 | 62 | If you need to manually add the .elibz library to your Symbol/Footprint library tables: 63 | 64 | ![image](https://github.com/user-attachments/assets/45583737-6747-4aa8-975c-2a90a6f192d6) 65 | 66 | ### Symbol library table: 67 | 68 | ![image](https://github.com/user-attachments/assets/a3ff3856-5637-46da-8349-0b965986680f) 69 | 70 | ### Footprint library table: 71 | 72 | ![image](https://github.com/user-attachments/assets/8512a77f-95e5-4d4f-bba6-4a2b5660e218) 73 | -------------------------------------------------------------------------------- /easyeda_lib_loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | easyeda_3d_loader 21 | 45 | 56 | 57 | 59 | 60 | 62 | 64 | easyeda_3d_loader 65 | 66 | 68 | 70 | 72 | 74 | 75 | 76 | 77 | 79 | 81 | 90 | 95 | 96 | 97 | 99 | 104 | Lib 111 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /config_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import configparser 3 | import wx 4 | from logging import info, warning, debug, error 5 | 6 | class ConfigManager: 7 | """Manages configuration for JLC KiCad Library Loader""" 8 | 9 | CONFIG_FILENAME = "jlc-kicad-lib-loader.ini" 10 | 11 | def __init__(self, kiprjmod): 12 | self.kiprjmod = kiprjmod 13 | self.config_path = os.path.join(kiprjmod, self.CONFIG_FILENAME) 14 | self.config = configparser.ConfigParser() 15 | self.load_config() 16 | 17 | def load_config(self): 18 | """Load configuration from INI file""" 19 | if os.path.exists(self.config_path): 20 | try: 21 | self.config.read(self.config_path) 22 | debug(f"Loaded configuration from {self.config_path}") 23 | except Exception as e: 24 | warning(f"Failed to load configuration: {e}") 25 | else: 26 | debug(f"Configuration file not found at {self.config_path}") 27 | 28 | def save_config(self): 29 | """Save configuration to INI file""" 30 | try: 31 | with open(self.config_path, 'w') as configfile: 32 | self.config.write(configfile) 33 | debug(f"Saved configuration to {self.config_path}") 34 | except Exception as e: 35 | error(f"Failed to save configuration: {e}") 36 | 37 | def get_library_name(self, default="EasyEDA_Lib"): 38 | """Get the library name from config""" 39 | if not self.config.has_section('Library'): 40 | return default 41 | return self.config.get('Library', 'name', fallback=default) 42 | 43 | def set_library_name(self, name): 44 | """Set the library name in config""" 45 | if not self.config.has_section('Library'): 46 | self.config.add_section('Library') 47 | self.config.set('Library', 'name', name) 48 | self.save_config() 49 | 50 | 51 | class LibraryTableManager: 52 | """Manages KiCad symbol and footprint library tables""" 53 | 54 | def __init__(self, kiprjmod): 55 | self.kiprjmod = kiprjmod 56 | self.sym_lib_table_path = os.path.join(kiprjmod, "sym-lib-table") 57 | self.fp_lib_table_path = os.path.join(kiprjmod, "fp-lib-table") 58 | 59 | def check_library_exists(self, lib_name, lib_type="symbol"): 60 | """Check if a library exists in the library table 61 | 62 | Args: 63 | lib_name: Name of the library 64 | lib_type: Either "symbol" or "footprint" 65 | 66 | Returns: 67 | True if library exists, False otherwise 68 | """ 69 | table_path = self.sym_lib_table_path if lib_type == "symbol" else self.fp_lib_table_path 70 | 71 | if not os.path.exists(table_path): 72 | debug(f"{lib_type} library table not found at {table_path}") 73 | return False 74 | 75 | try: 76 | with open(table_path, 'r', encoding='utf-8') as f: 77 | content = f.read() 78 | # Check if library name exists in the table 79 | # Library entries look like: (lib (name "LibraryName")...) 80 | return f'(name "{lib_name}")' in content 81 | except Exception as e: 82 | warning(f"Failed to read {lib_type} library table: {e}") 83 | return False 84 | 85 | def add_library_to_table(self, lib_name, lib_path, lib_type="symbol"): 86 | """Add a library to the library table 87 | 88 | Args: 89 | lib_name: Name of the library 90 | lib_path: Path to the library file (relative to project) 91 | lib_type: Either "symbol" or "footprint" 92 | 93 | Returns: 94 | True if successful, False otherwise 95 | """ 96 | table_path = self.sym_lib_table_path if lib_type == "symbol" else self.fp_lib_table_path 97 | 98 | # Determine the library entry format for EasyEDA library 99 | # Both symbol and footprint libraries use the same .elibz file 100 | if lib_type == "symbol": 101 | lib_entry = f' (lib (name "{lib_name}")(type "EasyEDA (JLCEDA) Pro")(uri "${{KIPRJMOD}}/{lib_name}/{lib_name}.elibz")(options "")(descr ""))\n' 102 | else: 103 | lib_entry = f' (lib (name "{lib_name}")(type "EasyEDA / JLCEDA Pro")(uri "${{KIPRJMOD}}/{lib_name}/{lib_name}.elibz")(options "")(descr ""))\n' 104 | 105 | try: 106 | # Create table if it doesn't exist 107 | if not os.path.exists(table_path): 108 | table_content = f"(sym_lib_table\n{lib_entry})\n" if lib_type == "symbol" else f"(fp_lib_table\n{lib_entry})\n" 109 | with open(table_path, 'w', encoding='utf-8') as f: 110 | f.write(table_content) 111 | info(f"Created new {lib_type} library table with {lib_name}") 112 | return True 113 | 114 | # Read existing table 115 | with open(table_path, 'r', encoding='utf-8') as f: 116 | lines = f.readlines() 117 | 118 | # Find the position to insert (before the closing parenthesis) 119 | insert_pos = -1 120 | for i in range(len(lines) - 1, -1, -1): 121 | if lines[i].strip() == ')': 122 | insert_pos = i 123 | break 124 | 125 | if insert_pos == -1: 126 | error(f"Invalid {lib_type} library table format") 127 | return False 128 | 129 | # Insert the new library entry 130 | lines.insert(insert_pos, lib_entry) 131 | 132 | # Write back to file 133 | with open(table_path, 'w', encoding='utf-8') as f: 134 | f.writelines(lines) 135 | 136 | info(f"Added {lib_name} to {lib_type} library table") 137 | return True 138 | 139 | except Exception as e: 140 | error(f"Failed to add library to {lib_type} table: {e}") 141 | return False 142 | 143 | def prompt_add_library(self, parent, lib_name, lib_path): 144 | """Prompt user to add library to symbol and footprint tables 145 | 146 | Args: 147 | parent: Parent window for the dialog 148 | lib_name: Name of the library 149 | lib_path: Path to the library 150 | 151 | Returns: 152 | True if libraries were added or already exist, False if user cancelled 153 | """ 154 | symbol_exists = self.check_library_exists(lib_name, "symbol") 155 | footprint_exists = self.check_library_exists(lib_name, "footprint") 156 | 157 | if symbol_exists and footprint_exists: 158 | debug(f"Library {lib_name} already exists in both tables") 159 | return True 160 | 161 | # Build message 162 | missing_libs = [] 163 | if not symbol_exists: 164 | missing_libs.append("Symbol") 165 | if not footprint_exists: 166 | missing_libs.append("Footprint") 167 | 168 | msg = f"The library '{lib_name}' is not found in the project-specific {' and '.join(missing_libs)} library table(s).\n\n" 169 | msg += "Would you like to add it automatically?" 170 | 171 | dlg = wx.MessageDialog( 172 | parent, 173 | msg, 174 | "Add Library to Project", 175 | wx.YES_NO | wx.ICON_QUESTION | wx.YES_DEFAULT 176 | ) 177 | 178 | result = dlg.ShowModal() 179 | dlg.Destroy() 180 | 181 | if result == wx.ID_YES: 182 | success = True 183 | 184 | if not symbol_exists: 185 | if not self.add_library_to_table(lib_name, lib_path, "symbol"): 186 | success = False 187 | 188 | if not footprint_exists: 189 | if not self.add_library_to_table(lib_name, lib_path, "footprint"): 190 | success = False 191 | 192 | if success: 193 | info_dlg = wx.MessageDialog( 194 | parent, 195 | f"Library '{lib_name}' has been added to the project library tables.\n\n" 196 | "Note: You may need to restart KiCad for the changes to take effect.", 197 | "Library Added Successfully", 198 | wx.OK | wx.ICON_INFORMATION 199 | ) 200 | info_dlg.ShowModal() 201 | info_dlg.Destroy() 202 | 203 | return success 204 | 205 | return False 206 | -------------------------------------------------------------------------------- /easyeda_lib_loader_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ########################################################################### 4 | ## Python code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6) 5 | ## http://www.wxformbuilder.org/ 6 | ## 7 | ## PLEASE DO *NOT* EDIT THIS FILE! 8 | ########################################################################### 9 | 10 | import wx 11 | import wx.xrc 12 | import wx.dataview 13 | import wx.adv 14 | 15 | ########################################################################### 16 | ## Class EasyEdaLibLoaderDialog 17 | ########################################################################### 18 | 19 | class EasyEdaLibLoaderDialog ( wx.Dialog ): 20 | 21 | def __init__( self, parent ): 22 | wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = u"JLCPCB/LCSC Library Loader. Unofficial, use at your own risk.", pos = wx.DefaultPosition, size = wx.Size( -1,-1 ), style = wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER ) 23 | 24 | self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) 25 | 26 | bSizer6 = wx.BoxSizer( wx.HORIZONTAL ) 27 | 28 | self.m_splitter2 = wx.SplitterWindow( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_3D|wx.SP_LIVE_UPDATE ) 29 | self.m_splitter2.SetSashGravity( 1 ) 30 | self.m_splitter2.Bind( wx.EVT_IDLE, self.m_splitter2OnIdle ) 31 | self.m_splitter2.SetMinimumPaneSize( 100 ) 32 | 33 | self.m_leftPanel = wx.Panel( self.m_splitter2, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 34 | bSizer4 = wx.BoxSizer( wx.VERTICAL ) 35 | 36 | bSizer5 = wx.BoxSizer( wx.HORIZONTAL ) 37 | 38 | m_libSourceChoiceChoices = [ u"All Sources", u"JLC System", u"JLC Public" ] 39 | self.m_libSourceChoice = wx.Choice( self.m_leftPanel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_libSourceChoiceChoices, 0 ) 40 | self.m_libSourceChoice.SetSelection( 0 ) 41 | bSizer5.Add( self.m_libSourceChoice, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) 42 | 43 | self.m_textCtrlSearch = wx.TextCtrl( self.m_leftPanel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_PROCESS_ENTER ) 44 | bSizer5.Add( self.m_textCtrlSearch, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) 45 | 46 | self.m_searchBtn = wx.Button( self.m_leftPanel, wx.ID_ANY, u"Find", wx.DefaultPosition, wx.DefaultSize, 0 ) 47 | bSizer5.Add( self.m_searchBtn, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) 48 | 49 | 50 | bSizer4.Add( bSizer5, 0, wx.EXPAND, 5 ) 51 | 52 | self.m_splitter5 = wx.SplitterWindow( self.m_leftPanel, wx.ID_ANY, wx.DefaultPosition, wx.Size( 650,680 ), wx.SP_3D|wx.SP_LIVE_UPDATE ) 53 | self.m_splitter5.SetSashGravity( 0 ) 54 | self.m_splitter5.Bind( wx.EVT_IDLE, self.m_splitter5OnIdle ) 55 | self.m_splitter5.SetMinimumPaneSize( 100 ) 56 | 57 | self.m_statusPanel = wx.Panel( self.m_splitter5, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 58 | bSizer18 = wx.BoxSizer( wx.VERTICAL ) 59 | 60 | self.m_searchResultsTree = wx.dataview.TreeListCtrl( self.m_statusPanel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.dataview.TL_MULTIPLE ) 61 | self.m_searchResultsTree.SetMinSize( wx.Size( 400,300 ) ) 62 | 63 | 64 | bSizer18.Add( self.m_searchResultsTree, 1, wx.EXPAND |wx.ALL, 5 ) 65 | 66 | bStatusSizer = wx.BoxSizer( wx.HORIZONTAL ) 67 | 68 | self.m_searchStatus = wx.StaticText( self.m_statusPanel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) 69 | self.m_searchStatus.Wrap( -1 ) 70 | 71 | bStatusSizer.Add( self.m_searchStatus, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) 72 | 73 | self.m_searchStatus2 = wx.StaticText( self.m_statusPanel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) 74 | self.m_searchStatus2.Wrap( -1 ) 75 | 76 | bStatusSizer.Add( self.m_searchStatus2, 0, wx.ALIGN_CENTER_VERTICAL|wx.TOP, 2 ) 77 | 78 | self.m_searchHyperlink1 = wx.adv.HyperlinkCtrl( self.m_statusPanel, wx.ID_ANY, wx.EmptyString, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.adv.HL_ALIGN_LEFT|wx.adv.HL_CONTEXTMENU ) 79 | bStatusSizer.Add( self.m_searchHyperlink1, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT|wx.ALL, 5 ) 80 | 81 | self.m_searchHyperlink2 = wx.adv.HyperlinkCtrl( self.m_statusPanel, wx.ID_ANY, wx.EmptyString, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.adv.HL_ALIGN_LEFT|wx.adv.HL_CONTEXTMENU ) 82 | bStatusSizer.Add( self.m_searchHyperlink2, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT|wx.ALL, 5 ) 83 | 84 | self.m_searchHyperlink3 = wx.adv.HyperlinkCtrl( self.m_statusPanel, wx.ID_ANY, wx.EmptyString, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.adv.HL_ALIGN_LEFT|wx.adv.HL_CONTEXTMENU ) 85 | bStatusSizer.Add( self.m_searchHyperlink3, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT|wx.ALL, 5 ) 86 | 87 | 88 | bStatusSizer.Add( ( 0, 0), 1, wx.EXPAND, 5 ) 89 | 90 | self.m_searchPage = wx.StaticText( self.m_statusPanel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) 91 | self.m_searchPage.Wrap( -1 ) 92 | 93 | bStatusSizer.Add( self.m_searchPage, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) 94 | 95 | self.m_prevPageBtn = wx.Button( self.m_statusPanel, wx.ID_ANY, u" < ", wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT ) 96 | self.m_prevPageBtn.Enable( False ) 97 | 98 | bStatusSizer.Add( self.m_prevPageBtn, 0, wx.ALIGN_CENTER_VERTICAL|wx.BOTTOM|wx.TOP, 5 ) 99 | 100 | self.m_nextPageBtn = wx.Button( self.m_statusPanel, wx.ID_ANY, u" > ", wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT ) 101 | self.m_nextPageBtn.Enable( False ) 102 | 103 | bStatusSizer.Add( self.m_nextPageBtn, 0, wx.ALIGN_CENTER_VERTICAL|wx.BOTTOM|wx.RIGHT|wx.TOP, 5 ) 104 | 105 | 106 | bSizer18.Add( bStatusSizer, 0, wx.EXPAND, 5 ) 107 | 108 | 109 | self.m_statusPanel.SetSizer( bSizer18 ) 110 | self.m_statusPanel.Layout() 111 | bSizer18.Fit( self.m_statusPanel ) 112 | self.m_webViewPanel = wx.Panel( self.m_splitter5, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 113 | self.m_webViewPanel.SetMinSize( wx.Size( 650,380 ) ) 114 | 115 | bSizer19 = wx.BoxSizer( wx.VERTICAL ) 116 | 117 | 118 | self.m_webViewPanel.SetSizer( bSizer19 ) 119 | self.m_webViewPanel.Layout() 120 | bSizer19.Fit( self.m_webViewPanel ) 121 | self.m_splitter5.SplitHorizontally( self.m_statusPanel, self.m_webViewPanel, 0 ) 122 | bSizer4.Add( self.m_splitter5, 1, wx.EXPAND, 5 ) 123 | 124 | 125 | self.m_leftPanel.SetSizer( bSizer4 ) 126 | self.m_leftPanel.Layout() 127 | bSizer4.Fit( self.m_leftPanel ) 128 | self.m_panel1 = wx.Panel( self.m_splitter2, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 129 | bSizer13 = wx.BoxSizer( wx.VERTICAL ) 130 | 131 | self.m_splitter3 = wx.SplitterWindow( self.m_panel1, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.SP_3D|wx.SP_LIVE_UPDATE ) 132 | self.m_splitter3.SetSashGravity( 0.5 ) 133 | self.m_splitter3.Bind( wx.EVT_IDLE, self.m_splitter3OnIdle ) 134 | self.m_splitter3.SetMinimumPaneSize( 100 ) 135 | 136 | self.m_panel5 = wx.Panel( self.m_splitter3, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 137 | bSizer1 = wx.BoxSizer( wx.VERTICAL ) 138 | 139 | self.m_staticText1 = wx.StaticText( self.m_panel5, wx.ID_ANY, u"Enter JLCPCB/LCSC codes or UUIDs to download:", wx.DefaultPosition, wx.DefaultSize, 0 ) 140 | self.m_staticText1.Wrap( -1 ) 141 | 142 | bSizer1.Add( self.m_staticText1, 0, wx.ALL, 5 ) 143 | 144 | self.m_textCtrlParts = wx.TextCtrl( self.m_panel5, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE ) 145 | bSizer1.Add( self.m_textCtrlParts, 1, wx.ALL|wx.EXPAND, 5 ) 146 | 147 | bSizer3 = wx.BoxSizer( wx.HORIZONTAL ) 148 | 149 | self.m_staticText2 = wx.StaticText( self.m_panel5, wx.ID_ANY, u"Library path:", wx.DefaultPosition, wx.DefaultSize, 0 ) 150 | self.m_staticText2.Wrap( -1 ) 151 | 152 | bSizer3.Add( self.m_staticText2, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) 153 | 154 | self.m_textCtrlOutLibName = wx.TextCtrl( self.m_panel5, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) 155 | bSizer3.Add( self.m_textCtrlOutLibName, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL|wx.EXPAND, 5 ) 156 | 157 | 158 | bSizer1.Add( bSizer3, 0, wx.EXPAND, 5 ) 159 | 160 | self.m_actionBtn = wx.Button( self.m_panel5, wx.ID_ANY, u"Download parts", wx.DefaultPosition, wx.DefaultSize, 0 ) 161 | bSizer1.Add( self.m_actionBtn, 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALL, 5 ) 162 | 163 | 164 | self.m_panel5.SetSizer( bSizer1 ) 165 | self.m_panel5.Layout() 166 | bSizer1.Fit( self.m_panel5 ) 167 | self.m_panel6 = wx.Panel( self.m_splitter3, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 168 | bSizer14 = wx.BoxSizer( wx.VERTICAL ) 169 | 170 | self.m_progress = wx.Gauge( self.m_panel6, wx.ID_ANY, 100, wx.DefaultPosition, wx.DefaultSize, wx.GA_HORIZONTAL ) 171 | self.m_progress.SetValue( 0 ) 172 | bSizer14.Add( self.m_progress, 0, wx.ALL|wx.EXPAND, 5 ) 173 | 174 | self.m_log = wx.TextCtrl( self.m_panel6, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_BESTWRAP|wx.TE_MULTILINE|wx.TE_READONLY ) 175 | self.m_log.SetMinSize( wx.Size( 400,40 ) ) 176 | 177 | bSizer14.Add( self.m_log, 1, wx.ALL|wx.EXPAND, 5 ) 178 | 179 | bSizer2 = wx.BoxSizer( wx.HORIZONTAL ) 180 | 181 | self.m_debug = wx.CheckBox( self.m_panel6, wx.ID_ANY, u"Debug", wx.DefaultPosition, wx.DefaultSize, 0 ) 182 | bSizer2.Add( self.m_debug, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT|wx.ALL, 5 ) 183 | 184 | 185 | bSizer2.Add( ( 0, 0), 1, wx.EXPAND, 5 ) 186 | 187 | self.m_closeButton = wx.Button( self.m_panel6, wx.ID_CANCEL, u"Close dialog", wx.DefaultPosition, wx.DefaultSize, 0 ) 188 | bSizer2.Add( self.m_closeButton, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) 189 | 190 | 191 | bSizer14.Add( bSizer2, 0, wx.EXPAND, 5 ) 192 | 193 | 194 | self.m_panel6.SetSizer( bSizer14 ) 195 | self.m_panel6.Layout() 196 | bSizer14.Fit( self.m_panel6 ) 197 | self.m_splitter3.SplitHorizontally( self.m_panel5, self.m_panel6, 0 ) 198 | bSizer13.Add( self.m_splitter3, 1, wx.EXPAND, 5 ) 199 | 200 | 201 | self.m_panel1.SetSizer( bSizer13 ) 202 | self.m_panel1.Layout() 203 | bSizer13.Fit( self.m_panel1 ) 204 | self.m_splitter2.SplitVertically( self.m_leftPanel, self.m_panel1, 650 ) 205 | bSizer6.Add( self.m_splitter2, 1, wx.EXPAND, 5 ) 206 | 207 | 208 | self.SetSizer( bSizer6 ) 209 | self.Layout() 210 | bSizer6.Fit( self ) 211 | 212 | self.Centre( wx.BOTH ) 213 | 214 | def __del__( self ): 215 | pass 216 | 217 | def m_splitter2OnIdle( self, event ): 218 | self.m_splitter2.SetSashPosition( 650 ) 219 | self.m_splitter2.Unbind( wx.EVT_IDLE ) 220 | 221 | def m_splitter5OnIdle( self, event ): 222 | self.m_splitter5.SetSashPosition( 0 ) 223 | self.m_splitter5.Unbind( wx.EVT_IDLE ) 224 | 225 | def m_splitter3OnIdle( self, event ): 226 | self.m_splitter3.SetSashPosition( 0 ) 227 | self.m_splitter3.Unbind( wx.EVT_IDLE ) 228 | 229 | 230 | -------------------------------------------------------------------------------- /component_loader.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import os 3 | import json 4 | import traceback 5 | import requests 6 | import concurrent.futures 7 | import zipfile 8 | import urllib 9 | 10 | from logging import info, warning, debug, error, critical 11 | from typing import Callable 12 | 13 | from pcbnew import * 14 | 15 | 16 | MODELS_DIR = "EASYEDA_MODELS" 17 | 18 | # UUID strings can be in the format |. This function gets the part 19 | def getUuidFirstPart(uuid): 20 | if not uuid: 21 | return None 22 | return uuid.split("|")[0] 23 | 24 | # Extract dataStr from component data. If dataStr is not available, try to decrypt and decompress the data from dataStrId URL. 25 | def extractDataStr(component_data): 26 | if not component_data: 27 | return None 28 | 29 | # Try direct dataStr first 30 | dataStr = component_data.get("dataStr") 31 | if dataStr: 32 | return dataStr 33 | 34 | # Try dataStrId if dataStr not available 35 | dataStrId = component_data.get("dataStrId") 36 | if dataStrId: 37 | try: 38 | keyHex = component_data.get("key") 39 | ivHex = component_data.get("iv") 40 | 41 | debug("dataStrId key: " + keyHex) 42 | debug("dataStrId iv: " + ivHex) 43 | 44 | dataStrResp = requests.get(dataStrId) 45 | dataStrResp.raise_for_status() 46 | 47 | debug("dataStrId encrypted content: " + dataStrResp.content.hex()) 48 | 49 | from . import decryptor 50 | decryptedStr = decryptor.decryptDataStrIdData(dataStrResp.content, keyHex, ivHex) 51 | 52 | debug("dataStrId decrypted content: " + decryptedStr) 53 | 54 | return decryptedStr 55 | except Exception as e: 56 | info(f"Failed to fetch/decrypt dataStrId: {e}") 57 | 58 | return None 59 | 60 | class ComponentLoader(): 61 | def __init__(self, kiprjmod, target_path, target_name, progress: Callable[[int, int], None]): 62 | self.kiprjmod = kiprjmod 63 | self.target_path = target_path 64 | self.target_name = target_name 65 | self.progress = progress 66 | 67 | def downloadAll(self, components): 68 | self.progress(0, 100) 69 | 70 | try: 71 | libDeviceFile, fetched_3dmodels = self.downloadSymFp(components) 72 | self.downloadModels(libDeviceFile, fetched_3dmodels) 73 | self.progress(100, 100) 74 | except Exception as e: 75 | traceback.print_exc() 76 | error(f"Failed to download components: {traceback.format_exc()}") 77 | 78 | def downloadSymFp(self, components): 79 | info(f"Fetching info...") 80 | 81 | # Separate components into code-based and direct UUIDs 82 | code_components = [] 83 | direct_uuids = [] 84 | 85 | for comp in components: 86 | if comp.startswith("C"): 87 | code_components.append(comp) 88 | else: 89 | direct_uuids.append(comp) 90 | 91 | fetched_devices = {} 92 | 93 | # Fetch UUIDs from code-based components 94 | if code_components: 95 | resp = requests.post("https://pro.easyeda.com/api/v2/devices/searchByCodes", data={"codes[]": code_components}) 96 | resp.raise_for_status() 97 | found = resp.json() 98 | 99 | debug("searchByCodes: " + json.dumps(found, indent=4)) 100 | 101 | if not found.get("success") or not found.get("result"): 102 | raise Exception(f"Unable to fetch device info: {found}") 103 | 104 | # Append fetched UUIDs to direct_uuids 105 | for entry in found["result"]: 106 | direct_uuids.append(entry['uuid']) 107 | 108 | # Fetch device info by UUID 109 | def fetch_device_info(dev_uuid): 110 | dev_info = requests.get(f"https://pro.easyeda.com/api/devices/{dev_uuid}") 111 | dev_info.raise_for_status() 112 | 113 | debug("device info: " + json.dumps(dev_info.json(), indent=4)) 114 | 115 | device = dev_info.json()["result"] 116 | fetched_devices[device["uuid"]] = device 117 | 118 | with concurrent.futures.ThreadPoolExecutor() as executor: 119 | for dev_uuid in direct_uuids: 120 | executor.submit(fetch_device_info, dev_uuid) 121 | 122 | # Collect symbol/footprint/3D model UUIDs to fetch 123 | fetched_symbols = {} 124 | fetched_footprints = {} 125 | fetched_3dmodels = {} 126 | uuid_to_obj_map = {} 127 | 128 | all_uuids = set() 129 | for entry in fetched_devices.values(): 130 | if entry['attributes'].get('Symbol'): 131 | all_uuids.add(entry['attributes']['Symbol']) 132 | uuid_to_obj_map[entry['attributes']['Symbol']] = fetched_symbols 133 | 134 | if entry['attributes'].get('Footprint'): 135 | all_uuids.add(entry['attributes']['Footprint']) 136 | uuid_to_obj_map[entry['attributes']['Footprint']] = fetched_footprints 137 | 138 | if entry['attributes'].get('3D Model'): 139 | all_uuids.add(getUuidFirstPart(entry['attributes']['3D Model'])) 140 | uuid_to_obj_map[getUuidFirstPart(entry['attributes']['3D Model'])] = fetched_3dmodels 141 | 142 | # Fetch symbols/footprints/3D models 143 | def fetch_component(uuid): 144 | url = f"https://pro.easyeda.com/api/v2/components/{uuid}" 145 | r = requests.get(url) 146 | r.raise_for_status() 147 | return r.json()["result"] 148 | 149 | with concurrent.futures.ThreadPoolExecutor() as executor: 150 | futures = {executor.submit(fetch_component, uuid): uuid for uuid in all_uuids} 151 | for future in concurrent.futures.as_completed(futures): 152 | try: 153 | compData = future.result() 154 | debug(f"Fetched component {json.dumps(compData, indent=4)}") 155 | 156 | uuid_to_obj_map[compData["uuid"]][compData["uuid"]] = compData 157 | except Exception as e: 158 | error(f"Failed to fetch component for uuid {futures[future]}: {e}") 159 | 160 | # Set symbol/footprint type fields 161 | for device in fetched_devices.values(): 162 | if device['attributes'].get('Symbol'): 163 | fetched_symbols[device["attributes"]["Symbol"]]["type"] = device["symbol_type"] 164 | 165 | if device['attributes'].get('Footprint'): 166 | fetched_footprints[device["attributes"]["Footprint"]]["type"] = device["footprint_type"] 167 | 168 | # Extract dataStr 169 | footprint_data_str = {} 170 | symbol_data_str = {} 171 | 172 | # Separate dataStr for footprints 173 | for f_uuid, f_data in fetched_footprints.items(): 174 | ds = extractDataStr(f_data) 175 | if ds: 176 | footprint_data_str[f_uuid] = ds 177 | 178 | f_data.pop("dataStr", None) # Remove the dataStr field if exists 179 | 180 | # Separate dataStr for symbols 181 | for s_uuid, s_data in fetched_symbols.items(): 182 | ds = extractDataStr(s_data) 183 | if ds: 184 | symbol_data_str[s_uuid] = ds 185 | 186 | s_data.pop("dataStr", None) # Remove the dataStr field if exists 187 | 188 | libDeviceFile = { 189 | "devices": fetched_devices, 190 | "symbols": fetched_symbols, 191 | "footprints": fetched_footprints 192 | } 193 | 194 | os.makedirs(self.target_path, exist_ok=True) 195 | 196 | zip_filename = f"{self.target_path}/{self.target_name}.elibz" 197 | merged_data = copy.deepcopy(libDeviceFile) 198 | 199 | try: 200 | if os.path.exists(zip_filename): 201 | with zipfile.ZipFile(zip_filename, "r") as old_zip: 202 | for name in old_zip.namelist(): 203 | if name == "device.json": 204 | old_data = json.loads(old_zip.read("device.json").decode("utf-8")) 205 | for entry_type in ["devices", "symbols", "footprints"]: 206 | for key in old_data[entry_type]: 207 | if key not in merged_data[entry_type]: 208 | merged_data[entry_type][key] = old_data[entry_type][key] 209 | if name.endswith('.esym'): 210 | symbol_uuid = os.path.splitext(os.path.basename(name))[0] 211 | if symbol_uuid not in symbol_data_str: 212 | symbol_data_str[symbol_uuid] = old_zip.read(name).decode('utf-8') 213 | elif name.endswith('.efoo'): 214 | footprint_uuid = os.path.splitext(os.path.basename(name))[0] 215 | if footprint_uuid not in footprint_data_str: 216 | footprint_data_str[footprint_uuid] = old_zip.read(name).decode('utf-8') 217 | except Exception as e: 218 | warning(f"Failed to merge device.json data, overwriting: {e}") 219 | 220 | with zipfile.ZipFile(zip_filename, "w", compression=zipfile.ZIP_DEFLATED) as zf: 221 | zf.writestr("device.json", json.dumps(merged_data, indent=4)) 222 | for fp_uuid, ds in footprint_data_str.items(): 223 | zf.writestr(f"FOOTPRINT/{fp_uuid}.efoo", ds) 224 | for sym_uuid, ds in symbol_data_str.items(): 225 | zf.writestr(f"SYMBOL/{sym_uuid}.esym", ds) 226 | 227 | info( "*****************************" ) 228 | info(f"Downloaded {len(fetched_devices)} devices, {len(fetched_symbols)} symbols, {len(fetched_footprints)} footprints and added to library: {zip_filename}") 229 | return libDeviceFile, fetched_3dmodels 230 | 231 | def downloadModels(self, libDeviceFile, fetched_3dmodels): 232 | self.totalToDownload = 0 233 | self.downloadedCounter = 0 234 | self.statExisting = 0 235 | self.statDownloaded = 0 236 | self.statFailed = 0 237 | 238 | info( "*****************************" ) 239 | info(f"Loading 3D models...") 240 | self.progress(0, 100) 241 | 242 | uuidToTargetFileMap = {} 243 | uuidsToTransform = {} 244 | 245 | debug("fetched_3dmodels: " + json.dumps(fetched_3dmodels, indent=4)) 246 | debug("libDeviceFile: " + json.dumps(libDeviceFile, indent=4)) 247 | 248 | for device in libDeviceFile["devices"].values(): 249 | try: 250 | modelUuid = getUuidFirstPart(device["attributes"].get("3D Model")) 251 | 252 | if not modelUuid or modelUuid not in fetched_3dmodels: 253 | info("No model for device '%s', footprint '%s'" 254 | % (device.get("product_code", device.get("uuid")), 255 | device.get("footprint").get("display_title") if device.get("footprint") else "None")) 256 | continue 257 | 258 | modelTitle = device["attributes"]["3D Model Title"] 259 | modelTransform = device["attributes"].get("3D Model Transform", "") 260 | 261 | dataStr = extractDataStr(fetched_3dmodels[modelUuid]) 262 | 263 | if dataStr: 264 | directUuid = json.loads(dataStr)["model"] 265 | else: 266 | info("Unable to extract model for device '%s', footprint '%s'" 267 | % (device.get("product_code", device.get("uuid")), 268 | device.get("footprint").get("display_title") if device.get("footprint") else "None")) 269 | continue 270 | 271 | uuidsToTransform[directUuid] = [float(x) for x in modelTransform.split(",")] 272 | 273 | easyEdaFilename = os.path.join(self.kiprjmod, MODELS_DIR, modelTitle + ".step") 274 | easyEdaFilename = os.path.normpath(easyEdaFilename) 275 | 276 | uuidToTargetFileMap[directUuid] = easyEdaFilename 277 | except KeyboardInterrupt: 278 | return 279 | except Exception as e: 280 | traceback.print_exc() 281 | info("Cannot get model for device '%s': %s" % (device.get("product_code", device.get("uuid")), str(e))) 282 | continue 283 | 284 | with concurrent.futures.ThreadPoolExecutor(1) as texecutor: 285 | def fixupModel(fixTaskArgs): 286 | directUuid, kfilePath = fixTaskArgs 287 | 288 | file_name = os.path.splitext( os.path.basename( kfilePath ) ) [0] 289 | jfilePath = kfilePath + "_jlc" 290 | 291 | debug( "Loading STEP model %s" % (file_name) ) 292 | model: UTILS_STEP_MODEL = UTILS_STEP_MODEL.LoadSTEP(jfilePath) 293 | 294 | if not model: 295 | error( "Error loading model '%s'" % (file_name) ) 296 | return 297 | 298 | debug( "Converting STEP model '%s'" % (file_name) ) 299 | bbox: UTILS_BOX3D = model.GetBoundingBox() 300 | 301 | try: 302 | if directUuid in uuidsToTransform: 303 | # Convert mils to mm 304 | fitXmm = uuidsToTransform[directUuid][0] / 39.37 305 | fitYmm = uuidsToTransform[directUuid][1] / 39.37 306 | 307 | bsize: VECTOR3D = bbox.GetSize() 308 | scaleFactorX = fitXmm / bsize.x; 309 | scaleFactorY = fitYmm / bsize.y; 310 | scaleFactor = ( scaleFactorX + scaleFactorY ) / 2 311 | 312 | debug( "Dimensions %f %f factors %f %f avg %f model '%s'" % 313 | (fitXmm, fitYmm, scaleFactorX, scaleFactorY, scaleFactor, file_name) ) 314 | 315 | if abs( scaleFactorX - scaleFactorY ) > 0.1: 316 | warning( "Scale factors do not match: X %.3f; Y %.3f for model '%s'." % 317 | (scaleFactorX, scaleFactorY, file_name) ) 318 | warning( "**** The model '%s' might be misoriented! ****" % (file_name) ) 319 | elif abs( scaleFactor - 1.0 ) > 0.01: 320 | warning( "Scaling '%s' by %f" % (file_name, scaleFactor) ) 321 | model.Scale( scaleFactor ); 322 | else: 323 | debug( "No scaling for %s" % (file_name) ) 324 | 325 | except Exception as e: 326 | traceback.print_exc() 327 | error( "Error scaling model '%s': %s" % (file_name, str(e)) ) 328 | return 329 | 330 | newbbox = model.GetBoundingBox() 331 | center: VECTOR3D = newbbox.GetCenter() 332 | 333 | model.Translate( -center.x, -center.y, -newbbox.Min().z ) 334 | 335 | debug( "Saving STEP model %s" % (file_name) ) 336 | model.SaveSTEP( kfilePath ) 337 | 338 | # Delete the temporary JLC file after successful conversion 339 | try: 340 | if os.path.exists(jfilePath): 341 | os.remove(jfilePath) 342 | debug(f"Deleted temporary file {jfilePath}") 343 | except Exception as e: 344 | info(f"Failed to delete temporary file {jfilePath}: {str(e)}") 345 | 346 | with concurrent.futures.ThreadPoolExecutor(8) as dexecutor: 347 | def downloadStep(dnlTaskArgs): 348 | directUuid, kfilePath = dnlTaskArgs 349 | file_name = os.path.splitext( os.path.basename( kfilePath ) ) [0] 350 | 351 | try: 352 | if not os.path.exists(kfilePath): 353 | stepUrlFormat = "https://modules.easyeda.com/qAxj6KHrDKw4blvCG8QJPs7Y/{uuid}" 354 | jfilePath = kfilePath + "_jlc" 355 | url = stepUrlFormat.format(uuid=directUuid) 356 | 357 | debug("Downloading '%s'" % (file_name)) 358 | debug("'%s' from '%s'" % (file_name, url)) 359 | os.makedirs(os.path.dirname(kfilePath), exist_ok=True) 360 | urllib.request.urlretrieve(url, jfilePath) 361 | 362 | if os.path.isfile(jfilePath): 363 | debug("Downloaded '%s'." % (file_name)) 364 | self.statDownloaded += 1 365 | 366 | fixTaskArgs = [directUuid, kfilePath] 367 | texecutor.submit(fixupModel, fixTaskArgs) 368 | else: 369 | warning( "Path '%s' is not a file." % jfilePath ) 370 | else: 371 | info("Skipping '%s': STEP model file already exists." % (file_name)) 372 | self.statExisting += 1 373 | 374 | except Exception as e: 375 | warning("Failed to download model '%s': %s" % (file_name, str(e))) 376 | self.statFailed += 1 377 | 378 | self.downloadedCounter += 1 379 | self.progress(self.downloadedCounter, self.totalToDownload) 380 | 381 | self.totalToDownload = len(uuidToTargetFileMap) 382 | dexecutor.map(downloadStep, uuidToTargetFileMap.items()) 383 | 384 | info( "" ) 385 | info( "*****************************" ) 386 | info( " All done. " ) 387 | info( "*****************************" ) 388 | info( "" ) 389 | info( "Total model count: %d" % len(uuidToTargetFileMap) ) 390 | info( "STEP models downloaded: %d" % self.statDownloaded ) 391 | info( "Already existing models: %d" % self.statExisting ) 392 | info( "Failed downloads: %d" % self.statFailed ) 393 | self.progress(100, 100) -------------------------------------------------------------------------------- /easyeda_lib_loader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import math 4 | import sys 5 | import os 6 | import traceback 7 | import requests 8 | import logging 9 | import wx 10 | 11 | wx_html2_available = True 12 | try: 13 | import wx.html2 14 | except ImportError as e: 15 | wx_html2_available = False 16 | 17 | from threading import Lock, Thread 18 | from logging import info, warning, debug, error, critical 19 | from io import StringIO 20 | 21 | import wx.dataview 22 | 23 | from .component_loader import * 24 | from .easyeda_lib_loader_dialog import EasyEdaLibLoaderDialog 25 | from .config_manager import ConfigManager, LibraryTableManager 26 | 27 | from pcbnew import * 28 | import ctypes 29 | 30 | log_stream = StringIO() 31 | logging.basicConfig(stream=log_stream, level=logging.INFO) 32 | 33 | def interrupt_thread(thread): 34 | print("interrupt_thread") 35 | if not thread.is_alive(): 36 | return 37 | 38 | exc = ctypes.py_object(KeyboardInterrupt) 39 | res = ctypes.pythonapi.PyThreadState_SetAsyncExc( 40 | ctypes.c_long(thread.ident), exc) 41 | 42 | if res == 0: 43 | print("nonexistent thread id") 44 | return False 45 | elif res > 1: 46 | # """if it returns a number greater than one, you're in trouble, 47 | # and you should call it again with exc=NULL to revert the effect""" 48 | ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None) 49 | print("PyThreadState_SetAsyncExc failed") 50 | 51 | return False 52 | 53 | print("interrupt_thread success") 54 | return True 55 | 56 | 57 | class WxTextCtrlHandler(logging.Handler): 58 | def __init__(self, ctrl: wx.TextCtrl): 59 | logging.Handler.__init__(self) 60 | self.ctrl = ctrl 61 | 62 | def emit(self, record): 63 | s = self.format(record) + '\n' 64 | wx.CallAfter(self.ctrl.AppendText, s) 65 | 66 | class EasyEDALibLoaderPlugin(ActionPlugin): 67 | downloadThread: Thread | None = None 68 | searchThread: Thread | None = None 69 | searchPage = 1 70 | components = [] 71 | 72 | def defaults(self): 73 | self.name = "EasyEDA (LCEDA) Library Loader" 74 | self.category = "3D data loader" 75 | self.description = "Load library parts from EasyEDA (LCEDA)" 76 | self.show_toolbar_button = True 77 | self.icon_file_name = os.path.join(os.path.dirname(__file__), 'easyeda_lib_loader.png') 78 | 79 | def Run(self): 80 | dlg = EasyEdaLibLoaderDialog(None) 81 | 82 | handler = WxTextCtrlHandler(dlg.m_log) 83 | logging.getLogger().handlers.clear(); 84 | logging.getLogger().addHandler(handler) 85 | FORMAT = "%(levelname)s: %(message)s" 86 | handler.setFormatter(logging.Formatter(FORMAT)) 87 | logging.getLogger().setLevel(level=logging.INFO) 88 | 89 | # Get KIPRJMOD early to initialize config 90 | kiprjmod = os.getenv("KIPRJMOD") or "" 91 | config_manager = None 92 | library_manager = None 93 | 94 | if kiprjmod: 95 | config_manager = ConfigManager(kiprjmod) 96 | library_manager = LibraryTableManager(kiprjmod) 97 | 98 | def progressHandler( current, total ): 99 | wx.CallAfter(dlg.m_progress.SetRange, total) 100 | wx.CallAfter(dlg.m_progress.SetValue, current) 101 | 102 | def onDebugCheckbox( event: wx.CommandEvent ): 103 | logging.getLogger().setLevel( logging.DEBUG if event.IsChecked() else logging.INFO ) 104 | 105 | def onDownload( event ): 106 | dlg.m_log.Clear() 107 | 108 | if not dlg.m_textCtrlParts.GetValue().strip(): 109 | for sel in dlg.m_searchResultsTree.GetSelections(): 110 | dlg.m_textCtrlParts.AppendText(dlg.m_searchResultsTree.GetItemText(sel) + "\n") 111 | 112 | components = dlg.m_textCtrlParts.GetValue().splitlines() 113 | 114 | if not components: 115 | error( "No parts to download." ) 116 | return 117 | 118 | kiprjmod = os.getenv("KIPRJMOD") or "" 119 | 120 | if not kiprjmod: 121 | error( "KIPRJMOD is not set properly." ) 122 | return 123 | 124 | lib_field = dlg.m_textCtrlOutLibName.GetValue() 125 | 126 | if os.path.isabs(lib_field): 127 | target_path = lib_field 128 | else: 129 | target_path = os.path.join(kiprjmod, lib_field) 130 | 131 | target_name = os.path.basename(target_path); 132 | 133 | # Save library name to config 134 | if config_manager: 135 | config_manager.set_library_name(lib_field) 136 | 137 | # Check if library exists in tables and prompt to add if not 138 | if library_manager: 139 | library_manager.prompt_add_library(dlg, target_name, target_path) 140 | 141 | def threadedFn(): 142 | loader = ComponentLoader(kiprjmod=kiprjmod, target_path=target_path, target_name=target_name, progress=progressHandler) 143 | loader.downloadAll(components) 144 | 145 | wx.CallAfter(dlg.m_actionBtn.Enable) 146 | 147 | dlg.m_actionBtn.Disable() 148 | self.downloadThread = Thread(target = threadedFn, daemon=True) 149 | self.downloadThread.start() 150 | 151 | def searchFn(facet, words, page): 152 | def setStatus( status ): 153 | wx.CallAfter(dlg.m_searchStatus.SetLabel, status) 154 | wx.CallAfter(dlg.m_statusPanel.Layout) 155 | 156 | def setPageText( pageText ): 157 | wx.CallAfter(dlg.m_searchPage.SetLabel, pageText) 158 | wx.CallAfter(dlg.m_statusPanel.Layout) 159 | 160 | def clearItems(): 161 | wx.CallAfter(dlg.m_searchResultsTree.DeleteAllItems) 162 | 163 | def appendItem( data ): 164 | treeItem = dlg.m_searchResultsTree.AppendItem( dlg.m_searchResultsTree.GetRootItem(), data[0] ) 165 | 166 | for i in range(1, len(data)): 167 | dlg.m_searchResultsTree.SetItemText(treeItem, i, data[i]); 168 | 169 | def addItem( item ): 170 | wx.CallAfter(appendItem, item) 171 | 172 | 173 | setStatus("Searching...") 174 | clearItems() 175 | 176 | wx.CallAfter(dlg.m_prevPageBtn.Disable) 177 | wx.CallAfter(dlg.m_nextPageBtn.Disable) 178 | 179 | try: 180 | pageSize = 50 181 | 182 | reqData={ 183 | "page": page, 184 | "pageSize": pageSize, 185 | "wd": words, 186 | "returnListStyle": "classifyarr" 187 | } 188 | 189 | if facet: 190 | reqData |= { 191 | "uid": facet, 192 | "path": facet, 193 | } 194 | 195 | resp = requests.post( "https://pro.easyeda.com/api/v2/devices/search", data=reqData ) 196 | resp.raise_for_status() 197 | found = resp.json() 198 | 199 | debug(json.dumps(found, indent=4)) 200 | 201 | if not found.get("success") or not found.get("result"): 202 | raise Exception(f"Unable to search: {found}") 203 | 204 | totalDevices = sum(found["result"]["facets"].values()) 205 | 206 | for facet in found["result"]["lists"].values(): 207 | for entry in facet: 208 | addItem([ 209 | entry.get("product_code", entry["uuid"]), 210 | entry["display_title"], 211 | entry["attributes"].get("Manufacturer", ""), 212 | entry["symbol"]["display_title"] if entry.get("symbol") else "", 213 | entry["footprint"]["display_title"] if entry.get("footprint") else "" 214 | ]) 215 | 216 | curPage = int(found['result']['page']) 217 | totalPages = math.ceil(totalDevices / pageSize) 218 | 219 | if(curPage > 1): 220 | wx.CallAfter(dlg.m_prevPageBtn.Enable) 221 | 222 | if(curPage < totalPages): 223 | wx.CallAfter(dlg.m_nextPageBtn.Enable) 224 | 225 | setStatus(f"{totalDevices} parts.") 226 | setPageText(f"Page {curPage}/{totalPages}") 227 | 228 | except KeyboardInterrupt: 229 | print("KeyboardInterrupt.") 230 | except Exception as e: 231 | traceback.print_exc() 232 | setStatus(f"Failed to search parts: {e}") 233 | 234 | finally: 235 | self.searchThread = None 236 | 237 | def loadSearchPage( facetId, words, page ): 238 | if self.searchThread: 239 | interrupt_thread(self.searchThread) 240 | self.searchThread.join() 241 | 242 | facet = [None, "lcsc", "user"][facetId] 243 | 244 | self.searchThread = Thread(target = searchFn, 245 | daemon=True, 246 | args=(facet, words, page)) 247 | self.searchThread.start() 248 | 249 | def onSearch( event ): 250 | self.searchPage = 1 251 | loadSearchPage(dlg.m_libSourceChoice.GetSelection(), dlg.m_textCtrlSearch.GetValue(), self.searchPage) 252 | 253 | def onNextPage( event ): 254 | self.searchPage += 1 255 | loadSearchPage(dlg.m_libSourceChoice.GetSelection(), dlg.m_textCtrlSearch.GetValue(), self.searchPage) 256 | 257 | def onPrevPage( event ): 258 | self.searchPage -= 1 259 | loadSearchPage(dlg.m_libSourceChoice.GetSelection(), dlg.m_textCtrlSearch.GetValue(), self.searchPage) 260 | 261 | def onSearchItemActivated( event ): 262 | if dlg.m_textCtrlParts.GetValue() and not dlg.m_textCtrlParts.GetValue().endswith("\n"): 263 | dlg.m_textCtrlParts.AppendText("\n") 264 | 265 | dlg.m_textCtrlParts.AppendText(dlg.m_searchResultsTree.GetItemText(event.GetItem()) + "\n") 266 | 267 | def onSearchItemSelected( event ): 268 | itemCode = dlg.m_searchResultsTree.GetItemText(event.GetItem()) 269 | 270 | if itemCode.startswith("C"): 271 | dlg.m_searchHyperlink1.SetLabelText( f"{itemCode} Preview" ) 272 | dlg.m_searchHyperlink1.SetURL( f"https://jlcpcb.com/user-center/lcsvg/svg.html?code={itemCode}" ) 273 | dlg.m_searchHyperlink1.Show() 274 | 275 | dlg.m_searchHyperlink2.SetLabelText( f"JLCPCB" ) 276 | dlg.m_searchHyperlink2.SetURL( f"https://jlcpcb.com/partdetail/{itemCode}" ) 277 | dlg.m_searchHyperlink2.Show() 278 | 279 | dlg.m_searchHyperlink3.SetLabelText( f"LCSC" ) 280 | dlg.m_searchHyperlink3.SetURL( f"https://www.lcsc.com/product-detail/{itemCode}.html" ) 281 | dlg.m_searchHyperlink3.Show() 282 | else: 283 | easyedaLink = None 284 | 285 | try: 286 | dev_info = requests.get(f"https://pro.easyeda.com/api/devices/{itemCode}") 287 | dev_info.raise_for_status() 288 | debug("device info: " + json.dumps(dev_info.json(), indent=4)) 289 | device = dev_info.json()["result"] 290 | attributes = device['attributes'] 291 | 292 | if attributes.get('Symbol') or attributes.get('Footprint'): 293 | # https://pro.easyeda.com/editor#tab=*!{sym_uuid}(device){dev_uuid}|!{fp_uuid}(device){dev_uuid} 294 | tabList = [] 295 | 296 | if attributes.get('Symbol'): 297 | tabList.append(f"!{attributes['Symbol']}(device){itemCode}") 298 | 299 | if attributes.get('Footprint'): 300 | tabList.append(f"!{attributes['Footprint']}(device){itemCode}") 301 | 302 | easyedaLink = f"https://pro.easyeda.com/editor#tab=*{'|'.join(tabList)}" 303 | except Exception as e: 304 | pass 305 | 306 | if easyedaLink: 307 | dlg.m_searchHyperlink1.SetLabelText( f"Open in EasyEDA Pro" ) 308 | dlg.m_searchHyperlink1.SetURL( easyedaLink ) 309 | dlg.m_searchHyperlink1.Show() 310 | else: 311 | dlg.m_searchHyperlink1.Hide() 312 | 313 | dlg.m_searchHyperlink2.Hide() 314 | dlg.m_searchHyperlink3.Hide() 315 | 316 | dlg.m_statusPanel.Layout() 317 | 318 | global wx_html2_available 319 | if wx_html2_available: 320 | self.webView.Hide() 321 | 322 | if itemCode.startswith("C"): 323 | self.webView.LoadURL( f"https://jlcpcb.com/user-center/lcsvg/svg.html?code={itemCode}" ) 324 | self.webView.SetZoomFactor(0.8) 325 | else: 326 | table_rows = ''.join( 327 | f""" 328 | {key} 329 | 330 | {value if not (isinstance(value, str) and value.startswith(('http://', 'https://'))) else f'{value}'} 331 | 332 | """ 333 | for key, value in attributes.items() 334 | ) 335 | 336 | style = """ 337 | body { 338 | font-family: sans-serif; 339 | } 340 | table { 341 | border:1px solid #CCC; 342 | border-collapse:collapse; 343 | } 344 | td { 345 | border:1px solid #CCC; 346 | padding: 2px; 347 | } 348 | """ 349 | 350 | html_content = f""" 351 | 352 | 353 | 356 | 357 | 358 |

Device UUID: {itemCode}

359 | 360 | {table_rows} 361 |
362 | 363 | 364 | """ 365 | self.webView.SetPage(html_content, "") 366 | self.webView.SetZoomFactor(1.0) 367 | 368 | def onWebviewLoaded( event ): 369 | self.webView.Show() 370 | 371 | def onWebviewNewWindow( event ): 372 | wx.LaunchDefaultBrowser( event.GetURL() ) 373 | 374 | def onClose( event ): 375 | if self.searchThread: 376 | interrupt_thread(self.searchThread) 377 | self.searchThread.join( 5 ) 378 | 379 | if self.downloadThread: 380 | interrupt_thread(self.downloadThread) 381 | self.downloadThread.join( 5 ) 382 | 383 | dlg.Destroy(); 384 | 385 | dlg.m_searchResultsTree.AppendColumn("Code/UUID", width=wx.COL_WIDTH_AUTOSIZE, flags=wx.COL_RESIZABLE | wx.COL_SORTABLE ) 386 | dlg.m_searchResultsTree.AppendColumn("Name", width=wx.COL_WIDTH_AUTOSIZE, flags=wx.COL_RESIZABLE | wx.COL_SORTABLE) 387 | dlg.m_searchResultsTree.AppendColumn("Manufacturer", width=wx.COL_WIDTH_AUTOSIZE, flags=wx.COL_RESIZABLE | wx.COL_SORTABLE) 388 | dlg.m_searchResultsTree.AppendColumn("Symbol", width=wx.COL_WIDTH_AUTOSIZE, flags=wx.COL_RESIZABLE | wx.COL_SORTABLE) 389 | dlg.m_searchResultsTree.AppendColumn("Footprint", width=wx.COL_WIDTH_AUTOSIZE, flags=wx.COL_RESIZABLE | wx.COL_SORTABLE) 390 | 391 | # Load library name from config or use default 392 | default_lib_name = "EasyEDA_Lib" 393 | if config_manager: 394 | default_lib_name = config_manager.get_library_name(default_lib_name) 395 | dlg.m_textCtrlOutLibName.SetValue(default_lib_name); 396 | 397 | global wx_html2_available 398 | if wx_html2_available: 399 | try: 400 | self.webView = wx.html2.WebView.New(dlg.m_webViewPanel) 401 | self.webView.Bind(wx.html2.EVT_WEBVIEW_LOADED, onWebviewLoaded) 402 | self.webView.Bind(wx.html2.EVT_WEBVIEW_NEWWINDOW, onWebviewNewWindow) 403 | except NotImplementedError as err: 404 | self.webView = wx.StaticText(dlg.m_webViewPanel, style=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTRE_HORIZONTAL) 405 | self.webView.SetLabel("Preview is not supported in this wxPython environment.") 406 | dlg.m_webViewPanel.SetMinSize( wx.Size(20, 20) ) 407 | wx_html2_available = False 408 | else: 409 | self.webView = wx.StaticText(dlg.m_webViewPanel, style=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTRE_HORIZONTAL) 410 | self.webView.SetLabel("wx.html2 is not available. Install python3-wxgtk-webview4.0 (Debian/Ubuntu)") 411 | dlg.m_webViewPanel.SetMinSize( wx.Size(20, 20) ) 412 | 413 | dlg.m_webViewPanel.GetSizer().Add(self.webView, 1, wx.EXPAND) 414 | dlg.m_webViewPanel.Layout() 415 | 416 | dlg.SetEscapeId(wx.ID_CANCEL) 417 | dlg.Bind(wx.EVT_CLOSE, onClose) 418 | 419 | dlg.m_searchResultsTree.Bind(wx.dataview.EVT_TREELIST_ITEM_ACTIVATED, onSearchItemActivated) 420 | dlg.m_searchResultsTree.Bind(wx.dataview.EVT_TREELIST_SELECTION_CHANGED, onSearchItemSelected) 421 | dlg.m_actionBtn.Bind(wx.EVT_BUTTON, onDownload) 422 | dlg.m_searchBtn.Bind(wx.EVT_BUTTON, onSearch) 423 | dlg.m_prevPageBtn.Bind(wx.EVT_BUTTON, onPrevPage) 424 | dlg.m_nextPageBtn.Bind(wx.EVT_BUTTON, onNextPage) 425 | dlg.m_textCtrlSearch.Bind(wx.EVT_TEXT_ENTER, onSearch) 426 | dlg.m_libSourceChoice.Bind(wx.EVT_CHOICE, onSearch) 427 | dlg.m_debug.Bind(wx.EVT_CHECKBOX, onDebugCheckbox) 428 | 429 | dlg.m_textCtrlSearch.SetFocus() 430 | dlg.ShowModal() 431 | -------------------------------------------------------------------------------- /easyeda_lib_loader_dialog.fbp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Python 6 | ; 7 | 1 8 | connect 9 | none 10 | 11 | 12 | 0 13 | 0 14 | res 15 | UTF-8 16 | easyeda_lib_loader_dialog 17 | 1000 18 | 0 19 | 1 20 | UI 21 | easyeda_lib_loader_dialog 22 | . 23 | 0 24 | source_name 25 | 1 26 | 0 27 | source_name 28 | 29 | 30 | 1 31 | 1 32 | 0 33 | 0 34 | 35 | 0 36 | wxAUI_MGR_DEFAULT 37 | 38 | wxBOTH 39 | 40 | 1 41 | 0 42 | 1 43 | impl_virtual 44 | 45 | 46 | 47 | 0 48 | wxID_ANY 49 | 50 | 51 | EasyEdaLibLoaderDialog 52 | 53 | -1,-1 54 | wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER 55 | ; ; forward_declare 56 | JLCPCB/LCSC Library Loader. Unofficial, use at your own risk. 57 | 58 | 0 59 | 60 | 61 | 62 | 63 | 64 | bSizer6 65 | wxHORIZONTAL 66 | none 67 | 68 | 5 69 | wxEXPAND 70 | 1 71 | 72 | 1 73 | 1 74 | 1 75 | 1 76 | 0 77 | 78 | 0 79 | 0 80 | 81 | 82 | 83 | 1 84 | 0 85 | 1 86 | 87 | 1 88 | 0 89 | Dock 90 | 0 91 | Left 92 | 0 93 | 1 94 | 95 | 1 96 | 97 | 0 98 | 0 99 | wxID_ANY 100 | 101 | 0 102 | 103 | 100 104 | 105 | 0 106 | 107 | 1 108 | m_splitter2 109 | 1 110 | 111 | 112 | protected 113 | 1 114 | 115 | Resizable 116 | 1 117 | 650 118 | -1 119 | 1 120 | 121 | wxSPLIT_VERTICAL 122 | wxSP_3D|wxSP_LIVE_UPDATE 123 | ; ; forward_declare 124 | 0 125 | 126 | 127 | 128 | 129 | 130 | 131 | 1 132 | 1 133 | 1 134 | 1 135 | 0 136 | 137 | 0 138 | 0 139 | 140 | 141 | 142 | 1 143 | 0 144 | 1 145 | 146 | 1 147 | 0 148 | Dock 149 | 0 150 | Left 151 | 0 152 | 1 153 | 154 | 1 155 | 156 | 0 157 | 0 158 | wxID_ANY 159 | 160 | 0 161 | 162 | 163 | 0 164 | 165 | 1 166 | m_leftPanel 167 | 1 168 | 169 | 170 | protected 171 | 1 172 | 173 | Resizable 174 | 1 175 | 176 | ; ; forward_declare 177 | 0 178 | 179 | 180 | 181 | wxTAB_TRAVERSAL 182 | 183 | 184 | bSizer4 185 | wxVERTICAL 186 | none 187 | 188 | 5 189 | wxEXPAND 190 | 0 191 | 192 | 193 | bSizer5 194 | wxHORIZONTAL 195 | none 196 | 197 | 5 198 | wxALIGN_CENTER_VERTICAL|wxALL 199 | 0 200 | 201 | 1 202 | 1 203 | 1 204 | 1 205 | 0 206 | 207 | 0 208 | 0 209 | 210 | 211 | 212 | 1 213 | 0 214 | "All Sources" "JLC System" "JLC Public" 215 | 1 216 | 217 | 1 218 | 0 219 | Dock 220 | 0 221 | Left 222 | 0 223 | 1 224 | 225 | 1 226 | 227 | 0 228 | 0 229 | wxID_ANY 230 | 231 | 0 232 | 233 | 234 | 0 235 | 236 | 1 237 | m_libSourceChoice 238 | 1 239 | 240 | 241 | protected 242 | 1 243 | 244 | Resizable 245 | 0 246 | 1 247 | 248 | 249 | ; ; forward_declare 250 | 0 251 | 252 | 253 | wxFILTER_NONE 254 | wxDefaultValidator 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 5 263 | wxALIGN_CENTER_VERTICAL|wxALL 264 | 1 265 | 266 | 1 267 | 1 268 | 1 269 | 1 270 | 0 271 | 272 | 0 273 | 0 274 | 275 | 276 | 277 | 1 278 | 0 279 | 1 280 | 281 | 1 282 | 0 283 | Dock 284 | 0 285 | Left 286 | 0 287 | 1 288 | 289 | 1 290 | 291 | 0 292 | 0 293 | wxID_ANY 294 | 295 | 0 296 | 297 | 0 298 | 299 | 0 300 | 301 | 1 302 | m_textCtrlSearch 303 | 1 304 | 305 | 306 | protected 307 | 1 308 | 309 | Resizable 310 | 1 311 | 312 | wxTE_PROCESS_ENTER 313 | ; ; forward_declare 314 | 0 315 | 316 | 317 | wxFILTER_NONE 318 | wxDefaultValidator 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 5 328 | wxALIGN_CENTER_VERTICAL|wxALL 329 | 0 330 | 331 | 1 332 | 1 333 | 1 334 | 1 335 | 0 336 | 337 | 0 338 | 0 339 | 0 340 | 341 | 342 | 343 | 344 | 1 345 | 0 346 | 1 347 | 348 | 1 349 | 350 | 0 351 | 0 352 | 353 | Dock 354 | 0 355 | Left 356 | 0 357 | 1 358 | 359 | 1 360 | 361 | 362 | 0 363 | 0 364 | wxID_ANY 365 | Find 366 | 367 | 0 368 | 369 | 0 370 | 371 | 372 | 0 373 | 374 | 1 375 | m_searchBtn 376 | 1 377 | 378 | 379 | protected 380 | 1 381 | 382 | 383 | 384 | Resizable 385 | 1 386 | 387 | 388 | ; ; forward_declare 389 | 0 390 | 391 | 392 | wxFILTER_NONE 393 | wxDefaultValidator 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 5 404 | wxEXPAND 405 | 1 406 | 407 | 1 408 | 1 409 | 1 410 | 1 411 | 0 412 | 413 | 0 414 | 0 415 | 416 | 417 | 418 | 1 419 | 0 420 | 1 421 | 422 | 1 423 | 0 424 | Dock 425 | 0 426 | Left 427 | 0 428 | 1 429 | 430 | 1 431 | 432 | 0 433 | 0 434 | wxID_ANY 435 | 436 | 0 437 | 438 | 100 439 | 440 | 0 441 | 442 | 1 443 | m_splitter5 444 | 1 445 | 446 | 447 | protected 448 | 1 449 | 450 | Resizable 451 | 0 452 | 0 453 | -1 454 | 1 455 | 650,680 456 | wxSPLIT_HORIZONTAL 457 | wxSP_3D|wxSP_LIVE_UPDATE 458 | ; ; forward_declare 459 | 0 460 | 461 | 462 | 463 | 464 | 465 | 466 | 1 467 | 1 468 | 1 469 | 1 470 | 0 471 | 472 | 0 473 | 0 474 | 475 | 476 | 477 | 1 478 | 0 479 | 1 480 | 481 | 1 482 | 0 483 | Dock 484 | 0 485 | Left 486 | 0 487 | 1 488 | 489 | 1 490 | 491 | 0 492 | 0 493 | wxID_ANY 494 | 495 | 0 496 | 497 | 498 | 0 499 | 500 | 1 501 | m_statusPanel 502 | 1 503 | 504 | 505 | protected 506 | 1 507 | 508 | Resizable 509 | 1 510 | 511 | ; ; forward_declare 512 | 0 513 | 514 | 515 | 516 | wxTAB_TRAVERSAL 517 | 518 | 519 | bSizer18 520 | wxVERTICAL 521 | none 522 | 523 | 5 524 | wxEXPAND | wxALL 525 | 1 526 | 527 | 1 528 | 1 529 | 1 530 | 1 531 | 0 532 | 533 | 0 534 | 0 535 | 536 | 537 | 538 | 1 539 | 0 540 | 1 541 | 542 | 1 543 | 0 544 | Dock 545 | 0 546 | Left 547 | 0 548 | 1 549 | 550 | 1 551 | 552 | 0 553 | 0 554 | wxID_ANY 555 | 556 | 0 557 | 558 | 559 | 0 560 | 400,300 561 | 1 562 | m_searchResultsTree 563 | 1 564 | 565 | 566 | protected 567 | 1 568 | 569 | Resizable 570 | 1 571 | 572 | wxTL_MULTIPLE 573 | ; ; forward_declare 574 | 0 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 5 583 | wxEXPAND 584 | 0 585 | 586 | 587 | bStatusSizer 588 | wxHORIZONTAL 589 | protected 590 | 591 | 5 592 | wxALIGN_CENTER_VERTICAL|wxALL 593 | 0 594 | 595 | 1 596 | 1 597 | 1 598 | 1 599 | 0 600 | 601 | 0 602 | 0 603 | 604 | 605 | 606 | 1 607 | 0 608 | 1 609 | 610 | 1 611 | 0 612 | Dock 613 | 0 614 | Left 615 | 0 616 | 1 617 | 618 | 1 619 | 620 | 0 621 | 0 622 | wxID_ANY 623 | 624 | 0 625 | 626 | 0 627 | 628 | 629 | 0 630 | 631 | 1 632 | m_searchStatus 633 | 1 634 | 635 | 636 | protected 637 | 1 638 | 639 | Resizable 640 | 1 641 | 642 | 643 | ; ; forward_declare 644 | 0 645 | 646 | 647 | 648 | 649 | -1 650 | 651 | 652 | 653 | 2 654 | wxALIGN_CENTER_VERTICAL|wxTOP 655 | 0 656 | 657 | 1 658 | 1 659 | 1 660 | 1 661 | 0 662 | 663 | 0 664 | 0 665 | 666 | 667 | 668 | 1 669 | 0 670 | 1 671 | 672 | 1 673 | 0 674 | Dock 675 | 0 676 | Left 677 | 0 678 | 1 679 | 680 | 1 681 | 682 | 0 683 | 0 684 | wxID_ANY 685 | 686 | 0 687 | 688 | 0 689 | 690 | 691 | 0 692 | 693 | 1 694 | m_searchStatus2 695 | 1 696 | 697 | 698 | protected 699 | 1 700 | 701 | Resizable 702 | 1 703 | 704 | 705 | ; ; forward_declare 706 | 0 707 | 708 | 709 | 710 | 711 | -1 712 | 713 | 714 | 715 | 5 716 | wxALIGN_CENTER_VERTICAL|wxALIGN_LEFT|wxALL 717 | 0 718 | 719 | 1 720 | 1 721 | 1 722 | 1 723 | 0 724 | 725 | 0 726 | 0 727 | 728 | 729 | 730 | 1 731 | 0 732 | 1 733 | 734 | 1 735 | 0 736 | Dock 737 | 0 738 | Left 739 | 0 740 | 1 741 | 742 | 1 743 | 744 | 0 745 | 0 746 | 747 | wxID_ANY 748 | 749 | 750 | 0 751 | 752 | 753 | 0 754 | 755 | 1 756 | m_searchHyperlink1 757 | 758 | 1 759 | 760 | 761 | protected 762 | 1 763 | 764 | Resizable 765 | 1 766 | 767 | wxHL_ALIGN_LEFT|wxHL_CONTEXTMENU 768 | ; ; forward_declare 769 | 0 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 5 780 | wxALIGN_CENTER_VERTICAL|wxALIGN_LEFT|wxALL 781 | 0 782 | 783 | 1 784 | 1 785 | 1 786 | 1 787 | 0 788 | 789 | 0 790 | 0 791 | 792 | 793 | 794 | 1 795 | 0 796 | 1 797 | 798 | 1 799 | 0 800 | Dock 801 | 0 802 | Left 803 | 0 804 | 1 805 | 806 | 1 807 | 808 | 0 809 | 0 810 | 811 | wxID_ANY 812 | 813 | 814 | 0 815 | 816 | 817 | 0 818 | 819 | 1 820 | m_searchHyperlink2 821 | 822 | 1 823 | 824 | 825 | protected 826 | 1 827 | 828 | Resizable 829 | 1 830 | 831 | wxHL_ALIGN_LEFT|wxHL_CONTEXTMENU 832 | ; ; forward_declare 833 | 0 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 5 844 | wxALIGN_CENTER_VERTICAL|wxALIGN_LEFT|wxALL 845 | 0 846 | 847 | 1 848 | 1 849 | 1 850 | 1 851 | 0 852 | 853 | 0 854 | 0 855 | 856 | 857 | 858 | 1 859 | 0 860 | 1 861 | 862 | 1 863 | 0 864 | Dock 865 | 0 866 | Left 867 | 0 868 | 1 869 | 870 | 1 871 | 872 | 0 873 | 0 874 | 875 | wxID_ANY 876 | 877 | 878 | 0 879 | 880 | 881 | 0 882 | 883 | 1 884 | m_searchHyperlink3 885 | 886 | 1 887 | 888 | 889 | protected 890 | 1 891 | 892 | Resizable 893 | 1 894 | 895 | wxHL_ALIGN_LEFT|wxHL_CONTEXTMENU 896 | ; ; forward_declare 897 | 0 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 5 908 | wxEXPAND 909 | 1 910 | 911 | 0 912 | protected 913 | 0 914 | 915 | 916 | 917 | 5 918 | wxALIGN_CENTER_VERTICAL|wxALL 919 | 0 920 | 921 | 1 922 | 1 923 | 1 924 | 1 925 | 0 926 | 927 | 0 928 | 0 929 | 930 | 931 | 932 | 1 933 | 0 934 | 1 935 | 936 | 1 937 | 0 938 | Dock 939 | 0 940 | Left 941 | 0 942 | 1 943 | 944 | 1 945 | 946 | 0 947 | 0 948 | wxID_ANY 949 | 950 | 0 951 | 952 | 0 953 | 954 | 955 | 0 956 | 957 | 1 958 | m_searchPage 959 | 1 960 | 961 | 962 | protected 963 | 1 964 | 965 | Resizable 966 | 1 967 | 968 | 969 | ; ; forward_declare 970 | 0 971 | 972 | 973 | 974 | 975 | -1 976 | 977 | 978 | 979 | 5 980 | wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxTOP 981 | 0 982 | 983 | 1 984 | 1 985 | 1 986 | 1 987 | 0 988 | 989 | 0 990 | 0 991 | 0 992 | 993 | 994 | 995 | 996 | 1 997 | 0 998 | 1 999 | 1000 | 1 1001 | 1002 | 0 1003 | 0 1004 | 1005 | Dock 1006 | 0 1007 | Left 1008 | 0 1009 | 0 1010 | 1011 | 1 1012 | 1013 | 1014 | 0 1015 | 0 1016 | wxID_ANY 1017 | < 1018 | 1019 | 0 1020 | 1021 | 0 1022 | 1023 | 1024 | 0 1025 | 1026 | 1 1027 | m_prevPageBtn 1028 | 1 1029 | 1030 | 1031 | protected 1032 | 1 1033 | 1034 | 1035 | 1036 | Resizable 1037 | 1 1038 | 1039 | wxBU_EXACTFIT 1040 | ; ; forward_declare 1041 | 0 1042 | 1043 | 1044 | wxFILTER_NONE 1045 | wxDefaultValidator 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 5 1054 | wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT|wxTOP 1055 | 0 1056 | 1057 | 1 1058 | 1 1059 | 1 1060 | 1 1061 | 0 1062 | 1063 | 0 1064 | 0 1065 | 0 1066 | 1067 | 1068 | 1069 | 1070 | 1 1071 | 0 1072 | 1 1073 | 1074 | 1 1075 | 1076 | 0 1077 | 0 1078 | 1079 | Dock 1080 | 0 1081 | Left 1082 | 0 1083 | 0 1084 | 1085 | 1 1086 | 1087 | 1088 | 0 1089 | 0 1090 | wxID_ANY 1091 | > 1092 | 1093 | 0 1094 | 1095 | 0 1096 | 1097 | 1098 | 0 1099 | 1100 | 1 1101 | m_nextPageBtn 1102 | 1 1103 | 1104 | 1105 | protected 1106 | 1 1107 | 1108 | 1109 | 1110 | Resizable 1111 | 1 1112 | 1113 | wxBU_EXACTFIT 1114 | ; ; forward_declare 1115 | 0 1116 | 1117 | 1118 | wxFILTER_NONE 1119 | wxDefaultValidator 1120 | 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | 1132 | 1133 | 1 1134 | 1 1135 | 1 1136 | 1 1137 | 0 1138 | 1139 | 0 1140 | 0 1141 | 1142 | 1143 | 1144 | 1 1145 | 0 1146 | 1 1147 | 1148 | 1 1149 | 0 1150 | Dock 1151 | 0 1152 | Left 1153 | 0 1154 | 1 1155 | 1156 | 1 1157 | 1158 | 0 1159 | 0 1160 | wxID_ANY 1161 | 1162 | 0 1163 | 1164 | 1165 | 0 1166 | 650,380 1167 | 1 1168 | m_webViewPanel 1169 | 1 1170 | 1171 | 1172 | protected 1173 | 1 1174 | 1175 | Resizable 1176 | 1 1177 | 1178 | ; ; forward_declare 1179 | 0 1180 | 1181 | 1182 | 1183 | wxTAB_TRAVERSAL 1184 | 1185 | -1,-1 1186 | bSizer19 1187 | wxVERTICAL 1188 | none 1189 | 1190 | 1191 | 1192 | 1193 | 1194 | 1195 | 1196 | 1197 | 1198 | 1199 | 1 1200 | 1 1201 | 1 1202 | 1 1203 | 0 1204 | 1205 | 0 1206 | 0 1207 | 1208 | 1209 | 1210 | 1 1211 | 0 1212 | 1 1213 | 1214 | 1 1215 | 0 1216 | Dock 1217 | 0 1218 | Left 1219 | 0 1220 | 1 1221 | 1222 | 1 1223 | 1224 | 0 1225 | 0 1226 | wxID_ANY 1227 | 1228 | 0 1229 | 1230 | 1231 | 0 1232 | 1233 | 1 1234 | m_panel1 1235 | 1 1236 | 1237 | 1238 | protected 1239 | 1 1240 | 1241 | Resizable 1242 | 1 1243 | 1244 | ; ; forward_declare 1245 | 0 1246 | 1247 | 1248 | 1249 | wxTAB_TRAVERSAL 1250 | 1251 | 1252 | bSizer13 1253 | wxVERTICAL 1254 | none 1255 | 1256 | 5 1257 | wxEXPAND 1258 | 1 1259 | 1260 | 1 1261 | 1 1262 | 1 1263 | 1 1264 | 0 1265 | 1266 | 0 1267 | 0 1268 | 1269 | 1270 | 1271 | 1 1272 | 0 1273 | 1 1274 | 1275 | 1 1276 | 0 1277 | Dock 1278 | 0 1279 | Left 1280 | 0 1281 | 1 1282 | 1283 | 1 1284 | 1285 | 0 1286 | 0 1287 | wxID_ANY 1288 | 1289 | 0 1290 | 1291 | 100 1292 | 1293 | 0 1294 | 1295 | 1 1296 | m_splitter3 1297 | 1 1298 | 1299 | 1300 | protected 1301 | 1 1302 | 1303 | Resizable 1304 | 0.5 1305 | 0 1306 | -1 1307 | 1 1308 | 1309 | wxSPLIT_HORIZONTAL 1310 | wxSP_3D|wxSP_LIVE_UPDATE 1311 | ; ; forward_declare 1312 | 0 1313 | 1314 | 1315 | 1316 | 1317 | 1318 | 1319 | 1 1320 | 1 1321 | 1 1322 | 1 1323 | 0 1324 | 1325 | 0 1326 | 0 1327 | 1328 | 1329 | 1330 | 1 1331 | 0 1332 | 1 1333 | 1334 | 1 1335 | 0 1336 | Dock 1337 | 0 1338 | Left 1339 | 0 1340 | 1 1341 | 1342 | 1 1343 | 1344 | 0 1345 | 0 1346 | wxID_ANY 1347 | 1348 | 0 1349 | 1350 | 1351 | 0 1352 | 1353 | 1 1354 | m_panel5 1355 | 1 1356 | 1357 | 1358 | protected 1359 | 1 1360 | 1361 | Resizable 1362 | 1 1363 | 1364 | ; ; forward_declare 1365 | 0 1366 | 1367 | 1368 | 1369 | wxTAB_TRAVERSAL 1370 | 1371 | 1372 | bSizer1 1373 | wxVERTICAL 1374 | none 1375 | 1376 | 5 1377 | wxALL 1378 | 0 1379 | 1380 | 1 1381 | 1 1382 | 1 1383 | 1 1384 | 0 1385 | 1386 | 0 1387 | 0 1388 | 1389 | 1390 | 1391 | 1 1392 | 0 1393 | 1 1394 | 1395 | 1 1396 | 0 1397 | Dock 1398 | 0 1399 | Left 1400 | 0 1401 | 1 1402 | 1403 | 1 1404 | 1405 | 0 1406 | 0 1407 | wxID_ANY 1408 | Enter JLCPCB/LCSC codes or UUIDs to download: 1409 | 0 1410 | 1411 | 0 1412 | 1413 | 1414 | 0 1415 | 1416 | 1 1417 | m_staticText1 1418 | 1 1419 | 1420 | 1421 | protected 1422 | 1 1423 | 1424 | Resizable 1425 | 1 1426 | 1427 | 1428 | ; ; forward_declare 1429 | 0 1430 | 1431 | 1432 | 1433 | 1434 | -1 1435 | 1436 | 1437 | 1438 | 5 1439 | wxALL|wxEXPAND 1440 | 1 1441 | 1442 | 1 1443 | 1 1444 | 1 1445 | 1 1446 | 0 1447 | 1448 | 0 1449 | 0 1450 | 1451 | 1452 | 1453 | 1 1454 | 0 1455 | 1 1456 | 1457 | 1 1458 | 0 1459 | Dock 1460 | 0 1461 | Left 1462 | 0 1463 | 1 1464 | 1465 | 1 1466 | 1467 | 0 1468 | 0 1469 | wxID_ANY 1470 | 1471 | 0 1472 | 1473 | 0 1474 | 1475 | 0 1476 | 1477 | 1 1478 | m_textCtrlParts 1479 | 1 1480 | 1481 | 1482 | protected 1483 | 1 1484 | 1485 | Resizable 1486 | 1 1487 | 1488 | wxTE_MULTILINE 1489 | ; ; forward_declare 1490 | 0 1491 | 1492 | 1493 | wxFILTER_NONE 1494 | wxDefaultValidator 1495 | 1496 | 1497 | 1498 | 1499 | 1500 | 1501 | 1502 | 1503 | 5 1504 | wxEXPAND 1505 | 0 1506 | 1507 | 1508 | bSizer3 1509 | wxHORIZONTAL 1510 | none 1511 | 1512 | 5 1513 | wxALIGN_CENTER_VERTICAL|wxALL 1514 | 0 1515 | 1516 | 1 1517 | 1 1518 | 1 1519 | 1 1520 | 0 1521 | 1522 | 0 1523 | 0 1524 | 1525 | 1526 | 1527 | 1 1528 | 0 1529 | 1 1530 | 1531 | 1 1532 | 0 1533 | Dock 1534 | 0 1535 | Left 1536 | 0 1537 | 1 1538 | 1539 | 1 1540 | 1541 | 0 1542 | 0 1543 | wxID_ANY 1544 | Library path: 1545 | 0 1546 | 1547 | 0 1548 | 1549 | 1550 | 0 1551 | 1552 | 1 1553 | m_staticText2 1554 | 1 1555 | 1556 | 1557 | protected 1558 | 1 1559 | 1560 | Resizable 1561 | 1 1562 | 1563 | 1564 | ; ; forward_declare 1565 | 0 1566 | 1567 | 1568 | 1569 | 1570 | -1 1571 | 1572 | 1573 | 1574 | 5 1575 | wxALIGN_CENTER_VERTICAL|wxALL|wxEXPAND 1576 | 1 1577 | 1578 | 1 1579 | 1 1580 | 1 1581 | 1 1582 | 0 1583 | 1584 | 0 1585 | 0 1586 | 1587 | 1588 | 1589 | 1 1590 | 0 1591 | 1 1592 | 1593 | 1 1594 | 0 1595 | Dock 1596 | 0 1597 | Left 1598 | 0 1599 | 1 1600 | 1601 | 1 1602 | 1603 | 0 1604 | 0 1605 | wxID_ANY 1606 | 1607 | 0 1608 | 1609 | 0 1610 | 1611 | 0 1612 | 1613 | 1 1614 | m_textCtrlOutLibName 1615 | 1 1616 | 1617 | 1618 | protected 1619 | 1 1620 | 1621 | Resizable 1622 | 1 1623 | 1624 | 1625 | ; ; forward_declare 1626 | 0 1627 | 1628 | 1629 | wxFILTER_NONE 1630 | wxDefaultValidator 1631 | 1632 | 1633 | 1634 | 1635 | 1636 | 1637 | 1638 | 1639 | 1640 | 1641 | 5 1642 | wxALIGN_CENTER_HORIZONTAL|wxALL 1643 | 0 1644 | 1645 | 1 1646 | 1 1647 | 1 1648 | 1 1649 | 0 1650 | 1651 | 0 1652 | 0 1653 | 0 1654 | 1655 | 1656 | 1657 | 1658 | 1 1659 | 0 1660 | 1 1661 | 1662 | 1 1663 | 1664 | 0 1665 | 0 1666 | 1667 | Dock 1668 | 0 1669 | Left 1670 | 0 1671 | 1 1672 | 1673 | 1 1674 | 1675 | 1676 | 0 1677 | 0 1678 | wxID_ANY 1679 | Download parts 1680 | 1681 | 0 1682 | 1683 | 0 1684 | 1685 | 1686 | 0 1687 | 1688 | 1 1689 | m_actionBtn 1690 | 1 1691 | 1692 | 1693 | protected 1694 | 1 1695 | 1696 | 1697 | 1698 | Resizable 1699 | 1 1700 | 1701 | 1702 | ; ; forward_declare 1703 | 0 1704 | 1705 | 1706 | wxFILTER_NONE 1707 | wxDefaultValidator 1708 | 1709 | 1710 | 1711 | 1712 | 1713 | 1714 | 1715 | 1716 | 1717 | 1718 | 1719 | 1 1720 | 1 1721 | 1 1722 | 1 1723 | 0 1724 | 1725 | 0 1726 | 0 1727 | 1728 | 1729 | 1730 | 1 1731 | 0 1732 | 1 1733 | 1734 | 1 1735 | 0 1736 | Dock 1737 | 0 1738 | Left 1739 | 0 1740 | 1 1741 | 1742 | 1 1743 | 1744 | 0 1745 | 0 1746 | wxID_ANY 1747 | 1748 | 0 1749 | 1750 | 1751 | 0 1752 | 1753 | 1 1754 | m_panel6 1755 | 1 1756 | 1757 | 1758 | protected 1759 | 1 1760 | 1761 | Resizable 1762 | 1 1763 | 1764 | ; ; forward_declare 1765 | 0 1766 | 1767 | 1768 | 1769 | wxTAB_TRAVERSAL 1770 | 1771 | 1772 | bSizer14 1773 | wxVERTICAL 1774 | none 1775 | 1776 | 5 1777 | wxALL|wxEXPAND 1778 | 0 1779 | 1780 | 1 1781 | 1 1782 | 1 1783 | 1 1784 | 0 1785 | 1786 | 0 1787 | 0 1788 | 1789 | 1790 | 1791 | 1 1792 | 0 1793 | 1 1794 | 1795 | 1 1796 | 0 1797 | Dock 1798 | 0 1799 | Left 1800 | 0 1801 | 1 1802 | 1803 | 1 1804 | 1805 | 0 1806 | 0 1807 | wxID_ANY 1808 | 1809 | 0 1810 | 1811 | 1812 | 0 1813 | 1814 | 1 1815 | m_progress 1816 | 1 1817 | 1818 | 1819 | protected 1820 | 1 1821 | 1822 | 100 1823 | Resizable 1824 | 1 1825 | 1826 | wxGA_HORIZONTAL 1827 | ; ; forward_declare 1828 | 0 1829 | 1830 | 1831 | wxFILTER_NONE 1832 | wxDefaultValidator 1833 | 1834 | 0 1835 | 1836 | 1837 | 1838 | 1839 | 1840 | 1841 | 5 1842 | wxALL|wxEXPAND 1843 | 1 1844 | 1845 | 1 1846 | 1 1847 | 1 1848 | 1 1849 | 0 1850 | 1851 | 0 1852 | 0 1853 | 1854 | 1855 | 1856 | 1 1857 | 0 1858 | 1 1859 | 1860 | 1 1861 | 0 1862 | Dock 1863 | 0 1864 | Left 1865 | 0 1866 | 1 1867 | 1868 | 1 1869 | 1870 | 0 1871 | 0 1872 | wxID_ANY 1873 | 1874 | 0 1875 | 1876 | 0 1877 | 1878 | 0 1879 | 400,40 1880 | 1 1881 | m_log 1882 | 1 1883 | 1884 | 1885 | protected 1886 | 1 1887 | 1888 | Resizable 1889 | 1 1890 | 1891 | wxTE_BESTWRAP|wxTE_MULTILINE|wxTE_READONLY 1892 | ; ; forward_declare 1893 | 0 1894 | 1895 | 1896 | wxFILTER_NONE 1897 | wxDefaultValidator 1898 | 1899 | 1900 | 1901 | 1902 | 1903 | 1904 | 1905 | 1906 | 5 1907 | wxEXPAND 1908 | 0 1909 | 1910 | 1911 | bSizer2 1912 | wxHORIZONTAL 1913 | none 1914 | 1915 | 5 1916 | wxALIGN_CENTER_VERTICAL|wxALIGN_LEFT|wxALL 1917 | 0 1918 | 1919 | 1 1920 | 1 1921 | 1 1922 | 1 1923 | 0 1924 | 1925 | 0 1926 | 0 1927 | 1928 | 1929 | 1930 | 1 1931 | 0 1932 | 0 1933 | 1 1934 | 1935 | 1 1936 | 0 1937 | Dock 1938 | 0 1939 | Left 1940 | 0 1941 | 1 1942 | 1943 | 1 1944 | 1945 | 0 1946 | 0 1947 | wxID_ANY 1948 | Debug 1949 | 1950 | 0 1951 | 1952 | 1953 | 0 1954 | 1955 | 1 1956 | m_debug 1957 | 1 1958 | 1959 | 1960 | protected 1961 | 1 1962 | 1963 | Resizable 1964 | 1 1965 | 1966 | 1967 | ; ; forward_declare 1968 | 0 1969 | 1970 | 1971 | wxFILTER_NONE 1972 | wxDefaultValidator 1973 | 1974 | 1975 | 1976 | 1977 | 1978 | 1979 | 1980 | 5 1981 | wxEXPAND 1982 | 1 1983 | 1984 | 0 1985 | protected 1986 | 0 1987 | 1988 | 1989 | 1990 | 5 1991 | wxALIGN_CENTER_VERTICAL|wxALL 1992 | 0 1993 | 1994 | 1 1995 | 1 1996 | 1 1997 | 1 1998 | 0 1999 | 2000 | 0 2001 | 0 2002 | 0 2003 | 2004 | 2005 | 2006 | 2007 | 1 2008 | 0 2009 | 1 2010 | 2011 | 1 2012 | 2013 | 0 2014 | 0 2015 | 2016 | Dock 2017 | 0 2018 | Left 2019 | 0 2020 | 1 2021 | 2022 | 1 2023 | 2024 | 2025 | 0 2026 | 0 2027 | wxID_CANCEL 2028 | Close dialog 2029 | 2030 | 0 2031 | 2032 | 0 2033 | 2034 | 2035 | 0 2036 | 2037 | 1 2038 | m_closeButton 2039 | 1 2040 | 2041 | 2042 | protected 2043 | 1 2044 | 2045 | 2046 | 2047 | Resizable 2048 | 1 2049 | 2050 | 2051 | ; ; forward_declare 2052 | 0 2053 | 2054 | 2055 | wxFILTER_NONE 2056 | wxDefaultValidator 2057 | 2058 | 2059 | 2060 | 2061 | 2062 | 2063 | 2064 | 2065 | 2066 | 2067 | 2068 | 2069 | 2070 | 2071 | 2072 | 2073 | 2074 | 2075 | 2076 | 2077 | 2078 | 2079 | --------------------------------------------------------------------------------