Advertisement
pepsimanfire

mlw_FST-F2Analysis_1.3.py

May 28th, 2025
45
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 33.15 KB | Source Code | 0 0
  1. import tkinter as tk
  2. from tkinter import filedialog, messagebox, scrolledtext
  3. import subprocess
  4. import tempfile
  5. import os
  6. import re
  7. from datetime import datetime
  8. import platform
  9. import shutil
  10.  
  11. population_history = []
  12. history_index = -1
  13.  
  14. def clear_output():
  15. answer = messagebox.askyesno("Confirm Clear", "Are you sure you want to clear the output?")
  16. if answer:
  17. output_text.delete('1.0', tk.END)
  18.  
  19. custom_r_code = None # Persistent variable to store modified R code
  20.  
  21. def format_pops(input_str):
  22. clean_input = input_str.replace('"', '').replace("'", '')
  23. pops = [p.strip() for p in re.split(r'[,\s]+', clean_input) if p.strip()]
  24. return ','.join(f'"{p}"' for p in pops)
  25.  
  26.  
  27. def get_r_library_paths(rscript_path='Rscript'):
  28. try:
  29. result = subprocess.run(
  30. [rscript_path, '-e', 'cat(.libPaths())'],
  31. stdout=subprocess.PIPE,
  32. stderr=subprocess.PIPE,
  33. text=True
  34. )
  35. paths_raw = result.stdout.strip()
  36. paths = re.findall(r'\/[^\s"]+', paths_raw)
  37. return paths
  38. except Exception:
  39. return []
  40.  
  41. def check_package_in_paths(package_name, paths):
  42. for path in paths:
  43. if os.path.isdir(os.path.join(path, package_name)):
  44. return path
  45. return None
  46.  
  47.  
  48. def get_rscript_path(r_folder):
  49. if os.name == 'nt':
  50. # Windows paths
  51. paths_to_try = [
  52. os.path.join(r_folder, 'bin', 'x64', 'Rscript.exe'),
  53. os.path.join(r_folder, 'bin', 'Rscript.exe')
  54. ]
  55. else:
  56. # Linux/Mac paths
  57. if r_folder:
  58. paths_to_try = [os.path.join(r_folder, 'bin', 'Rscript')]
  59. else:
  60. # Auto-detect from PATH or common locations
  61. rscript_path = shutil.which('Rscript')
  62. if rscript_path:
  63. return rscript_path
  64. paths_to_try = [
  65. '/Library/Frameworks/R.framework/Versions/Current/Resources/bin/Rscript',
  66. '/usr/local/bin/Rscript',
  67. '/usr/bin/Rscript',
  68. '/opt/R/bin/Rscript',
  69. '/opt/homebrew/bin/Rscript' # For Homebrew installations
  70. ]
  71. # Check all possible paths
  72. for path in paths_to_try:
  73. if os.path.isfile(path):
  74. return path
  75. return None
  76.  
  77. def run_fst_analysis():
  78. pop1_raw = pop1_entry.get()
  79. pop2_raw = pop2_entry.get()
  80. dataset_prefix = prefix_entry.get().strip()
  81. f2_dir = f2_entry.get().strip()
  82. adjust_ph = pseudohaploid_var.get()
  83. auto_only_value = auto_only_var.get()
  84. use_fst = f2_checkbox_var.get() # True = FST, False = F2
  85.  
  86. if not (pop1_raw and pop2_raw and dataset_prefix and f2_dir):
  87. messagebox.showerror("Missing info", "Please fill in all fields.")
  88. return
  89.  
  90. # Determine Rscript path based on OS
  91. if platform.system() == "Windows":
  92. r_folder = r_folder_entry.get().strip()
  93. if not r_folder:
  94. messagebox.showerror("Missing R folder", "Please specify the R installation folder.")
  95. return
  96. rscript_path = os.path.join(r_folder, 'bin', 'x64', 'Rscript.exe')
  97. if not os.path.isfile(rscript_path):
  98. rscript_path_alt = os.path.join(r_folder, 'bin', 'Rscript.exe')
  99. if os.path.isfile(rscript_path_alt):
  100. rscript_path = rscript_path_alt
  101. else:
  102. messagebox.showerror("Rscript not found", "Rscript.exe not found in specified folder.")
  103. return
  104. else:
  105. rscript_path = 'Rscript'
  106. if not shutil.which(rscript_path):
  107. messagebox.showerror("Rscript not found", "Install R or ensure 'Rscript' is in your PATH.")
  108. return
  109.  
  110. # Fix: Remove these R-style lines from Python code
  111. # pop1 <- c({pop1})
  112. # pop2 <- c({pop2})
  113. # mypops = c(pop1, pop2) # This is correct R syntax
  114.  
  115. # Instead, format the population strings properly
  116. pop1 = format_pops(pop1_raw)
  117. pop2 = format_pops(pop2_raw)
  118.  
  119. r_lib_paths = get_r_library_paths(rscript_path)
  120. package_path = check_package_in_paths("admixtools", r_lib_paths)
  121. lib_path_code = f'.libPaths("{package_path}")\n' if package_path else ''
  122.  
  123. adj_flag = "TRUE" if adjust_ph else "FALSE"
  124. auto_only_flag = "TRUE" if auto_only_value else "FALSE"
  125.  
  126. # Determine analysis type based on checkbox
  127. analysis_type = "fst" if use_fst else "f2"
  128.  
  129. r_code = f"""
  130. options(warn = -1) # <-- ADD THIS LINE TO SUPPRESS WARNINGS
  131. options(lubridate.verbose = FALSE) # Disable lubridate system checks
  132. Sys.setenv(TZ = "UTC") # Set default timezone to avoid system checks
  133. Sys.setenv(TZDIR = "/usr/share/zoneinfo") # Explicit path to zoneinfo
  134. {lib_path_code}
  135. suppressPackageStartupMessages(library(admixtools)) # <-- MODIFIED
  136. suppressPackageStartupMessages(library(tidyverse)) # <-- MODIFIED
  137. library(admixtools)
  138. library(tidyverse)
  139.  
  140. prefix = "{dataset_prefix}"
  141. my_f2_dir = "{f2_dir}"
  142.  
  143. # Explicit population definitions
  144. pop1 <- c({pop1})
  145. pop2 <- c({pop2})
  146. mypops <- c({pop1}, {pop2})
  147.  
  148. extract_f2(prefix, my_f2_dir, pops = c(mypops), overwrite = TRUE, maxmiss = 1)
  149. f2_blocks = f2_from_precomp(my_f2_dir, pops = mypops, afprod = TRUE)
  150.  
  151. {analysis_type}_result <- {analysis_type}(data = prefix, pop1 = pop1, pop2 = pop2, boot = FALSE, adjust_pseudohaploid = {adj_flag}, auto_only = {auto_only_flag})
  152. print({analysis_type}_result, n = Inf, width = Inf)
  153. """
  154.  
  155.  
  156. with tempfile.NamedTemporaryFile(delete=False, suffix=".R") as r_script:
  157. r_script.write(r_code.encode('utf-8'))
  158. r_script_path = r_script.name
  159.  
  160. try:
  161. process = subprocess.Popen(
  162. [rscript_path, '--quiet', r_script_path],
  163. stdout=subprocess.PIPE,
  164. stderr=subprocess.STDOUT,
  165. text=True,
  166. encoding='utf-8'
  167. )
  168. output_text.insert(tk.END, f"\n--- Done by pepsimanfire - Run started at {datetime.now()} ---\n")
  169. output_text.insert(tk.END, f"Pop1: {pop1_raw}\nPop2: {pop2_raw}\n\n")
  170. # Add these 3 lines directly below:
  171. output_text.insert(tk.END, f"Settings - Auto Only: {'✅ Enabled' if auto_only_var.get() else '❌ Disabled'}, "
  172. f"Pseudohaploid Adjust: {'✅ Enabled' if pseudohaploid_var.get() else '❌ Disabled'}, "
  173. f"F2 Mode: {'✅ Enabled' if f2_checkbox_var.get() else '❌ Disabled'}\n")
  174. output_text.see(tk.END)
  175.  
  176. # Buffer to store the last SNP read line
  177. last_snp_line = ""
  178.  
  179. for line in process.stdout:
  180. if "SNPs read" in line:
  181. # Strip and overwrite the previous SNP line
  182. last_snp_line = line.strip()
  183. output_text.delete("end-2l", "end-1l")
  184. output_text.insert(tk.END, last_snp_line + "\n")
  185. else:
  186. output_text.insert(tk.END, line)
  187. output_text.see(tk.END)
  188.  
  189. process.wait()
  190. status_label.config(text="FST/F2 analysis completed.")
  191. except Exception as e:
  192. messagebox.showerror("Error", str(e))
  193. finally:
  194. os.remove(r_script_path)
  195.  
  196. def edit_and_run_r_code():
  197. global custom_r_code
  198. dataset_prefix = prefix_entry.get().strip()
  199. f2_dir = f2_entry.get().strip()
  200.  
  201. # Determine Rscript path based on OS
  202. if platform.system() == "Windows":
  203. r_folder = r_folder_entry.get().strip()
  204. if not r_folder:
  205. messagebox.showerror("Missing R folder", "Please specify the R installation folder.")
  206. return
  207. rscript_path = os.path.join(r_folder, 'bin', 'x64', 'Rscript.exe')
  208. if not os.path.isfile(rscript_path):
  209. rscript_path_alt = os.path.join(r_folder, 'bin', 'Rscript.exe')
  210. if os.path.isfile(rscript_path_alt):
  211. rscript_path = rscript_path_alt
  212. else:
  213. messagebox.showerror("Rscript not found", "Rscript.exe not found in specified folder.")
  214. return
  215. else:
  216. rscript_path = 'Rscript'
  217. if not shutil.which(rscript_path):
  218. messagebox.showerror("Rscript not found", "Install R or ensure 'Rscript' is in your PATH.")
  219. return
  220.  
  221. r_lib_paths = get_r_library_paths(rscript_path)
  222. package_path = check_package_in_paths("admixtools", r_lib_paths)
  223. lib_path_code = f'.libPaths("{package_path}")\n' if package_path else ''
  224.  
  225. default_r_code = f"""
  226. {lib_path_code}
  227. library(admixtools)
  228. library(tidyverse)
  229.  
  230. prefix = "{dataset_prefix}"
  231. my_f2_dir = "{f2_dir}"
  232.  
  233. # Edit these manually:
  234. pop1 <- c("POP1") # Replace with your actual population
  235. pop2 <- c("POP2") # Replace with your actual population
  236. mypops = c(pop1, pop2)
  237.  
  238. extract_f2(prefix, my_f2_dir, pops = c(mypops), overwrite = TRUE, maxmiss = 1)
  239. f2_blocks = f2_from_precomp(my_f2_dir, pops = mypops, afprod = TRUE)
  240.  
  241. fst_result <- fst(data = prefix, pop1 = pop1, pop2 = pop2, boot = FALSE, adjust_pseudohaploid = FALSE, auto_only = FALSE)
  242. # Adjust pseudohaploid enable only if not all samples are DG. For auto only, enable only if working with
  243. # ancients, if it's only modern with moderns, then disable.
  244. print(fst_result, n = Inf, width = Inf)
  245. """.strip()
  246.  
  247. if custom_r_code is None:
  248. custom_r_code = default_r_code
  249.  
  250. editor_win = tk.Toplevel(root)
  251. editor_win.attributes('-topmost', True)
  252. editor_win.title("Edit FST R Code")
  253.  
  254. def enforce_always_on_top():
  255. try:
  256. editor_win.attributes("-topmost", True)
  257. except tk.TclError:
  258. return # Window likely closed
  259. editor_win.after(1000, enforce_always_on_top)
  260.  
  261. enforce_always_on_top() # Start the loop
  262.  
  263. text_editor = tk.Text(editor_win, wrap=tk.NONE, width=100, height=30, undo=True)
  264. text_editor.insert('1.0', custom_r_code)
  265. text_editor.pack(fill=tk.BOTH, expand=True)
  266.  
  267. def run_edited_r_code():
  268. global custom_r_code
  269. edited_code = text_editor.get("1.0", tk.END)
  270. custom_r_code = edited_code
  271.  
  272. with tempfile.NamedTemporaryFile(delete=False, suffix=".R") as temp_r_file:
  273. temp_r_file.write(edited_code.encode('utf-8'))
  274. r_script_path = temp_r_file.name
  275.  
  276. try:
  277. process = subprocess.Popen(
  278. [rscript_path, r_script_path],
  279. stdout=subprocess.PIPE,
  280. stderr=subprocess.STDOUT,
  281. text=True,
  282. encoding='utf-8'
  283. )
  284. timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  285. output_text.insert(tk.END, f"\n---\n[Custom FST R Code Output - {timestamp}]\n")
  286.  
  287. # Buffer to store the last SNP read line
  288. last_snp_line = ""
  289.  
  290. for line in process.stdout:
  291. if "SNPs read" in line:
  292. # Strip and overwrite the previous SNP line
  293. last_snp_line = line.strip()
  294. output_text.delete("end-2l", "end-1l")
  295. output_text.insert(tk.END, last_snp_line + "\n")
  296. else:
  297. output_text.insert(tk.END, line)
  298. output_text.see(tk.END)
  299.  
  300. process.wait()
  301. status_label.config(text="Custom R code executed.")
  302. except Exception as e:
  303. messagebox.showerror("Execution Error", str(e))
  304. finally:
  305. os.remove(r_script_path)
  306.  
  307. def restore_original_code():
  308. nonlocal text_editor
  309. text_editor.delete("1.0", tk.END)
  310. text_editor.insert("1.0", default_r_code)
  311. status_label.config(text="Original code restored.")
  312.  
  313. def on_editor_close():
  314. current_code = text_editor.get("1.0", tk.END)
  315. # Always save the edited code before closing
  316. global custom_r_code
  317. custom_r_code = current_code
  318. editor_win.destroy()
  319.  
  320. editor_win.protocol("WM_DELETE_WINDOW", on_editor_close)
  321.  
  322. button_frame = tk.Frame(editor_win)
  323. button_frame.pack(fill=tk.X, pady=5)
  324.  
  325. tk.Button(button_frame, text="Restore Original Code", command=restore_original_code, bg="orange").pack(side=tk.LEFT, padx=5)
  326. tk.Button(editor_win, text="Run This Code", command=run_edited_r_code, bg="lightgreen").pack(pady=5)
  327.  
  328.  
  329. # --- MAIN WINDOW SETUP ---
  330. root = tk.Tk()
  331. # Adjust scaling factor for Linux
  332. if platform.system() == "Linux":
  333. root.tk.call('tk', 'scaling', 1.3) # You can tweak the value (e.g., 2.0 for 200%)
  334. elif platform.system == "darwin": # macOS
  335. root.tk.call('tk', 'scaling', 1.0)
  336.  
  337. root.title("FST+F2 Runner")
  338.  
  339. # --- Scrollable Window Setup ---
  340. main_frame = tk.Frame(root)
  341. main_frame.pack(fill=tk.BOTH, expand=1)
  342.  
  343. canvas = tk.Canvas(main_frame)
  344. scrollbar = tk.Scrollbar(main_frame, orient=tk.VERTICAL, command=canvas.yview)
  345. scrollable_frame = tk.Frame(canvas)
  346.  
  347. scrollable_frame.bind(
  348. "<Configure>",
  349. lambda e: canvas.configure(
  350. scrollregion=canvas.bbox("all")
  351. )
  352. )
  353.  
  354. canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
  355. canvas.configure(yscrollcommand=scrollbar.set)
  356.  
  357. canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
  358. scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
  359.  
  360. # --- GUI ELEMENTS ---
  361. tk.Label(scrollable_frame, text="Pop1 (single or comma-separated):").grid(row=0, column=0, sticky='w')
  362. pop1_entry = tk.Entry(scrollable_frame, width=70)
  363. pop1_entry.grid(row=0, column=1, columnspan=2, pady=2, sticky='we')
  364.  
  365. tk.Label(scrollable_frame, text="Pop2 (one or more):").grid(row=1, column=0, sticky='w')
  366.  
  367. # Use StringVar to enable syncing with the popup editor
  368. pop2_var = tk.StringVar()
  369. pop2_entry = tk.Entry(scrollable_frame, width=70, textvariable=pop2_var)
  370. pop2_entry.grid(row=1, column=1, columnspan=2, pady=2, sticky='we')
  371.  
  372. # Add small button next to pop2 to open large editor
  373. def open_large_pop2_editor():
  374. top = tk.Toplevel(root)
  375. top.title("Edit Pop2 - Large View")
  376. top.geometry("800x400")
  377. top.resizable(True, True)
  378.  
  379. # Keep the window on top permanently
  380. top.attributes("-topmost", True)
  381.  
  382. def keep_on_top():
  383. top.attributes("-topmost", True) # Reapply the attribute
  384. top.after(1000, keep_on_top) # Repeat every second
  385.  
  386. keep_on_top() # Start the loop
  387.  
  388. # Create text editor
  389. text = tk.Text(top, wrap=tk.WORD, font=("Consolas", 12))
  390. text.pack(expand=True, fill="both")
  391. text.insert("1.0", pop2_var.get())
  392.  
  393. # Flag to prevent infinite feedback loop
  394. syncing_from_main = False
  395. syncing_from_popup = False
  396.  
  397. # --- Real-time popup ➝ entry sync ---
  398. def on_popup_key(event=None):
  399. nonlocal syncing_from_popup
  400. syncing_from_popup = True
  401. pop2_var.set(text.get("1.0", "end-1c"))
  402. syncing_from_popup = False
  403.  
  404. text.bind("<KeyRelease>", on_popup_key)
  405.  
  406. # --- Real-time entry ➝ popup sync ---
  407. def on_var_change(*args):
  408. nonlocal syncing_from_main
  409. if not syncing_from_popup:
  410. syncing_from_main = True
  411. new_value = pop2_var.get()
  412. text_content = text.get("1.0", "end-1c")
  413. if new_value != text_content:
  414. text.delete("1.0", tk.END)
  415. text.insert("1.0", new_value)
  416. syncing_from_main = False
  417.  
  418. # Attach the trace
  419. trace_id = pop2_var.trace_add("write", on_var_change)
  420.  
  421. def on_close():
  422. pop2_var.trace_remove("write", trace_id)
  423. top.destroy()
  424.  
  425. # Add a close button
  426. tk.Button(top, text="Close", command=on_close).pack(pady=5)
  427.  
  428. tk.Button(scrollable_frame, text="🡕", width=3, command=open_large_pop2_editor).grid(row=1, column=3, padx=2)
  429.  
  430. tk.Label(scrollable_frame, text="Dataset prefix (path):").grid(row=2, column=0, sticky='w')
  431. prefix_entry_var = tk.StringVar()
  432. prefix_entry = tk.Entry(scrollable_frame, width=70, textvariable=prefix_entry_var)
  433. prefix_entry.grid(row=2, column=1, pady=2, sticky='we')
  434.  
  435. def browse_prefix():
  436. folder = filedialog.askdirectory()
  437. if folder:
  438. prefix_entry_var.set(folder)
  439. tk.Button(scrollable_frame, text="Browse...", command=browse_prefix).grid(row=2, column=2, pady=2)
  440.  
  441. tk.Label(scrollable_frame, text="F2 Directory:").grid(row=3, column=0, sticky='w')
  442. f2_entry_var = tk.StringVar()
  443. f2_entry = tk.Entry(scrollable_frame, width=70, textvariable=f2_entry_var)
  444. f2_entry.grid(row=3, column=1, pady=2, sticky='we')
  445.  
  446. def browse_f2_dir():
  447. folder = filedialog.askdirectory()
  448. if folder:
  449. f2_entry_var.set(folder)
  450. tk.Button(scrollable_frame, text="Browse...", command=browse_f2_dir).grid(row=3, column=2, pady=2)
  451.  
  452. if platform.system() == "Windows":
  453. tk.Label(scrollable_frame, text="R Installation Folder:").grid(row=4, column=0, sticky='w')
  454. r_folder_entry = tk.Entry(scrollable_frame, width=70)
  455. r_folder_entry.grid(row=4, column=1, pady=2, sticky='we')
  456.  
  457. def browse_r_folder():
  458. folder = filedialog.askdirectory()
  459. if folder:
  460. r_folder_entry.delete(0, tk.END)
  461. r_folder_entry.insert(0, folder)
  462. tk.Button(scrollable_frame, text="Browse...", command=browse_r_folder).grid(row=4, column=2, pady=2)
  463. else:
  464. # Hide R folder widgets on Linux
  465. r_folder_entry = None
  466.  
  467. pseudohaploid_var = tk.BooleanVar(value=False)
  468. auto_only_var = tk.BooleanVar(value=False)
  469. f2_checkbox_var = tk.BooleanVar(value=False)
  470. checkbox_frame = tk.Frame(scrollable_frame)
  471. checkbox_frame.grid(row=5, column=1, columnspan=3, sticky='w', padx=5, pady=(0,5))
  472.  
  473. tk.Checkbutton(checkbox_frame, text="Adjust Pseudohaploid", variable=pseudohaploid_var).pack(side=tk.LEFT)
  474. tk.Checkbutton(checkbox_frame, text="Auto Only", variable=auto_only_var).pack(side=tk.LEFT, padx=(10,0))
  475. tk.Checkbutton(checkbox_frame, text="Run FST (instead of F2)", variable=f2_checkbox_var).pack(side=tk.LEFT, padx=(10,0))
  476.  
  477. tk.Button(scrollable_frame, text="Run FST/F2", command=run_fst_analysis, bg="lightblue").grid(row=6, column=1, pady=10, sticky='we')
  478. tk.Button(scrollable_frame, text="Edit and Run R Code", command=edit_and_run_r_code, bg="lightyellow").grid(row=6, column=2, pady=10, sticky='we')
  479.  
  480. # --- .ind file display and selection ---
  481. ind_frame = tk.LabelFrame(scrollable_frame, text=".ind File Editor", padx=5, pady=5)
  482. ind_frame.grid(row=7, column=0, columnspan=3, sticky='we', padx=5, pady=10)
  483.  
  484. # Controls: Undo/Redo/Save
  485. editor_controls = tk.Frame(ind_frame)
  486. editor_controls.pack(fill=tk.X)
  487.  
  488. def undo_ind_changes():
  489. try: ind_text.edit_undo()
  490. except: pass
  491.  
  492. def redo_ind_changes():
  493. try: ind_text.edit_redo()
  494. except: pass
  495.  
  496. def save_ind_file():
  497. folder_path = prefix_entry_var.get().strip()
  498. if not folder_path:
  499. messagebox.showerror("Error", "Dataset folder is empty. Cannot save .ind file.")
  500. return
  501. parent_folder = os.path.dirname(folder_path)
  502. last_folder_name = os.path.basename(folder_path)
  503. ind_file_name = f"{last_folder_name}.ind"
  504. ind_path = os.path.join(parent_folder, ind_file_name)
  505. try:
  506. content = ind_text.get('1.0', tk.END)
  507. with open(ind_path, 'w') as f:
  508. f.write(content)
  509. status_label.config(text=f".ind file saved: {ind_file_name}")
  510. except Exception as e:
  511. messagebox.showerror("Error", f"Failed to save .ind file: {str(e)}")
  512.  
  513. def undo_population_addition():
  514. global population_history, history_index
  515. if history_index >= 0:
  516. entry = population_history[history_index]
  517. field, old_value = entry['field'], entry['old_value']
  518. if field == "pop1":
  519. pop1_entry.delete(0, tk.END)
  520. pop1_entry.insert(0, old_value)
  521. elif field == "pop2":
  522. pop2_entry.delete(0, tk.END)
  523. pop2_entry.insert(0, old_value)
  524. history_index -= 1
  525. status_label.config(text=f"Undo: {field} reverted")
  526.  
  527. def redo_population_addition():
  528. global population_history, history_index
  529. if history_index < len(population_history) - 1:
  530. history_index += 1
  531. entry = population_history[history_index]
  532. field, new_value = entry['field'], entry['new_value']
  533. if field == "pop1":
  534. pop1_entry.delete(0, tk.END)
  535. pop1_entry.insert(0, new_value)
  536. elif field == "pop2":
  537. pop2_entry.delete(0, tk.END)
  538. pop2_entry.insert(0, new_value)
  539. status_label.config(text=f"Redo: {field} reapplied")
  540.  
  541.  
  542. left_controls = tk.Frame(editor_controls)
  543. left_controls.pack(side=tk.LEFT)
  544.  
  545.  
  546. tk.Button(left_controls, text="Undo", command=undo_ind_changes).pack(side=tk.LEFT, padx=2)
  547. tk.Button(left_controls, text="Redo", command=redo_ind_changes).pack(side=tk.LEFT, padx=2)
  548. tk.Button(left_controls, text="Save", command=save_ind_file).pack(side=tk.LEFT, padx=2)
  549.  
  550.  
  551. # Search bar
  552. search_frame = tk.Frame(ind_frame)
  553. search_frame.pack(fill=tk.X)
  554.  
  555. search_var = tk.StringVar()
  556. search_entry = tk.Entry(search_frame, textvariable=search_var, width=40)
  557. search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
  558. tk.Label(search_frame, text="Search:").pack(side=tk.LEFT)
  559.  
  560. # Search buttons
  561. tk.Button(search_frame, text="▲", command=lambda: jump_to_prev_match(), width=2).pack(side=tk.LEFT)
  562. tk.Button(search_frame, text="▼", command=lambda: jump_to_next_match(), width=2).pack(side=tk.LEFT)
  563.  
  564. # Text editor with scrollbars
  565. text_frame = tk.Frame(ind_frame)
  566. text_frame.pack(fill=tk.BOTH, expand=True)
  567.  
  568. ind_text = tk.Text(text_frame, wrap=tk.NONE, width=80, height=15, undo=True, autoseparators=True, maxundo=-1)
  569. ind_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
  570.  
  571. y_scroll = tk.Scrollbar(text_frame, command=ind_text.yview)
  572. y_scroll.pack(side=tk.RIGHT, fill=tk.Y)
  573. ind_text.config(yscrollcommand=y_scroll.set)
  574.  
  575. # Utility to extract selected population(s)
  576.  
  577. def save_to_history(field, old_value, new_value):
  578. global population_history, history_index
  579. if history_index < len(population_history) - 1:
  580. population_history = population_history[:history_index + 1]
  581. population_history.append({
  582. 'field': field,
  583. 'old_value': old_value,
  584. 'new_value': ','.join(new_value) if isinstance(new_value, list) else new_value
  585. })
  586. history_index = len(population_history) - 1
  587.  
  588. def get_selected_populations():
  589. selected_lines = []
  590. try:
  591. sel_start = ind_text.index(tk.SEL_FIRST)
  592. sel_end = ind_text.index(tk.SEL_LAST)
  593. selected_text = ind_text.get(sel_start, sel_end)
  594. selected_lines = selected_text.split('\n')
  595. except:
  596. current_line = ind_text.index(tk.INSERT).split('.')[0]
  597. lines = ind_text.get("1.0", tk.END).split('\n')
  598. if 0 < int(current_line) <= len(lines):
  599. selected_lines = [lines[int(current_line)-1]]
  600.  
  601. pops = set()
  602. for line in selected_lines:
  603. parts = line.strip().split()
  604. if len(parts) >= 3:
  605. pops.add(parts[2])
  606. return sorted(pops)
  607.  
  608. def save_output_to_file():
  609. content = output_text.get("1.0", tk.END)
  610. if not content.strip():
  611. messagebox.showerror("Error", "No output to save")
  612. return
  613.  
  614. file_path = filedialog.asksaveasfilename(
  615. defaultextension=".log",
  616. filetypes=[("Text Files", "*.log"), ("All Files", "*.*")]
  617. )
  618.  
  619. if file_path:
  620. try:
  621. # Add encoding='utf-8' here
  622. with open(file_path, 'w', encoding='utf-8') as f:
  623. f.write(content)
  624. status_label.config(text=f"Output saved to {file_path}")
  625. except Exception as e:
  626. messagebox.showerror("Error", f"Failed to save file: {str(e)}")
  627.  
  628. def add_to_pop1():
  629. global population_history, history_index
  630. pops = get_selected_populations()
  631. if pops:
  632. current = pop1_entry.get().strip()
  633. current_pops = [p.strip() for p in re.split(r'[,\s]+', current) if p.strip()]
  634. new_pops = [p for p in pops if p not in current_pops]
  635. if new_pops:
  636. save_to_history("pop1", current, current_pops + new_pops)
  637. pop1_entry.delete(0, tk.END)
  638. pop1_entry.insert(0, ','.join(current_pops + new_pops))
  639.  
  640. def add_to_pop2():
  641. global population_history, history_index
  642. pops = get_selected_populations()
  643. if pops:
  644. current = pop2_entry.get().strip()
  645. current_pops = [p.strip() for p in re.split(r'[,\s]+', current) if p.strip()]
  646. new_pops = [p for p in pops if p not in current_pops]
  647. if new_pops:
  648. save_to_history("pop2", current, current_pops + new_pops)
  649. pop2_entry.delete(0, tk.END)
  650. pop2_entry.insert(0, ','.join(current_pops + new_pops))
  651.  
  652.  
  653.  
  654. # Population addition buttons in their own row below
  655. button_frame = tk.Frame(ind_frame)
  656. button_frame.pack(fill=tk.X, pady=(5, 0))
  657.  
  658. right_controls = tk.Frame(editor_controls)
  659. right_controls.pack(side=tk.RIGHT)
  660.  
  661.  
  662. tk.Button(button_frame, text="Add to Pop1", command=add_to_pop1).pack(side=tk.LEFT, padx=5, expand=True)
  663. tk.Button(button_frame, text="Add to Pop2", command=add_to_pop2).pack(side=tk.LEFT, padx=5, expand=True)
  664. tk.Button(right_controls, text="Undo Add", command=undo_population_addition).pack(side=tk.LEFT, padx=2)
  665. tk.Button(right_controls, text="Redo Add", command=redo_population_addition).pack(side=tk.LEFT, padx=2)
  666.  
  667.  
  668. def load_ind_file():
  669. folder_path = prefix_entry_var.get().strip()
  670. if not folder_path:
  671. return
  672. parent_folder = os.path.dirname(folder_path)
  673. last_folder_name = os.path.basename(folder_path)
  674. ind_path = os.path.join(parent_folder, f"{last_folder_name}.ind")
  675. if not os.path.isfile(ind_path):
  676. return
  677. try:
  678. with open(ind_path, 'r') as f:
  679. content = f.read()
  680. ind_text.delete('1.0', tk.END)
  681. ind_text.insert(tk.END, content)
  682. status_label.config(text=f".ind file loaded: {os.path.basename(ind_path)}")
  683. except Exception as e:
  684. status_label.config(text=f"Error loading .ind file: {str(e)}")
  685.  
  686. def on_prefix_change(*args):
  687. load_ind_file()
  688.  
  689. prefix_entry_var.trace_add('write', on_prefix_change)
  690.  
  691. def search_ind_file(*args):
  692. search_term = search_var.get()
  693. if not search_term:
  694. ind_text.tag_remove('highlight', '1.0', tk.END)
  695. ind_text.tag_remove('current_match', '1.0', tk.END)
  696. return
  697.  
  698. ind_text.tag_remove('highlight', '1.0', tk.END)
  699. ind_text.tag_remove('current_match', '1.0', tk.END)
  700.  
  701. start_pos = '1.0'
  702. count_var = tk.IntVar()
  703. matches = []
  704.  
  705. while True:
  706. pos = ind_text.search(search_term, start_pos, stopindex=tk.END, count=count_var, nocase=1)
  707. if not pos: break
  708. end_pos = f"{pos}+{count_var.get()}c"
  709. matches.append((pos, end_pos))
  710. ind_text.tag_add('highlight', pos, end_pos)
  711. start_pos = end_pos
  712.  
  713. ind_text.tag_config('highlight', background='yellow')
  714. ind_text.tag_config('current_match', background='orange')
  715.  
  716. if hasattr(ind_text, 'search_matches'):
  717. del ind_text.search_matches
  718. ind_text.search_matches = matches
  719. ind_text.current_match = -1
  720. if matches:
  721. jump_to_match(0)
  722.  
  723. def jump_to_match(index):
  724. if not hasattr(ind_text, 'search_matches'): return
  725. matches = ind_text.search_matches
  726. if 0 <= ind_text.current_match < len(matches):
  727. prev_pos, prev_end = matches[ind_text.current_match]
  728. ind_text.tag_add('highlight', prev_pos, prev_end)
  729. ind_text.current_match = index % len(matches)
  730. pos, end_pos = matches[ind_text.current_match]
  731. ind_text.tag_add('current_match', pos, end_pos)
  732. ind_text.mark_set(tk.INSERT, pos)
  733. ind_text.see(pos)
  734.  
  735. def jump_to_next_match(event=None):
  736. if hasattr(ind_text, 'search_matches'):
  737. jump_to_match(ind_text.current_match + 1)
  738.  
  739. def jump_to_prev_match(event=None):
  740. if hasattr(ind_text, 'search_matches'):
  741. jump_to_match(ind_text.current_match - 1)
  742.  
  743. search_var.trace_add('write', search_ind_file)
  744. search_entry.bind("<Return>", jump_to_next_match)
  745. search_entry.bind("<Down>", jump_to_next_match)
  746. search_entry.bind("<Up>", jump_to_prev_match)
  747.  
  748. output_text = scrolledtext.ScrolledText(scrollable_frame, width=150, height=30)
  749. output_text.grid(row=9, column=0, columnspan=3, padx=10, pady=5, sticky='nsew')
  750.  
  751. # --- Status/Button Row ---
  752. status_button_frame = tk.Frame(scrollable_frame)
  753. status_button_frame.grid(row=10, column=0, columnspan=3, sticky='we', padx=10, pady=(0, 10))
  754.  
  755. # Status label on left
  756. status_label = tk.Label(status_button_frame, text="Ready.", anchor='w')
  757. status_label.pack(side=tk.LEFT)
  758.  
  759. # Buttons on right
  760. button_frame = tk.Frame(status_button_frame)
  761. button_frame.pack(side=tk.RIGHT)
  762.  
  763. # Clear button
  764. clear_button = tk.Button(button_frame, text="Clear", command=clear_output, bg="lightgray", width=10, height=1)
  765. clear_button.pack(side=tk.LEFT, padx=5)
  766.  
  767. # Add this near the other search functions (around line 1000)
  768. def setup_output_search():
  769. # Create search frame for output console (placed at bottom)
  770. output_search_frame = tk.Frame(scrollable_frame)
  771. output_search_frame.grid(row=8, column=0, columnspan=3, sticky='we', padx=5, pady=(0, 10))
  772.  
  773. # Left-aligned search components
  774. search_components = tk.Frame(output_search_frame)
  775. search_components.pack(side=tk.LEFT, fill=tk.X, expand=True)
  776.  
  777. tk.Label(search_components, text="Output Search:").pack(side=tk.LEFT, padx=(0, 5))
  778. output_search_var = tk.StringVar()
  779. output_search_entry = tk.Entry(search_components, textvariable=output_search_var, width=40)
  780. output_search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
  781.  
  782. # Search navigation buttons
  783. output_search_button_frame = tk.Frame(search_components)
  784. output_search_button_frame.pack(side=tk.LEFT, padx=(5, 0))
  785.  
  786. tk.Button(output_search_button_frame, text="▲",
  787. command=lambda: jump_to_prev_output_match(), width=2).pack(side=tk.LEFT)
  788. tk.Button(output_search_button_frame, text="▼",
  789. command=lambda: jump_to_next_output_match(), width=2).pack(side=tk.LEFT)
  790.  
  791. # Save button placed to the right of search bar
  792. save_output_button = tk.Button(button_frame, text="💾 Save", command=save_output_to_file, bg="lightgray", width=10, height=1)
  793. save_output_button.pack(side=tk.LEFT)
  794.  
  795.  
  796. def search_output(*args):
  797. search_term = output_search_var.get()
  798. if not search_term:
  799. output_text.tag_remove('output_highlight', '1.0', tk.END)
  800. output_text.tag_remove('output_current_match', '1.0', tk.END)
  801. return
  802.  
  803. output_text.tag_remove('output_highlight', '1.0', tk.END)
  804. output_text.tag_remove('output_current_match', '1.0', tk.END)
  805.  
  806. start_pos = '1.0'
  807. count_var = tk.IntVar()
  808. matches = []
  809.  
  810. while True:
  811. pos = output_text.search(search_term, start_pos, stopindex=tk.END, count=count_var, nocase=1)
  812. if not pos: break
  813. end_pos = f"{pos}+{count_var.get()}c"
  814. matches.append((pos, end_pos))
  815. output_text.tag_add('output_highlight', pos, end_pos)
  816. start_pos = end_pos
  817.  
  818. output_text.tag_config('output_highlight', background='yellow', foreground='black')
  819. output_text.tag_config('output_current_match', background='orange', foreground='black')
  820.  
  821. if hasattr(output_text, 'output_search_matches'):
  822. del output_text.output_search_matches
  823. output_text.output_search_matches = matches
  824. output_text.output_current_match = -1
  825.  
  826. if matches:
  827. jump_to_output_match(0)
  828.  
  829. def jump_to_output_match(index):
  830. if not hasattr(output_text, 'output_search_matches') or not output_text.output_search_matches:
  831. return
  832.  
  833. matches = output_text.output_search_matches
  834. if 0 <= output_text.output_current_match < len(matches):
  835. prev_pos, prev_end = matches[output_text.output_current_match]
  836. output_text.tag_add('output_highlight', prev_pos, prev_end)
  837.  
  838. output_text.output_current_match = index % len(matches)
  839. pos, end_pos = matches[output_text.output_current_match]
  840.  
  841. output_text.tag_add('output_current_match', pos, end_pos)
  842. output_text.mark_set(tk.INSERT, pos)
  843. output_text.see(pos)
  844.  
  845. def jump_to_next_output_match(event=None):
  846. if hasattr(output_text, 'output_search_matches') and output_text.output_search_matches:
  847. jump_to_output_match(output_text.output_current_match + 1)
  848.  
  849. def jump_to_prev_output_match(event=None):
  850. if hasattr(output_text, 'output_search_matches') and output_text.output_search_matches:
  851. jump_to_output_match(output_text.output_current_match - 1)
  852.  
  853. output_search_var.trace_add('write', search_output)
  854. output_search_entry.bind("<Return>", jump_to_next_output_match)
  855. output_search_entry.bind("<Down>", jump_to_next_output_match)
  856. output_search_entry.bind("<Up>", jump_to_prev_output_match)
  857.  
  858. # Call this function after creating the output_text widget
  859. setup_output_search()
  860.  
  861. # Make columns expandable
  862. scrollable_frame.grid_columnconfigure(1, weight=1)
  863. scrollable_frame.grid_rowconfigure(7, weight=1)
  864.  
  865. # --- Repeating Vertical Text Label on the Right ---
  866. word = 'pepsimanfire'
  867. repeat_count = 5 # You can increase this to make it go further down
  868. vertical_text = '\n'.join(list(word) * repeat_count)
  869.  
  870. vertical_label = tk.Label(scrollable_frame, text=vertical_text, font=("Helvetica", 12, "bold"), fg="gray")
  871. vertical_label.grid(row=0, column=5, rowspan=999, sticky='ns', padx=(0, 5), pady=10)
  872.  
  873. root.mainloop()
  874.  
Tags: admixtools
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement