diff --git a/beamline_editor/table_utils.py b/beamline_editor/table_utils.py index d9217dc..b712d68 100644 --- a/beamline_editor/table_utils.py +++ b/beamline_editor/table_utils.py @@ -910,3 +910,159 @@ def populate_table_comparison( header.reset_filters() table.setUpdatesEnabled(True) + + +# ── Excel export ────────────────────────────────────────────────────────────── + +def export_table_to_excel( + table: QTableWidget, + file_path: str, + sheet_name: str, + include_hidden_rows: bool = False, +) -> bool: + """ + Write the visible contents of *table* to an Excel file, preserving the + row background colours and bold fonts used in the widget. + + Nothing is written when the table has no rows (or no visible rows) — + the function returns False in that case and leaves any existing file + untouched. + + Parameters + ---------- + table : QTableWidget + The table to export. Header labels become the first sheet row. + file_path : str + Destination .xlsx path. Created if missing, overwritten if present. + sheet_name : str + Name of the worksheet tab inside the workbook. + include_hidden_rows : bool + When False (default) rows hidden by column filters are skipped, so + the export matches exactly what the user sees. When True every row + is written regardless of filter state. + + Returns + ------- + bool + True — file written (table had at least one exported row). + False — nothing written (table was empty / fully filtered out). + + Raises + ------ + ImportError + If openpyxl is not installed. + + Notes + ----- + - Cell background colours are taken from each QTableWidgetItem's + background brush, so the same _GROUP_COLORS palette used on screen + is reproduced in the workbook. + - Cells rendered in bold on screen (e.g. differing values or the + "New Element" / "Difference" / "Unresolved Element" comments) are + written in bold too. + - Numeric-looking strings are written as numbers so Excel can sort + and chart them; everything else is written as text. + """ + try: + from openpyxl import Workbook + from openpyxl.styles import Font, PatternFill, Alignment, Border, Side + from openpyxl.utils import get_column_letter + except ImportError: + raise ImportError( + "openpyxl is required for Excel export: pip install openpyxl" + ) from None + + n_rows = table.rowCount() + n_cols = table.columnCount() + if n_rows == 0 or n_cols == 0: + return False + + # Determine which rows to export + export_rows = [ + r for r in range(n_rows) + if include_hidden_rows or not table.isRowHidden(r) + ] + if not export_rows: + return False + + wb = Workbook() + ws = wb.active + ws.title = sheet_name + + # ── header row ──────────────────────────────────────────────────────────── + header_font = Font(name="Arial", size=10, bold=True, color="FFFFFFFF") + header_fill = PatternFill("solid", fgColor="FF404040") + thin = Side(style="thin", color="FFBFBFBF") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + + for col in range(n_cols): + hdr_item = table.horizontalHeaderItem(col) + label = hdr_item.text() if hdr_item is not None else f"Column {col+1}" + cell = ws.cell(row=1, column=col + 1, value=label) + cell.font = header_font + cell.fill = header_fill + cell.alignment = Alignment(horizontal="center", vertical="center") + cell.border = border + + # ── data rows ───────────────────────────────────────────────────────────── + def _argb(qcolor) -> str | None: + """Convert a QColor to an openpyxl ARGB hex string, or None if white.""" + if qcolor is None or not qcolor.isValid(): + return None + # Skip pure white / fully transparent — leave the cell unfilled + if qcolor.alpha() == 0: + return None + if (qcolor.red(), qcolor.green(), qcolor.blue()) == (255, 255, 255): + return None + return f"FF{qcolor.red():02X}{qcolor.green():02X}{qcolor.blue():02X}" + + for out_row, src_row in enumerate(export_rows, start=2): + for col in range(n_cols): + item = table.item(src_row, col) + text = item.text() if item is not None else "" + + # Write numbers as numbers so Excel can sort/plot them + value: Any = text + if text not in ("", "-"): + try: + if "/" not in text: # skip "a / b" diff strings + value = float(text) + if value.is_integer() and "." not in text: + value = int(value) + except (ValueError, TypeError): + value = text + + cell = ws.cell(row=out_row, column=col + 1, value=value) + cell.border = border + + if item is not None: + # Background colour + argb = _argb(item.background().color()) + if argb: + cell.fill = PatternFill("solid", fgColor=argb) + # Bold font + is_bold = item.font().bold() + cell.font = Font(name="Arial", size=10, bold=is_bold, + color="FF000000") + else: + cell.font = Font(name="Arial", size=10, color="FF000000") + + # ── column widths ───────────────────────────────────────────────────────── + for col in range(n_cols): + max_len = 0 + hdr_item = table.horizontalHeaderItem(col) + if hdr_item is not None: + max_len = len(hdr_item.text()) + for src_row in export_rows: + item = table.item(src_row, col) + if item is not None: + max_len = max(max_len, len(item.text())) + ws.column_dimensions[get_column_letter(col + 1)].width = min( + max(10, max_len + 3), 40 + ) + + # Freeze the header row so it stays visible while scrolling + ws.freeze_panes = "A2" + + wb.save(file_path) + return True diff --git a/main.py b/main.py index 60a9986..47dc813 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,7 @@ from PyQt5 import QtWidgets from ui.BeamlineGUI import Ui_BeamlineGUI from beamline_editor import BeamlineEditorWidget -from beamline_editor.table_utils import populate_table, populate_table_comparison +from beamline_editor.table_utils import populate_table, populate_table_comparison, export_table_to_excel from beamline_editor.excel_utils import read_excel_sheet from beamline_editor.checkbox_table import ( populate_checkbox_table, @@ -54,6 +54,7 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): self.actionLoad.triggered.connect(self.load) self.actionCompareLayout.triggered.connect(self.compareLayout) + self.actionExport_as_Proto_List.triggered.connect(self.exportLayoutasPL) self.actionExportOnlineModel.triggered.connect(self.exportOnlineModel) # elements, lines and branch points @@ -193,6 +194,10 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): self.exporter.writeDomain(references[sec],sec,domain.LengthRes,subsec) self.exporter.buildCaseFooter() + def exportLayoutasPL(self): + ok = export_table_to_excel(self.UIProtoList,"Protolist/Export/protolist.xlsx","Proto-HL") + if not ok: + print("Table is empty — nothing exported") def compareLayout(self): if len(self.plrecs) == 0: