diff --git a/README.md b/README.md index 8a4cebe..d78cb78 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ This plugin makes use of a lot of icons from the excellent [Material Design Icon ## Development 1. Fork repo -2. Fit clone forked repo +2. Git clone forked repo 3. Install pre-commit `pip install pre-commit` 4. Setup pre-commit `pre-commit run` 5. Create feature branch `git switch -c my-awesome-feature` diff --git a/events.py b/events.py index 504cce8..fcb5bb4 100644 --- a/events.py +++ b/events.py @@ -8,3 +8,4 @@ AssignPartsEvent, EVT_ASSIGN_PARTS_EVENT = NewEvent() PopulateFootprintListEvent, EVT_POPULATE_FOOTPRINT_LIST_EVENT = NewEvent() UpdateSetting, EVT_UPDATE_SETTING = NewEvent() +LogboxAppendEvent, EVT_LOGBOX_APPEND_EVENT = NewEvent() diff --git a/fabrication.py b/fabrication.py index 69d6f9d..b98440e 100644 --- a/fabrication.py +++ b/fabrication.py @@ -35,7 +35,6 @@ except ImportError: NO_DRILL_SHAPE = PCB_PLOT_PARAMS.NO_DRILL_SHAPE -from .helpers import get_exclude_from_pos class Fabrication: @@ -241,7 +240,7 @@ def zip_gerber_excellon(self): continue filePath = os.path.join(folderName, filename) zipfile.write(filePath, os.path.basename(filePath)) - self.logger.info("Finished generating ZIP file") + self.logger.info("Finished generating ZIP file %s", os.path.join(self.outputdir, zipname)) def generate_cpl(self): """Generate placement file (CPL).""" @@ -258,9 +257,12 @@ def generate_cpl(self): writer.writerow( ["Designator", "Val", "Package", "Mid X", "Mid Y", "Rotation", "Layer"] ) - for part in self.parent.store.read_pos_parts(): - fp = self.board.FindFootprintByReference(part[0]) - if get_exclude_from_pos(fp): + footprints = sorted(self.board.Footprints(), key = lambda x: x.GetReference()) + for fp in footprints: + part = self.parent.store.get_part(fp.GetReference()) + if not part: # No matching part in the database, continue + continue + if part[6] == 1: # Exclude from POS continue if not add_without_lcsc and not part[3]: continue @@ -276,7 +278,8 @@ def generate_cpl(self): "top" if fp.GetLayer() == 0 else "bottom", ] ) - self.logger.info("Finished generating CPL file") + self.logger.info("Finished generating CPL file %s", os.path.join(self.outputdir, cplname)) + def generate_bom(self): """Generate BOM file.""" @@ -293,4 +296,4 @@ def generate_bom(self): if not add_without_lcsc and not part[3]: continue writer.writerow(part) - self.logger.info("Finished generating BOM file") + self.logger.info("Finished generating BOM file %s", os.path.join(self.outputdir, bomname)) diff --git a/library.py b/library.py index b240f1b..9eb94db 100644 --- a/library.py +++ b/library.py @@ -128,18 +128,36 @@ def search(self, parameters): query = f"SELECT {s} FROM parts WHERE " match_chunks = [] + like_chunks = [] + query_chunks = [] + # Build 'match_chunks' and 'like_chunks' arrays + # + # FTS5 (https://www.sqlite.org/fts5.html) has a substring limit of + # at least 3 characters. + # 'Substrings consisting of fewer than 3 unicode characters do not + # match any rows when used with a full-text query' + # + # However, they will still match with a LIKE. + # + # So extract out the <3 character strings and add a 'LIKE' term + # for each of those. if parameters["keyword"] != "": keywords = parameters["keyword"].split(" ") - keywords_intermediate = [] + match_keywords_intermediate = [] for w in keywords: # skip over empty keywords if w != "": - kw = f'"{w}"' - keywords_intermediate.append(kw) - keywords_entry = " AND ".join(keywords_intermediate) - match_chunks.append(f"{keywords_entry}") + if len(w) < 3: # LIKE entry + kw = f"description LIKE '%{w}%'" + like_chunks.append(kw) + else: # MATCH entry + kw = f'"{w}"' + match_keywords_intermediate.append(kw) + if match_keywords_intermediate: + match_entry = " AND ".join(match_keywords_intermediate) + match_chunks.append(f"{match_entry}") if "manufacturer" in parameters and parameters["manufacturer"] != "": p = parameters["manufacturer"] @@ -171,7 +189,7 @@ def search(self, parameters): if parameters["stock"]: query_chunks.append('"Stock" > "0"') - if not match_chunks and not query_chunks: + if not match_chunks and not like_chunks and not query_chunks: return [] if match_chunks: @@ -179,9 +197,14 @@ def search(self, parameters): query += " AND ".join(match_chunks) query += "'" - if query_chunks: + if like_chunks: if match_chunks: query += " AND " + query += " AND ".join(like_chunks) + + if query_chunks: + if match_chunks or like_chunks: + query += " AND " query += " AND ".join(query_chunks) query += f' ORDER BY "{self.order_by}" COLLATE naturalsort {self.order_dir}' @@ -426,7 +449,7 @@ def download(self): "Parts db is split into %s parts. Proceeding to download...", r.text ) cnt = int(r.text) - self.logger.debug("Removing any spurios old zip part files...") + self.logger.debug("Removing any spurious old zip part files...") for p in glob(str(Path(self.datadir) / (chunk_file_stub + "*"))): self.logger.debug("Removing %s.", p) os.unlink(p) diff --git a/mainwindow.py b/mainwindow.py index f6a56a1..d9328bc 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -15,11 +15,13 @@ from .const import Column from .events import ( EVT_ASSIGN_PARTS_EVENT, + EVT_LOGBOX_APPEND_EVENT, EVT_MESSAGE_EVENT, EVT_POPULATE_FOOTPRINT_LIST_EVENT, EVT_RESET_GAUGE_EVENT, EVT_UPDATE_GAUGE_EVENT, EVT_UPDATE_SETTING, + LogboxAppendEvent, ) from .fabrication import Fabrication from .helpers import ( @@ -65,6 +67,7 @@ ID_CONTEXT_MENU_ADD_ROT_BY_PACKAGE = wx.NewIdRef() ID_CONTEXT_MENU_ADD_ROT_BY_NAME = wx.NewIdRef() + class KicadProvider: """KiCad implementation of the provider, see standalone_impl.py for the stub version.""" @@ -72,6 +75,7 @@ def get_pcbnew(self): """Get the pcbnew instance.""" return kicad_pcbnew + class JLCPCBTools(wx.Dialog): """JLCPCBTools main dialog.""" @@ -218,7 +222,7 @@ def __init__(self, parent, kicad_provider=KicadProvider()): self, wx.ID_ANY, wx.DefaultPosition, - wx.Size(128, -1), + wx.Size(int(self.scale_factor * 128), -1), wx.TB_VERTICAL | wx.TB_TEXT | wx.TB_NODIVIDER, ) @@ -501,6 +505,7 @@ def __init__(self, parent, kicad_provider=KicadProvider()): self.Bind(EVT_ASSIGN_PARTS_EVENT, self.assign_parts) self.Bind(EVT_POPULATE_FOOTPRINT_LIST_EVENT, self.populate_footprint_list) self.Bind(EVT_UPDATE_SETTING, self.update_settings) + self.Bind(EVT_LOGBOX_APPEND_EVENT, self.logbox_append) self.enable_part_specific_toolbar_buttons(False) @@ -515,6 +520,10 @@ def __init__(self, parent, kicad_provider=KicadProvider()): def quit_dialog(self, *_): """Destroy dialog on close.""" + root = logging.getLogger() + root.removeHandler(self.logging_handler1) + root.removeHandler(self.logging_handler2) + self.Destroy() self.EndModal(0) @@ -616,13 +625,13 @@ def populate_footprint_list(self, *_): # First check if the part name mathes for regex, correction in corrections: if re.search(regex, str(part[1])): - part[8] = correction + part[8] = str(correction) break # If there was no match for the part name, check if the package matches if part[8] == "": for regex, correction in corrections: if re.search(regex, str(part[2])): - part[8] = correction + part[8] = str(correction) break self.footprint_list.AppendItem(part) @@ -897,6 +906,10 @@ def update_settings(self, e): self.settings[e.section][e.setting] = e.value self.save_settings() + def logbox_append(self, e): + """Write text to the logbox.""" + self.logbox.WriteText(e.msg) + def load_settings(self): """Load settings from settings.json.""" with open(os.path.join(PLUGIN_PATH, "settings.json"), encoding="utf-8") as j: @@ -1089,19 +1102,19 @@ def init_logger(self): root = logging.getLogger() root.setLevel(logging.DEBUG) # Log to stderr - handler1 = logging.StreamHandler(sys.stderr) - handler1.setLevel(logging.DEBUG) + self.logging_handler1 = logging.StreamHandler(sys.stderr) + self.logging_handler1.setLevel(logging.DEBUG) # and to our GUI - handler2 = LogBoxHandler(self.logbox) - handler2.setLevel(logging.DEBUG) + self.logging_handler2 = LogBoxHandler(self) + self.logging_handler2.setLevel(logging.DEBUG) formatter = logging.Formatter( "%(asctime)s - %(levelname)s - %(funcName)s - %(message)s", datefmt="%Y.%m.%d %H:%M:%S", ) - handler1.setFormatter(formatter) - handler2.setFormatter(formatter) - root.addHandler(handler1) - root.addHandler(handler2) + self.logging_handler1.setFormatter(formatter) + self.logging_handler2.setFormatter(formatter) + root.addHandler(self.logging_handler1) + root.addHandler(self.logging_handler2) self.logger = logging.getLogger(__name__) def __del__(self): @@ -1112,15 +1125,11 @@ def __del__(self): class LogBoxHandler(logging.StreamHandler): """Logging class for the logging textbox at th ebottom of the mainwindow.""" - def __init__(self, textctrl): + def __init__(self, event_destination): logging.StreamHandler.__init__(self) - self.textctrl = textctrl + self.event_destination = event_destination def emit(self, record): - """Pokemon exception that hopefully helps getting this working with threads.""" - try: - msg = self.format(record) - self.textctrl.WriteText(msg + "\n") - self.flush() - except: # pylint: disable=bare-except - pass + """Marshal the event over to the main thread.""" + msg = self.format(record) + wx.QueueEvent(self.event_destination, LogboxAppendEvent(msg=f"{msg}\n")) diff --git a/unzip_parts.py b/unzip_parts.py index 99d1e2e..9d00688 100644 --- a/unzip_parts.py +++ b/unzip_parts.py @@ -25,10 +25,9 @@ def unzip_parts(path): # Open the split file with open(split_path, "rb") as split_file: # Read the file data - file_data = split_file.read() - - # Append the file data to the original file - db.write(file_data) + while (file_data := split_file.read(1024 * 1024)): + # Append the file data to the original file + db.write(file_data) # Delete the split file os.unlink(split_path)