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

Post popolari in questo blog

No Man's Sky Similar Games

Tower Defense Games

Auto Copy Folder - Source Code