Text Comparison
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, colorchooser, filedialog
from difflib import SequenceMatcher
import json
import os
import sys
from datetime import datetime
import re
class SearchReplaceDialog(tk.Toplevel):
def __init__(self, parent, text_widget, search_color):
super().__init__(parent)
self.text_widget = text_widget
self.search_color = search_color
self.title("Search & Replace")
self.geometry("450x180")
self.resizable(False, False)
# Search entry
ttk.Label(self, text="Find:").grid(row=0, column=0, padx=10, pady=10, sticky=tk.W)
self.search_var = tk.StringVar()
self.search_entry = ttk.Entry(self, textvariable=self.search_var, width=35)
self.search_entry.grid(row=0, column=1, padx=10, pady=10, columnspan=2)
self.search_entry.focus()
# Replace entry
ttk.Label(self, text="Replace:").grid(row=1, column=0, padx=10, pady=10, sticky=tk.W)
self.replace_var = tk.StringVar()
self.replace_entry = ttk.Entry(self, textvariable=self.replace_var, width=35)
self.replace_entry.grid(row=1, column=1, padx=10, pady=10, columnspan=2)
# Options
self.case_sensitive_var = tk.BooleanVar(value=False)
ttk.Checkbutton(self, text="Case Sensitive",
variable=self.case_sensitive_var).grid(row=2, column=1, sticky=tk.W, pady=5)
# Buttons
btn_frame = ttk.Frame(self)
btn_frame.grid(row=3, column=0, columnspan=3, pady=10)
ttk.Button(btn_frame, text="Find Next", command=self.find_next).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Replace", command=self.replace_current).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Replace All", command=self.replace_all).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Clear", command=self.clear_search).pack(side=tk.LEFT, padx=5)
# Position tracking
self.current_pos = '1.0'
# Bind Enter key
self.search_entry.bind('<Return>', lambda e: self.find_next())
# Configure search tag
self.text_widget.tag_config('search_highlight', background=self.search_color)
self.text_widget.tag_raise('search_highlight')
def find_next(self):
"""Find next occurrence of search term"""
search_term = self.search_var.get()
if not search_term:
return
nocase = not self.case_sensitive_var.get()
# Search from current position
pos = self.text_widget.search(search_term, self.current_pos, tk.END, nocase=nocase)
if pos:
# Calculate end position
end_pos = f"{pos}+{len(search_term)}c"
# Highlight the found text
self.text_widget.tag_remove('search_highlight', '1.0', tk.END)
self.text_widget.tag_add('search_highlight', pos, end_pos)
# Scroll to make it visible
self.text_widget.see(pos)
# Update current position for next search
self.current_pos = end_pos
else:
# No more matches, wrap around
self.current_pos = '1.0'
messagebox.showinfo("Search", "No more matches found. Wrapping to beginning.")
def replace_current(self):
"""Replace current highlighted occurrence"""
try:
# Check if there's a highlighted selection
ranges = self.text_widget.tag_ranges('search_highlight')
if ranges:
start, end = ranges[0], ranges[1]
self.text_widget.delete(start, end)
self.text_widget.insert(start, self.replace_var.get())
self.text_widget.tag_remove('search_highlight', '1.0', tk.END)
self.find_next()
except:
messagebox.showwarning("Replace", "No text selected. Use Find Next first.")
def replace_all(self):
"""Replace all occurrences"""
search_term = self.search_var.get()
replace_term = self.replace_var.get()
if not search_term:
return
content = self.text_widget.get('1.0', tk.END)
if self.case_sensitive_var.get():
new_content = content.replace(search_term, replace_term)
count = content.count(search_term)
else:
# Case insensitive replace
pattern = re.compile(re.escape(search_term), re.IGNORECASE)
new_content = pattern.sub(replace_term, content)
count = len(pattern.findall(content))
self.text_widget.delete('1.0', tk.END)
self.text_widget.insert('1.0', new_content)
messagebox.showinfo("Replace All", f"Replaced {count} occurrence(s)")
def clear_search(self):
"""Clear all search highlights"""
self.text_widget.tag_remove('search_highlight', '1.0', tk.END)
self.search_var.set("")
self.replace_var.set("")
self.current_pos = '1.0'
class HistoryDialog(tk.Toplevel):
def __init__(self, parent, app):
super().__init__(parent)
self.app = app
self.title("Comparison History")
self.geometry("600x400")
# Create listbox with scrollbar
frame = ttk.Frame(self, padding=10)
frame.pack(fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.listbox = tk.Listbox(frame, yscrollcommand=scrollbar.set, font=('Arial', 10))
self.listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.listbox.yview)
# Load history
self.load_history()
# Buttons
btn_frame = ttk.Frame(self)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="Load Selected", command=self.load_selected).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Delete Selected", command=self.delete_selected).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Clear All", command=self.clear_all).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Close", command=self.destroy).pack(side=tk.LEFT, padx=5)
# Double click to load
self.listbox.bind('<Double-Button-1>', lambda e: self.load_selected())
def load_history(self):
"""Load and display history"""
self.listbox.delete(0, tk.END)
history = self.app.settings.get('history', [])
for idx, item in enumerate(reversed(history)):
timestamp = item.get('timestamp', 'Unknown')
preview1 = item.get('text1', '')[:50].replace('\n', ' ')
preview2 = item.get('text2', '')[:50].replace('\n', ' ')
display = f"{timestamp} | {preview1}... vs {preview2}..."
self.listbox.insert(tk.END, display)
def load_selected(self):
"""Load selected history item"""
selection = self.listbox.curselection()
if not selection:
messagebox.showwarning("History", "Please select an item first.")
return
idx = len(self.app.settings.get('history', [])) - 1 - selection[0]
history_item = self.app.settings['history'][idx]
self.app.text1.delete('1.0', tk.END)
self.app.text1.insert('1.0', history_item.get('text1', ''))
self.app.text2.delete('1.0', tk.END)
self.app.text2.insert('1.0', history_item.get('text2', ''))
self.app.update_line_numbers()
self.destroy()
def delete_selected(self):
"""Delete selected history item"""
selection = self.listbox.curselection()
if not selection:
messagebox.showwarning("History", "Please select an item first.")
return
idx = len(self.app.settings.get('history', [])) - 1 - selection[0]
self.app.settings['history'].pop(idx)
self.app.save_settings_to_file()
self.load_history()
def clear_all(self):
"""Clear all history"""
if messagebox.askyesno("Clear History", "Clear all history?"):
self.app.settings['history'] = []
self.app.save_settings_to_file()
self.load_history()
class StatisticsDialog(tk.Toplevel):
def __init__(self, parent, text1, text2):
super().__init__(parent)
self.title("Comparison Statistics")
self.geometry("500x400")
self.resizable(False, False)
# Get texts
content1 = text1.get('1.0', tk.END)[:-1]
content2 = text2.get('1.0', tk.END)[:-1]
# Calculate statistics
stats = self.calculate_stats(content1, content2)
# Display
main_frame = ttk.Frame(self, padding=20)
main_frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(main_frame, text="Comparison Statistics",
font=('Arial', 14, 'bold')).pack(pady=(0, 20))
# Create stats display
stats_frame = ttk.Frame(main_frame)
stats_frame.pack(fill=tk.BOTH, expand=True)
row = 0
for category, values in stats.items():
ttk.Label(stats_frame, text=category,
font=('Arial', 11, 'bold')).grid(row=row, column=0, columnspan=3,
sticky=tk.W, pady=(10, 5))
row += 1
for key, value in values.items():
ttk.Label(stats_frame, text=f" {key}:").grid(row=row, column=0, sticky=tk.W, padx=(20, 10))
ttk.Label(stats_frame, text=str(value[0])).grid(row=row, column=1, sticky=tk.E, padx=10)
ttk.Label(stats_frame, text=str(value[1])).grid(row=row, column=2, sticky=tk.E, padx=10)
row += 1
# Column headers
ttk.Label(stats_frame, text="Text 1",
font=('Arial', 10, 'bold')).grid(row=0, column=1, sticky=tk.E, padx=10)
ttk.Label(stats_frame, text="Text 2",
font=('Arial', 10, 'bold')).grid(row=0, column=2, sticky=tk.E, padx=10)
# Close button
ttk.Button(main_frame, text="Close", command=self.destroy).pack(pady=20)
def calculate_stats(self, text1, text2):
"""Calculate comparison statistics"""
stats = {
'Basic Counts': {
'Characters': (len(text1), len(text2)),
'Words': (len(text1.split()), len(text2.split())),
'Lines': (len(text1.split('\n')), len(text2.split('\n')))
},
'Whitespace': {
'Spaces': (text1.count(' '), text2.count(' ')),
'Tabs': (text1.count('\t'), text2.count('\t')),
'Newlines': (text1.count('\n'), text2.count('\n'))
}
}
# Calculate differences
matcher = SequenceMatcher(None, text1, text2)
diff_chars = sum(max(i2-i1, j2-j1) for tag, i1, i2, j1, j2 in matcher.get_opcodes()
if tag != 'equal')
stats['Differences'] = {
'Different Characters': (diff_chars, diff_chars),
'Similarity': (f"{matcher.ratio()*100:.1f}%", f"{matcher.ratio()*100:.1f}%")
}
return stats
class SettingsDialog(tk.Toplevel):
def __init__(self, parent, app):
super().__init__(parent)
self.app = app
self.title("Settings")
self.geometry("700x750")
self.resizable(False, False)
# Create notebook for tabbed interface
notebook = ttk.Notebook(self)
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Highlighting settings tab
self.create_highlighting_tab(notebook)
# Color settings tab
self.create_color_tab(notebook)
# Features tab
self.create_features_tab(notebook)
# Keyboard shortcuts tab
self.create_shortcuts_tab(notebook)
# Buttons
btn_frame = ttk.Frame(self)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="Save", command=self.save_settings).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Cancel", command=self.destroy).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Reset to Defaults", command=self.reset_defaults).pack(side=tk.LEFT, padx=5)
def create_highlighting_tab(self, notebook):
"""Create highlighting preferences tab"""
frame = ttk.Frame(notebook, padding=10)
notebook.add(frame, text="Highlighting")
# Left textbox settings
left_frame = ttk.LabelFrame(frame, text="Left Textbox", padding=10)
left_frame.grid(row=0, column=0, padx=10, pady=10, sticky=(tk.W, tk.E, tk.N, tk.S))
self.left_char_var = tk.BooleanVar(value=self.app.settings['left']['show_char'])
self.left_space_var = tk.BooleanVar(value=self.app.settings['left']['show_space'])
self.left_newline_var = tk.BooleanVar(value=self.app.settings['left']['show_newline'])
ttk.Checkbutton(left_frame, text="Show Different Characters",
variable=self.left_char_var).pack(anchor=tk.W, pady=5)
ttk.Checkbutton(left_frame, text="Show Different Spaces",
variable=self.left_space_var).pack(anchor=tk.W, pady=5)
ttk.Checkbutton(left_frame, text="Show Different Newlines",
variable=self.left_newline_var).pack(anchor=tk.W, pady=5)
# Right textbox settings
right_frame = ttk.LabelFrame(frame, text="Right Textbox", padding=10)
right_frame.grid(row=0, column=1, padx=10, pady=10, sticky=(tk.W, tk.E, tk.N, tk.S))
self.right_char_var = tk.BooleanVar(value=self.app.settings['right']['show_char'])
self.right_space_var = tk.BooleanVar(value=self.app.settings['right']['show_space'])
self.right_newline_var = tk.BooleanVar(value=self.app.settings['right']['show_newline'])
ttk.Checkbutton(right_frame, text="Show Different Characters",
variable=self.right_char_var).pack(anchor=tk.W, pady=5)
ttk.Checkbutton(right_frame, text="Show Different Spaces",
variable=self.right_space_var).pack(anchor=tk.W, pady=5)
ttk.Checkbutton(right_frame, text="Show Different Newlines",
variable=self.right_newline_var).pack(anchor=tk.W, pady=5)
def create_color_tab(self, notebook):
"""Create color preferences tab"""
frame = ttk.Frame(notebook, padding=10)
notebook.add(frame, text="Colors")
# Left textbox colors
left_frame = ttk.LabelFrame(frame, text="Left Textbox Colors", padding=10)
left_frame.grid(row=0, column=0, padx=10, pady=10, sticky=(tk.W, tk.E, tk.N, tk.S))
self.create_color_picker(left_frame, "Characters:",
self.app.settings['left']['color_char'], 'left_char')
self.create_color_picker(left_frame, "Spaces:",
self.app.settings['left']['color_space'], 'left_space')
self.create_color_picker(left_frame, "Newlines:",
self.app.settings['left']['color_newline'], 'left_newline')
self.create_color_picker(left_frame, "Search Highlight:",
self.app.settings['left']['search_color'], 'left_search')
# Right textbox colors
right_frame = ttk.LabelFrame(frame, text="Right Textbox Colors", padding=10)
right_frame.grid(row=0, column=1, padx=10, pady=10, sticky=(tk.W, tk.E, tk.N, tk.S))
self.create_color_picker(right_frame, "Characters:",
self.app.settings['right']['color_char'], 'right_char')
self.create_color_picker(right_frame, "Spaces:",
self.app.settings['right']['color_space'], 'right_space')
self.create_color_picker(right_frame, "Newlines:",
self.app.settings['right']['color_newline'], 'right_newline')
self.create_color_picker(right_frame, "Search Highlight:",
self.app.settings['right']['search_color'], 'right_search')
def create_features_tab(self, notebook):
"""Create features preferences tab"""
frame = ttk.Frame(notebook, padding=10)
notebook.add(frame, text="Features")
# Comparison features
comp_frame = ttk.LabelFrame(frame, text="Comparison Options", padding=10)
comp_frame.pack(fill=tk.X, padx=10, pady=10)
self.ignore_case_var = tk.BooleanVar(value=self.app.settings['features']['ignore_case'])
self.ignore_whitespace_var = tk.BooleanVar(value=self.app.settings['features']['ignore_whitespace'])
self.line_numbers_var = tk.BooleanVar(value=self.app.settings['features']['show_line_numbers'])
ttk.Checkbutton(comp_frame, text="Ignore Case (compare as lowercase)",
variable=self.ignore_case_var).pack(anchor=tk.W, pady=3)
ttk.Checkbutton(comp_frame, text="Ignore Extra Whitespace",
variable=self.ignore_whitespace_var).pack(anchor=tk.W, pady=3)
ttk.Checkbutton(comp_frame, text="Show Line Numbers",
variable=self.line_numbers_var).pack(anchor=tk.W, pady=3)
# UI features
ui_frame = ttk.LabelFrame(frame, text="Interface Options", padding=10)
ui_frame.pack(fill=tk.X, padx=10, pady=10)
self.sync_scroll_var = tk.BooleanVar(value=self.app.settings['features']['sync_scroll'])
self.auto_save_var = tk.BooleanVar(value=self.app.settings['features']['auto_save'])
self.show_stats_var = tk.BooleanVar(value=self.app.settings['features']['show_stats_after_compare'])
ttk.Checkbutton(ui_frame, text="Synchronized Scrolling",
variable=self.sync_scroll_var).pack(anchor=tk.W, pady=3)
ttk.Checkbutton(ui_frame, text="Auto-Save Work in Progress",
variable=self.auto_save_var).pack(anchor=tk.W, pady=3)
ttk.Checkbutton(ui_frame, text="Show Statistics After Compare",
variable=self.show_stats_var).pack(anchor=tk.W, pady=3)
# History settings
hist_frame = ttk.LabelFrame(frame, text="History Settings", padding=10)
hist_frame.pack(fill=tk.X, padx=10, pady=10)
self.enable_history_var = tk.BooleanVar(value=self.app.settings['features']['enable_history'])
ttk.Checkbutton(hist_frame, text="Enable Comparison History",
variable=self.enable_history_var).pack(anchor=tk.W, pady=3)
history_limit_frame = ttk.Frame(hist_frame)
history_limit_frame.pack(fill=tk.X, pady=5)
ttk.Label(history_limit_frame, text="Max History Items:").pack(side=tk.LEFT, padx=5)
self.history_limit_var = tk.StringVar(value=str(self.app.settings['features']['history_limit']))
ttk.Entry(history_limit_frame, textvariable=self.history_limit_var, width=10).pack(side=tk.LEFT)
def create_color_picker(self, parent, label, color, key):
"""Create a color picker row"""
frame = ttk.Frame(parent)
frame.pack(fill=tk.X, pady=5)
ttk.Label(frame, text=label, width=15).pack(side=tk.LEFT, padx=5)
color_display = tk.Label(frame, bg=color, width=5, relief=tk.SUNKEN)
color_display.pack(side=tk.LEFT, padx=5)
def choose_color():
new_color = colorchooser.askcolor(color=color, title=f"Choose {label}")
if new_color[1]:
color_display.config(bg=new_color[1])
setattr(self, f"{key}_color", new_color[1])
setattr(self, f"{key}_color", color)
ttk.Button(frame, text="Choose Color", command=choose_color).pack(side=tk.LEFT, padx=5)
def create_shortcuts_tab(self, notebook):
"""Create keyboard shortcuts tab"""
frame = ttk.Frame(notebook, padding=10)
notebook.add(frame, text="Keyboard Shortcuts")
ttk.Label(frame, text="Search Shortcuts:", font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 10))
# Left search shortcut
left_frame = ttk.Frame(frame)
left_frame.pack(fill=tk.X, pady=5)
ttk.Label(left_frame, text="Left Textbox Search:", width=25).pack(side=tk.LEFT, padx=5)
self.left_shortcut_var = tk.StringVar(value=self.app.settings['shortcuts']['left_search'])
ttk.Entry(left_frame, textvariable=self.left_shortcut_var, width=20).pack(side=tk.LEFT, padx=5)
# Right search shortcut
right_frame = ttk.Frame(frame)
right_frame.pack(fill=tk.X, pady=5)
ttk.Label(right_frame, text="Right Textbox Search:", width=25).pack(side=tk.LEFT, padx=5)
self.right_shortcut_var = tk.StringVar(value=self.app.settings['shortcuts']['right_search'])
ttk.Entry(right_frame, textvariable=self.right_shortcut_var, width=20).pack(side=tk.LEFT, padx=5)
ttk.Label(frame, text="\nOther Shortcuts:", font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(10, 10))
# Zoom shortcuts
zoom_in_frame = ttk.Frame(frame)
zoom_in_frame.pack(fill=tk.X, pady=5)
ttk.Label(zoom_in_frame, text="Zoom In:", width=25).pack(side=tk.LEFT, padx=5)
self.zoom_in_shortcut_var = tk.StringVar(value=self.app.settings['shortcuts']['zoom_in'])
ttk.Entry(zoom_in_frame, textvariable=self.zoom_in_shortcut_var, width=20).pack(side=tk.LEFT, padx=5)
zoom_out_frame = ttk.Frame(frame)
zoom_out_frame.pack(fill=tk.X, pady=5)
ttk.Label(zoom_out_frame, text="Zoom Out:", width=25).pack(side=tk.LEFT, padx=5)
self.zoom_out_shortcut_var = tk.StringVar(value=self.app.settings['shortcuts']['zoom_out'])
ttk.Entry(zoom_out_frame, textvariable=self.zoom_out_shortcut_var, width=20).pack(side=tk.LEFT, padx=5)
ttk.Label(frame, text="\nFormat: <Control-f> or <Alt-f> or <Control-Shift-f>",
font=('Arial', 8, 'italic')).pack(anchor=tk.W)
def save_settings(self):
"""Save all settings"""
# Highlighting settings
self.app.settings['left']['show_char'] = self.left_char_var.get()
self.app.settings['left']['show_space'] = self.left_space_var.get()
self.app.settings['left']['show_newline'] = self.left_newline_var.get()
self.app.settings['right']['show_char'] = self.right_char_var.get()
self.app.settings['right']['show_space'] = self.right_space_var.get()
self.app.settings['right']['show_newline'] = self.right_newline_var.get()
# Color settings
self.app.settings['left']['color_char'] = self.left_char_color
self.app.settings['left']['color_space'] = self.left_space_color
self.app.settings['left']['color_newline'] = self.left_newline_color
self.app.settings['left']['search_color'] = self.left_search_color
self.app.settings['right']['color_char'] = self.right_char_color
self.app.settings['right']['color_space'] = self.right_space_color
self.app.settings['right']['color_newline'] = self.right_newline_color
self.app.settings['right']['search_color'] = self.right_search_color
# Feature settings
self.app.settings['features']['ignore_case'] = self.ignore_case_var.get()
self.app.settings['features']['ignore_whitespace'] = self.ignore_whitespace_var.get()
self.app.settings['features']['show_line_numbers'] = self.line_numbers_var.get()
self.app.settings['features']['sync_scroll'] = self.sync_scroll_var.get()
self.app.settings['features']['auto_save'] = self.auto_save_var.get()
self.app.settings['features']['show_stats_after_compare'] = self.show_stats_var.get()
self.app.settings['features']['enable_history'] = self.enable_history_var.get()
try:
self.app.settings['features']['history_limit'] = int(self.history_limit_var.get())
except:
self.app.settings['features']['history_limit'] = 50
# Keyboard shortcuts
self.app.settings['shortcuts']['left_search'] = self.left_shortcut_var.get()
self.app.settings['shortcuts']['right_search'] = self.right_shortcut_var.get()
self.app.settings['shortcuts']['zoom_in'] = self.zoom_in_shortcut_var.get()
self.app.settings['shortcuts']['zoom_out'] = self.zoom_out_shortcut_var.get()
# Apply settings
self.app.apply_settings()
self.app.save_settings_to_file()
messagebox.showinfo("Settings", "Settings saved successfully!")
self.destroy()
def reset_defaults(self):
"""Reset all settings to defaults"""
if messagebox.askyesno("Reset", "Reset all settings to defaults?"):
self.app.load_default_settings()
self.app.apply_settings()
self.app.save_settings_to_file()
self.destroy()
messagebox.showinfo("Reset", "Settings reset to defaults!")
class TextComparisonTool:
def __init__(self, root):
self.root = root
self.root.title("Advanced Text Comparison Tool")
self.root.geometry("1600x900")
# Settings file path - works for both script and exe
if getattr(sys, 'frozen', False):
# Running as compiled exe
script_dir = os.path.dirname(sys.executable)
else:
# Running as script
script_dir = os.path.dirname(os.path.abspath(__file__))
self.settings_file = os.path.join(script_dir, "text_comparison_settings.json")
self.autosave_file = os.path.join(script_dir, "text_comparison_autosave.json")
# Font size
self.current_font_size = 10
# Current file path for manual save
self.current_file_path = None
# Load or create default settings
self.load_settings()
self.setup_ui()
self.apply_settings()
self.apply_theme()
# Load autosave if exists
self.load_autosave()
# Start autosave timer
if self.settings['features']['auto_save']:
self.start_autosave_timer()
# Bind closing event
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def load_default_settings(self):
"""Load default settings"""
self.settings = {
'theme': 'light',
'left': {
'show_char': True,
'show_space': True,
'show_newline': True,
'color_char': '#FF6B6B',
'color_space': '#FFE66D',
'color_newline': '#4ECDC4',
'search_color': '#90EE90'
},
'right': {
'show_char': True,
'show_space': True,
'show_newline': True,
'color_char': '#FF6B6B',
'color_space': '#FFE66D',
'color_newline': '#4ECDC4',
'search_color': '#DDA0DD'
},
'shortcuts': {
'left_search': '<Control-f>',
'right_search': '<Control-g>',
'zoom_in': '<Control-plus>',
'zoom_out': '<Control-minus>'
},
'features': {
'ignore_case': False,
'ignore_whitespace': False,
'show_line_numbers': True,
'sync_scroll': True,
'auto_save': True,
'show_stats_after_compare': False,
'enable_history': True,
'history_limit': 50
},
'history': []
}
def load_settings(self):
"""Load settings from file or use defaults"""
if os.path.exists(self.settings_file):
try:
with open(self.settings_file, 'r') as f:
self.settings = json.load(f)
# Ensure theme field exists
if 'theme' not in self.settings:
self.settings['theme'] = 'light'
# Ensure features field exists
if 'features' not in self.settings:
defaults = self.get_default_features()
self.settings['features'] = defaults
# Ensure history field exists
if 'history' not in self.settings:
self.settings['history'] = []
except:
self.load_default_settings()
else:
self.load_default_settings()
def get_default_features(self):
"""Get default feature settings"""
return {
'ignore_case': False,
'ignore_whitespace': False,
'show_line_numbers': True,
'sync_scroll': True,
'auto_save': True,
'show_stats_after_compare': False,
'enable_history': True,
'history_limit': 50
}
def save_settings_to_file(self):
"""Save settings to file"""
try:
with open(self.settings_file, 'w') as f:
json.dump(self.settings, f, indent=2)
except Exception as e:
messagebox.showerror("Error", f"Could not save settings: {str(e)}")
def apply_settings(self):
"""Apply current settings to the UI"""
# Update text widget tag colors
self.text1.tag_config('different_char_left',
background=self.settings['left']['color_char'])
self.text1.tag_config('different_space_left',
background=self.settings['left']['color_space'])
self.text1.tag_config('different_newline_left',
background=self.settings['left']['color_newline'])
self.text2.tag_config('different_char_right',
background=self.settings['right']['color_char'])
self.text2.tag_config('different_space_right',
background=self.settings['right']['color_space'])
self.text2.tag_config('different_newline_right',
background=self.settings['right']['color_newline'])
# Rebind keyboard shortcuts
for key in ['left_search', 'right_search', 'zoom_in', 'zoom_out']:
try:
self.root.unbind_all(self.settings['shortcuts'][key])
except:
pass
self.root.bind(self.settings['shortcuts']['left_search'],
lambda e: self.open_search(self.text1, 'left'))
self.root.bind(self.settings['shortcuts']['right_search'],
lambda e: self.open_search(self.text2, 'right'))
self.root.bind(self.settings['shortcuts']['zoom_in'],
lambda e: self.zoom_in())
self.root.bind(self.settings['shortcuts']['zoom_out'],
lambda e: self.zoom_out())
self.root.bind("<Control-s>",
lambda e: self.save_file())
# Apply sync scroll
if self.settings['features']['sync_scroll']:
self.enable_sync_scroll()
else:
self.disable_sync_scroll()
# Apply line numbers
self.update_line_numbers()
def setup_ui(self):
"""Setup the user interface"""
# Menu bar
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
# File menu
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Import Left", command=lambda: self.import_file('left'))
file_menu.add_command(label="Import Right", command=lambda: self.import_file('right'))
file_menu.add_separator()
file_menu.add_command(label="Load Save", command=self.load_save)
file_menu.add_separator()
file_menu.add_command(label="Export Left", command=lambda: self.export_text('left'))
file_menu.add_command(label="Export Right", command=lambda: self.export_text('right'))
file_menu.add_command(label="Export All", command=self.export_all)
file_menu.add_separator()
file_menu.add_command(label="Save", command=self.save_file, accelerator="Ctrl+S")
file_menu.add_command(label="Save As...", command=self.save_file_as)
file_menu.add_separator()
file_menu.add_command(label="Export Report (HTML)", command=self.export_report_html)
file_menu.add_separator()
file_menu.add_command(label="Exit", command=self.on_closing)
# Edit menu
edit_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Edit", menu=edit_menu)
edit_menu.add_command(label="Settings", command=self.open_settings)
edit_menu.add_separator()
edit_menu.add_command(label="Undo Left", command=lambda: self.undo('left'))
edit_menu.add_command(label="Undo Right", command=lambda: self.undo('right'))
edit_menu.add_command(label="Redo Left", command=lambda: self.redo('left'))
edit_menu.add_command(label="Redo Right", command=lambda: self.redo('right'))
# View menu
view_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="View", menu=view_menu)
view_menu.add_command(label="Zoom In", command=self.zoom_in)
view_menu.add_command(label="Zoom Out", command=self.zoom_out)
view_menu.add_command(label="Reset Zoom", command=self.reset_zoom)
view_menu.add_separator()
view_menu.add_command(label="Statistics", command=self.show_statistics)
view_menu.add_command(label="History", command=self.show_history)
# Tools menu
tools_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Tools", menu=tools_menu)
tools_menu.add_command(label="Next Difference", command=self.next_difference)
tools_menu.add_command(label="Previous Difference", command=self.prev_difference)
tools_menu.add_separator()
tools_menu.add_command(label="Copy Left to Right", command=self.copy_left_to_right)
tools_menu.add_command(label="Copy Right to Left", command=self.copy_right_to_left)
# Main frame
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Configure grid weights
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
main_frame.rowconfigure(2, weight=1)
# Labels and action buttons for left text
left_header = ttk.Frame(main_frame)
left_header.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 5))
ttk.Label(left_header, text="Text 1:", font=('Arial', 12, 'bold')).pack(side=tk.LEFT)
left_btn_frame = ttk.Frame(left_header)
left_btn_frame.pack(side=tk.RIGHT)
ttk.Button(left_btn_frame, text="Import", width=8,
command=lambda: self.import_file('left')).pack(side=tk.LEFT, padx=2)
ttk.Button(left_btn_frame, text="Clear", width=8,
command=lambda: self.clear_text('left')).pack(side=tk.LEFT, padx=2)
ttk.Button(left_btn_frame, text="Export", width=8,
command=lambda: self.export_text('left')).pack(side=tk.LEFT, padx=2)
# Labels and action buttons for right text
right_header = ttk.Frame(main_frame)
right_header.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=(0, 5), padx=(10, 0))
ttk.Label(right_header, text="Text 2:", font=('Arial', 12, 'bold')).pack(side=tk.LEFT)
right_btn_frame = ttk.Frame(right_header)
right_btn_frame.pack(side=tk.RIGHT)
ttk.Button(right_btn_frame, text="Import", width=8,
command=lambda: self.import_file('right')).pack(side=tk.LEFT, padx=2)
ttk.Button(right_btn_frame, text="Clear", width=8,
command=lambda: self.clear_text('right')).pack(side=tk.LEFT, padx=2)
ttk.Button(right_btn_frame, text="Export", width=8,
command=lambda: self.export_text('right')).pack(side=tk.LEFT, padx=2)
# Text widgets with line numbers
left_container = ttk.Frame(main_frame)
left_container.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
# Left line numbers
self.line_numbers_left = tk.Text(left_container, width=4, padx=3, takefocus=0,
border=0, background='#f0f0f0', state='disabled',
font=('Courier New', 10))
self.line_numbers_left.pack(side=tk.LEFT, fill=tk.Y)
# Left text widget
self.text1 = scrolledtext.ScrolledText(
left_container, wrap=tk.WORD, width=50, height=30,
font=('Courier New', self.current_font_size), undo=True)
self.text1.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
right_container = ttk.Frame(main_frame)
right_container.grid(row=2, column=1, sticky=(tk.W, tk.E, tk.N, tk.S),
pady=(0, 10), padx=(10, 0))
# Right line numbers
self.line_numbers_right = tk.Text(right_container, width=4, padx=3, takefocus=0,
border=0, background='#f0f0f0', state='disabled',
font=('Courier New', 10))
self.line_numbers_right.pack(side=tk.LEFT, fill=tk.Y)
# Right text widget
self.text2 = scrolledtext.ScrolledText(
right_container, wrap=tk.WORD, width=50, height=30,
font=('Courier New', self.current_font_size), undo=True)
self.text2.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Bind events for line numbers
self.text1.bind('<KeyRelease>', lambda e: self.update_line_numbers())
self.text2.bind('<KeyRelease>', lambda e: self.update_line_numbers())
self.text1.bind('<MouseWheel>', lambda e: self.update_line_numbers())
self.text2.bind('<MouseWheel>', lambda e: self.update_line_numbers())
# Context menus
self.create_context_menu(self.text1, 'left')
self.create_context_menu(self.text2, 'right')
# Button frame
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=3, column=0, columnspan=2, pady=10)
# Compare button
ttk.Button(button_frame, text="Compare Texts",
command=self.compare_texts).pack(side=tk.LEFT, padx=5)
# Navigation buttons
ttk.Button(button_frame, text="◀ Prev Diff",
command=self.prev_difference).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="Next Diff ▶",
command=self.next_difference).pack(side=tk.LEFT, padx=5)
# Clear All button
ttk.Button(button_frame, text="Clear All",
command=self.clear_all).pack(side=tk.LEFT, padx=5)
# Statistics button
ttk.Button(button_frame, text="Statistics",
command=self.show_statistics).pack(side=tk.LEFT, padx=5)
# History button
ttk.Button(button_frame, text="History",
command=self.show_history).pack(side=tk.LEFT, padx=5)
# Settings button
ttk.Button(button_frame, text="Settings",
command=self.open_settings).pack(side=tk.LEFT, padx=5)
# Light/Dark theme toggle button
self.theme_button = ttk.Button(button_frame, text="🌙 Dark Mode",
command=self.toggle_theme)
self.theme_button.pack(side=tk.LEFT, padx=5)
# Store differences for navigation
self.difference_positions = []
self.current_diff_index = -1
def create_context_menu(self, text_widget, side):
"""Create context menu for text widget"""
menu = tk.Menu(text_widget, tearoff=0)
menu.add_command(label="Copy", command=lambda: self.copy_text(text_widget))
menu.add_command(label="Cut", command=lambda: self.cut_text(text_widget))
menu.add_command(label="Paste", command=lambda: self.paste_text(text_widget))
menu.add_command(label="Delete", command=lambda: self.delete_selection(text_widget))
menu.add_separator()
menu.add_command(label="Undo", command=lambda: self.undo(side))
menu.add_command(label="Redo", command=lambda: self.redo(side))
menu.add_separator()
menu.add_command(label="Clear", command=lambda: self.clear_text(side))
menu.add_separator()
menu.add_command(label="Search & Replace", command=lambda: self.open_search(text_widget, side))
menu.add_separator()
menu.add_command(label="Save", command=self.save_file)
menu.add_command(label="Load Save", command=self.load_save)
def show_menu(event):
menu.tk_popup(event.x_root, event.y_root)
text_widget.bind("<Button-3>", show_menu)
def copy_text(self, text_widget):
"""Copy selected text to clipboard"""
try:
selected_text = text_widget.get(tk.SEL_FIRST, tk.SEL_LAST)
self.root.clipboard_clear()
self.root.clipboard_append(selected_text)
except tk.TclError:
pass
def cut_text(self, text_widget):
"""Cut selected text to clipboard"""
try:
selected_text = text_widget.get(tk.SEL_FIRST, tk.SEL_LAST)
self.root.clipboard_clear()
self.root.clipboard_append(selected_text)
text_widget.delete(tk.SEL_FIRST, tk.SEL_LAST)
except tk.TclError:
pass
def paste_text(self, text_widget):
"""Paste text from clipboard"""
try:
text_widget.insert(tk.INSERT, self.root.clipboard_get())
except tk.TclError:
pass
def delete_selection(self, text_widget):
"""Delete selected text"""
try:
text_widget.delete(tk.SEL_FIRST, tk.SEL_LAST)
except tk.TclError:
pass
def undo(self, side):
"""Undo last action"""
text_widget = self.text1 if side == 'left' else self.text2
try:
text_widget.edit_undo()
except tk.TclError:
pass
def redo(self, side):
"""Redo last undone action"""
text_widget = self.text1 if side == 'left' else self.text2
try:
text_widget.edit_redo()
except tk.TclError:
pass
def clear_text(self, side):
"""Clear text from specified textbox"""
if side == 'left':
self.text1.delete('1.0', tk.END)
else:
self.text2.delete('1.0', tk.END)
self.update_line_numbers()
def clear_all(self):
"""Clear all text and highlighting"""
self.text1.delete('1.0', tk.END)
self.text2.delete('1.0', tk.END)
self.update_line_numbers()
def import_file(self, side):
"""Import file into specified textbox"""
filename = filedialog.askopenfilename(
title=f"Import File to {side.capitalize()}",
filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
)
if filename:
try:
with open(filename, 'r', encoding='utf-8') as f:
content = f.read()
text_widget = self.text1 if side == 'left' else self.text2
text_widget.delete('1.0', tk.END)
text_widget.insert('1.0', content)
self.update_line_numbers()
except Exception as e:
messagebox.showerror("Import Error", f"Could not import file: {str(e)}")
def export_text(self, side):
"""Export text from specified textbox"""
text_widget = self.text1 if side == 'left' else self.text2
content = text_widget.get('1.0', tk.END)[:-1]
if not content.strip():
messagebox.showwarning("Export", "No text to export!")
return
filename = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("Text files", "*.txt"), ("All files", "*.*")],
title=f"Export Text {side.capitalize()}"
)
if filename:
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(content)
messagebox.showinfo("Export", f"Text exported to {filename}")
except Exception as e:
messagebox.showerror("Error", f"Could not export: {str(e)}")
def export_all(self):
"""Export both texts to separate files"""
base_filename = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("Text files", "*.txt"), ("All files", "*.*")],
title="Export All (choose base filename)"
)
if base_filename:
try:
base = os.path.splitext(base_filename)[0]
content1 = self.text1.get('1.0', tk.END)[:-1]
with open(f"{base}_left.txt", 'w', encoding='utf-8') as f:
f.write(content1)
content2 = self.text2.get('1.0', tk.END)[:-1]
with open(f"{base}_right.txt", 'w', encoding='utf-8') as f:
f.write(content2)
messagebox.showinfo("Export", f"Texts exported to:\n{base}_left.txt\n{base}_right.txt")
except Exception as e:
messagebox.showerror("Error", f"Could not export: {str(e)}")
def export_report_html(self):
"""Export comparison report as HTML"""
filename = filedialog.asksaveasfilename(
defaultextension=".html",
filetypes=[("HTML files", "*.html"), ("All files", "*.*")],
title="Export Report as HTML"
)
if not filename:
return
text1_content = self.text1.get('1.0', tk.END)[:-1]
text2_content = self.text2.get('1.0', tk.END)[:-1]
matcher = SequenceMatcher(None, text1_content, text2_content)
html_content = f"""<!DOCTYPE html>
<html>
<head>
<title>Text Comparison Report</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
h1 {{ color: #333; }}
.container {{ display: flex; gap: 20px; }}
.column {{ flex: 1; border: 1px solid #ddd; padding: 10px; }}
.diff-char {{ background-color: #FF6B6B; }}
.diff-space {{ background-color: #FFE66D; }}
.diff-newline {{ background-color: #4ECDC4; }}
pre {{ white-space: pre-wrap; font-family: 'Courier New', monospace; }}
.stats {{ background: #f5f5f5; padding: 15px; margin: 20px 0; border-radius: 5px; }}
</style>
</head>
<body>
<h1>Text Comparison Report</h1>
<p>Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<div class="stats">
<h2>Statistics</h2>
<p>Similarity: {matcher.ratio()*100:.1f}%</p>
<p>Text 1: {len(text1_content)} characters, {len(text1_content.split())} words, {len(text1_content.split(chr(10)))} lines</p>
<p>Text 2: {len(text2_content)} characters, {len(text2_content.split())} words, {len(text2_content.split(chr(10)))} lines</p>
</div>
<div class="container">
<div class="column">
<h3>Text 1</h3>
<pre>{text1_content}</pre>
</div>
<div class="column">
<h3>Text 2</h3>
<pre>{text2_content}</pre>
</div>
</div>
</body>
</html>"""
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(html_content)
messagebox.showinfo("Export", f"Report exported to {filename}")
except Exception as e:
messagebox.showerror("Error", f"Could not export report: {str(e)}")
def open_search(self, text_widget, side):
"""Open search and replace dialog for specified textbox"""
search_color = self.settings[side]['search_color']
SearchReplaceDialog(self.root, text_widget, search_color)
def open_settings(self):
"""Open settings dialog"""
SettingsDialog(self.root, self)
def show_statistics(self):
"""Show comparison statistics"""
StatisticsDialog(self.root, self.text1, self.text2)
def show_history(self):
"""Show comparison history"""
HistoryDialog(self.root, self)
def zoom_in(self):
"""Increase font size"""
self.current_font_size += 1
self.update_font_size()
def zoom_out(self):
"""Decrease font size"""
if self.current_font_size > 6:
self.current_font_size -= 1
self.update_font_size()
def reset_zoom(self):
"""Reset font size to default"""
self.current_font_size = 10
self.update_font_size()
def update_font_size(self):
"""Update font size for all text widgets"""
font = ('Courier New', self.current_font_size)
self.text1.config(font=font)
self.text2.config(font=font)
self.line_numbers_left.config(font=font)
self.line_numbers_right.config(font=font)
self.update_line_numbers()
def update_line_numbers(self):
"""Update line numbers display"""
if not self.settings['features']['show_line_numbers']:
self.line_numbers_left.pack_forget()
self.line_numbers_right.pack_forget()
return
# Show line numbers
self.line_numbers_left.pack(side=tk.LEFT, fill=tk.Y, before=self.text1)
self.line_numbers_right.pack(side=tk.LEFT, fill=tk.Y, before=self.text2)
# Update left
self.line_numbers_left.config(state='normal')
self.line_numbers_left.delete('1.0', tk.END)
line_count = int(self.text1.index('end-1c').split('.')[0])
line_numbers_string = "\n".join(str(i) for i in range(1, line_count + 1))
self.line_numbers_left.insert('1.0', line_numbers_string)
self.line_numbers_left.config(state='disabled')
# Update right
self.line_numbers_right.config(state='normal')
self.line_numbers_right.delete('1.0', tk.END)
line_count = int(self.text2.index('end-1c').split('.')[0])
line_numbers_string = "\n".join(str(i) for i in range(1, line_count + 1))
self.line_numbers_right.insert('1.0', line_numbers_string)
self.line_numbers_right.config(state='disabled')
def enable_sync_scroll(self):
"""Enable synchronized scrolling"""
def sync_scroll_left(*args):
self.text2.yview_moveto(args[0])
self.line_numbers_left.yview_moveto(args[0])
self.line_numbers_right.yview_moveto(args[0])
def sync_scroll_right(*args):
self.text1.yview_moveto(args[0])
self.line_numbers_left.yview_moveto(args[0])
self.line_numbers_right.yview_moveto(args[0])
self.text1.config(yscrollcommand=sync_scroll_left)
self.text2.config(yscrollcommand=sync_scroll_right)
def disable_sync_scroll(self):
"""Disable synchronized scrolling"""
self.text1.config(yscrollcommand=lambda *args: None)
self.text2.config(yscrollcommand=lambda *args: None)
def compare_texts(self):
"""Compare the two texts and highlight differences"""
# Clear previous highlights
for text_widget in [self.text1, self.text2]:
for tag in ['different_char_left', 'different_space_left', 'different_newline_left',
'different_char_right', 'different_space_right', 'different_newline_right']:
text_widget.tag_remove(tag, '1.0', tk.END)
# Get texts
text1_content = self.text1.get('1.0', tk.END)[:-1]
text2_content = self.text2.get('1.0', tk.END)[:-1]
# Apply comparison options
if self.settings['features']['ignore_case']:
text1_compare = text1_content.lower()
text2_compare = text2_content.lower()
else:
text1_compare = text1_content
text2_compare = text2_content
if self.settings['features']['ignore_whitespace']:
text1_compare = ' '.join(text1_compare.split())
text2_compare = ' '.join(text2_compare.split())
# Use SequenceMatcher to find differences
matcher = SequenceMatcher(None, text1_compare, text2_compare)
# Store difference positions
self.difference_positions = []
# Process differences
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == 'replace' or tag == 'delete' or tag == 'insert':
# Highlight in text1
if tag in ['replace', 'delete'] and i1 < i2:
pos = self.char_index_to_tk_index(text1_content, i1)
self.difference_positions.append(('left', pos))
self.highlight_segment(self.text1, text1_content, i1, i2, 'left')
# Highlight in text2
if tag in ['replace', 'insert'] and j1 < j2:
pos = self.char_index_to_tk_index(text2_content, j1)
self.difference_positions.append(('right', pos))
self.highlight_segment(self.text2, text2_content, j1, j2, 'right')
self.current_diff_index = -1
# Add to history
if self.settings['features']['enable_history']:
self.add_to_history(text1_content, text2_content)
# Show statistics if enabled
if self.settings['features']['show_stats_after_compare']:
self.show_statistics()
def highlight_segment(self, text_widget, full_text, start_idx, end_idx, side):
"""Highlight a segment of text based on character types"""
segment = full_text[start_idx:end_idx]
settings = self.settings[side]
for i, char in enumerate(segment):
current_pos = self.char_index_to_tk_index(full_text, start_idx + i)
next_pos = self.char_index_to_tk_index(full_text, start_idx + i + 1)
if char == '\n':
if settings['show_newline']:
text_widget.tag_add(f'different_newline_{side}', current_pos, next_pos)
elif char in [' ', '\t']:
if settings['show_space']:
text_widget.tag_add(f'different_space_{side}', current_pos, next_pos)
else:
if settings['show_char']:
text_widget.tag_add(f'different_char_{side}', current_pos, next_pos)
def next_difference(self):
"""Navigate to next difference"""
if not self.difference_positions:
messagebox.showinfo("Navigation", "No differences found. Run comparison first.")
return
self.current_diff_index = (self.current_diff_index + 1) % len(self.difference_positions)
side, pos = self.difference_positions[self.current_diff_index]
text_widget = self.text1 if side == 'left' else self.text2
text_widget.see(pos)
text_widget.mark_set('insert', pos)
def prev_difference(self):
"""Navigate to previous difference"""
if not self.difference_positions:
messagebox.showinfo("Navigation", "No differences found. Run comparison first.")
return
self.current_diff_index = (self.current_diff_index - 1) % len(self.difference_positions)
side, pos = self.difference_positions[self.current_diff_index]
text_widget = self.text1 if side == 'left' else self.text2
text_widget.see(pos)
text_widget.mark_set('insert', pos)
def copy_left_to_right(self):
"""Copy left text to right"""
if messagebox.askyesno("Merge", "Copy left text to right?"):
content = self.text1.get('1.0', tk.END)
self.text2.delete('1.0', tk.END)
self.text2.insert('1.0', content)
def copy_right_to_left(self):
"""Copy right text to left"""
if messagebox.askyesno("Merge", "Copy right text to left?"):
content = self.text2.get('1.0', tk.END)
self.text1.delete('1.0', tk.END)
self.text1.insert('1.0', content)
def add_to_history(self, text1, text2):
"""Add comparison to history"""
if not self.settings['features']['enable_history']:
return
history_item = {
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'text1': text1,
'text2': text2
}
self.settings['history'].insert(0, history_item)
# Limit history size
limit = self.settings['features'].get('history_limit', 50)
self.settings['history'] = self.settings['history'][:limit]
self.save_settings_to_file()
def start_autosave_timer(self):
"""Start autosave timer"""
def autosave():
if self.settings['features']['auto_save']:
self.save_autosave()
self.root.after(60000, autosave) # Every 60 seconds
self.root.after(60000, autosave)
def save_autosave(self):
"""Save current work to autosave file"""
try:
autosave_data = {
'text1': self.text1.get('1.0', tk.END)[:-1],
'text2': self.text2.get('1.0', tk.END)[:-1],
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
with open(self.autosave_file, 'w', encoding='utf-8') as f:
json.dump(autosave_data, f, indent=2)
except Exception as e:
print(f"Autosave error: {e}")
def load_autosave(self):
"""Load autosave if it exists"""
if not os.path.exists(self.autosave_file):
return
try:
with open(self.autosave_file, 'r', encoding='utf-8') as f:
autosave_data = json.load(f)
if messagebox.askyesno("Autosave",
f"Found autosave from {autosave_data['timestamp']}. Load it?"):
self.text1.insert('1.0', autosave_data.get('text1', ''))
self.text2.insert('1.0', autosave_data.get('text2', ''))
self.update_line_numbers()
except Exception as e:
print(f"Autosave load error: {e}")
def on_closing(self):
"""Handle window closing"""
if self.settings['features']['auto_save']:
self.save_autosave()
self.root.destroy()
def toggle_theme(self):
"""Toggle between light and dark theme"""
current_theme = self.settings.get('theme', 'light')
new_theme = 'dark' if current_theme == 'light' else 'light'
self.settings['theme'] = new_theme
self.save_settings_to_file()
self.apply_theme()
def apply_theme(self):
"""Apply the current theme to all widgets"""
theme = self.settings.get('theme', 'light')
if theme == 'dark':
bg_color = '#2b2b2b'
fg_color = '#e0e0e0'
text_bg = '#1e1e1e'
text_fg = '#d4d4d4'
line_num_bg = '#2b2b2b'
button_text = '☀️ Light Mode'
self.root.configure(bg=bg_color)
self.text1.configure(bg=text_bg, fg=text_fg, insertbackground=text_fg)
self.text2.configure(bg=text_bg, fg=text_fg, insertbackground=text_fg)
self.line_numbers_left.configure(bg=line_num_bg, fg=fg_color)
self.line_numbers_right.configure(bg=line_num_bg, fg=fg_color)
else:
bg_color = '#f0f0f0'
fg_color = '#000000'
text_bg = '#ffffff'
text_fg = '#000000'
line_num_bg = '#f0f0f0'
button_text = '🌙 Dark Mode'
self.root.configure(bg=bg_color)
self.text1.configure(bg=text_bg, fg=text_fg, insertbackground=text_fg)
self.text2.configure(bg=text_bg, fg=text_fg, insertbackground=text_fg)
self.line_numbers_left.configure(bg=line_num_bg, fg=fg_color)
self.line_numbers_right.configure(bg=line_num_bg, fg=fg_color)
self.theme_button.configure(text=button_text)
def char_index_to_tk_index(self, text, char_idx):
"""Convert character index to tkinter text index (line.column)"""
if char_idx == 0:
return '1.0'
lines = text[:char_idx].split('\n')
line_num = len(lines)
col_num = len(lines[-1])
return f"{line_num}.{col_num}"
def save_file(self):
"""Save file - first time asks for location, subsequent saves use same location"""
if self.current_file_path is None:
# First time saving - act like "Save As"
self.save_file_as()
else:
# Subsequent saves - save directly to current path
try:
content = {
'text1': self.text1.get('1.0', tk.END)[:-1],
'text2': self.text2.get('1.0', tk.END)[:-1]
}
with open(self.current_file_path, 'w', encoding='utf-8') as f:
json.dump(content, f, indent=2)
messagebox.showinfo("Save", f"File saved successfully to {self.current_file_path}")
except Exception as e:
messagebox.showerror("Save Error", f"Could not save file:\n{str(e)}")
def save_file_as(self):
"""Save file with dialog to choose location"""
filepath = filedialog.asksaveasfilename(
defaultextension=".json",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if filepath:
try:
content = {
'text1': self.text1.get('1.0', tk.END)[:-1],
'text2': self.text2.get('1.0', tk.END)[:-1]
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(content, f, indent=2)
self.current_file_path = filepath
messagebox.showinfo("Save As", f"File saved successfully to {filepath}")
except Exception as e:
messagebox.showerror("Save Error", f"Could not save file:\n{str(e)}")
def load_save(self):
"""Load a previously saved file (manual save or autosave)"""
filepath = filedialog.askopenfilename(
title="Load Save File",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if filepath:
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = json.load(f)
# Clear current content
self.text1.delete('1.0', tk.END)
self.text2.delete('1.0', tk.END)
# Load saved content
self.text1.insert('1.0', content.get('text1', ''))
self.text2.insert('1.0', content.get('text2', ''))
# Update current file path if it's not the autosave file
if filepath != self.autosave_file:
self.current_file_path = filepath
# Update line numbers
self.update_line_numbers()
messagebox.showinfo("Load Save", f"File loaded successfully from {filepath}")
except Exception as e:
messagebox.showerror("Load Error", f"Could not load file:\n{str(e)}")
def main():
root = tk.Tk()
app = TextComparisonTool(root)
root.mainloop()
if __name__ == "__main__":
main()
Commenti
Posta un commento