538 lines
24 KiB
Python
538 lines
24 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk, messagebox
|
|
import pandas as pd
|
|
import json
|
|
import difflib
|
|
from typing import List, Dict, Any
|
|
|
|
class Tooltip:
|
|
"""
|
|
Create a tooltip for a given widget.
|
|
"""
|
|
def __init__(self, widget, text):
|
|
self.widget = widget
|
|
self.text = text
|
|
self.tooltip_window = None
|
|
self.widget.bind("<Enter>", self.show_tooltip)
|
|
self.widget.bind("<Leave>", self.hide_tooltip)
|
|
|
|
def show_tooltip(self, event=None):
|
|
x, y, _, _ = self.widget.bbox("insert")
|
|
x += self.widget.winfo_rootx() + 25
|
|
y += self.widget.winfo_rooty() + 25
|
|
|
|
self.tooltip_window = tk.Toplevel(self.widget)
|
|
self.tooltip_window.wm_overrideredirect(True)
|
|
self.tooltip_window.wm_geometry(f"+{x}+{y}")
|
|
|
|
label = tk.Label(self.tooltip_window, text=self.text, justify='left',
|
|
background="#ffffe0", relief='solid', borderwidth=1,
|
|
font=("tahoma", "8", "normal"))
|
|
label.pack(ipadx=1)
|
|
|
|
def hide_tooltip(self, event=None):
|
|
if self.tooltip_window:
|
|
self.tooltip_window.destroy()
|
|
self.tooltip_window = None
|
|
|
|
class FullValueWindow(tk.Toplevel):
|
|
"""A simple window to show the full, pretty-printed value with search functionality."""
|
|
def __init__(self, parent, title: str, value_str: str):
|
|
super().__init__(parent)
|
|
self.title(title)
|
|
self.transient(parent)
|
|
self.grab_set()
|
|
|
|
# --- State ---
|
|
self.matches = []
|
|
self.current_match_index = -1
|
|
self.search_term_var = tk.StringVar()
|
|
self.search_status_var = tk.StringVar()
|
|
|
|
# --- UI ---
|
|
self.geometry("800x600")
|
|
self._create_widgets(value_str)
|
|
self._bind_events()
|
|
|
|
def _create_widgets(self, value_str: str):
|
|
main_frame = ttk.Frame(self)
|
|
main_frame.pack(expand=True, fill=tk.BOTH)
|
|
main_frame.rowconfigure(1, weight=1)
|
|
main_frame.columnconfigure(0, weight=1)
|
|
|
|
# --- Search Bar ---
|
|
search_frame = ttk.Frame(main_frame, padding=(5, 5))
|
|
search_frame.grid(row=0, column=0, sticky="ew")
|
|
search_frame.columnconfigure(1, weight=1)
|
|
|
|
ttk.Label(search_frame, text="Find:").grid(row=0, column=0, padx=(0, 5))
|
|
|
|
self.search_entry = ttk.Entry(search_frame, textvariable=self.search_term_var)
|
|
self.search_entry.grid(row=0, column=1, sticky="ew")
|
|
|
|
search_button = ttk.Button(search_frame, text="Find", command=self._perform_search)
|
|
search_button.grid(row=0, column=2, padx=5)
|
|
|
|
search_status_label = ttk.Label(search_frame, textvariable=self.search_status_var, anchor="w")
|
|
search_status_label.grid(row=0, column=3, padx=5, sticky="w")
|
|
|
|
hint_label = ttk.Label(search_frame, text="(F3 for next)", foreground="gray")
|
|
hint_label.grid(row=0, column=4, padx=(5, 0), sticky="w")
|
|
|
|
# --- Text Area ---
|
|
text_frame = ttk.Frame(main_frame)
|
|
text_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0,5))
|
|
text_frame.rowconfigure(0, weight=1)
|
|
text_frame.columnconfigure(0, weight=1)
|
|
|
|
self.text_widget = tk.Text(text_frame, wrap=tk.NONE, font=("Consolas", 10))
|
|
self.text_widget.grid(row=0, column=0, sticky="nsew")
|
|
|
|
v_scroll = ttk.Scrollbar(text_frame, orient="vertical", command=self.text_widget.yview)
|
|
v_scroll.grid(row=0, column=1, sticky="ns")
|
|
h_scroll = ttk.Scrollbar(text_frame, orient="horizontal", command=self.text_widget.xview)
|
|
h_scroll.grid(row=1, column=0, sticky="ew")
|
|
self.text_widget.configure(yscrollcommand=v_scroll.set, xscrollcommand=h_scroll.set)
|
|
|
|
# --- Populate and Configure Text ---
|
|
try:
|
|
pretty_value = json.dumps(json.loads(value_str), indent=4)
|
|
self.text_widget.insert("1.0", pretty_value)
|
|
except (json.JSONDecodeError, TypeError):
|
|
self.text_widget.insert("1.0", value_str)
|
|
|
|
self.text_widget.config(state=tk.DISABLED)
|
|
self.text_widget.tag_configure("match", background="yellow", foreground="black")
|
|
self.text_widget.tag_configure("current_match", background="orange", foreground="black")
|
|
|
|
def _bind_events(self):
|
|
self.bind("<Escape>", lambda e: self.destroy())
|
|
self.bind("<F3>", self._find_next)
|
|
self.search_entry.bind("<Return>", self._perform_search)
|
|
|
|
def _clear_search(self):
|
|
self.text_widget.config(state=tk.NORMAL)
|
|
self.text_widget.tag_remove("match", "1.0", tk.END)
|
|
self.text_widget.tag_remove("current_match", "1.0", tk.END)
|
|
self.text_widget.config(state=tk.DISABLED)
|
|
self.matches = []
|
|
self.current_match_index = -1
|
|
self.search_status_var.set("")
|
|
|
|
def _perform_search(self, event=None):
|
|
self._clear_search()
|
|
term = self.search_term_var.get()
|
|
if not term:
|
|
return
|
|
|
|
self.text_widget.config(state=tk.NORMAL)
|
|
|
|
start_pos = "1.0"
|
|
while True:
|
|
start_pos = self.text_widget.search(term, start_pos, stopindex=tk.END, nocase=True, count=tk.IntVar())
|
|
if not start_pos:
|
|
break
|
|
end_pos = f"{start_pos}+{len(term)}c"
|
|
self.matches.append((start_pos, end_pos))
|
|
self.text_widget.tag_add("match", start_pos, end_pos)
|
|
start_pos = end_pos
|
|
|
|
self.text_widget.config(state=tk.DISABLED)
|
|
|
|
if self.matches:
|
|
self.search_status_var.set(f"{len(self.matches)} found")
|
|
self._find_next()
|
|
else:
|
|
self.search_status_var.set("Not found")
|
|
|
|
def _find_next(self, event=None):
|
|
if not self.matches:
|
|
return
|
|
|
|
self.text_widget.config(state=tk.NORMAL)
|
|
# Remove tag from previous current match
|
|
if self.current_match_index != -1:
|
|
start, end = self.matches[self.current_match_index]
|
|
self.text_widget.tag_add("match", start, end)
|
|
self.text_widget.tag_remove("current_match", start, end)
|
|
|
|
# Move to next match
|
|
self.current_match_index = (self.current_match_index + 1) % len(self.matches)
|
|
|
|
# Highlight new current match
|
|
start, end = self.matches[self.current_match_index]
|
|
self.text_widget.tag_remove("match", start, end)
|
|
self.text_widget.tag_add("current_match", start, end)
|
|
self.text_widget.see(start)
|
|
self.text_widget.config(state=tk.DISABLED)
|
|
|
|
self.search_status_var.set(f"{self.current_match_index + 1}/{len(self.matches)}")
|
|
|
|
|
|
class TimelineWindow(tk.Toplevel):
|
|
"""
|
|
A window to display a master-detail view of variable change events.
|
|
"""
|
|
def __init__(self, parent, timeline_df: pd.DataFrame, summary_data: Dict[str, Any]):
|
|
super().__init__(parent)
|
|
|
|
self.summary_data = summary_data
|
|
profile_name = self.summary_data.get("profile_name", "Unknown Profile")
|
|
|
|
self.title(f"Change Event Analysis for: {profile_name}")
|
|
|
|
self.is_fullscreen = True
|
|
self.attributes('-fullscreen', True)
|
|
self.bind("<Escape>", self._close_window)
|
|
self.grab_set()
|
|
|
|
self.all_change_events: List[Dict[str, Any]] = []
|
|
self.filter_var = tk.StringVar(value="ALL")
|
|
|
|
self._process_data_for_event_table(timeline_df)
|
|
self._create_widgets()
|
|
self._populate_filter_combobox()
|
|
self._populate_event_table()
|
|
|
|
def _process_data_for_event_table(self, df: pd.DataFrame):
|
|
self.all_change_events = []
|
|
if df.empty:
|
|
return
|
|
|
|
for var_name, group in df.groupby('variable_name'):
|
|
sorted_group = group.sort_values(by='timestamp').reset_index()
|
|
for i in range(1, len(sorted_group)):
|
|
prev_row = sorted_group.iloc[i-1]
|
|
curr_row = sorted_group.iloc[i]
|
|
|
|
if prev_row['value'] != curr_row['value']:
|
|
event = {
|
|
"timestamp": curr_row['timestamp'],
|
|
"variable_name": var_name,
|
|
"previous_value": prev_row['value'],
|
|
"new_value": curr_row['value']
|
|
}
|
|
self.all_change_events.append(event)
|
|
|
|
self.all_change_events.sort(key=lambda x: x['timestamp'])
|
|
|
|
def _create_widgets(self):
|
|
main_frame = ttk.Frame(self, padding="10")
|
|
main_frame.pack(expand=True, fill=tk.BOTH)
|
|
main_frame.rowconfigure(2, weight=1) # Main content (paned window) will expand
|
|
main_frame.columnconfigure(0, weight=1)
|
|
|
|
# --- Header Frame ---
|
|
header_frame = ttk.Frame(main_frame)
|
|
header_frame.grid(row=0, column=0, sticky="ew", pady=(0, 5))
|
|
header_frame.columnconfigure(0, weight=1)
|
|
profile_name = self.summary_data.get("profile_name", "Unknown Profile")
|
|
run_start_time = self.summary_data.get("start_time", "Unknown Time")
|
|
title_label = ttk.Label(
|
|
header_frame,
|
|
text=f"Profile: '{profile_name}' (Run: {run_start_time})",
|
|
font=("TkDefaultFont", 14, "bold")
|
|
)
|
|
title_label.grid(row=0, column=0, sticky="w")
|
|
button_group = ttk.Frame(header_frame)
|
|
button_group.grid(row=0, column=1, sticky="e")
|
|
toggle_fullscreen_button = ttk.Button(
|
|
button_group, text="Toggle Fullscreen", command=self._toggle_fullscreen
|
|
)
|
|
toggle_fullscreen_button.pack(side=tk.LEFT, padx=5)
|
|
close_button = ttk.Button(button_group, text="Close", command=self._close_window)
|
|
close_button.pack(side=tk.LEFT, padx=5)
|
|
|
|
# --- Breakpoint Status Frame ---
|
|
bp_status_frame = ttk.LabelFrame(main_frame, text="Breakpoint Status", padding=5)
|
|
bp_status_frame.grid(row=1, column=0, sticky="ew", pady=5)
|
|
|
|
# Add "View Summary" button
|
|
view_summary_button = ttk.Button(
|
|
bp_status_frame, text="View Summary", command=self._view_summary
|
|
)
|
|
view_summary_button.grid(row=0, column=0, sticky="w", padx=5)
|
|
|
|
actions = self.summary_data.get('actions_summary', [])
|
|
for i, action in enumerate(actions):
|
|
spec = action.get("breakpoint_spec", "N/A")
|
|
status = action.get("status", "Unknown")
|
|
|
|
if "Error" in status:
|
|
color = "red"
|
|
elif "Completed" in status:
|
|
color = "green"
|
|
else:
|
|
color = "orange" # For pending or other states
|
|
|
|
bp_label = ttk.Label(bp_status_frame, text=f"● {spec}", foreground=color)
|
|
bp_label.grid(row=0, column=i+1, sticky="w", padx=10) # Shift columns for breakpoint labels
|
|
Tooltip(bp_label, text=status)
|
|
|
|
# --- Paned Window for Master-Detail (Left/Right) ---
|
|
paned_window = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL)
|
|
paned_window.grid(row=2, column=0, sticky="nsew")
|
|
|
|
# --- Left Pane (Master): Event Table & Filter ---
|
|
master_frame = ttk.Frame(paned_window, padding="5")
|
|
paned_window.add(master_frame, weight=1)
|
|
master_frame.rowconfigure(1, weight=1)
|
|
master_frame.columnconfigure(0, weight=1)
|
|
|
|
filter_frame = ttk.Frame(master_frame)
|
|
filter_frame.grid(row=0, column=0, sticky="ew", pady=(0, 5))
|
|
filter_frame.columnconfigure(1, weight=1)
|
|
ttk.Label(filter_frame, text="Filter by Variable:").grid(row=0, column=0, sticky="w", padx=(0,5))
|
|
self.filter_combo = ttk.Combobox(filter_frame, textvariable=self.filter_var, state="readonly")
|
|
self.filter_combo.grid(row=0, column=1, sticky="ew")
|
|
self.filter_combo.bind("<<ComboboxSelected>>", self._on_filter_selected)
|
|
|
|
table_container = ttk.LabelFrame(master_frame, text="Change Events")
|
|
table_container.grid(row=1, column=0, sticky="nsew")
|
|
table_container.rowconfigure(0, weight=1)
|
|
table_container.columnconfigure(0, weight=1)
|
|
|
|
self.event_tree = ttk.Treeview(
|
|
table_container, columns=("timestamp", "variable"), show="headings", selectmode="browse"
|
|
)
|
|
self.event_tree.grid(row=0, column=0, sticky="nsew")
|
|
self.event_tree.heading("timestamp", text="Timestamp")
|
|
self.event_tree.heading("variable", text="Variable Name")
|
|
self.event_tree.column("timestamp", width=180, stretch=False)
|
|
self.event_tree.column("variable", width=200, stretch=True)
|
|
tree_scroll = ttk.Scrollbar(table_container, orient="vertical", command=self.event_tree.yview)
|
|
tree_scroll.grid(row=0, column=1, sticky="ns")
|
|
self.event_tree.configure(yscrollcommand=tree_scroll.set)
|
|
self.event_tree.bind("<<TreeviewSelect>>", self._on_event_selected)
|
|
self.event_tree.bind("<Button-3>", self._show_context_menu)
|
|
|
|
# --- Right Pane (Detail): Diff View ---
|
|
detail_frame = ttk.LabelFrame(paned_window, text="Value Diff", padding="5")
|
|
paned_window.add(detail_frame, weight=3)
|
|
detail_frame.rowconfigure(0, weight=1)
|
|
detail_frame.columnconfigure(0, weight=1)
|
|
|
|
# Frame for Unified Diff (JSON)
|
|
self.unified_diff_frame = ttk.Frame(detail_frame)
|
|
self.unified_diff_frame.grid(row=0, column=0, sticky="nsew")
|
|
self.unified_diff_frame.rowconfigure(0, weight=1)
|
|
self.unified_diff_frame.columnconfigure(0, weight=1)
|
|
|
|
self.diff_text = tk.Text(self.unified_diff_frame, wrap=tk.NONE, state=tk.DISABLED, font=("Consolas", 10))
|
|
self.diff_text.grid(row=0, column=0, sticky="nsew")
|
|
diff_v_scroll = ttk.Scrollbar(self.unified_diff_frame, orient="vertical", command=self.diff_text.yview)
|
|
diff_v_scroll.grid(row=0, column=1, sticky="ns")
|
|
diff_h_scroll = ttk.Scrollbar(self.unified_diff_frame, orient="horizontal", command=self.diff_text.xview)
|
|
diff_h_scroll.grid(row=1, column=0, sticky="ew")
|
|
self.diff_text.configure(yscrollcommand=diff_v_scroll.set, xscrollcommand=diff_h_scroll.set)
|
|
|
|
self.diff_text.tag_configure("added", foreground="#008800")
|
|
self.diff_text.tag_configure("removed", foreground="#CC0000")
|
|
self.diff_text.tag_configure("header", foreground="blue", font=("Consolas", 10, "bold"))
|
|
|
|
# Frame for Side-by-Side Diff (CSV)
|
|
self.side_by_side_diff_frame = ttk.Frame(detail_frame)
|
|
# Initially hidden by unified_diff_frame, will be shown with grid()
|
|
self.side_by_side_diff_frame.grid(row=0, column=0, sticky="nsew")
|
|
self.side_by_side_diff_frame.rowconfigure(0, weight=1)
|
|
self.side_by_side_diff_frame.columnconfigure(0, weight=1)
|
|
self.side_by_side_diff_frame.columnconfigure(1, weight=1)
|
|
|
|
# PanedWindow for side-by-side view
|
|
self.side_by_side_paned_window = ttk.PanedWindow(self.side_by_side_diff_frame, orient=tk.HORIZONTAL)
|
|
self.side_by_side_paned_window.pack(expand=True, fill=tk.BOTH)
|
|
|
|
# Left Text Widget (Previous Value)
|
|
prev_frame = ttk.Frame(self.side_by_side_paned_window)
|
|
self.side_by_side_paned_window.add(prev_frame, weight=1)
|
|
|
|
# Use grid for better control over Text and Scrollbars
|
|
prev_frame.rowconfigure(1, weight=1) # Row for Text widget
|
|
prev_frame.columnconfigure(0, weight=1) # Column for Text widget
|
|
|
|
ttk.Label(prev_frame, text="Previous Value", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, columnspan=2, sticky="ew")
|
|
|
|
self.prev_value_text = tk.Text(prev_frame, wrap=tk.NONE, state=tk.DISABLED, font=("Consolas", 10))
|
|
self.prev_value_text.grid(row=1, column=0, sticky="nsew")
|
|
|
|
prev_v_scroll = ttk.Scrollbar(prev_frame, orient="vertical", command=self.prev_value_text.yview)
|
|
prev_v_scroll.grid(row=1, column=1, sticky="ns")
|
|
|
|
prev_h_scroll = ttk.Scrollbar(prev_frame, orient="horizontal", command=self.prev_value_text.xview)
|
|
prev_h_scroll.grid(row=2, column=0, columnspan=2, sticky="ew") # Placed below the text, spanning its column
|
|
|
|
self.prev_value_text.configure(yscrollcommand=prev_v_scroll.set, xscrollcommand=prev_h_scroll.set)
|
|
|
|
# Right Text Widget (New Value)
|
|
new_frame = ttk.Frame(self.side_by_side_paned_window)
|
|
self.side_by_side_paned_window.add(new_frame, weight=1)
|
|
|
|
# Use grid for better control over Text and Scrollbars
|
|
new_frame.rowconfigure(1, weight=1) # Row for Text widget
|
|
new_frame.columnconfigure(0, weight=1) # Column for Text widget
|
|
|
|
ttk.Label(new_frame, text="New Value", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, columnspan=2, sticky="ew")
|
|
|
|
self.new_value_text = tk.Text(new_frame, wrap=tk.NONE, state=tk.DISABLED, font=("Consolas", 10))
|
|
self.new_value_text.grid(row=1, column=0, sticky="nsew")
|
|
|
|
new_v_scroll = ttk.Scrollbar(new_frame, orient="vertical", command=self.new_value_text.yview)
|
|
new_v_scroll.grid(row=1, column=1, sticky="ns")
|
|
|
|
new_h_scroll = ttk.Scrollbar(new_frame, orient="horizontal", command=self.new_value_text.xview)
|
|
new_h_scroll.grid(row=2, column=0, columnspan=2, sticky="ew") # Placed below the text, spanning its column
|
|
|
|
self.new_value_text.configure(yscrollcommand=new_v_scroll.set, xscrollcommand=new_h_scroll.set)
|
|
|
|
# Synchronize scrolling for side-by-side view
|
|
self.prev_value_text.configure(yscrollcommand=lambda *args: self._on_yview_scroll(self.prev_value_text, self.new_value_text, *args))
|
|
self.new_value_text.configure(yscrollcommand=lambda *args: self._on_yview_scroll(self.new_value_text, self.prev_value_text, *args))
|
|
|
|
# --- Context Menu ---
|
|
self.context_menu = tk.Menu(self, tearoff=0)
|
|
self.context_menu.add_command(label="View Full Value at this Timestamp", command=self._view_full_value)
|
|
|
|
def _on_yview_scroll(self, source_widget, target_widget, *args):
|
|
# The first argument in *args is the fraction representing the top of the visible content
|
|
source_widget.yview_moveto(args[0])
|
|
target_widget.yview_moveto(args[0])
|
|
|
|
def _populate_filter_combobox(self):
|
|
variable_names = sorted(list(set(event['variable_name'] for event in self.all_change_events)))
|
|
self.filter_combo['values'] = ["ALL"] + variable_names
|
|
|
|
def _on_filter_selected(self, event=None):
|
|
self._populate_event_table()
|
|
|
|
def _populate_event_table(self):
|
|
for item in self.event_tree.get_children():
|
|
self.event_tree.delete(item)
|
|
|
|
filter_value = self.filter_var.get()
|
|
|
|
events_to_show = self.all_change_events
|
|
if filter_value != "ALL":
|
|
events_to_show = [e for e in self.all_change_events if e['variable_name'] == filter_value]
|
|
|
|
for i, event in enumerate(events_to_show):
|
|
ts = event['timestamp'].strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
|
var_name = event['variable_name']
|
|
# Use the original index from all_change_events as the IID
|
|
original_index = self.all_change_events.index(event)
|
|
self.event_tree.insert("", tk.END, iid=str(original_index), values=(ts, var_name))
|
|
|
|
def _is_json(self, value_str):
|
|
try:
|
|
json.loads(value_str)
|
|
return True
|
|
except (json.JSONDecodeError, TypeError):
|
|
return False
|
|
|
|
def _on_event_selected(self, event=None):
|
|
selection = self.event_tree.selection()
|
|
if not selection:
|
|
# Clear all diff views
|
|
self.unified_diff_frame.grid_remove()
|
|
self.side_by_side_diff_frame.grid_remove()
|
|
return
|
|
|
|
event_index = int(selection[0])
|
|
event_data = self.all_change_events[event_index]
|
|
prev_val_str = event_data['previous_value']
|
|
new_val_str = event_data['new_value']
|
|
|
|
is_prev_json = self._is_json(prev_val_str)
|
|
is_new_json = self._is_json(new_val_str)
|
|
|
|
# Clear previous content and hide all frames
|
|
self.diff_text.config(state=tk.NORMAL)
|
|
self.diff_text.delete('1.0', tk.END)
|
|
self.diff_text.config(state=tk.DISABLED)
|
|
|
|
self.prev_value_text.config(state=tk.NORMAL)
|
|
self.prev_value_text.delete('1.0', tk.END)
|
|
self.prev_value_text.config(state=tk.DISABLED)
|
|
|
|
self.new_value_text.config(state=tk.NORMAL)
|
|
self.new_value_text.delete('1.0', tk.END)
|
|
self.new_value_text.config(state=tk.DISABLED)
|
|
|
|
self.unified_diff_frame.grid_remove()
|
|
self.side_by_side_diff_frame.grid_remove()
|
|
|
|
if is_prev_json and is_new_json:
|
|
# Show Unified Diff for JSON
|
|
self.unified_diff_frame.grid(row=0, column=0, sticky="nsew")
|
|
self.diff_text.config(state=tk.NORMAL)
|
|
try:
|
|
prev_pretty = json.dumps(json.loads(prev_val_str), indent=4).splitlines()
|
|
new_pretty = json.dumps(json.loads(new_val_str), indent=4).splitlines()
|
|
diff = difflib.unified_diff(prev_pretty, new_pretty, lineterm='', fromfile='Previous', tofile='New')
|
|
|
|
for line in diff:
|
|
if line.startswith('+') and not line.startswith('+++'):
|
|
self.diff_text.insert(tk.END, line + '\n', "added")
|
|
elif line.startswith('-') and not line.startswith('---'):
|
|
self.diff_text.insert(tk.END, line + '\n', "removed")
|
|
elif line.startswith('@@'):
|
|
self.diff_text.insert(tk.END, line + '\n', "header")
|
|
else:
|
|
self.diff_text.insert(tk.END, line + '\n')
|
|
except (json.JSONDecodeError, TypeError):
|
|
# Fallback to plain text if pretty-printing fails unexpectedly
|
|
self.diff_text.insert(tk.END, "--- PREVIOUS VALUE ---\n", "header")
|
|
self.diff_text.insert(tk.END, prev_val_str + "\n\n")
|
|
self.diff_text.insert(tk.END, "--- NEW VALUE ---\n", "header")
|
|
self.diff_text.insert(tk.END, new_val_str + "\n")
|
|
|
|
finally:
|
|
self.diff_text.config(state=tk.DISABLED)
|
|
else:
|
|
# Show Side-by-Side Diff for CSV/Plain Text
|
|
self.side_by_side_diff_frame.grid(row=0, column=0, sticky="nsew")
|
|
self.prev_value_text.config(state=tk.NORMAL)
|
|
self.prev_value_text.insert(tk.END, prev_val_str)
|
|
self.prev_value_text.config(state=tk.DISABLED)
|
|
|
|
self.new_value_text.config(state=tk.NORMAL)
|
|
self.new_value_text.insert(tk.END, new_val_str)
|
|
self.new_value_text.config(state=tk.DISABLED)
|
|
|
|
def _show_context_menu(self, event):
|
|
selection = self.event_tree.identify_row(event.y)
|
|
if selection:
|
|
self.event_tree.selection_set(selection)
|
|
self.context_menu.post(event.x_root, event.y_root)
|
|
|
|
def _view_full_value(self):
|
|
selection = self.event_tree.selection()
|
|
if not selection:
|
|
return
|
|
|
|
event_index = int(selection[0])
|
|
event_data = self.all_change_events[event_index]
|
|
value_str = event_data['new_value']
|
|
var_name = event_data['variable_name']
|
|
timestamp = event_data['timestamp'].strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
|
|
|
title = f"Full Value of '{var_name}' at {timestamp}"
|
|
FullValueWindow(self, title=title, value_str=value_str)
|
|
|
|
def _toggle_fullscreen(self, event=None):
|
|
self.is_fullscreen = not self.is_fullscreen
|
|
self.attributes("-fullscreen", self.is_fullscreen)
|
|
|
|
def _view_summary(self):
|
|
"""Opens a FullValueWindow to display the profile summary data."""
|
|
try:
|
|
summary_json_str = json.dumps(self.summary_data, indent=4)
|
|
except TypeError:
|
|
# Fallback for non-JSON serializable data
|
|
summary_json_str = str(self.summary_data)
|
|
|
|
title = "Profile Summary"
|
|
FullValueWindow(self, title=title, value_str=summary_json_str)
|
|
|
|
def _close_window(self, event=None):
|
|
self.destroy()
|