CAD_LAYOUT/export_layouts_ui.py

761 lines
34 KiB
Python

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_<rest>_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('<Configure>', lambda e: show_scrollbar_if_needed(op_text, op_scroll))
err_text.bind('<Configure>', 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('<Configure>', lambda e: n_show_scrollbar_if_needed(n_op_text, n_op_scroll))
n_err_text.bind('<Configure>', 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()