diff --git a/const.py b/const.py
deleted file mode 100644
index 94fa93cc..00000000
--- a/const.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""Constants used througout the plugin."""
-
-from enum import IntEnum
-
-
-class Column(IntEnum):
- """Column positions for main parts table."""
-
- REFERENCE = 0
- VALUE = 1
- FOOTPRINT = 2
- LCSC = 3
- TYPE = 4
- STOCK = 5
- BOM = 6
- POS = 7
- ROTATION = 8
- SIDE = 9
diff --git a/datamodel.py b/datamodel.py
new file mode 100644
index 00000000..f68a2eea
--- /dev/null
+++ b/datamodel.py
@@ -0,0 +1,257 @@
+"""Implementation of the Datamodel for the parts list with natural sort."""
+
+import logging
+import re
+
+import wx.dataview as dv
+
+from .helpers import loadIconScaled
+
+
+class PartListDataModel(dv.PyDataViewModel):
+ """Datamodel for use with the DataViewCtrl of the mainwindow."""
+
+ def __init__(self, scale_factor):
+ super().__init__()
+ self.data = []
+ self.columns = {
+ "REF_COL": 0,
+ "VALUE_COL": 1,
+ "FP_COL": 2,
+ "LCSC_COL": 3,
+ "TYPE_COL": 4,
+ "STOCK_COL": 5,
+ "BOM_COL": 6,
+ "POS_COL": 7,
+ "ROT_COL": 8,
+ "SIDE_COL": 9,
+ }
+
+ self.bom_pos_icons = [
+ loadIconScaled(
+ "mdi-check-color.png",
+ scale_factor,
+ ),
+ loadIconScaled(
+ "mdi-close-color.png",
+ scale_factor,
+ ),
+ ]
+ self.side_icons = [
+ loadIconScaled(
+ "TOP.png",
+ scale_factor,
+ ),
+ loadIconScaled(
+ "BOT.png",
+ scale_factor,
+ ),
+ ]
+ self.logger = logging.getLogger(__name__)
+
+ @staticmethod
+ def natural_sort_key(s):
+ """Return a tuple that can be used for natural sorting."""
+ return [
+ int(text) if text.isdigit() else text.lower()
+ for text in re.split("([0-9]+)", s)
+ ]
+
+ def GetColumnCount(self):
+ """Get number of columns."""
+ return len(self.columns)
+
+ def GetColumnType(self, col):
+ """Get type of each column."""
+ columntypes = (
+ "string",
+ "string",
+ "string",
+ "string",
+ "string",
+ "string",
+ "wxDataViewIconText",
+ "wxDataViewIconText",
+ "string",
+ "wxDataViewIconText",
+ )
+ return columntypes[col]
+
+ def GetChildren(self, parent, children):
+ """Get child items of a parent."""
+ if not parent:
+ for row in self.data:
+ children.append(self.ObjectToItem(row))
+ return len(self.data)
+ return 0
+
+ def IsContainer(self, item):
+ """Check if tem is a container."""
+ return not item
+
+ def GetParent(self, item):
+ """Get parent item."""
+ return dv.NullDataViewItem
+
+ def GetValue(self, item, col):
+ """Get value of an item."""
+ row = self.ItemToObject(item)
+ if col in [
+ self.columns["BOM_COL"],
+ self.columns["POS_COL"],
+ self.columns["SIDE_COL"],
+ ]:
+ icon = row[col]
+ return dv.DataViewIconText("", icon)
+ return row[col]
+
+ def SetValue(self, value, item, col):
+ """Set value of an item."""
+ row = self.ItemToObject(item)
+ if col in [
+ self.columns["BOM_COL"],
+ self.columns["POS_COL"],
+ self.columns["SIDE_COL"],
+ ]:
+ return False
+ row[col] = value
+ return True
+
+ def Compare(self, item1, item2, column, ascending):
+ """Override to implement natural sorting."""
+ val1 = self.GetValue(item1, column)
+ val2 = self.GetValue(item2, column)
+
+ key1 = self.natural_sort_key(val1)
+ key2 = self.natural_sort_key(val2)
+
+ if ascending:
+ return (key1 > key2) - (key1 < key2)
+ else:
+ return (key2 > key1) - (key2 < key1)
+
+ def find_index(self, ref):
+ """Get the index of a part within the data list by its reference."""
+ try:
+ return self.data.index([x for x in self.data if x[0] == ref].pop())
+ except (ValueError, IndexError):
+ return None
+
+ def get_bom_pos_icon(self, state: str):
+ """Get an icon for a state."""
+ return self.bom_pos_icons[int(state)]
+
+ def get_side_icon(self, side: str):
+ """Get The side for a layer number."""
+ return self.side_icons[0] if side == "0" else self.side_icons[1]
+
+ def AddEntry(self, data: list):
+ """Add a new entry to the data model."""
+ data[self.columns["BOM_COL"]] = self.get_bom_pos_icon(
+ data[self.columns["BOM_COL"]]
+ )
+ data[self.columns["POS_COL"]] = self.get_bom_pos_icon(
+ data[self.columns["POS_COL"]]
+ )
+ data[self.columns["SIDE_COL"]] = self.get_side_icon(
+ data[self.columns["SIDE_COL"]]
+ )
+ self.data.append(data)
+ self.ItemAdded(dv.NullDataViewItem, self.ObjectToItem(data))
+
+ def UpdateEntry(self, data: list):
+ """Update an entry in the data model."""
+ if (index := self.find_index(data[0])) is None:
+ return
+ item = self.data[index]
+ for i in range(0, len(data)):
+ if i in [self.columns["BOM_COL"], self.columns["POS_COL"]]:
+ item[i] = self.get_bom_pos_icon(data[i])
+ elif i == self.columns["SIDE_COL"]:
+ item[i] = self.get_side_icon(data[i])
+ else:
+ item[i] = data[i]
+ self.ItemChanged(self.ObjectToItem(item))
+
+ def RemoveEntry(self, ref: str):
+ """Remove an entry from the data model."""
+ if (index := self.find_index(ref)) is None:
+ return
+ item = self.ObjectToItem(self.data[index])
+ self.data.remove(self.data[index])
+ self.ItemDeleted(dv.NullDataViewItem, item)
+
+ def RemoveAll(self):
+ """Remove all entries from the data model."""
+ self.data.clear()
+ self.Cleared()
+
+ def get_all(self):
+ """Get tall items."""
+ return self.data
+
+ def get_reference(self, item):
+ """Get the reference of an item."""
+ return self.ItemToObject(item)[self.columns["REF_COL"]]
+
+ def get_value(self, item):
+ """Get the value of an item."""
+ return self.ItemToObject(item)[self.columns["VALUE_COL"]]
+
+ def get_lcsc(self, item):
+ """Get the lcsc of an item."""
+ return self.ItemToObject(item)[self.columns["LCSC_COL"]]
+
+ def get_footprint(self, item):
+ """Get the footprint of an item."""
+ return self.ItemToObject(item)[self.columns["FP_COL"]]
+
+ def select_alike(self, item):
+ """Select all items that have the same value and footprint."""
+ obj = self.ItemToObject(item)
+ alike = []
+ for data in self.data:
+ if data[1:3] == obj[1:3]:
+ alike.append(self.ObjectToItem(data))
+ return alike
+
+ def set_lcsc(self, ref, lcsc, type, stock):
+ """Set an lcsc number, type and stock for given reference."""
+ if (index := self.find_index(ref)) is None:
+ return
+ item = self.data[index]
+ item[self.columns["LCSC_COL"]] = lcsc
+ item[self.columns["TYPE_COL"]] = type
+ item[self.columns["STOCK_COL"]] = stock
+ self.ItemChanged(self.ObjectToItem(item))
+
+ def remove_lcsc_number(self, item):
+ """Remove the LCSC number of an item."""
+ obj = self.ItemToObject(item)
+ obj[self.columns["LCSC_COL"]] = ""
+ obj[self.columns["TYPE_COL"]] = ""
+ obj[self.columns["STOCK_COL"]] = ""
+ self.ItemChanged(self.ObjectToItem(obj))
+
+ def toggle_bom(self, item):
+ """Toggle BOM for a given item."""
+ obj = self.ItemToObject(item)
+ if obj[self.columns["BOM_COL"]] == self.bom_pos_icons[0]:
+ obj[self.columns["BOM_COL"]] = self.bom_pos_icons[1]
+ else:
+ obj[self.columns["BOM_COL"]] = self.bom_pos_icons[0]
+ self.ItemChanged(self.ObjectToItem(obj))
+
+ def toggle_pos(self, item):
+ """Toggle POS for a given item."""
+ obj = self.ItemToObject(item)
+ if obj[self.columns["POS_COL"]] == self.bom_pos_icons[0]:
+ obj[self.columns["POS_COL"]] = self.bom_pos_icons[1]
+ else:
+ obj[self.columns["POS_COL"]] = self.bom_pos_icons[0]
+ self.ItemChanged(self.ObjectToItem(obj))
+
+ def toggle_bom_pos(self, item):
+ """Toggle BOM and POS for a given item."""
+ self.toggle_bom(item)
+ self.toggle_pos(item)
diff --git a/fabrication.py b/fabrication.py
index 9d4f3af4..68092a5d 100644
--- a/fabrication.py
+++ b/fabrication.py
@@ -323,12 +323,7 @@ def generate_bom(self):
if not add_without_lcsc and not part["lcsc"]:
continue
writer.writerow(
- [
- part["value"],
- part["refs"],
- part["footprint"],
- part["lcsc"]
- ]
+ [part["value"], part["refs"], part["footprint"], part["lcsc"]]
)
self.logger.info(
"Finished generating BOM file %s", os.path.join(self.outputdir, bomname)
diff --git a/icons/BOT.png b/icons/BOT.png
new file mode 100644
index 00000000..bc089ee9
Binary files /dev/null and b/icons/BOT.png differ
diff --git a/icons/TOP.png b/icons/TOP.png
new file mode 100644
index 00000000..d9c6c698
Binary files /dev/null and b/icons/TOP.png differ
diff --git a/icons/svg/BOT.svg b/icons/svg/BOT.svg
new file mode 100644
index 00000000..51d341b6
--- /dev/null
+++ b/icons/svg/BOT.svg
@@ -0,0 +1,72 @@
+
+
+
+
diff --git a/icons/svg/TOP.svg b/icons/svg/TOP.svg
new file mode 100644
index 00000000..1768c0b9
--- /dev/null
+++ b/icons/svg/TOP.svg
@@ -0,0 +1,72 @@
+
+
+
+
diff --git a/lcsc_api.py b/lcsc_api.py
index 0cf0135f..d2723b19 100644
--- a/lcsc_api.py
+++ b/lcsc_api.py
@@ -1,4 +1,5 @@
"""Unofficial LCSC API."""
+
import io
from pathlib import Path
diff --git a/mainwindow.py b/mainwindow.py
index b216c9d5..42c68471 100644
--- a/mainwindow.py
+++ b/mainwindow.py
@@ -10,9 +10,9 @@
import pcbnew as kicad_pcbnew
import wx # pylint: disable=import-error
from wx import adv # pylint: disable=import-error
-import wx.dataview # pylint: disable=import-error
+import wx.dataview as dv # pylint: disable=import-error
-from .const import Column
+from .datamodel import PartListDataModel
from .events import (
EVT_ASSIGN_PARTS_EVENT,
EVT_LOGBOX_APPEND_EVENT,
@@ -26,12 +26,10 @@
from .fabrication import Fabrication
from .helpers import (
PLUGIN_PATH,
- GetListIcon,
GetScaleFactor,
HighResWxSize,
getVersion,
loadBitmapScaled,
- loadIconScaled,
toggle_exclude_from_bom,
toggle_exclude_from_pos,
)
@@ -54,7 +52,7 @@
ID_DOWNLOAD = 4
ID_SETTINGS = 5
ID_SELECT_PART = 6
-ID_REMOVE_PART = 7
+ID_REMOVE_LCSC_NUMBER = 7
ID_SELECT_ALIKE = 8
ID_TOGGLE_BOM_POS = 9
ID_TOGGLE_BOM = 10
@@ -64,8 +62,12 @@
ID_HIDE_POS = 14
ID_SAVE_MAPPINGS = 15
ID_EXPORT_TO_SCHEMATIC = 16
+ID_CONTEXT_MENU_COPY_LCSC = wx.NewIdRef()
+ID_CONTEXT_MENU_PASTE_LCSC = wx.NewIdRef()
ID_CONTEXT_MENU_ADD_ROT_BY_PACKAGE = wx.NewIdRef()
ID_CONTEXT_MENU_ADD_ROT_BY_NAME = wx.NewIdRef()
+ID_CONTEXT_MENU_FIND_MAPPING = wx.NewIdRef()
+ID_CONTEXT_MENU_ADD_MAPPING = wx.NewIdRef()
class KicadProvider:
@@ -236,8 +238,8 @@ def __init__(self, parent, kicad_provider=KicadProvider()):
"Assign a LCSC number to a footprint",
)
- self.remove_part_button = self.right_toolbar.AddTool(
- ID_REMOVE_PART,
+ self.remove_lcsc_number_button = self.right_toolbar.AddTool(
+ ID_REMOVE_LCSC_NUMBER,
"Remove LCSC number",
loadBitmapScaled(
"mdi-close-box-outline.png",
@@ -339,7 +341,7 @@ def __init__(self, parent, kicad_provider=KicadProvider()):
)
self.Bind(wx.EVT_TOOL, self.select_part, self.select_part_button)
- self.Bind(wx.EVT_TOOL, self.remove_part, self.remove_part_button)
+ self.Bind(wx.EVT_TOOL, self.remove_lcsc_number, self.remove_lcsc_number_button)
self.Bind(wx.EVT_TOOL, self.select_alike, self.select_alike_button)
self.Bind(wx.EVT_TOOL, self.toggle_bom_pos, self.toggle_bom_pos_button)
self.Bind(wx.EVT_TOOL, self.toggle_bom, self.toggle_bom_button)
@@ -355,107 +357,69 @@ def __init__(self, parent, kicad_provider=KicadProvider()):
# ---------------------------------------------------------------------
# ----------------------- Footprint List ------------------------------
# ---------------------------------------------------------------------
+
table_sizer = wx.BoxSizer(wx.HORIZONTAL)
table_sizer.SetMinSize(HighResWxSize(self.window, wx.Size(-1, 600)))
- self.footprint_list = wx.dataview.DataViewListCtrl(
+
+ self.footprint_list = dv.DataViewCtrl(
self,
- wx.ID_ANY,
- wx.DefaultPosition,
- wx.DefaultSize,
- style=wx.dataview.DV_MULTIPLE,
+ style=wx.BORDER_THEME | dv.DV_ROW_LINES | dv.DV_VERT_RULES | dv.DV_MULTIPLE,
)
- self.footprint_list.SetMinSize(HighResWxSize(self.window, wx.Size(750, 400)))
- self.reference = self.footprint_list.AppendTextColumn(
- "Reference",
- mode=wx.dataview.DATAVIEW_CELL_INERT,
- width=int(self.scale_factor * 100),
- align=wx.ALIGN_CENTER,
- flags=wx.dataview.DATAVIEW_COL_RESIZABLE,
+
+ reference = self.footprint_list.AppendTextColumn(
+ "Reference", 0, width=50, mode=dv.DATAVIEW_CELL_INERT, align=wx.ALIGN_CENTER
)
- self.value = self.footprint_list.AppendTextColumn(
- "Value",
- mode=wx.dataview.DATAVIEW_CELL_INERT,
- width=int(self.scale_factor * 200),
- align=wx.ALIGN_CENTER,
- flags=wx.dataview.DATAVIEW_COL_RESIZABLE,
+ value = self.footprint_list.AppendTextColumn(
+ "Value", 1, width=250, mode=dv.DATAVIEW_CELL_INERT, align=wx.ALIGN_CENTER
)
- self.footprint = self.footprint_list.AppendTextColumn(
+ footprint = self.footprint_list.AppendTextColumn(
"Footprint",
- mode=wx.dataview.DATAVIEW_CELL_INERT,
- width=int(self.scale_factor * 300),
+ 2,
+ width=250,
+ mode=dv.DATAVIEW_CELL_INERT,
align=wx.ALIGN_CENTER,
- flags=wx.dataview.DATAVIEW_COL_RESIZABLE,
)
- self.lcsc = self.footprint_list.AppendTextColumn(
- "LCSC",
- mode=wx.dataview.DATAVIEW_CELL_INERT,
- width=int(self.scale_factor * 100),
- align=wx.ALIGN_CENTER,
- flags=wx.dataview.DATAVIEW_COL_RESIZABLE,
+ lcsc = self.footprint_list.AppendTextColumn(
+ "LCSC", 3, width=100, mode=dv.DATAVIEW_CELL_INERT, align=wx.ALIGN_CENTER
)
- self.type_column = self.footprint_list.AppendTextColumn(
- "Type",
- mode=wx.dataview.DATAVIEW_CELL_INERT,
- width=int(self.scale_factor * 100),
- align=wx.ALIGN_CENTER,
- flags=wx.dataview.DATAVIEW_COL_RESIZABLE,
- )
- self.stock = self.footprint_list.AppendTextColumn(
- "Stock",
- mode=wx.dataview.DATAVIEW_CELL_INERT,
- width=int(self.scale_factor * 100),
- align=wx.ALIGN_CENTER,
- flags=wx.dataview.DATAVIEW_COL_RESIZABLE,
+ type = self.footprint_list.AppendTextColumn(
+ "Type", 4, width=100, mode=dv.DATAVIEW_CELL_INERT, align=wx.ALIGN_CENTER
)
- self.bom = self.footprint_list.AppendIconTextColumn(
- "BOM",
- mode=wx.dataview.DATAVIEW_CELL_INERT,
- width=int(self.scale_factor * 40),
- align=wx.ALIGN_CENTER,
- flags=wx.dataview.DATAVIEW_COL_RESIZABLE,
+ stock = self.footprint_list.AppendTextColumn(
+ "Stock", 5, width=100, mode=dv.DATAVIEW_CELL_INERT, align=wx.ALIGN_CENTER
)
- self.pos = self.footprint_list.AppendIconTextColumn(
- "POS",
- mode=wx.dataview.DATAVIEW_CELL_INERT,
- width=int(self.scale_factor * 40),
- align=wx.ALIGN_CENTER,
- flags=wx.dataview.DATAVIEW_COL_RESIZABLE,
+ bom = self.footprint_list.AppendIconTextColumn(
+ "BOM", 6, width=50, mode=dv.DATAVIEW_CELL_INERT
)
- self.rot = self.footprint_list.AppendTextColumn(
- "Rotation",
- mode=wx.dataview.DATAVIEW_CELL_INERT,
- width=int(self.scale_factor * 60),
- align=wx.ALIGN_CENTER,
- flags=wx.dataview.DATAVIEW_COL_RESIZABLE,
+ pos = self.footprint_list.AppendIconTextColumn(
+ "POS", 7, width=50, mode=dv.DATAVIEW_CELL_INERT
)
- self.side = self.footprint_list.AppendTextColumn(
- "Side",
- mode=wx.dataview.DATAVIEW_CELL_INERT,
- width=int(self.scale_factor * 40),
- align=wx.ALIGN_CENTER,
- flags=wx.dataview.DATAVIEW_COL_RESIZABLE,
+ rotation = self.footprint_list.AppendTextColumn(
+ "Rotation", 8, width=70, mode=dv.DATAVIEW_CELL_INERT, align=wx.ALIGN_CENTER
)
- self.footprint_list.AppendTextColumn(
- "",
- mode=wx.dataview.DATAVIEW_CELL_INERT,
- align=wx.ALIGN_CENTER,
- width=1,
- flags=wx.dataview.DATAVIEW_COL_RESIZABLE,
+ side = self.footprint_list.AppendIconTextColumn(
+ "Side", 9, width=50, mode=dv.DATAVIEW_CELL_INERT
)
- table_sizer.Add(self.footprint_list, 20, wx.ALL | wx.EXPAND, 5)
- self.footprint_list.Bind(
- wx.dataview.EVT_DATAVIEW_COLUMN_HEADER_CLICK, self.OnSortFootprintList
- )
+ reference.SetSortable(True)
+ value.SetSortable(True)
+ footprint.SetSortable(True)
+ lcsc.SetSortable(True)
+ type.SetSortable(True)
+ stock.SetSortable(True)
+ bom.SetSortable(True)
+ pos.SetSortable(False)
+ rotation.SetSortable(True)
+ side.SetSortable(True)
- self.footprint_list.Bind(
- wx.dataview.EVT_DATAVIEW_SELECTION_CHANGED, self.OnFootprintSelected
- )
+ table_sizer.Add(self.footprint_list, 20, wx.ALL | wx.EXPAND, 5)
self.footprint_list.Bind(
- wx.dataview.EVT_DATAVIEW_ITEM_CONTEXT_MENU, self.OnRightDown
+ dv.EVT_DATAVIEW_SELECTION_CHANGED, self.OnFootprintSelected
)
+ self.footprint_list.Bind(dv.EVT_DATAVIEW_ITEM_CONTEXT_MENU, self.OnRightDown)
+
table_sizer.Add(self.right_toolbar, 1, wx.EXPAND, 5)
# ---------------------------------------------------------------------
# --------------------- Bottom Logbox and Gauge -----------------------
@@ -510,6 +474,8 @@ def __init__(self, parent, kicad_provider=KicadProvider()):
self.enable_part_specific_toolbar_buttons(False)
self.init_logger()
+ self.partlist_data_model = PartListDataModel(self.scale_factor)
+ self.footprint_list.AssociateModel(self.partlist_data_model)
self.init_library()
self.init_fabrication()
if self.library.state == LibraryState.UPDATE_NEEDED:
@@ -555,7 +521,7 @@ def assign_parts(self, e):
for reference in e.references:
self.store.set_lcsc(reference, e.lcsc)
self.store.set_stock(reference, int(e.stock))
- self.populate_footprint_list()
+ self.partlist_data_model.set_lcsc(reference, e.lcsc, e.type, e.stock)
def display_message(self, e):
"""Dispaly a message with the data from the event."""
@@ -576,29 +542,13 @@ def get_correction(self, part: dict, corrections: list) -> str:
for regex, correction in corrections:
if re.search(regex, str(part["footprint"])):
return str(correction)
- return ""
+ return "0"
def populate_footprint_list(self, *_):
- """Populate/Refresh list of footprints."""
+ """Populate list of footprints."""
if not self.store:
self.init_store()
- self.footprint_list.DeleteAllItems()
- icons = {
- 0: wx.dataview.DataViewIconText(
- "",
- loadIconScaled(
- "mdi-check-color.png",
- self.scale_factor,
- ),
- ),
- 1: wx.dataview.DataViewIconText(
- "",
- loadIconScaled(
- "mdi-close-color.png",
- self.scale_factor,
- ),
- ),
- }
+ self.partlist_data_model.RemoveAll()
details = {}
corrections = self.library.get_all_correction_data()
for part in self.store.read_all():
@@ -612,7 +562,7 @@ def populate_footprint_list(self, *_):
# don't show the part if hide POS is set
if self.hide_pos_parts and part["exclude_from_pos"]:
continue
- self.footprint_list.AppendItem(
+ self.partlist_data_model.AddEntry(
[
part["reference"],
part["value"],
@@ -620,23 +570,13 @@ def populate_footprint_list(self, *_):
part["lcsc"],
details.get(part["lcsc"], {}).get("type", ""), # type
details.get(part["lcsc"], {}).get("stock", ""), # stock
- icons.get(
- part["exclude_from_bom"], icons.get(0)
- ), # exclude_from_bom icon
- icons.get(
- part["exclude_from_pos"], icons.get(0)
- ), # exclude_from_pos icon
- self.get_correction(part, corrections), # rotation
- "Top" if fp.GetLayer() == 0 else "Bot", # Side
- "",
+ part["exclude_from_bom"],
+ part["exclude_from_pos"],
+ str(self.get_correction(part, corrections)),
+ str(fp.GetLayer()),
]
)
- def OnSortFootprintList(self, e):
- """Set order_by to the clicked column and trigger list refresh."""
- self.store.set_order_by(e.GetColumn())
- self.populate_footprint_list()
-
def OnBomHide(self, *_):
"""Hide all parts from the list that have 'in BOM' set to No."""
self.hide_bom_parts = not self.hide_bom_parts
@@ -717,12 +657,9 @@ def OnFootprintSelected(self, *_):
# select all of the selected items in the footprint_list
if self.footprint_list.GetSelectedItemsCount() > 0:
for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- ref = self.footprint_list.GetTextValue(row, 0)
+ ref = self.partlist_data_model.get_reference(item)
fp = self.pcbnew.GetBoard().FindFootprintByReference(ref)
-
fp.SetSelected()
-
# cause pcbnew to refresh the board with the changes to the selected footprint(s)
self.pcbnew.Refresh()
@@ -740,7 +677,7 @@ def enable_part_specific_toolbar_buttons(self, state):
"""Control the state of all the buttons that relate to parts in toolbar on the right side."""
for button in (
ID_SELECT_PART,
- ID_REMOVE_PART,
+ ID_REMOVE_LCSC_NUMBER,
ID_SELECT_ALIKE,
ID_TOGGLE_BOM_POS,
ID_TOGGLE_BOM,
@@ -753,89 +690,58 @@ def enable_part_specific_toolbar_buttons(self, state):
def toggle_bom_pos(self, *_):
"""Toggle the exclude from BOM/POS attribute of a footprint."""
- selected_rows = []
for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- selected_rows.append(row)
- ref = self.footprint_list.GetTextValue(row, 0)
+ ref = self.partlist_data_model.get_reference(item)
board = self.pcbnew.GetBoard()
fp = board.FindFootprintByReference(ref)
bom = toggle_exclude_from_bom(fp)
pos = toggle_exclude_from_pos(fp)
self.store.set_bom(ref, int(bom))
self.store.set_pos(ref, int(pos))
- self.footprint_list.SetValue(
- GetListIcon(bom, self.scale_factor), row, Column.BOM
- )
- self.footprint_list.SetValue(
- GetListIcon(pos, self.scale_factor), row, Column.POS
- )
+ self.partlist_data_model.toggle_bom_pos(item)
def toggle_bom(self, *_):
"""Toggle the exclude from BOM attribute of a footprint."""
- selected_rows = []
for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- selected_rows.append(row)
- ref = self.footprint_list.GetTextValue(row, 0)
+ ref = self.partlist_data_model.get_reference(item)
board = self.pcbnew.GetBoard()
fp = board.FindFootprintByReference(ref)
bom = toggle_exclude_from_bom(fp)
self.store.set_bom(ref, int(bom))
- self.footprint_list.SetValue(
- GetListIcon(bom, self.scale_factor), row, Column.BOM
- )
+ self.partlist_data_model.toggle_bom(item)
def toggle_pos(self, *_):
"""Toggle the exclude from POS attribute of a footprint."""
- selected_rows = []
for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- selected_rows.append(row)
- ref = self.footprint_list.GetTextValue(row, 0)
+ ref = self.partlist_data_model.get_reference(item)
board = self.pcbnew.GetBoard()
fp = board.FindFootprintByReference(ref)
pos = toggle_exclude_from_pos(fp)
self.store.set_pos(ref, int(pos))
- self.footprint_list.SetValue(
- GetListIcon(pos, self.scale_factor), row, Column.POS
- )
+ self.partlist_data_model.toggle_pos(item)
- def remove_part(self, *_):
+ def remove_lcsc_number(self, *_):
"""Remove an assigned a LCSC Part number to a footprint."""
for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- ref = self.footprint_list.GetTextValue(row, 0)
+ ref = self.partlist_data_model.get_reference(item)
self.store.set_lcsc(ref, "")
- self.populate_footprint_list()
+ self.store.set_stock(ref, None)
+ self.partlist_data_model.remove_lcsc_number(item)
def select_alike(self, *_):
"""Select all parts that have the same value and footprint."""
- num_sel = (
- self.footprint_list.GetSelectedItemsCount()
- ) # could have selected more than 1 item (by mistake?)
- if num_sel == 1:
- item = self.footprint_list.GetSelection()
- else:
+ if self.footprint_list.GetSelectedItemsCount() > 1:
self.logger.warning("Select only one component, please.")
return
- row = self.footprint_list.ItemToRow(item)
- ref = self.footprint_list.GetValue(row, 0)
- part = self.store.get_part(ref)
- for r in range(self.footprint_list.GetItemCount()):
- value = self.footprint_list.GetValue(r, 1)
- fp = self.footprint_list.GetValue(r, 2)
- if part["value"] == value and part["footprint"] == fp:
- self.footprint_list.SelectRow(r)
+ item = self.footprint_list.GetSelection()
+ for item in self.partlist_data_model.select_alike(item):
+ self.footprint_list.Select(item)
def get_part_details(self, *_):
"""Fetch part details from LCSC and show them one after another each in a modal."""
- parts = self.get_selected_part_id_from_gui()
- if not parts:
- return
-
- for part in parts:
- self.show_part_details_dialog(part)
+ for item in self.footprint_list.GetSelections():
+ if lcsc := self.partlist_data_model.get_lcsc(item):
+ self.show_part_details_dialog(lcsc)
def get_column_by_name(self, column_title_to_find):
"""Lookup a column in our main footprint table by matching its title."""
@@ -851,25 +757,6 @@ def get_column_position_by_name(self, column_title_to_find):
return -1
return self.footprint_list.GetColumnPosition(col)
- def get_selected_part_id_from_gui(self):
- """Get a list of LCSC part#s currently selected."""
- lcsc_ids_selected = []
- for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- if row == -1:
- continue
-
- lcsc_id = self.get_row_item_in_column(row, "LCSC")
- lcsc_ids_selected.append(lcsc_id)
-
- return lcsc_ids_selected
-
- def get_row_item_in_column(self, row, column_title):
- """Get an item from a row based on the column title."""
- return self.footprint_list.GetTextValue(
- row, self.get_column_position_by_name(column_title)
- )
-
def show_part_details_dialog(self, part):
"""Show the part details modal dialog."""
wx.BeginBusyCursor()
@@ -925,14 +812,13 @@ def select_part(self, *_):
"""Select a part from the library and assign it to the selected footprint(s)."""
selection = {}
for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- reference = self.footprint_list.GetTextValue(row, 0)
- value = self.footprint_list.GetTextValue(row, 1)
- lcsc = self.footprint_list.GetTextValue(row, 3)
+ ref = self.partlist_data_model.get_reference(item)
+ lcsc = self.partlist_data_model.get_lcsc(item)
+ value = self.partlist_data_model.get_value(item)
if lcsc != "":
- selection[reference] = lcsc
+ selection[ref] = lcsc
else:
- selection[reference] = value
+ selection[ref] = value
PartSelectorDialog(self, selection).ShowModal()
def generate_fabrication_data(self, *_):
@@ -953,13 +839,9 @@ def generate_fabrication_data(self, *_):
def copy_part_lcsc(self, *_):
"""Fetch part details from LCSC and show them in a modal."""
for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- if row == -1:
- return
- part = self.footprint_list.GetTextValue(row, 3)
- if part != "":
+ if lcsc := self.partlist_data_model.get_lcsc(item):
if wx.TheClipboard.Open():
- wx.TheClipboard.SetData(wx.TextDataObject(part))
+ wx.TheClipboard.SetData(wx.TextDataObject(lcsc))
wx.TheClipboard.Close()
def paste_part_lcsc(self, *_):
@@ -969,41 +851,36 @@ def paste_part_lcsc(self, *_):
success = wx.TheClipboard.GetData(text_data)
wx.TheClipboard.Close()
if success:
- lcsc = self.sanitize_lcsc(text_data.GetText())
- if lcsc == "":
- return
- for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- reference = self.footprint_list.GetTextValue(row, 0)
- self.store.set_lcsc(reference, lcsc)
- self.populate_footprint_list()
+ if (lcsc := self.sanitize_lcsc(text_data.GetText())) != "":
+ for item in self.footprint_list.GetSelections():
+ details = self.library.get_part_details(lcsc)
+ reference = self.partlist_data_model.get_reference(item)
+ self.partlist_data_model.set_lcsc(
+ reference, lcsc, details["type"], details["stock"]
+ )
+ self.store.set_lcsc(reference, lcsc)
- def add_part_rot(self, e):
+ def add_rotation(self, e):
"""Add part rotation for the current part."""
for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- if row == -1:
- return
if e.GetId() == ID_CONTEXT_MENU_ADD_ROT_BY_PACKAGE:
- package = self.footprint_list.GetTextValue(row, 2)
- if package != "":
- RotationManagerDialog(self, "^" + re.escape(package)).ShowModal()
+ if footprint := self.partlist_data_model.get_footprint(item):
+ RotationManagerDialog(self, "^" + re.escape(footprint)).ShowModal()
elif e.GetId() == ID_CONTEXT_MENU_ADD_ROT_BY_NAME:
- name = self.footprint_list.GetTextValue(row, 1)
- if name != "":
- RotationManagerDialog(self, re.escape(name)).ShowModal()
+ if value := self.partlist_data_model.get_value(item):
+ RotationManagerDialog(self, re.escape(value)).ShowModal()
def save_all_mappings(self, *_):
"""Save all mappings."""
- for r in range(self.footprint_list.GetItemCount()):
- footp = self.footprint_list.GetTextValue(r, 2)
- partval = self.footprint_list.GetTextValue(r, 1)
- lcscpart = self.footprint_list.GetTextValue(r, 3)
- if footp != "" and partval != "" and lcscpart != "":
- if self.library.get_mapping_data(footp, partval):
- self.library.update_mapping_data(footp, partval, lcscpart)
+ for item in self.partlist_data_model.get_all():
+ value = item[1]
+ footprint = item[2]
+ lcsc = item[3]
+ if footprint != "" and value != "" and lcsc != "":
+ if self.library.get_mapping_data(footprint, value):
+ self.library.update_mapping_data(footprint, value, lcsc)
else:
- self.library.insert_mapping_data(footp, partval, lcscpart)
+ self.library.insert_mapping_data(footprint, value, lcsc)
self.logger.info("All mappings saved")
def export_to_schematic(self, *_):
@@ -1024,33 +901,30 @@ def export_to_schematic(self, *_):
def add_foot_mapping(self, *_):
"""Add a footprint mapping."""
for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- if row == -1:
- return
- footp = self.footprint_list.GetTextValue(row, 2)
- partval = self.footprint_list.GetTextValue(row, 1)
- lcscpart = self.footprint_list.GetTextValue(row, 3)
- if footp != "" and partval != "" and lcscpart != "":
- if self.library.get_mapping_data(footp, partval):
- self.library.update_mapping_data(footp, partval, lcscpart)
+ footprint = self.partlist_data_model.get_footprint(item)
+ value = self.partlist_data_model.get_value(item)
+ lcsc = self.partlist_data_model.get_lcsc(item)
+ if footprint != "" and value != "" and lcsc != "":
+ if self.library.get_mapping_data(footprint, value):
+ self.library.update_mapping_data(footprint, value, lcsc)
else:
- self.library.insert_mapping_data(footp, partval, lcscpart)
+ self.library.insert_mapping_data(footprint, value, lcsc)
def search_foot_mapping(self, *_):
"""Search for a footprint mapping."""
for item in self.footprint_list.GetSelections():
- row = self.footprint_list.ItemToRow(item)
- if row == -1:
- return
- footp = self.footprint_list.GetTextValue(row, 2)
- partval = self.footprint_list.GetTextValue(row, 1)
- if footp != "" and partval != "":
- if self.library.get_mapping_data(footp, partval):
- lcsc = self.library.get_mapping_data(footp, partval)[2]
- reference = self.footprint_list.GetTextValue(row, 0)
+ reference = self.partlist_data_model.get_reference(item)
+ footprint = self.partlist_data_model.get_footprint(item)
+ value = self.partlist_data_model.get_value(item)
+ if footprint != "" and value != "":
+ if self.library.get_mapping_data(footprint, value):
+ lcsc = self.library.get_mapping_data(footprint, value)[2]
self.store.set_lcsc(reference, lcsc)
self.logger.info("Found %s", lcsc)
- self.populate_footprint_list()
+ details = self.library.get_part_details(lcsc)
+ self.partlist_data_model.set_lcsc(
+ reference, lcsc, details["type"], details["stock"]
+ )
def sanitize_lcsc(self, lcsc_PN):
"""Sanitize a given LCSC number using a regex."""
@@ -1061,37 +935,48 @@ def sanitize_lcsc(self, lcsc_PN):
def OnRightDown(self, *_):
"""Right click context menu for action on parts table."""
- conMenu = wx.Menu()
- copy_lcsc = wx.MenuItem(conMenu, wx.NewIdRef(), "Copy LCSC")
- conMenu.Append(copy_lcsc)
- conMenu.Bind(wx.EVT_MENU, self.copy_part_lcsc, copy_lcsc)
+ right_click_menu = wx.Menu()
+
+ copy_lcsc = wx.MenuItem(
+ right_click_menu, ID_CONTEXT_MENU_COPY_LCSC, "Copy LCSC"
+ )
+ right_click_menu.Append(copy_lcsc)
+ right_click_menu.Bind(wx.EVT_MENU, self.copy_part_lcsc, copy_lcsc)
- paste_lcsc = wx.MenuItem(conMenu, wx.NewIdRef(), "Paste LCSC")
- conMenu.Append(paste_lcsc)
- conMenu.Bind(wx.EVT_MENU, self.paste_part_lcsc, paste_lcsc)
+ paste_lcsc = wx.MenuItem(
+ right_click_menu, ID_CONTEXT_MENU_PASTE_LCSC, "Paste LCSC"
+ )
+ right_click_menu.Append(paste_lcsc)
+ right_click_menu.Bind(wx.EVT_MENU, self.paste_part_lcsc, paste_lcsc)
rotation_by_package = wx.MenuItem(
- conMenu, ID_CONTEXT_MENU_ADD_ROT_BY_PACKAGE, "Add Rotation by package"
+ right_click_menu,
+ ID_CONTEXT_MENU_ADD_ROT_BY_PACKAGE,
+ "Add Rotation by package",
)
- conMenu.Append(rotation_by_package)
- conMenu.Bind(wx.EVT_MENU, self.add_part_rot, rotation_by_package)
+ right_click_menu.Append(rotation_by_package)
+ right_click_menu.Bind(wx.EVT_MENU, self.add_rotation, rotation_by_package)
rotation_by_name = wx.MenuItem(
- conMenu, ID_CONTEXT_MENU_ADD_ROT_BY_NAME, "Add Rotation by name"
+ right_click_menu, ID_CONTEXT_MENU_ADD_ROT_BY_NAME, "Add Rotation by name"
)
- conMenu.Append(rotation_by_name)
- conMenu.Bind(wx.EVT_MENU, self.add_part_rot, rotation_by_name)
+ right_click_menu.Append(rotation_by_name)
+ right_click_menu.Bind(wx.EVT_MENU, self.add_rotation, rotation_by_name)
- find_mapping = wx.MenuItem(conMenu, wx.NewIdRef(), "Find LCSC from Mappings")
- conMenu.Append(find_mapping)
- conMenu.Bind(wx.EVT_MENU, self.search_foot_mapping, find_mapping)
+ find_mapping = wx.MenuItem(
+ right_click_menu, ID_CONTEXT_MENU_FIND_MAPPING, "Find LCSC from Mappings"
+ )
+ right_click_menu.Append(find_mapping)
+ right_click_menu.Bind(wx.EVT_MENU, self.search_foot_mapping, find_mapping)
- add_mapping = wx.MenuItem(conMenu, wx.NewIdRef(), "Add Footprint Mapping")
- conMenu.Append(add_mapping)
- conMenu.Bind(wx.EVT_MENU, self.add_foot_mapping, add_mapping)
+ add_mapping = wx.MenuItem(
+ right_click_menu, ID_CONTEXT_MENU_ADD_MAPPING, "Add Footprint Mapping"
+ )
+ right_click_menu.Append(add_mapping)
+ right_click_menu.Bind(wx.EVT_MENU, self.add_foot_mapping, add_mapping)
- self.footprint_list.PopupMenu(conMenu)
- conMenu.Destroy() # destroy to avoid memory leak
+ self.footprint_list.PopupMenu(right_click_menu)
+ right_click_menu.Destroy() # destroy to avoid memory leak
def init_logger(self):
"""Initialize logger to log into textbox."""
diff --git a/partdetails.py b/partdetails.py
index 6be55f27..f30f2e81 100644
--- a/partdetails.py
+++ b/partdetails.py
@@ -147,7 +147,9 @@ def savepdf(self, *_):
filename = self.pdfurl.rsplit("/", maxsplit=1)[1]
self.logger.info("Save datasheet %s to %s", filename, self.datasheet_path)
self.datasheet_path.mkdir(parents=True, exist_ok=True)
- result = self.lcsc_api.download_datasheet(self.pdfurl, self.datasheet_path / filename)
+ result = self.lcsc_api.download_datasheet(
+ self.pdfurl, self.datasheet_path / filename
+ )
title = "Success" if result["success"] else "Error"
style = "info" if result["success"] else "error"
wx.PostEvent(
diff --git a/partselector.py b/partselector.py
index 1ea714a4..67a1d1ac 100644
--- a/partselector.py
+++ b/partselector.py
@@ -651,8 +651,8 @@ def get_price(self, quantity, prices) -> float:
return float(price)
for p in price_ranges:
range, price = p.split(":")
- lower,upper = range.split("-")
- if not upper: # upper bound of price ranges
+ lower, upper = range.split("-")
+ if not upper: # upper bound of price ranges
return float(price)
lower = int(lower)
upper = int(upper)
@@ -678,8 +678,8 @@ def populate_part_list(self, parts, search_duration):
self.result_count.SetLabel(f"{count} Results in {search_duration_text}")
for p in parts:
item = [str(c) for c in p]
- pricecol = 8 # Must match order in library.py search function
- price = round(self.get_price(len(self.parts), item[pricecol]) , 3)
+ pricecol = 8 # Must match order in library.py search function
+ price = round(self.get_price(len(self.parts), item[pricecol]), 3)
sum = round(price * len(self.parts), 3)
item[pricecol] = f"{len(self.parts)} parts: ${price} each / ${sum} total"
self.part_list.AppendItem(item)
@@ -691,11 +691,13 @@ def select_part(self, *_):
if row == -1:
return
selection = self.part_list.GetTextValue(row, 0)
+ type = self.part_list.GetTextValue(row, 4)
stock = self.part_list.GetTextValue(row, 5)
wx.PostEvent(
self.parent,
AssignPartsEvent(
lcsc=selection,
+ type=type,
stock=stock,
references=self.parts.keys(),
),
diff --git a/schematicexport.py b/schematicexport.py
index 506843a6..40343d0e 100644
--- a/schematicexport.py
+++ b/schematicexport.py
@@ -228,7 +228,9 @@ def _update_schematic8(self, path):
value = m.group(2)
lastLcsc = value
if newLcsc not in (lastLcsc, ""):
- self.logger.info("Updating %s on %s in %s", newLcsc, lastRef, path)
+ self.logger.info(
+ "Updating %s on %s in %s", newLcsc, lastRef, path
+ )
outLine = outLine.replace(
'"' + lastLcsc + '"', '"' + newLcsc + '"'
)
diff --git a/store.py b/store.py
index cad8d887..8f338317 100644
--- a/store.py
+++ b/store.py
@@ -107,7 +107,10 @@ def read_bom_parts(self) -> dict:
def create_part(self, part: dict):
"""Create a part in the database."""
with contextlib.closing(sqlite3.connect(self.dbfile)) as con, con as cur:
- cur.execute("INSERT INTO part_info VALUES (:reference, :value, :footprint, :lcsc, '', :exclude_from_bom, :exclude_from_pos)", part)
+ cur.execute(
+ "INSERT INTO part_info VALUES (:reference, :value, :footprint, :lcsc, '', :exclude_from_bom, :exclude_from_pos)",
+ part,
+ )
cur.commit()
def update_part(self, part: dict):
@@ -124,15 +127,16 @@ def get_part(self, ref: str) -> dict:
with contextlib.closing(sqlite3.connect(self.dbfile)) as con, con as cur:
con.row_factory = dict_factory
return cur.execute(
- "SELECT * FROM part_info WHERE reference = :reference", {"reference": ref}
+ "SELECT * FROM part_info WHERE reference = :reference",
+ {"reference": ref},
).fetchone()
-
- def set_stock(self, ref: str, stock: int):
+ def set_stock(self, ref: str, stock: int | None):
"""Set the stock value for a part in the database."""
with contextlib.closing(sqlite3.connect(self.dbfile)) as con, con as cur:
cur.execute(
- "UPDATE part_info SET stock = :stock WHERE reference = :reference", {"reference": ref, "stock": stock}
+ "UPDATE part_info SET stock = :stock WHERE reference = :reference",
+ {"reference": ref, "stock": stock},
)
cur.commit()
@@ -140,23 +144,26 @@ def set_bom(self, ref: str, state: int):
"""Change the BOM attribute for a part in the database."""
with contextlib.closing(sqlite3.connect(self.dbfile)) as con, con as cur:
cur.execute(
- "UPDATE part_info SET exclude_from_bom = :state WHERE reference = :reference", {"reference": ref, "state": state}
+ "UPDATE part_info SET exclude_from_bom = :state WHERE reference = :reference",
+ {"reference": ref, "state": state},
)
cur.commit()
def set_pos(self, ref: str, state: int):
- """Change the BOM attribute for a part in the database."""
+ """Change the POS attribute for a part in the database."""
with contextlib.closing(sqlite3.connect(self.dbfile)) as con, con as cur:
cur.execute(
- "UPDATE part_info SET exclude_from_pos = :state WHERE reference = :reference", {"reference": ref, "state": state}
+ "UPDATE part_info SET exclude_from_pos = :state WHERE reference = :reference",
+ {"reference": ref, "state": state},
)
cur.commit()
def set_lcsc(self, ref: str, lcsc: str):
- """Change the BOM attribute for a part in the database."""
+ """Change the LCSC attribute for a part in the database."""
with contextlib.closing(sqlite3.connect(self.dbfile)) as con, con as cur:
cur.execute(
- "UPDATE part_info SET lcsc = :lcsc WHERE reference = :reference", {"reference": ref, "lcsc": lcsc}
+ "UPDATE part_info SET lcsc = :lcsc WHERE reference = :reference",
+ {"reference": ref, "lcsc": lcsc},
)
cur.commit()