commit 3ea09dccf80b19f7ec22837aa7d7d4a8f525086f Author: Salijoghli <107577102+Salijoghli@users.noreply.github.com> Date: Wed Mar 25 16:26:37 2026 +0400 initial commit with script, doc and one sample dwg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..193445b --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Python +.venv/ +__pycache__/ +*.pyc + +# Build / generated +build/ +dist/ +*.spec +export_progress.json + +# AutoCAD generated/temp +*.bak +*.dwl +*.dwl2 + +# CAD source files (keep out of git unless intentionally tracked) +*.dwg \ No newline at end of file diff --git a/2501-AMZ-TPA8-MCM01-600.dwg b/2501-AMZ-TPA8-MCM01-600.dwg new file mode 100644 index 0000000..9d2e03f Binary files /dev/null and b/2501-AMZ-TPA8-MCM01-600.dwg differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..bcc71a7 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# CAD Layout Batch Export Tool + +This project automates DWG layout preparation in AutoCAD. + +It opens selected drawings, attaches a master drawing as XREF, imports a template layout if missing, applies viewport zoom positioning, updates title block attributes, removes default layouts, saves, and closes each file. + +The app has two modes: +- IO +- NETWORK + +The script is in export_layouts_ui.py. + +## What This Tool Does + +For each drawing in the selected batch, the tool: +1. Opens the DWG in AutoCAD (COM automation). +2. Uses the selected Master DWG as an XREF source (name: XREF1), except when processing the master itself. +3. Ensures the target template layout exists by importing from the selected Template DWG if needed. +4. Activates that layout and runs viewport zoom using mode-specific coordinates. +5. Updates title block attributes: + - IO mode: uses grouped names from the names file. + - NETWORK mode: uses one name per sheet and transforms naming for network labels. +6. Deletes Layout1 and Layout2. +7. Saves and closes the DWG. +8. Writes progress to export_progress.json for resume support. +9. Deletes matching .bak right after each successful DWG. +10. On full completion of all files, performs a final recursive cleanup of remaining .bak files. + +## Requirements + +System: +- Windows +- AutoCAD installed +- AutoCAD must be available through COM + +Python: +- Python 3.x +- Package: pywin32 + +Standard library modules used: +- os +- re +- json +- threading +- time +- tkinter + +## Installation + +1. Open PowerShell in the project folder. +2. Install dependency: + +python -m pip install pywin32 + +If you use a virtual environment, activate it first and run the same command. + +## How To Run + +Start the app: + +python export_layouts_ui.py + +In the UI, select: +1. Master DWG (XREF source) +2. Template DWG (layout source) +3. Drawing files to process +4. Block Names File (TXT) + +Then click Run Batch. + +You can also use the same DWG as both Master and Template if that drawing contains a valid paper-space layout. + +## Input File Notes + +Block Names TXT: +- One name per line +- Empty lines are ignored + +Example names file is list.txt. + +Progress file: +- export_progress.json is auto-created/updated. +- It stores completed DWG file names. +- Start Over in the UI resets this progress file. + +## Zoom Logic Overview + +The script computes viewport center per file index: +- center_x = base_x + (index * block_width) +- center_y = base_y + +It then sends AutoCAD command: +- ZOOM C center_x,center_y height + +IO and NETWORK modes use different base values and height. + +## Resume and Cleanup Behavior + +- If a batch is interrupted, rerun and it resumes using export_progress.json. +- After each successful file, the related .bak is removed. +- After full completion (all files done and not canceled), remaining .bak files are deleted recursively from the current working folder. + +## Troubleshooting + +ModuleNotFoundError: pythoncom +- Cause: pywin32 not installed in the same Python interpreter used to run the script. +- Fix: + 1. Check python path: + where python + 2. Install in that interpreter: + python -m pip install pywin32 + +AutoCAD COM errors (for example call rejected by callee) +- Keep AutoCAD open and responsive during batch processing. +- Retry the batch (the script already includes retry logic for open calls). + +No layout found in template file +- Ensure the template DWG has at least one paper-space layout (not only Model). + +## Main Project Files + +- export_layouts_ui.py: main application +- list.txt: example block names input +- export_progress.json: generated progress log during runs + +## Safety Notes + +- This tool edits and saves DWG files in place. +- Test with a small file set first. +- Keep a separate backup of critical project drawings before first production run. diff --git a/export_layouts_ui.py b/export_layouts_ui.py new file mode 100644 index 0000000..6d8e104 --- /dev/null +++ b/export_layouts_ui.py @@ -0,0 +1,761 @@ +import os +import re +import json +import threading +import tkinter as tk +from tkinter import filedialog, messagebox, ttk +import pythoncom +import win32com.client +import time +from win32com.client import pythoncom as pycom + +PROGRESS_LOG = 'export_progress.json' + +def safe_open_doc(docs, dwg_path, retries=5, delay=3): + for attempt in range(retries): + try: + return docs.Open(dwg_path) + except Exception as e: + if 'Call was rejected by callee' in str(e) and attempt < retries - 1: + time.sleep(delay) + else: + raise + +# --- Backend Logic --- +def set_block_attribute(doc, block_name, attr_tag, value): + # Search PaperSpace for block references + for entity in doc.PaperSpace: + if entity.ObjectName == 'AcDbBlockReference' and entity.Name == block_name: + try: + for attrib in entity.GetAttributes(): + if attrib.TagString.upper() == attr_tag.upper(): + attrib.TextString = value + return True + except Exception: + continue + return False + +def set_wyncorp_block(doc, chunk, log_operation=None): + # First try to find UPS.AE 11X17 (V0) block and set DWGDESC1 + for entity in doc.PaperSpace: + if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "UPS.AE 11X17 (V0)": + try: + value = "IO LAYOUT " + ", ".join(chunk) + for attrib in entity.GetAttributes(): + if attrib.TagString.upper() == "DWGDESC1": + attrib.TextString = value + if log_operation: + log_operation(f"Inputting block DWGDESC1: {value}") + return True + except Exception: + continue + + # If UPS.AE block not found, try WYNCORP-DSIZE_AS block and set TITLE2 and TITLE3 + for entity in doc.PaperSpace: + if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "WYNCORP-DSIZE_AS": + try: + title2_val = "/".join(chunk[:2]) if chunk[:2] else "" + title3_val = "/".join(chunk[2:4]) if chunk[2:4] else "" + if title3_val: + title3_val += " IO DRAWINGS" + else: + title3_val = "IO DRAWINGS" + for attrib in entity.GetAttributes(): + if attrib.TagString.upper() == "TITLE2": + attrib.TextString = title2_val + if log_operation: + log_operation(f"Inputting block TITLE2: {title2_val}") + if attrib.TagString.upper() == "TITLE3": + attrib.TextString = title3_val + if log_operation: + log_operation(f"Inputting block TITLE3: {title3_val}") + return True + except Exception: + continue + return False + +def transform_dpm_name_for_network(raw_name): + try: + import re as _re + m = _re.search(r"^(.*)_(DPM\d+)$", raw_name, _re.IGNORECASE) + if m: + rest = m.group(1) + dpm = m.group(2).upper() + return f"{dpm}_{rest}_ENET" + return f"{raw_name}_ENET" + except Exception: + return f"{raw_name}_ENET" + +def set_network_block(doc, raw_name, log_operation=None): + value_title3 = transform_dpm_name_for_network(raw_name) + # Prefer WYNCORP-DSIZE_AS: TITLE2 = NETWORK DETAILS, TITLE3 = transformed + for entity in doc.PaperSpace: + if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "WYNCORP-DSIZE_AS": + try: + for attrib in entity.GetAttributes(): + if attrib.TagString.upper() == "TITLE2": + attrib.TextString = "NETWORK DETAILS" + if log_operation: + log_operation("Inputting block TITLE2: NETWORK DETAILS") + if attrib.TagString.upper() == "TITLE3": + attrib.TextString = value_title3 + if log_operation: + log_operation(f"Inputting block TITLE3: {value_title3}") + return True + except Exception: + continue + # Fallback to UPS.AE 11X17 (V0) -> DWGDESC1 (use raw DPM name, no transform) + for entity in doc.PaperSpace: + if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "UPS.AE 11X17 (V0)": + try: + for attrib in entity.GetAttributes(): + if attrib.TagString.upper() == "DWGDESC1": + fallback_val = f"NETWORK DETAILS {raw_name}" + attrib.TextString = fallback_val + if log_operation: + log_operation(f"Inputting block DWGDESC1: {fallback_val}") + return True + except Exception: + continue + return False + +def process_single_file(dwg_path, base_dwg_path, layout_template, template_layout_name, index, total, acad, docs, block_names, mode="IO", log_operation=None, error_callback=None): + new_doc = None + # Auto-close if already open + for doc in list(docs): + try: + if os.path.abspath(doc.FullName).lower() == os.path.abspath(dwg_path).lower(): + doc.Close(False) + # time.sleep(2) + except Exception: + continue + try: + if log_operation: + log_operation(f"Opening file: {os.path.basename(dwg_path)}") + new_doc = safe_open_doc(docs, dwg_path) + # time.sleep(2) + # Check if Xref already exists (skip attaching when processing the master file itself) + is_master_file = os.path.abspath(dwg_path).lower() == os.path.abspath(base_dwg_path).lower() + xref_exists = False + if not is_master_file: + for item in new_doc.ModelSpace: + if item.ObjectName == "AcDbBlockReference": + try: + if item.Name == "XREF1": + xref_exists = True + break + except: + continue + if not xref_exists: + if log_operation: + log_operation("Attaching Xref...") + # Use relative path - just the filename if in same folder + master_filename = os.path.basename(base_dwg_path) + insertion_point = win32com.client.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8, [0, 0, 0]) + new_doc.ModelSpace.AttachExternalReference( + master_filename, "XREF1", insertion_point, 1, 1, 1, 0, False) + # Check if layout exists + layout_exists = False + for layout in new_doc.Layouts: + if layout.Name.upper() == template_layout_name.upper(): + layout_exists = True + break + if not layout_exists: + if log_operation: + log_operation("Adding template layout...") + filedia_original = new_doc.GetVariable("FILEDIA") + new_doc.SetVariable("FILEDIA", 0) + command_sequence = ( + "_-LAYOUT\n" + "T\n" + f'"{layout_template}"\n' + f"{template_layout_name}\n" + "\n" + ) + acad.ActiveDocument.SendCommand(command_sequence) + # time.sleep(2) + new_doc.SetVariable("FILEDIA", filedia_original) + # Activate layout + new_doc.ActiveLayout = new_doc.Layouts.Item(template_layout_name) + # time.sleep(0.5) + # Pan/zoom viewport + if log_operation: + log_operation("Zooming to layout area...") + + # Set viewport values based on mode + if mode == "IO": + base_x = 19.13 + base_y = 0.7881 + block_width = 38.5 + height = 22.2955 + else: # NETWORK mode + # NETWORK mode needs different values - working backwards from your feedback + # For 19 files with total width 826.5: block_width = 826.5/19 = 43.5 + # For 10th file (index 9) to be 369: 369 = base_x + (9 * 43.5) + # So base_x = 369 - 391.5 = -22.5 + base_x = -22 + base_y = 13.25 + block_width = 43.5 + height = 26.1748 + + acad.ActiveDocument.SendCommand("MSPACE\n") + # time.sleep(0.5) + # For first file (index 0), use base position. For subsequent files, add block_width * index + center_x = base_x + (index * block_width) + center_y = base_y + zoom_command = f'ZOOM C {center_x},{center_y} {height}\n' + acad.ActiveDocument.SendCommand(zoom_command) + # time.sleep(1) + acad.ActiveDocument.SendCommand("PSPACE\n") + # time.sleep(0.5) + # --- Set block attribute for this layout --- + if mode == "IO": + # IO mode: use chunks of 4 names + start_idx = index * 4 + chunk = block_names[start_idx:start_idx+4] + block_set = False + if chunk: + value = "IO LAYOUT " + "/".join(chunk) + if log_operation: + log_operation(f"Inputting block names: {value}") + block_set = set_block_attribute(new_doc, "UPS.AE 11X17 (V0)", "DWGDESC1", value) + # If not found, try WYNCORP-DSIZE_AS logic + if not block_set and chunk: + if log_operation: + log_operation(f"Trying WYNCORP-DSIZE_AS block for chunk: {chunk}") + set_wyncorp_block(new_doc, chunk, log_operation=log_operation) + else: # NETWORK mode + # NETWORK mode: use single name transformed: DPMx__ENET, + # and set TITLE2/TITLE3 on WYNCORP when present; fallback to UPS.AE + if block_names and index < len(block_names): + raw_name = block_names[index] + if log_operation: + log_operation(f"Preparing NETWORK labels for: {raw_name}") + set_network_block(new_doc, raw_name, log_operation=log_operation) + # Delete default layouts + if log_operation: + log_operation("Deleting default layouts (Layout1, Layout2)...") + for layout_name in ["Layout1", "Layout2"]: + delete_command = f"_-LAYOUT\nD\n{layout_name}\n" + acad.ActiveDocument.SendCommand(delete_command) + time.sleep(1) + # Save and restore FILEDIA, then close + if log_operation: + log_operation("Saving file and restoring FILEDIA...") + new_doc.Save() + try: + new_doc.SetVariable("FILEDIA", 1) + except Exception: + pass + except Exception as e: + if error_callback: + error_callback(str(e)) + raise + finally: + try: + if new_doc is not None: + new_doc.Close(False) + # time.sleep(2) + except Exception: + pass + return True + +# --- Progress Log --- +def load_progress(): + if os.path.exists(PROGRESS_LOG): + with open(PROGRESS_LOG, 'r') as f: + return set(json.load(f)) + return set() + +def save_progress(done_files): + with open(PROGRESS_LOG, 'w') as f: + json.dump(list(done_files), f) + +def is_autocad_running(): + try: + win32com.client.GetActiveObject("AutoCAD.Application") + return True + except Exception: + return False + +def read_block_names(names_file): + if not names_file or not os.path.exists(names_file): + return [] + with open(names_file, 'r', encoding='utf-8') as f: + return [line.strip() for line in f if line.strip()] + +def delete_bak_for_dwg(dwg_path, log_operation=None, error_callback=None): + bak_path = os.path.splitext(dwg_path)[0] + '.bak' + try: + if os.path.exists(bak_path): + os.remove(bak_path) + if log_operation: + log_operation(f"Deleted backup file: {os.path.basename(bak_path)}") + except Exception as e: + if error_callback: + error_callback(f"Failed to delete backup file {os.path.basename(bak_path)}: {e}") + +def delete_all_bak_files(root_dir, log_operation=None, error_callback=None): + deleted_count = 0 + try: + for current_root, _, files in os.walk(root_dir): + for file_name in files: + if file_name.lower().endswith('.bak'): + bak_path = os.path.join(current_root, file_name) + try: + os.remove(bak_path) + deleted_count += 1 + except Exception as e: + if error_callback: + error_callback(f"Failed to delete backup file {file_name}: {e}") + if log_operation: + log_operation(f"Deleted {deleted_count} remaining .bak file(s) after full completion.") + except Exception as e: + if error_callback: + error_callback(f"Failed during final backup cleanup: {e}") + +# --- UI --- +def main(): + root = tk.Tk() + root.title("Export Layouts Batch Processor") + root.geometry("750x750") + root.configure(bg="#181818") + root.resizable(False, False) + + # Modern font + FONT = ("Segoe UI", 11) + FONT_BOLD = ("Segoe UI", 12, "bold") + GOLD = "#FFD700" + BLACK = "#181818" + WHITE = "#FFFFFF" + + style = ttk.Style() + style.theme_use('clam') + style.configure("TButton", font=FONT, background=GOLD, foreground=BLACK, borderwidth=0, focusthickness=3, focuscolor=GOLD, padding=8) + style.map("TButton", background=[('active', GOLD), ('pressed', GOLD)]) + style.configure("TEntry", fieldbackground=BLACK, foreground=GOLD, borderwidth=1) + style.configure("TLabel", background=BLACK, foreground=GOLD, font=FONT) + style.configure("TProgressbar", troughcolor=BLACK, bordercolor=BLACK, background=GOLD, lightcolor=GOLD, darkcolor=GOLD, thickness=20) + + master_path = tk.StringVar() + template_path = tk.StringVar() + drawings_files = [] # List of selected DWG files + drawings_files_var = tk.StringVar(value="No drawings selected.") + progress_var = tk.IntVar(value=0) + status_var = tk.StringVar(value="Idle.") + current_file_var = tk.StringVar(value="No file processing yet.") + error_log_var = tk.StringVar(value="") + names_file = tk.StringVar() + block_names = [] + + # --- Logging variables --- + operations_log_var = tk.StringVar(value="") + error_log_var = tk.StringVar(value="") + + def log_operation(msg): + prev = operations_log_var.get() + operations_log_var.set((prev + "\n" if prev else "") + msg) + + def append_error(msg): + prev = error_log_var.get() + error_log_var.set((prev + "\n" if prev else "") + msg) + + def select_master(): + path = filedialog.askopenfilename(title="Select Master DWG", filetypes=[("DWG Files", "*.dwg")]) + if path: + master_path.set(path) + + def select_template(): + path = filedialog.askopenfilename(title="Select Template DWG", filetypes=[("DWG Files", "*.dwg")]) + if path: + template_path.set(path) + + def select_drawings_files(): + nonlocal drawings_files + files = filedialog.askopenfilenames(title="Select Drawing Files", filetypes=[("DWG Files", "*.dwg")]) + if files: + drawings_files = list(files) + drawings_files_var.set(f"{len(drawings_files)} files selected.") + else: + drawings_files = [] + drawings_files_var.set("No drawings selected.") + + def select_names_file(): + path = filedialog.askopenfilename(title="Select Block Names File", filetypes=[("Text Files", "*.txt")]) + if path: + names_file.set(path) + + cancel_requested = tk.BooleanVar(value=False) + + def set_buttons_state(state): + # IO tab buttons + try: + run_btn.config(state=state) + start_over_btn.config(state=state) + cancel_btn.config(state="normal" if state == "disabled" else "disabled") + except Exception: + pass + # NETWORK tab buttons + try: + n_run_btn.config(state=state) + n_start_over_btn.config(state=state) + n_cancel_btn.config(state="normal" if state == "disabled" else "disabled") + except Exception: + pass + + def start_batch(): + nonlocal block_names + if not master_path.get() or not template_path.get() or not drawings_files or not names_file.get(): + messagebox.showerror("Error", "Please select master, template DWG files, drawing files, and block names file.") + return + block_names = read_block_names(names_file.get()) + if not block_names: + messagebox.showerror("Error", "Block names file is empty or not found.") + return + + # Determine which tab is active + current_tab = main_notebook.select() + if "IO" in main_notebook.tab(current_tab, "text"): + mode = "IO" + else: + mode = "NETWORK" + + progress_var.set(0) + status_var.set("Processing...") + current_file_var.set("") + operations_log_var.set("") + error_log_var.set("") + cancel_requested.set(False) + set_buttons_state("disabled") + threading.Thread(target=lambda: run_batch(mode), daemon=True).start() + + def start_over(): + if os.path.exists(PROGRESS_LOG): + os.remove(PROGRESS_LOG) + progress_var.set(0) + status_var.set("Progress reset. Ready.") + current_file_var.set("") + error_log_var.set("") + operations_log_var.set("") + + def cancel_batch(): + cancel_requested.set(True) + status_var.set("Cancelling... (will stop after current file)") + + def run_batch(mode="IO"): + try: + # Build processing order: master first, then sorted drawings (excluding master if present) + master = master_path.get() + drawings = [f for f in list(drawings_files) if os.path.abspath(f).lower() != os.path.abspath(master).lower()] + def sort_key(file_name): + match = re.search(r'LAYOUT\s*(\d+)', os.path.basename(file_name), re.IGNORECASE) + if match: + return int(match.group(1)) + return float('inf') + drawings.sort(key=sort_key) + files = [master] + drawings + total = len(files) + done_files = load_progress() + pythoncom.CoInitialize() + acad = win32com.client.Dispatch("AutoCAD.Application") + docs = acad.Documents + + template_doc = docs.Open(template_path.get()) + time.sleep(2) # Give AutoCAD time to fully load the template + template_layout_name = None + try: + for layout in template_doc.Layouts: + if layout.Name.upper() != "MODEL": + template_layout_name = layout.Name + break + except Exception as e: + append_error(f"Error reading template layouts: {e}") + template_layout_name = "TITLE PAGE" # Fallback to default name + finally: + try: + template_doc.Close(False) + except Exception: + pass + if not template_layout_name: + status_var.set("No layout found in template file!") + pythoncom.CoUninitialize() + set_buttons_state("normal") + return + idx = 0 + while idx < total: + if cancel_requested.get(): + status_var.set("Batch cancelled by user.") + break + fname = files[idx] + base_fname = os.path.basename(fname) + if base_fname in done_files: + progress_var.set(int(100 * (idx + 1) / total)) + idx += 1 + continue + dwg_path = fname + current_file_var.set(f"Processing: {base_fname} ({idx+1}/{total})") + try: + ok = process_single_file( + dwg_path, master_path.get(), template_path.get(), template_layout_name, idx, total, acad, docs, block_names, mode, log_operation=log_operation, error_callback=append_error + ) + if ok: + done_files.add(base_fname) + save_progress(done_files) + delete_bak_for_dwg(dwg_path, log_operation=log_operation, error_callback=append_error) + progress_var.set(int(100 * (idx + 1) / total)) + idx += 1 + else: + append_error(f"Failed: {base_fname}") + idx += 1 # Continue to next file + except Exception as e: + append_error(f"Error processing {base_fname}: {e}") + idx += 1 # Continue to next file + if not cancel_requested.get() and len(done_files) >= total: + delete_all_bak_files(os.getcwd(), log_operation=log_operation, error_callback=append_error) + + if not cancel_requested.get(): + status_var.set("All files processed (or resumed to last file).") + current_file_var.set("") + except Exception as e: + append_error(f"Batch processing error: {e}") + status_var.set("Batch processing failed.") + finally: + set_buttons_state("normal") + try: + pythoncom.CoUninitialize() + except Exception: + pass + + # --- Modern UI Layout --- + pad = {'padx': 16, 'pady': 8} + frame = tk.Frame(root, bg=BLACK) + frame.pack(fill="both", expand=True) + + tk.Label(frame, text="Export Layouts Batch Processor", font=FONT_BOLD, fg=GOLD, bg=BLACK).pack(pady=(18, 10)) + + # Main tabbed interface + main_notebook = ttk.Notebook(frame) + main_notebook.pack(fill="both", expand=True, padx=16, pady=(0, 10)) + + # IO Tab + io_frame = tk.Frame(main_notebook, bg=BLACK) + network_frame = tk.Frame(main_notebook, bg=BLACK) + main_notebook.add(io_frame, text="IO") + main_notebook.add(network_frame, text="NETWORK") + + # IO Tab Content + # Master DWG + row1 = tk.Frame(io_frame, bg=BLACK) + row1.pack(fill="x", **pad) + row1.grid_columnconfigure(1, weight=1) + tk.Label(row1, text="Master DWG (Xref):", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w") + master_entry = tk.Entry(row1, textvariable=master_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD) + master_entry.grid(row=0, column=1, sticky="ew", padx=(0,8)) + ttk.Button(row1, text="Browse...", command=select_master).grid(row=0, column=2, sticky="e") + + # Template DWG + row2 = tk.Frame(io_frame, bg=BLACK) + row2.pack(fill="x", **pad) + row2.grid_columnconfigure(1, weight=1) + tk.Label(row2, text="Template DWG:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w") + template_entry = tk.Entry(row2, textvariable=template_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD) + template_entry.grid(row=0, column=1, sticky="ew", padx=(0,8)) + ttk.Button(row2, text="Browse...", command=select_template).grid(row=0, column=2, sticky="e") + + # Drawings files (multi-select) + row3 = tk.Frame(io_frame, bg=BLACK) + row3.pack(fill="x", **pad) + row3.grid_columnconfigure(1, weight=1) + tk.Label(row3, text="Drawings:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w") + tk.Label(row3, textvariable=drawings_files_var, font=FONT, fg=GOLD, bg=BLACK, anchor="w").grid(row=0, column=1, sticky="ew", padx=(0,8)) + ttk.Button(row3, text="Browse...", command=select_drawings_files).grid(row=0, column=2, sticky="e") + + # Block names file + row4 = tk.Frame(io_frame, bg=BLACK) + row4.pack(fill="x", **pad) + row4.grid_columnconfigure(1, weight=1) + tk.Label(row4, text="Block Names File:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w") + tk.Entry(row4, textvariable=names_file, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD).grid(row=0, column=1, sticky="ew", padx=(0,8)) + ttk.Button(row4, text="Browse...", command=select_names_file).grid(row=0, column=2, sticky="e") + + # Progress bar with white border + tk.Label(io_frame, text="Progress:", font=FONT, fg=GOLD, bg=BLACK, anchor="w").pack(fill="x", **pad) + pb_border = tk.Frame(io_frame, bg=WHITE, padx=2, pady=2) + pb_border.pack(pady=(0,10), padx=32, fill="x") + ttk.Progressbar(pb_border, variable=progress_var, maximum=100, length=500, style="TProgressbar").pack(fill="x") + + # Status and current file + tk.Label(io_frame, textvariable=status_var, font=FONT, fg=WHITE, bg=BLACK).pack(fill="x", **pad) + tk.Label(io_frame, textvariable=current_file_var, font=FONT, fg=GOLD, bg=BLACK).pack(fill="x", **pad) + + # Tabbed log area + log_notebook = ttk.Notebook(io_frame) + log_notebook.pack(padx=32, pady=(10,10), fill="both", expand=False) + op_frame = tk.Frame(log_notebook, bg=BLACK) + err_frame = tk.Frame(log_notebook, bg=BLACK) + log_notebook.add(op_frame, text="Operations Log") + log_notebook.add(err_frame, text="Error Log") + # Operations log with auto-hiding scrollbar + op_scroll = tk.Scrollbar(op_frame) + op_text = tk.Text(op_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=op_scroll.set, wrap="word") + op_scroll.config(command=op_text.yview) + op_text.pack(side="left", fill="both", expand=True) + # Error log with auto-hiding scrollbar + err_scroll = tk.Scrollbar(err_frame) + err_text = tk.Text(err_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=err_scroll.set, wrap="word") + err_scroll.config(command=err_text.yview) + err_text.pack(side="left", fill="both", expand=True) + + def show_scrollbar_if_needed(text_widget, scrollbar): + text_widget.update_idletasks() + if text_widget.yview()[0] > 0 or text_widget.yview()[1] < 1: + scrollbar.pack(side="right", fill="y") + else: + scrollbar.pack_forget() + + def update_op_log(*args): + op_text.config(state="normal") + op_text.delete(1.0, tk.END) + op_text.insert(tk.END, operations_log_var.get()) + op_text.see(tk.END) + op_text.config(state="disabled") + show_scrollbar_if_needed(op_text, op_scroll) + def update_err_log(*args): + err_text.config(state="normal") + err_text.delete(1.0, tk.END) + err_text.insert(tk.END, error_log_var.get()) + err_text.see(tk.END) + err_text.config(state="disabled") + show_scrollbar_if_needed(err_text, err_scroll) + operations_log_var.trace_add('write', update_op_log) + error_log_var.trace_add('write', update_err_log) + op_text.bind('', lambda e: show_scrollbar_if_needed(op_text, op_scroll)) + err_text.bind('', lambda e: show_scrollbar_if_needed(err_text, err_scroll)) + + # Style the selected tab + style.layout("TNotebook.Tab", [ + ('Notebook.tab', {'sticky': 'nswe', 'children': [ + ('Notebook.padding', {'side': 'top', 'sticky': 'nswe', 'children': [ + ('Notebook.focus', {'side': 'top', 'sticky': 'nswe', 'children': [ + ('Notebook.label', {'side': 'top', 'sticky': ''}) + ]}) + ]}) + ]}) + ]) + style.map("TNotebook.Tab", + background=[('selected', GOLD), ('!selected', BLACK)], + foreground=[('selected', BLACK), ('!selected', GOLD)]) + + # Buttons + btn_frame = tk.Frame(io_frame, bg=BLACK) + btn_frame.pack(pady=(10, 18)) + run_btn = ttk.Button(btn_frame, text="Run Batch", command=start_batch) + run_btn.pack(side="left", padx=12) + start_over_btn = ttk.Button(btn_frame, text="Start Over", command=start_over) + start_over_btn.pack(side="left", padx=12) + cancel_btn = ttk.Button(btn_frame, text="Cancel", command=cancel_batch, state="disabled") + cancel_btn.pack(side="left", padx=12) + + # NETWORK Tab Content + # Master DWG + n_row1 = tk.Frame(network_frame, bg=BLACK) + n_row1.pack(fill="x", **pad) + n_row1.grid_columnconfigure(1, weight=1) + tk.Label(n_row1, text="Master DWG (Xref):", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w") + n_master_entry = tk.Entry(n_row1, textvariable=master_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD) + n_master_entry.grid(row=0, column=1, sticky="ew", padx=(0,8)) + ttk.Button(n_row1, text="Browse...", command=select_master).grid(row=0, column=2, sticky="e") + + # Template DWG + n_row2 = tk.Frame(network_frame, bg=BLACK) + n_row2.pack(fill="x", **pad) + n_row2.grid_columnconfigure(1, weight=1) + tk.Label(n_row2, text="Template DWG:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w") + n_template_entry = tk.Entry(n_row2, textvariable=template_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD) + n_template_entry.grid(row=0, column=1, sticky="ew", padx=(0,8)) + ttk.Button(n_row2, text="Browse...", command=select_template).grid(row=0, column=2, sticky="e") + + # Drawings files (multi-select) + n_row3 = tk.Frame(network_frame, bg=BLACK) + n_row3.pack(fill="x", **pad) + n_row3.grid_columnconfigure(1, weight=1) + tk.Label(n_row3, text="Drawings:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w") + tk.Label(n_row3, textvariable=drawings_files_var, font=FONT, fg=GOLD, bg=BLACK, anchor="w").grid(row=0, column=1, sticky="ew", padx=(0,8)) + ttk.Button(n_row3, text="Browse...", command=select_drawings_files).grid(row=0, column=2, sticky="e") + + # Block names file + n_row4 = tk.Frame(network_frame, bg=BLACK) + n_row4.pack(fill="x", **pad) + n_row4.grid_columnconfigure(1, weight=1) + tk.Label(n_row4, text="Block Names File:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w") + tk.Entry(n_row4, textvariable=names_file, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD).grid(row=0, column=1, sticky="ew", padx=(0,8)) + ttk.Button(n_row4, text="Browse...", command=select_names_file).grid(row=0, column=2, sticky="e") + + # Progress bar with white border + tk.Label(network_frame, text="Progress:", font=FONT, fg=GOLD, bg=BLACK, anchor="w").pack(fill="x", **pad) + n_pb_border = tk.Frame(network_frame, bg=WHITE, padx=2, pady=2) + n_pb_border.pack(pady=(0,10), padx=32, fill="x") + ttk.Progressbar(n_pb_border, variable=progress_var, maximum=100, length=500, style="TProgressbar").pack(fill="x") + + # Status and current file + tk.Label(network_frame, textvariable=status_var, font=FONT, fg=WHITE, bg=BLACK).pack(fill="x", **pad) + tk.Label(network_frame, textvariable=current_file_var, font=FONT, fg=GOLD, bg=BLACK).pack(fill="x", **pad) + + # Tabbed log area + n_log_notebook = ttk.Notebook(network_frame) + n_log_notebook.pack(padx=32, pady=(10,10), fill="both", expand=False) + n_op_frame = tk.Frame(n_log_notebook, bg=BLACK) + n_err_frame = tk.Frame(n_log_notebook, bg=BLACK) + n_log_notebook.add(n_op_frame, text="Operations Log") + n_log_notebook.add(n_err_frame, text="Error Log") + # Operations log with auto-hiding scrollbar + n_op_scroll = tk.Scrollbar(n_op_frame) + n_op_text = tk.Text(n_op_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=n_op_scroll.set, wrap="word") + n_op_scroll.config(command=n_op_text.yview) + n_op_text.pack(side="left", fill="both", expand=True) + # Error log with auto-hiding scrollbar + n_err_scroll = tk.Scrollbar(n_err_frame) + n_err_text = tk.Text(n_err_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=n_err_scroll.set, wrap="word") + n_err_scroll.config(command=n_err_text.yview) + n_err_text.pack(side="left", fill="both", expand=True) + + def n_show_scrollbar_if_needed(text_widget, scrollbar): + text_widget.update_idletasks() + if text_widget.yview()[0] > 0 or text_widget.yview()[1] < 1: + scrollbar.pack(side="right", fill="y") + else: + scrollbar.pack_forget() + + def n_update_op_log(*args): + n_op_text.config(state="normal") + n_op_text.delete(1.0, tk.END) + n_op_text.insert(tk.END, operations_log_var.get()) + n_op_text.see(tk.END) + n_op_text.config(state="disabled") + n_show_scrollbar_if_needed(n_op_text, n_op_scroll) + def n_update_err_log(*args): + n_err_text.config(state="normal") + n_err_text.delete(1.0, tk.END) + n_err_text.insert(tk.END, error_log_var.get()) + n_err_text.see(tk.END) + n_err_text.config(state="disabled") + n_show_scrollbar_if_needed(n_err_text, n_err_scroll) + operations_log_var.trace_add('write', n_update_op_log) + error_log_var.trace_add('write', n_update_err_log) + n_op_text.bind('', lambda e: n_show_scrollbar_if_needed(n_op_text, n_op_scroll)) + n_err_text.bind('', lambda e: n_show_scrollbar_if_needed(n_err_text, n_err_scroll)) + + # Buttons + n_btn_frame = tk.Frame(network_frame, bg=BLACK) + n_btn_frame.pack(pady=(10, 18)) + n_run_btn = ttk.Button(n_btn_frame, text="Run Batch", command=start_batch) + n_run_btn.pack(side="left", padx=12) + n_start_over_btn = ttk.Button(n_btn_frame, text="Start Over", command=start_over) + n_start_over_btn.pack(side="left", padx=12) + n_cancel_btn = ttk.Button(n_btn_frame, text="Cancel", command=cancel_batch, state="disabled") + n_cancel_btn.pack(side="left", padx=12) + + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/list.txt b/list.txt new file mode 100644 index 0000000..714bc33 --- /dev/null +++ b/list.txt @@ -0,0 +1,86 @@ +PDP01_FIO1 +PDP01_FIOH1 +PDP06_FIO1 +PDP06_FIOH1 +PS1_1_VFD +PS1_2_FIO1 +PS1_2_VFD +PS1_3_VFD +PS1_4_VFD +PS1_5_VFD +PS2_1_VFD +PS2_2_VFD +PS2_3_VFD +PS2_4_VFD +PS2_5_VFD +PS2_6_VFD +UL1_3_SIO1 +UL1_3_VFD +UL1_4_VFD +UL1_5_VFD +UL1_6_VFD +UL1_7_VFD +UL1_8_VFD +UL1_9_VFD +UL1_10_FIO1 +UL1_10_VFD +UL1_11A_VFD +UL1_11B_VFD +UL1_12_VFD +UL1_13_VFD +UL2_3_SIO1 +UL2_3_VFD +UL2_4_VFD +UL2_5_VFD +UL2_6_VFD +UL2_7_VFD +UL2_8_VFD +UL2_9_VFD +UL2_10_VFD +UL2_11_VFD +UL3_1_VFD +UL3_2_FIO1 +UL3_2_VFD +UL3_3_VFD +UL3_4_VFD +UL3_5_VFD +UL3_6_VFD +UL3_7_VFD +UL3_8_VFD +UL3_9_VFD +UL3_10_VFD +UL4_3_SIO1 +UL4_3_VFD +UL4_4_VFD +UL4_5_VFD +UL4_6_VFD +UL4_7_VFD +UL4_8_VFD +UL4_9_VFD +UL4_10_FIO1 +UL4_10_VFD +UL4_11A_VFD +UL4_11B_VFD +UL4_12_VFD +UL4_13_VFD +UL5_3_SIO1 +UL5_3_VFD +UL5_4_VFD +UL5_5_VFD +UL5_6_VFD +UL5_7_VFD +UL5_8_VFD +UL5_9_VFD +UL5_10_VFD +UL5_11_VFD +UL6_1_VFD +UL6_2_FIO1 +UL6_2_VFD +UL6_3_VFD +UL6_4_VFD +UL6_5_VFD +UL6_6_VFD +UL6_7_VFD +UL6_8_VFD +UL6_9_VFD +UL6_10_VFD \ No newline at end of file