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()