671 lines
22 KiB
Python
671 lines
22 KiB
Python
"""
|
|
Action handlers for PyUCC GUI.
|
|
Separates business logic from GUI construction.
|
|
"""
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
from tkinter import messagebox
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
from ..core.scanner import find_source_files
|
|
from ..core.differ import Differ, BaselineManager
|
|
from ..config import settings as app_settings
|
|
|
|
|
|
class ActionHandlers:
|
|
"""Handles all main actions (scan, countings, metrics, differ) for the GUI."""
|
|
|
|
def __init__(self, gui_app):
|
|
"""
|
|
Args:
|
|
gui_app: Reference to the main GUI application instance
|
|
"""
|
|
self.app = gui_app
|
|
|
|
def handle_scan(self):
|
|
"""Execute scan action: find source files matching profile filters."""
|
|
self.app.log("Action: Scan started", level='INFO')
|
|
self.app._set_phase("Scanning...")
|
|
self.app._current_mode = 'scan'
|
|
|
|
# Reset UI
|
|
self._reset_progress_ui()
|
|
self.app._set_results_columns(("name", "path"))
|
|
self._clear_results()
|
|
self.app.export_btn.config(state='disabled')
|
|
self.app.update_idletasks()
|
|
|
|
# Get paths and filters
|
|
paths, allowed_exts, ignore_patterns, pr = self.app._resolve_profile_and_filters()
|
|
if not paths:
|
|
messagebox.showwarning("Missing path", "Please select a folder or profile to analyze first.")
|
|
return
|
|
|
|
# Submit scan task
|
|
def _scan_task(paths):
|
|
files = []
|
|
for p in paths:
|
|
pth = Path(p)
|
|
try:
|
|
if pth.is_dir():
|
|
files.extend(find_source_files(pth, allowed_extensions=allowed_exts, ignore_patterns=ignore_patterns))
|
|
elif pth.is_file():
|
|
files.append(pth)
|
|
except Exception:
|
|
continue
|
|
# Deduplicate
|
|
seen = set()
|
|
unique = []
|
|
for f in files:
|
|
s = str(f)
|
|
if s not in seen:
|
|
seen.add(s)
|
|
unique.append(f)
|
|
return unique
|
|
|
|
self.app._current_task_id = self.app.worker.submit(_scan_task, paths, kind='thread', on_done=self.app._scan_done)
|
|
self.app.cancel_btn.config(state='normal')
|
|
|
|
def handle_countings(self):
|
|
"""Execute countings action: analyze code lines (code/comment/blank)."""
|
|
self.app.log("Action: Countings started", level='INFO')
|
|
self.app._set_phase("Counting...")
|
|
self.app._current_mode = 'countings'
|
|
|
|
# Reset UI
|
|
self._reset_progress_ui()
|
|
self.app._set_results_columns(("name", "path", "code", "comment", "blank", "total", "language"))
|
|
self._clear_results()
|
|
self.app.export_btn.config(state='disabled')
|
|
self.app.update_idletasks()
|
|
|
|
paths, allowed_exts, ignore_patterns, pr = self.app._resolve_profile_and_filters()
|
|
if not paths:
|
|
messagebox.showwarning("Missing path", "Please select a folder or profile to analyze first.")
|
|
return
|
|
|
|
# Gather files first, then analyze
|
|
def _gather_files(paths):
|
|
return self._gather_source_files(paths, allowed_exts, ignore_patterns)
|
|
|
|
def _on_gather_done(results):
|
|
files = results or []
|
|
if not files:
|
|
messagebox.showinfo("Countings", "No files found to analyze.")
|
|
self.app.cancel_btn.config(state='disabled')
|
|
self.app._set_phase('Idle')
|
|
return
|
|
|
|
from ..core.countings_impl import analyze_file_counts
|
|
|
|
self.app._total_files = len(files)
|
|
self.app._processed_files = 0
|
|
try:
|
|
self.app.progress['maximum'] = max(1, self.app._total_files)
|
|
self.app.progress['value'] = 0
|
|
self.app._lbl_files.config(text=f"Files: 0/{self.app._total_files}")
|
|
except Exception:
|
|
pass
|
|
|
|
self.app._set_phase(f"Analyzing {self.app._total_files} files...")
|
|
self.app.update_idletasks()
|
|
|
|
self.app._current_task_id = self.app.worker.map_iterable(
|
|
func=analyze_file_counts,
|
|
items=files,
|
|
kind='thread',
|
|
on_progress=self.app._on_count_progress,
|
|
on_done=self.app._on_count_done
|
|
)
|
|
|
|
self.app._set_phase("Gathering files...")
|
|
self.app.update_idletasks()
|
|
self.app._current_task_id = self.app.worker.submit(_gather_files, paths, kind='thread', on_done=_on_gather_done)
|
|
self.app.cancel_btn.config(state='normal')
|
|
|
|
def handle_metrics(self):
|
|
"""Execute metrics action: compute complexity metrics (CC, MI)."""
|
|
self.app.log("Action: Metrics started", level='INFO')
|
|
self.app._set_phase("Computing metrics...")
|
|
self.app._current_mode = 'metrics'
|
|
|
|
# Reset UI
|
|
self._reset_progress_ui()
|
|
self.app._set_results_columns(("name", "path", "avg_cc", "max_cc", "func_count", "mi", "language"))
|
|
self._clear_results()
|
|
self.app.export_btn.config(state='disabled')
|
|
self.app.update_idletasks()
|
|
|
|
paths, allowed_exts, ignore_patterns, pr = self.app._resolve_profile_and_filters()
|
|
if not paths:
|
|
messagebox.showwarning("Missing path", "Please select a folder or profile to analyze first.")
|
|
return
|
|
|
|
# Gather files first, then analyze
|
|
def _gather_files(paths):
|
|
return self._gather_source_files(paths, allowed_exts, ignore_patterns)
|
|
|
|
def _on_gather_done(results):
|
|
files = results or []
|
|
if not files:
|
|
messagebox.showinfo("Metrics", "No files found to analyze.")
|
|
self.app.cancel_btn.config(state='disabled')
|
|
self.app._set_phase('Idle')
|
|
return
|
|
|
|
try:
|
|
from ..core.metrics import analyze_file_metrics as analyzer
|
|
except Exception:
|
|
messagebox.showerror("Metrics", "Metrics analyzer not available")
|
|
self.app._set_phase('Idle')
|
|
return
|
|
|
|
self.app._total_files = len(files)
|
|
self.app._processed_files = 0
|
|
try:
|
|
self.app.progress['maximum'] = max(1, self.app._total_files)
|
|
self.app.progress['value'] = 0
|
|
self.app._lbl_files.config(text=f"Files: 0/{self.app._total_files}")
|
|
except Exception:
|
|
pass
|
|
|
|
self.app._set_phase(f"Computing metrics for {self.app._total_files} files...")
|
|
self.app.update_idletasks()
|
|
|
|
self.app._current_task_id = self.app.worker.map_iterable(
|
|
func=analyzer,
|
|
items=files,
|
|
kind='thread',
|
|
on_progress=self.app._on_metrics_progress,
|
|
on_done=self.app._on_metrics_done
|
|
)
|
|
|
|
self.app._set_phase("Gathering files...")
|
|
self.app.update_idletasks()
|
|
self.app._current_task_id = self.app.worker.submit(_gather_files, paths, kind='thread', on_done=_on_gather_done)
|
|
self.app.cancel_btn.config(state='normal')
|
|
|
|
def handle_differ(self):
|
|
"""Execute differ action: compare current code with baseline."""
|
|
self.app.log("Action: Differing started", level='INFO')
|
|
self.app._set_phase("Differing...")
|
|
self.app._set_results_columns(("fileA", "fileB", "added", "deleted", "modified", "unmodified", "Δ code_lines", "Δ comment_lines", "Δ blank_lines", "Δ func_count", "Δ avg_cc", "Δ mi"))
|
|
self._clear_results()
|
|
self.app._current_mode = 'differ'
|
|
self.app.export_btn.config(state='disabled')
|
|
|
|
try:
|
|
self.app._lbl_files.config(text="Files: 0/0")
|
|
self.app.progress['maximum'] = 1
|
|
self.app.progress['value'] = 0
|
|
except Exception:
|
|
pass
|
|
|
|
self.app.update_idletasks()
|
|
|
|
paths, allowed_exts, ignore_patterns, pr = self.app._resolve_profile_and_filters()
|
|
if not paths:
|
|
messagebox.showwarning("Missing path", "Please select a folder or profile to analyze first.")
|
|
return
|
|
|
|
project = paths[0]
|
|
self.app._set_phase("Loading baselines...")
|
|
self.app.update_idletasks()
|
|
|
|
bm = BaselineManager(project)
|
|
baselines = bm.list_baselines()
|
|
|
|
profile_name = pr.get('name') if pr else None
|
|
max_keep = app_settings.get_max_keep()
|
|
|
|
# Callback handlers
|
|
def _on_create_done(baseline_id):
|
|
try:
|
|
self.app._set_phase('Idle')
|
|
self.app._current_task_id = None
|
|
self.app.cancel_btn.config(state='disabled')
|
|
messagebox.showinfo("Differing", f"Baseline created: {baseline_id}")
|
|
except Exception:
|
|
self.app._set_phase('Idle')
|
|
self.app._current_task_id = None
|
|
self.app.cancel_btn.config(state='disabled')
|
|
|
|
def _on_diff_done(result):
|
|
try:
|
|
total = result.get('total', {})
|
|
self.app.log(f"Differ finished: added={total.get('added',0)} deleted={total.get('deleted',0)} modified={total.get('modified',0)}", level='INFO')
|
|
self.app.export_btn.config(state='normal')
|
|
|
|
# Create new baseline after successful diff
|
|
try:
|
|
self.app._set_phase('Creating baseline...')
|
|
self.app._current_task_id = self.app.worker.submit(
|
|
bm.create_baseline_from_dir,
|
|
project, None, True, True, ignore_patterns, profile_name, max_keep,
|
|
kind='thread',
|
|
on_done=_on_create_done
|
|
)
|
|
self.app.cancel_btn.config(state='normal')
|
|
except Exception:
|
|
self.app._set_phase('Idle')
|
|
self.app.cancel_btn.config(state='disabled')
|
|
self.app._current_task_id = None
|
|
except Exception as e:
|
|
messagebox.showerror('Differ Error', str(e))
|
|
|
|
# No baseline exists - create one
|
|
if not baselines:
|
|
create = messagebox.askyesno("Differing", "No baseline found for this project. Create baseline now?")
|
|
if not create:
|
|
self.app._set_phase('Idle')
|
|
return
|
|
|
|
self.app._set_phase("Creating baseline...")
|
|
self.app.update_idletasks()
|
|
self.app._current_task_id = self.app.worker.submit(
|
|
bm.create_baseline_from_dir,
|
|
project, None, True, True, ignore_patterns, profile_name, 5,
|
|
kind='thread',
|
|
on_done=_on_create_done
|
|
)
|
|
self.app.cancel_btn.config(state='normal')
|
|
return
|
|
|
|
# Select baseline to compare with
|
|
chosen = self._select_baseline_dialog(baselines)
|
|
if not chosen:
|
|
self.app._set_phase('Idle')
|
|
return
|
|
|
|
try:
|
|
meta = bm.load_metadata(chosen)
|
|
self.app.log(f"Loaded baseline: {chosen}", level='INFO')
|
|
except Exception as e:
|
|
messagebox.showerror("Differing", f"Failed to load baseline metadata: {e}")
|
|
self.app._set_phase('Idle')
|
|
return
|
|
|
|
# Setup baseline and current roots for diff viewer
|
|
self._setup_differ_roots(bm, chosen, meta, project)
|
|
|
|
# Build file lists and start diff
|
|
differ = Differ(meta, project, ignore_patterns=ignore_patterns)
|
|
|
|
self.app._set_phase("Scanning current files...")
|
|
self.app.update_idletasks()
|
|
|
|
try:
|
|
current_files = differ.build_current_file_list()
|
|
except Exception:
|
|
messagebox.showerror('Differing', 'Failed to scan current project files')
|
|
self.app._set_phase('Idle')
|
|
return
|
|
|
|
self.app._set_phase("Building pairs...")
|
|
self.app.update_idletasks()
|
|
|
|
try:
|
|
pairs = differ.match_files(differ.baseline.files, current_files)
|
|
except Exception:
|
|
messagebox.showerror('Differing', 'Failed to build file pairs')
|
|
self.app._set_phase('Idle')
|
|
return
|
|
|
|
if not pairs:
|
|
messagebox.showinfo("Differing", "No files to compare")
|
|
self.app._set_phase('Idle')
|
|
return
|
|
|
|
# Process pairs in parallel
|
|
self._run_differ_worker(differ, pairs, project, meta, _on_diff_done)
|
|
|
|
def _run_differ_worker(self, differ, pairs, project, meta, on_diff_done):
|
|
"""Start background worker to process differ pairs."""
|
|
from ..core.countings_impl import analyze_file_counts
|
|
from ..core.metrics import analyze_file_metrics
|
|
|
|
self.app._processed_files = 0
|
|
try:
|
|
self.app.progress['maximum'] = max(1, len(pairs))
|
|
self.app.progress['value'] = 0
|
|
self.app._lbl_files.config(text=f"Files: 0/{len(pairs)}")
|
|
except Exception:
|
|
pass
|
|
|
|
self.app._set_phase(f"Comparing {len(pairs)} file pairs...")
|
|
self.app.update_idletasks()
|
|
|
|
# Process pair function
|
|
def _process_pair(pair):
|
|
a, b = pair
|
|
res = {
|
|
'fileA': a.path if a is not None else None,
|
|
'fileB': b.path if b is not None else None,
|
|
'counts': None,
|
|
'file_meta': None,
|
|
'baseline_countings': None,
|
|
'current_countings': None,
|
|
'countings_delta': None,
|
|
'baseline_metrics': None,
|
|
'current_metrics': None,
|
|
'metrics_delta': None,
|
|
}
|
|
|
|
# Diff counts
|
|
fa = os.path.join(meta.project_root, a.path) if a is not None else None
|
|
fb = os.path.join(project, b.path) if b is not None else None
|
|
res['counts'] = Differ._diff_file_pair(fa, fb)
|
|
|
|
# Extract baseline countings and metrics
|
|
if a is not None:
|
|
if hasattr(a, 'countings') and a.countings:
|
|
res['baseline_countings'] = a.countings
|
|
if hasattr(a, 'metrics') and a.metrics:
|
|
res['baseline_metrics'] = a.metrics
|
|
|
|
# Extract current countings and metrics
|
|
if b is not None:
|
|
file_entry = {
|
|
'path': b.path,
|
|
'size': b.size,
|
|
'mtime': b.mtime,
|
|
'sha1': b.sha1,
|
|
}
|
|
if hasattr(b, 'countings') and b.countings:
|
|
file_entry['countings'] = b.countings
|
|
res['current_countings'] = b.countings
|
|
else:
|
|
file_entry['countings'] = None
|
|
|
|
if hasattr(b, 'metrics') and b.metrics:
|
|
file_entry['metrics'] = b.metrics
|
|
res['current_metrics'] = b.metrics
|
|
else:
|
|
file_entry['metrics'] = None
|
|
|
|
res['file_meta'] = file_entry
|
|
|
|
# Compute deltas
|
|
if res['baseline_countings'] and res['current_countings']:
|
|
res['countings_delta'] = {
|
|
'physical_lines': res['current_countings']['physical_lines'] - res['baseline_countings'].get('physical_lines', 0),
|
|
'code_lines': res['current_countings']['code_lines'] - res['baseline_countings'].get('code_lines', 0),
|
|
'comment_lines': res['current_countings']['comment_lines'] - res['baseline_countings'].get('comment_lines', 0),
|
|
'blank_lines': res['current_countings']['blank_lines'] - res['baseline_countings'].get('blank_lines', 0),
|
|
}
|
|
|
|
if res['baseline_metrics'] and res['current_metrics']:
|
|
res['metrics_delta'] = {
|
|
'func_count': res['current_metrics']['func_count'] - res['baseline_metrics'].get('func_count', 0),
|
|
'avg_cc': res['current_metrics']['avg_cc'] - res['baseline_metrics'].get('avg_cc', 0.0),
|
|
'max_cc': res['current_metrics']['max_cc'] - res['baseline_metrics'].get('max_cc', 0),
|
|
'mi': res['current_metrics']['mi'] - res['baseline_metrics'].get('mi', 0.0),
|
|
}
|
|
|
|
return res
|
|
|
|
# Batch buffer for GUI updates
|
|
_batch_buffer = []
|
|
_batch_size = 50
|
|
|
|
# Progress handler
|
|
def _on_progress(item_res):
|
|
try:
|
|
fileA = item_res.get('fileA') or ''
|
|
fileB = item_res.get('fileB') or ''
|
|
counts = item_res.get('counts') or {}
|
|
|
|
countings_delta = item_res.get('countings_delta')
|
|
metrics_delta = item_res.get('metrics_delta')
|
|
|
|
# Format delta values
|
|
def format_delta(value, is_float=False):
|
|
if value is None:
|
|
return ''
|
|
if is_float:
|
|
formatted = f"{value:.2f}"
|
|
if value > 0:
|
|
return f"+{formatted}"
|
|
return formatted
|
|
else:
|
|
if value > 0:
|
|
return f"+{value}"
|
|
return str(value)
|
|
|
|
delta_code = format_delta(countings_delta.get('code_lines') if countings_delta else None)
|
|
delta_comment = format_delta(countings_delta.get('comment_lines') if countings_delta else None)
|
|
delta_blank = format_delta(countings_delta.get('blank_lines') if countings_delta else None)
|
|
delta_func = format_delta(metrics_delta.get('func_count') if metrics_delta else None)
|
|
delta_cc = format_delta(metrics_delta.get('avg_cc') if metrics_delta else None, is_float=True)
|
|
delta_mi = format_delta(metrics_delta.get('mi') if metrics_delta else None, is_float=True)
|
|
|
|
# Add to batch buffer
|
|
_batch_buffer.append((fileA, fileB, counts.get('added',0), counts.get('deleted',0), counts.get('modified',0), counts.get('unmodified',0), delta_code, delta_comment, delta_blank, delta_func, delta_cc, delta_mi))
|
|
|
|
# Insert batch when buffer is full
|
|
if len(_batch_buffer) >= _batch_size:
|
|
for row_values in _batch_buffer:
|
|
self.app.results_tree.insert('', 'end', values=row_values)
|
|
_batch_buffer.clear()
|
|
|
|
# Update progress (less frequent)
|
|
try:
|
|
self.app._processed_files = getattr(self.app, '_processed_files', 0) + 1
|
|
if self.app._processed_files % 10 == 0 or self.app._processed_files == len(pairs):
|
|
self.app._lbl_files.config(text=f"Files: {self.app._processed_files}/{len(pairs)}")
|
|
self.app.progress['maximum'] = max(1, len(pairs))
|
|
self.app.progress['value'] = self.app._processed_files
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
# Done handler
|
|
def _on_done(all_results):
|
|
# Flush remaining batch buffer
|
|
if _batch_buffer:
|
|
for row_values in _batch_buffer:
|
|
self.app.results_tree.insert('', 'end', values=row_values)
|
|
_batch_buffer.clear()
|
|
|
|
# Compute totals
|
|
total = {"added":0, "deleted":0, "modified":0, "unmodified":0}
|
|
current_files_list = []
|
|
for it in all_results:
|
|
c = it.get('counts', {})
|
|
total['added'] += c.get('added',0)
|
|
total['deleted'] += c.get('deleted',0)
|
|
total['modified'] += c.get('modified',0)
|
|
total['unmodified'] += c.get('unmodified',0)
|
|
fm = it.get('file_meta')
|
|
if fm:
|
|
current_files_list.append(fm)
|
|
|
|
result = {"baseline_id": meta.baseline_id, "compared_at": time.time(), "total": total, "pairs": all_results}
|
|
|
|
# Build summary statistics
|
|
try:
|
|
baseline_counts = {"physical_lines": 0, "code_lines": 0, "comment_lines": 0, "blank_lines": 0, "file_count": 0}
|
|
baseline_metrics = {"file_count": 0, "total_func_count": 0, "avg_avg_cc": 0.0, "avg_mi": 0.0}
|
|
baseline_metrics_count = 0
|
|
|
|
for fm in meta.files:
|
|
if hasattr(fm, 'countings') and fm.countings:
|
|
baseline_counts["physical_lines"] += fm.countings.get("physical_lines", 0)
|
|
baseline_counts["code_lines"] += fm.countings.get("code_lines", 0)
|
|
baseline_counts["comment_lines"] += fm.countings.get("comment_lines", 0)
|
|
baseline_counts["blank_lines"] += fm.countings.get("blank_lines", 0)
|
|
baseline_counts["file_count"] += 1
|
|
if hasattr(fm, 'metrics') and fm.metrics:
|
|
baseline_metrics["total_func_count"] += fm.metrics.get("func_count", 0)
|
|
baseline_metrics["avg_avg_cc"] += fm.metrics.get("avg_cc", 0.0)
|
|
baseline_metrics["avg_mi"] += fm.metrics.get("mi", 0.0)
|
|
baseline_metrics["file_count"] += 1
|
|
baseline_metrics_count += 1
|
|
|
|
if baseline_metrics_count > 0:
|
|
baseline_metrics["avg_avg_cc"] /= baseline_metrics_count
|
|
baseline_metrics["avg_mi"] /= baseline_metrics_count
|
|
|
|
current_counts = {"physical_lines": 0, "code_lines": 0, "comment_lines": 0, "blank_lines": 0, "file_count": 0}
|
|
current_metrics = {"file_count": 0, "total_func_count": 0, "avg_avg_cc": 0.0, "avg_mi": 0.0}
|
|
current_metrics_count = 0
|
|
|
|
for fm in current_files_list:
|
|
c = fm.get('countings')
|
|
if c:
|
|
current_counts["physical_lines"] += c.get("physical_lines", 0)
|
|
current_counts["code_lines"] += c.get("code_lines", 0)
|
|
current_counts["comment_lines"] += c.get("comment_lines", 0)
|
|
current_counts["blank_lines"] += c.get("blank_lines", 0)
|
|
current_counts["file_count"] += 1
|
|
m = fm.get('metrics')
|
|
if m:
|
|
current_metrics["total_func_count"] += m.get("func_count", 0)
|
|
current_metrics["avg_avg_cc"] += m.get("avg_cc", 0.0)
|
|
current_metrics["avg_mi"] += m.get("mi", 0.0)
|
|
current_metrics["file_count"] += 1
|
|
current_metrics_count += 1
|
|
|
|
if current_metrics_count > 0:
|
|
current_metrics["avg_avg_cc"] /= current_metrics_count
|
|
current_metrics["avg_mi"] /= current_metrics_count
|
|
|
|
result["summary"] = {
|
|
"baseline": {"countings": baseline_counts, "metrics": baseline_metrics},
|
|
"current": {"countings": current_counts, "metrics": current_metrics}
|
|
}
|
|
except Exception:
|
|
pass
|
|
|
|
self.app._current_results = result
|
|
on_diff_done(result)
|
|
|
|
self.app._current_task_id = self.app.worker.map_iterable(_process_pair, pairs, kind='thread', on_progress=_on_progress, on_done=_on_done)
|
|
self.app.cancel_btn.config(state='normal')
|
|
|
|
def _setup_differ_roots(self, bm, chosen, meta, project):
|
|
"""Setup baseline and current roots for diff viewer."""
|
|
import zipfile
|
|
|
|
baseline_dir = bm._baseline_dir(chosen)
|
|
baseline_snapshot_dir = os.path.join(baseline_dir, "files")
|
|
baseline_zip = os.path.join(baseline_dir, "files.zip")
|
|
|
|
if os.path.isdir(baseline_snapshot_dir):
|
|
self.app._differ_baseline_root = baseline_snapshot_dir
|
|
elif os.path.exists(baseline_zip):
|
|
try:
|
|
with zipfile.ZipFile(baseline_zip, 'r') as zip_ref:
|
|
zip_ref.extractall(baseline_snapshot_dir)
|
|
self.app._differ_baseline_root = baseline_snapshot_dir
|
|
except Exception as e:
|
|
self.app.log(f"Failed to extract baseline snapshot: {e}", level='WARNING')
|
|
self.app._differ_baseline_root = meta.project_root
|
|
else:
|
|
self.app.log("No baseline snapshot found, using project_root (may be inaccurate)", level='WARNING')
|
|
self.app._differ_baseline_root = meta.project_root
|
|
|
|
self.app._differ_current_root = project
|
|
|
|
def _select_baseline_dialog(self, baselines):
|
|
"""Show modal dialog to select a baseline."""
|
|
sorted_b = sorted(baselines)
|
|
|
|
dlg = tk.Toplevel(self.app)
|
|
dlg.title("Select baseline to compare")
|
|
dlg.transient(self.app)
|
|
dlg.grab_set()
|
|
|
|
# Center dialog
|
|
dlg.update_idletasks()
|
|
pw = self.app.winfo_width()
|
|
ph = self.app.winfo_height()
|
|
px = self.app.winfo_rootx()
|
|
py = self.app.winfo_rooty()
|
|
dw = dlg.winfo_reqwidth()
|
|
dh = dlg.winfo_reqheight()
|
|
x = px + (pw - dw) // 2
|
|
y = py + (ph - dh) // 2
|
|
dlg.geometry(f"+{x}+{y}")
|
|
|
|
ttk.Label(dlg, text="Select a baseline:").grid(row=0, column=0, padx=12, pady=(12,4), sticky='w')
|
|
listbox = tk.Listbox(dlg, height=min(10, len(sorted_b)), width=60, exportselection=False)
|
|
for item in sorted_b:
|
|
listbox.insert('end', item)
|
|
listbox.grid(row=1, column=0, padx=12, pady=(0,12))
|
|
listbox.select_set(len(sorted_b)-1) # Select latest
|
|
|
|
selected = {'id': None}
|
|
|
|
def _on_ok():
|
|
sel = listbox.curselection()
|
|
if not sel:
|
|
messagebox.showwarning('Select baseline', 'Please select a baseline to compare')
|
|
return
|
|
selected['id'] = listbox.get(sel[0])
|
|
dlg.destroy()
|
|
|
|
def _on_cancel():
|
|
selected['id'] = None
|
|
dlg.destroy()
|
|
|
|
btn_frame = ttk.Frame(dlg)
|
|
btn_frame.grid(row=2, column=0, pady=(0,12), sticky='e', padx=12)
|
|
ok_btn = ttk.Button(btn_frame, text='OK', command=_on_ok)
|
|
ok_btn.grid(row=0, column=0, padx=(0,8))
|
|
cancel_btn = ttk.Button(btn_frame, text='Cancel', command=_on_cancel)
|
|
cancel_btn.grid(row=0, column=1)
|
|
|
|
def _on_dbl(evt):
|
|
idx = listbox.curselection()
|
|
if idx:
|
|
selected['id'] = listbox.get(idx[0])
|
|
dlg.destroy()
|
|
|
|
listbox.bind('<Double-Button-1>', _on_dbl)
|
|
self.app.wait_window(dlg)
|
|
|
|
return selected.get('id')
|
|
|
|
def _gather_source_files(self, paths, allowed_exts, ignore_patterns):
|
|
"""Gather source files from given paths."""
|
|
files = []
|
|
for p in paths:
|
|
pth = Path(p)
|
|
try:
|
|
if pth.is_dir():
|
|
files.extend(find_source_files(pth, allowed_extensions=allowed_exts, ignore_patterns=ignore_patterns))
|
|
elif pth.is_file():
|
|
files.append(pth)
|
|
except Exception:
|
|
continue
|
|
|
|
# Deduplicate
|
|
seen = set()
|
|
unique = []
|
|
for f in files:
|
|
s = str(f)
|
|
if s not in seen:
|
|
seen.add(s)
|
|
unique.append(f)
|
|
return unique
|
|
|
|
def _reset_progress_ui(self):
|
|
"""Reset progress counters and UI elements."""
|
|
self.app._processed_files = 0
|
|
self.app._total_files = 0
|
|
try:
|
|
self.app.progress['maximum'] = 1
|
|
self.app.progress['value'] = 0
|
|
self.app._lbl_files.config(text="Files: 0/0")
|
|
except Exception:
|
|
pass
|
|
|
|
def _clear_results(self):
|
|
"""Clear results tree."""
|
|
for c in self.app.results_tree.get_children(""):
|
|
self.app.results_tree.delete(c)
|