#!/usr/bin/env python # -*- coding: utf-8 -*- """ insert_table.py - Insert a table from Grok into Scribus as separate text frames. Workflow: 1. In Scribus, create a frame (any type) as a placeholder at the desired position and size. Select it. It must be empty (no text content if it's a text frame). 2. Run this script via Script > Execute Script. 3. In the dialog: - Set table width (default 180 mm) - Set row height (default 6 mm) - Choose background top color (default Grey-20) and alternating color (default Grey-05) - Choose paragraph style for cells (default Table) - Paste the ready-to-copy table text from Grok - Click Insert Table 4. The script creates individual text frames for each cell. - Rows alternate background starting with the chosen color on top. - Numeric cells (number or number+unit) are right-aligned. - No cell borders (stroke removed). Expected format of the pasted text from Grok (example): | Header 1 | Header 2 | Header 3 | | Cell A1 | Cell A2 | Cell A3 | | 12.5 | 10 mm | Text | - Optional first lines: WIDTH:180 or ROWHEIGHT:10 (can override dialog values) - Then one line per row, cells separated by | - Leading/trailing whitespace and empty lines ignored. - Lines starting with # are ignored as comments. Default table width: 180 mm. Default row height: 6 mm. Column widths are equal. Uses the placeholder's top-left position. Errors: - No document open - No placeholder selected - Selected placeholder has content (text length > 0) Requires Tkinter for the dialog (usually available in Scribus). Tested concept with Scribus 1.5+/1.6+ (Python 3). """ import sys import re try: import scribus except ImportError: print("This script only runs from within Scribus.") sys.exit(1) try: # Python 3 from tkinter import Tk, Button, Label, Entry, Frame, END, StringVar, OptionMenu from tkinter.scrolledtext import ScrolledText except ImportError: # Python 2 fallback from Tkinter import Tk, Button, Label, Entry, Frame, END, StringVar, OptionMenu from ScrolledText import ScrolledText def show_error(title, message): scribus.messageBox(title, message, scribus.ICON_WARNING, scribus.BUTTON_OK) def is_numeric_cell(text): """Return True if text is only a number or a number with a unit (e.g. 12.5, 10 mm, -3.2kg, 5%).""" if not text or not text.strip(): return False # Match optional sign, digits, optional decimal, optional whitespace + unit letters/%/° pattern = r'^\s*[-+]?\d+(\.\d+)?(\s*[a-zA-Z%°]*)?\s*$' return bool(re.match(pattern, text.strip())) def get_dialog_values(): """Show dialog with fields for parameters and text paste. Returns dict or None.""" root = Tk() root.title("Insert Grok Table") root.geometry("720x580") # Parameters frame param_frame = Frame(root) param_frame.pack(fill="x", padx=10, pady=5) Label(param_frame, text="Table name:").grid(row=0, column=0, sticky="w", padx=5, pady=2) name_var = StringVar(value="table1") Entry(param_frame, textvariable=name_var, width=12).grid(row=0, column=1, sticky="w", padx=5) Label(param_frame, text="Width (mm):").grid(row=1, column=0, sticky="w", padx=5, pady=2) width_var = StringVar(value="180") Entry(param_frame, textvariable=width_var, width=12).grid(row=1, column=1, sticky="w", padx=5) Label(param_frame, text="Base height (mm):").grid(row=0, column=2, sticky="w", padx=5, pady=2) rowheight_var = StringVar(value="6") Entry(param_frame, textvariable=rowheight_var, width=12).grid(row=0, column=3, sticky="w", padx=5) Label(param_frame, text="Height step (mm):").grid(row=2, column=2, sticky="w", padx=5, pady=2) step_var = StringVar(value="5") Entry(param_frame, textvariable=step_var, width=12).grid(row=2, column=3, sticky="w", padx=5) # Background colors: two alternating try: color_names = scribus.getColorNames() for c in ["Grey-20", "Grey-05", "Grey-10"]: if c not in color_names: color_names.insert(0, c) except Exception: color_names = ["Grey-20", "Grey-05", "Grey-10", "None", "Black", "White"] Label(param_frame, text="Background top:").grid(row=1, column=0, sticky="w", padx=5, pady=2) bg1_var = StringVar(value="Grey-20") bg1_menu = OptionMenu(param_frame, bg1_var, *color_names) bg1_menu.config(width=15) bg1_menu.grid(row=1, column=1, sticky="w", padx=5) Label(param_frame, text="Background alt:").grid(row=1, column=2, sticky="w", padx=5, pady=2) bg2_var = StringVar(value="Grey-05") bg2_menu = OptionMenu(param_frame, bg2_var, *color_names) bg2_menu.config(width=15) bg2_menu.grid(row=1, column=3, sticky="w", padx=5) # Text style selection try: style_names = scribus.getParagraphStyles() if "Table" not in style_names: style_names.insert(0, "Table") except Exception: style_names = ["Table", "Default Paragraph Style"] Label(param_frame, text="Text style:").grid(row=2, column=0, sticky="w", padx=5, pady=2) style_var = StringVar(value="Table") style_menu = OptionMenu(param_frame, style_var, *style_names) style_menu.config(width=15) style_menu.grid(row=2, column=1, sticky="w", padx=5) Label(root, text="Paste the ready-to-copy table text from Grok here:").pack(pady=5) text_widget = ScrolledText(root, width=80, height=18, wrap="word") text_widget.pack(padx=10, pady=5, fill="both", expand=True) text_widget.focus_set() result = [None] def on_insert(): try: w = float(width_var.get().strip() or "180") rh = float(rowheight_var.get().strip() or "6") step = float(step_var.get().strip() or "5") except ValueError: show_error("Insert Table", "Width, base height and step must be numbers.") return pasted = text_widget.get("1.0", END).strip() result[0] = { "width": w, "rowheight": rh, "step": step, "bg1": bg1_var.get(), "bg2": bg2_var.get(), "text_style": style_var.get(), "text": pasted, "name": sanitize_table_name(name_var.get().strip() or "table1") } root.destroy() def on_cancel(): root.destroy() btn_frame = Frame(root) btn_frame.pack(fill="x", pady=5) Button(btn_frame, text="Insert Table", command=on_insert, width=15).pack(side="left", padx=20) Button(btn_frame, text="Cancel", command=on_cancel, width=15).pack(side="right", padx=20) root.mainloop() return result[0] def parse_table_text(text, default_width, default_rowheight): """Parse the pasted text into width, rowheight, and list of rows (list of cells).""" if not text: return default_width, default_rowheight, [] lines = [line.strip() for line in text.splitlines() if line.strip() and not line.strip().startswith("#")] width = default_width rowheight = default_rowheight rows = [] for line in lines: upper = line.upper() if upper.startswith("WIDTH:"): try: width = float(line.split(":", 1)[1].strip()) except ValueError: pass elif upper.startswith("ROWHEIGHT:") or upper.startswith("HEIGHT:"): try: rowheight = float(line.split(":", 1)[1].strip()) except ValueError: pass else: # Table row: split on | cells = [cell.strip() for cell in line.split("|")] cells = [c for c in cells if c or len(cells) == 1] if cells: rows.append(cells) # Clean cells: remove pure empty from ends if any cleaned_rows = [] for row in rows: while row and not row[0]: row.pop(0) while row and not row[-1]: row.pop() if row: cleaned_rows.append(row) return width, rowheight, cleaned_rows def estimate_text_width_mm(text, font_size_pt=12): """Rough estimate of single-line text width in mm.""" if not text: return 5.0 # ~0.45 mm per pt of font size per character (FreeSans-like) char_width = font_size_pt * 0.45 / 2.83465 # pt to mm return max(8.0, len(text) * char_width + 2.0) def estimate_lines(text, col_width_mm, font_size_pt=12, line_spacing_pt=15): """Estimate number of lines needed for text in a given column width.""" if not text or not text.strip(): return 1 single_width = estimate_text_width_mm(text, font_size_pt) if single_width <= col_width_mm: return 1 # Rough wrapping chars_per_line = max(5, int(col_width_mm / (font_size_pt * 0.45 / 2.83465))) return max(1, (len(text) + chars_per_line - 1) // chars_per_line) def optimize_column_widths(rows, total_width, base_height, step): """Return list of column widths and list of row heights.""" if not rows: return [], [] num_cols = max(len(r) for r in rows) # Pad rows padded = [] for row in rows: padded.append(row + [""] * (num_cols - len(row))) # Estimate max single-line width per column max_single = [] is_single = [] for c in range(num_cols): widths = [estimate_text_width_mm(padded[r][c]) for r in range(len(padded))] max_w = max(widths) if widths else 10.0 max_single.append(max_w) # Treat as single-line if longest cell is not much wider than a modest column is_single.append(max_w < 50) # Assign widths single_width_sum = sum(max_single[c] for c in range(num_cols) if is_single[c]) multi_cols = [c for c in range(num_cols) if not is_single[c]] remaining = max(20.0, total_width - single_width_sum) col_widths = [0.0] * num_cols for c in range(num_cols): if is_single[c]: col_widths[c] = max(15.0, max_single[c]) else: col_widths[c] = max(15.0, remaining / max(1, len(multi_cols))) # Simple balancing for multi-line columns: try a few distributions if len(multi_cols) >= 2: best_score = float("inf") best_widths = col_widths[:] base_multi = remaining / len(multi_cols) for delta in range(-15, 16, 5): trial = col_widths[:] trial[multi_cols[0]] = max(15, base_multi + delta) trial[multi_cols[1]] = max(15, remaining - trial[multi_cols[0]]) if len(multi_cols) > 2: rest = remaining - trial[multi_cols[0]] - trial[multi_cols[1]] for mc in multi_cols[2:]: trial[mc] = rest / (len(multi_cols) - 2) # Score = max lines in any row max_lines = 1 for r in range(len(padded)): row_lines = 1 for c in range(num_cols): lines = estimate_lines(padded[r][c], trial[c]) row_lines = max(row_lines, lines) max_lines = max(max_lines, row_lines) if max_lines < best_score: best_score = max_lines best_widths = trial col_widths = best_widths # Normalize so widths sum exactly to total_width and none is below 15 mm min_w = 15.0 total = sum(col_widths) if total <= 0: col_widths = [total_width / max(1, num_cols)] * num_cols else: col_widths = [max(min_w, w * total_width / total) for w in col_widths] # re-normalize after minimum clamp total = sum(col_widths) if total > 0: col_widths = [w * total_width / total for w in col_widths] # Compute row heights row_heights = [] for r in range(len(padded)): max_lines = 1 for c in range(num_cols): lines = estimate_lines(padded[r][c], col_widths[c]) max_lines = max(max_lines, lines) height = base_height + (max_lines - 1) * step row_heights.append(height) return col_widths, row_heights def get_layer_name(): try: return scribus.getActiveLayer() except Exception: return "Background" def delete_existing_names(names): """Delete any objects that already use the planned names so Scribus does not create 'Copy of'.""" to_delete = [] to_delete.append(names.get("original")) to_delete.extend(names.get("bg", [])) for row in names.get("cells", []): to_delete.extend(row) existing = set() try: existing = set(scribus.getAllObjects()) except Exception: pass for name in to_delete: if name and name in existing: try: scribus.deleteObject(name) except Exception: pass def make_names(table_name, layer, num_rows, num_cols): layer_safe = layer.replace(" ", "_") bg = [f"back_{table_name}_{layer_safe}_{r+1:02d}" for r in range(num_rows)] cells = [] for r in range(num_rows): row_cells = [f"cell_{table_name}_{layer_safe}_{c+1:02d}_{r+1:02d}" for c in range(num_cols)] cells.append(row_cells) original = f"original_{table_name}_{layer_safe}" return {"bg": bg, "cells": cells, "original": original} def sanitize_table_name(name): """Restrict to letters, digits and hyphen. Underscore is forbidden. Replaces everything else with hyphen and collapses repeats.""" import re cleaned = re.sub(r"[^A-Za-z0-9-]", "-", name.strip()) cleaned = re.sub(r"-+", "-", cleaned).strip("-") return cleaned or "table1" def extract_table_name(obj_name): """Extract table name from back_/cell_/original_ object name. Format: prefix_tablename_layer_nn or prefix_tablename_layer_nn_nn Table name may contain hyphens but not underscores. Trailing numeric parts are indices. """ if not (obj_name.startswith("back_") or obj_name.startswith("cell_") or obj_name.startswith("original_")): return None parts = obj_name.split("_") if len(parts) < 3: return None # Drop prefix rest = parts[1:] # Drop trailing numeric indices while rest and rest[-1].isdigit(): rest.pop() if len(rest) < 2: return None # Last remaining part is the layer; everything before is the table name table_parts = rest[:-1] return "_".join(table_parts) def get_all_objects_all_layers(): """Return every object name from every layer.""" all_items = [] try: layers = scribus.getLayers() for layer in layers: try: items = scribus.getAllObjects(layer) all_items.extend(items) except Exception: pass except Exception: try: all_items = scribus.getAllObjects() except Exception: pass return all_items def collect_names_recursive(names, result): """Add names and, if an object is a group, try to add its children.""" for name in names: result.append(name) try: if scribus.getObjectType(name) == "Group": # Some Scribus versions expose group members via getGroupItems try: members = scribus.getGroupItems(name) if members: collect_names_recursive(members, result) except Exception: pass except Exception: pass def find_existing_tables(): """Scan objects on the current page of the active layer and return table names.""" tables = set() try: items = scribus.getAllObjects() for name in items: tname = extract_table_name(name) if tname: tables.add(tname) elif name.startswith("back_") or name.startswith("cell_") or name.startswith("original_"): parts = name.split("_") if len(parts) >= 2 and parts[1]: tables.add(parts[1]) except Exception: pass return sorted(tables) def start_dialog(): """Selector: one button per table (including new) on the current page.""" tables = find_existing_tables() try: page_nr = scribus.currentPage() layer_name = scribus.getActiveLayer() except Exception: page_nr = "?" layer_name = "?" root = Tk() root.title("Insert Table") root.geometry("420x260") Label(root, text="Only objects from page %s, layer %s will show" % (page_nr, layer_name)).pack(pady=4) Label(root, text="Select:").pack(pady=5) result = [None] def choose(name): result[0] = name root.destroy() Button(root, text="new", command=lambda: choose("new"), width=20).pack(pady=2) for t in tables: Button(root, text=t, command=lambda n=t: choose(n), width=20).pack(pady=2) root.mainloop() return result[0] def main(): if not scribus.haveDoc(): show_error("Insert Table", "No document open.") return def reconstruct_from_names(table_name): """Build state by scanning objects that match the naming convention.""" state = {"name": table_name, "col_widths": [], "row_lines": [], "bg_names": [], "text_names": [], "pos_x": 0, "pos_y": 0} try: items = scribus.getAllObjects() bgs = sorted([n for n in items if n.startswith(f"back_{table_name}_")]) cells = [n for n in items if n.startswith(f"cell_{table_name}_")] if not bgs: return None state["bg_names"] = bgs state["pos_x"], state["pos_y"] = scribus.getPosition(bgs[0]) rows = {} max_col = 0 for cname in cells: parts = cname.split("_") if len(parts) >= 5: col = int(parts[-2]) row = int(parts[-1]) rows.setdefault(row, {})[col] = cname max_col = max(max_col, col) state["text_names"] = [] widths = [0.0] * max_col for r in range(1, max(rows.keys() or [0]) + 1): row_list = [] for c in range(1, max_col + 1): name = rows.get(r, {}).get(c) row_list.append(name) if name and (widths[c-1] == 0 or widths[c-1] < 1): try: w, h = scribus.getSize(name) if w > 1: widths[c-1] = w except Exception: pass state["text_names"].append(row_list) state["col_widths"] = widths or [40.0] state["total_width"] = sum(state["col_widths"]) for bg in bgs: try: w, h = scribus.getSize(bg) lines = max(1, int(round((h - 1 - 6) / 5)) + 1) state["row_lines"].append(lines) except Exception: state["row_lines"].append(1) except Exception: return None return state def work_on_table(table_name): state = reconstruct_from_names(table_name) if not state: show_error("Work mode", "Could not read table '%s'." % table_name) return root = Tk() root.title("Work on %s" % table_name) root.geometry("640x300") Label(root, text="Column widths (mm)").pack(pady=5) col_frame = Frame(root) col_frame.pack() col_labels = [] for i, w in enumerate(state["col_widths"]): f = Frame(col_frame) f.pack(side="left", padx=4) def dec(idx=i): state["col_widths"][idx] = max(10, state["col_widths"][idx] - 1) other = (idx + 1) % len(state["col_widths"]) state["col_widths"][other] += 1 for j, lbl in enumerate(col_labels): lbl.config(text="%.0f" % state["col_widths"][j]) def inc(idx=i): state["col_widths"][idx] += 1 other = (idx + 1) % len(state["col_widths"]) state["col_widths"][other] = max(10, state["col_widths"][other] - 1) for j, lbl in enumerate(col_labels): lbl.config(text="%.0f" % state["col_widths"][j]) Button(f, text="<", command=dec, width=2).pack() lbl = Label(f, text="%.0f" % w, width=4) lbl.pack() col_labels.append(lbl) Button(f, text=">", command=inc, width=2).pack() Label(root, text="Row line counts").pack(pady=10) row_frame = Frame(root) row_frame.pack() row_labels = [] for i, ln in enumerate(state["row_lines"]): f = Frame(row_frame) f.pack(side="left", padx=4) def dec_r(idx=i): state["row_lines"][idx] = max(1, state["row_lines"][idx] - 1) row_labels[idx].config(text="%d" % state["row_lines"][idx]) def inc_r(idx=i): state["row_lines"][idx] += 1 row_labels[idx].config(text="%d" % state["row_lines"][idx]) Button(f, text="<", command=dec_r, width=2).pack() lbl = Label(f, text="%d" % ln, width=3) lbl.pack() row_labels.append(lbl) Button(f, text=">", command=inc_r, width=2).pack() def on_do_and_close(): apply_work_changes(state) root.destroy() def on_close_without(): root.destroy() btnf = Frame(root) btnf.pack(pady=10) Button(btnf, text="Do changes and close", command=on_do_and_close, width=20).pack(side="left", padx=5) Button(btnf, text="Close without changes", command=on_close_without, width=20).pack(side="left", padx=5) root.mainloop() def apply_work_changes(state): """Resize named objects according to the adjusted state. Keep original total width.""" scribus.setRedraw(False) try: pos_x = state.get("pos_x", 0) pos_y = state.get("pos_y", 0) widths = state["col_widths"] # Constrain sum to the original table width target = state.get("total_width", sum(widths)) current_sum = sum(widths) if current_sum > 0 and abs(current_sum - target) > 0.5: widths = [w * target / current_sum for w in widths] state["col_widths"] = widths lines = state["row_lines"] current_y = pos_y for r, bg_name in enumerate(state["bg_names"]): text_h = 6 + (lines[r] - 1) * 5 bg_h = text_h + 1 try: scribus.moveObjectAbs(pos_x, current_y, bg_name) scribus.sizeObject(sum(widths), bg_h, bg_name) except Exception: pass if r < len(state["text_names"]): current_x = pos_x for c, frame_name in enumerate(state["text_names"][r]): if frame_name and c < len(widths): try: scribus.moveObjectAbs(current_x, current_y + 1, frame_name) scribus.sizeObject(widths[c], text_h, frame_name) except Exception: pass current_x += widths[c] if c < len(widths) else 40 current_y += bg_h finally: scribus.setRedraw(True) scribus.docChanged(True) try: scribus.redrawAll() except Exception: pass def main(): if not scribus.haveDoc(): show_error("Insert Table", "No document open.") return choice = start_dialog() if not choice or choice == "new": pass else: work_on_table(choice) return if scribus.selectionCount() != 1: show_error("Insert Table", "No placeholder selected.") return placeholder = scribus.getSelectedObject() obj_type = scribus.getObjectType(placeholder) # Check for content if it's a text frame if obj_type == "TextFrame": try: text_len = scribus.getTextLength(placeholder) if text_len > 0: show_error("Insert Table", "The selected placeholder has content.") return except Exception: show_error("Insert Table", "The selected placeholder has content.") return # Switch to millimeters for calculations original_unit = scribus.getUnit() scribus.setUnit(scribus.UNIT_MILLIMETERS) # Get position (in mm) pos_x, pos_y = scribus.getPosition(placeholder) # Get dialog values dialog = get_dialog_values() if not dialog or not dialog.get("text"): scribus.setUnit(original_unit) return width, rowheight, rows = parse_table_text( dialog["text"], dialog["width"], dialog["rowheight"] ) bg1 = dialog["bg1"] bg2 = dialog["bg2"] text_style = dialog["text_style"] base_height = dialog["rowheight"] step = dialog.get("step", 5.0) if not rows: show_error("Insert Table", "No table rows found in the pasted text.") scribus.setUnit(original_unit) return num_rows = len(rows) num_cols = max(len(r) for r in rows) # Optimize column widths and compute per-row heights col_widths, row_heights = optimize_column_widths(rows, width, base_height, step) # Naming layer = get_layer_name() table_name = dialog.get("name", "table1") names = make_names(table_name, layer, num_rows, num_cols) delete_existing_names(names) # Create cells and backgrounds current_y = pos_y text_offset = 1.0 # text frames start 1 mm below background created_objects = [] # collect for grouping row_positions = [] # remember y and heights for text pass bg_names = [] text_names = [] scribus.setRedraw(False) try: # Original text frame created first → naturally at the lowest level try: orig = scribus.createText(pos_x, pos_y, width, 40) try: scribus.setItemName(names["original"], orig) orig = names["original"] except Exception: pass if dialog.get("text"): scribus.setText(dialog["text"], orig) try: if text_style and text_style in (scribus.getParagraphStyles() or []): scribus.setParagraphStyle(text_style, orig) length = scribus.getTextLength(orig) if length > 0: scribus.selectText(0, length, orig) scribus.setFontSize(3, orig) try: scribus.setLineSpacing(1, orig) except Exception: pass scribus.deselectAll() except Exception: pass scribus.setFillColor("None", orig) scribus.setLineColor("None", orig) scribus.setLineWidth(0, orig) except Exception: pass # Pass 1: all background rectangles first (so they stay behind) temp_y = pos_y for r_idx, row in enumerate(rows): text_h = row_heights[r_idx] if r_idx < len(row_heights) else base_height bg_h = text_h + text_offset bg_color = bg1 if r_idx % 2 == 0 else bg2 try: bg_rect = scribus.createRect(pos_x, temp_y, width, bg_h) try: scribus.setItemName(names["bg"][r_idx], bg_rect) bg_rect = names["bg"][r_idx] except Exception: pass bg_names.append(bg_rect) created_objects.append(bg_rect) scribus.setFillColor(bg_color, bg_rect) scribus.setLineWidth(0, bg_rect) scribus.setLineColor("None", bg_rect) except Exception: pass row_positions.append((temp_y, text_h, bg_h)) temp_y += bg_h # Pass 2: all text frames on top for r_idx, row in enumerate(rows): temp_y, text_h, bg_h = row_positions[r_idx] current_x = pos_x while len(row) < num_cols: row.append("") text_y = temp_y + text_offset row_text = [] for c_idx, cell_text in enumerate(row): col_w = col_widths[c_idx] if c_idx < len(col_widths) else 40.0 frame_name = scribus.createText(current_x, text_y, col_w, text_h) try: scribus.setItemName(names["cells"][r_idx][c_idx], frame_name) frame_name = names["cells"][r_idx][c_idx] except Exception: pass row_text.append(frame_name) created_objects.append(frame_name) if cell_text: try: scribus.setText(cell_text, frame_name) except Exception: pass # Apply paragraph style if it exists try: if text_style and text_style in (scribus.getParagraphStyles() or []): scribus.setParagraphStyle(text_style, frame_name) except Exception: pass # Vertical alignment top try: scribus.setTextVerticalAlignment(scribus.ALIGNV_TOP, frame_name) except Exception: pass # Right-align if numeric (or number with unit), otherwise left if is_numeric_cell(cell_text): try: scribus.setTextAlignment(2, frame_name) # 2 = right except Exception: pass else: try: scribus.setTextAlignment(0, frame_name) # 0 = left except Exception: pass # Text frames have no fill and no stroke try: scribus.setFillColor("None", frame_name) scribus.setLineWidth(1, frame_name) scribus.setLineColor("None", frame_name) scribus.setTextDistances(0, 0, 0, 0, frame_name) except Exception: pass current_x += col_w # No grouping — keep individual objects like manual frames # if created_objects: # try: # scribus.groupObjects(created_objects) # except Exception: # pass # Apply names only at the very end try: for i, bg in enumerate(bg_names): try: scribus.setItemName(names["bg"][i], bg) except Exception: pass for r, row_frames in enumerate(text_names if 'text_names' in dir() else []): for c, fr in enumerate(row_frames): try: scribus.setItemName(names["cells"][r][c], fr) except Exception: pass try: scribus.setItemName(names["original"], orig) except Exception: pass except Exception: pass # Final cleanup pass (same calls that worked in the console) try: for bg_name in names.get("bg", []): try: scribus.setLineWidth(0, bg_name) scribus.setLineColor("None", bg_name) scribus.sendToBack(bg_name) except Exception: pass try: scribus.sendToBack(names["original"]) except Exception: pass for row_cells in names.get("cells", []): for cell_name in row_cells: try: scribus.bringToFront(cell_name) except Exception: pass except Exception: pass # Delete the placeholder scribus.deleteObject(placeholder) finally: scribus.setRedraw(True) scribus.setUnit(original_unit) scribus.messageBox("Insert Table", "Table inserted with %d rows and %d columns." % (num_rows, num_cols), scribus.ICON_NONE, scribus.BUTTON_OK) if __name__ == "__main__": main()