Skip to content

Instantly share code, notes, and snippets.

@dmknght
Last active April 10, 2025 08:44
Show Gist options
  • Save dmknght/c8a1fa1fad1255eeb01042d93793e684 to your computer and use it in GitHub Desktop.
Save dmknght/c8a1fa1fad1255eeb01042d93793e684 to your computer and use it in GitHub Desktop.
Likely signature management of Kaspersky 2008
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import struct
import os
import hashlib
from datetime import datetime
from dataclasses import dataclass
from typing import List, Optional, Tuple
import re
# Record Types
RT_KERNEL = 0
RT_JUMP = 1
RT_MEMORY = 2
RT_SECTOR = 3
RT_FILE = 4
RT_CA = 5
RT_UNPACK = 6
RT_EXTRACT = 7
RT_SEPARATOR = 8
# Record Type Strings
TYPE_STRINGS = [
"Kernel",
"Jump",
"Mem",
"Sector",
"File",
"CA",
"Unpack",
"Extract",
""
]
# SubType Strings for File/CA/Unpack/Extract
SUBTYPE_STRINGS = [
"Com ",
"Exe ",
"Sys ",
"NE ",
"OLE2 ",
"",
"",
""
]
# SubType Strings for Sector
SUBTYPE_SECTOR = [
"A", # ABOOT
"F", # FDBOOT
"H", # HDBOOT
"M", # MBR
""
]
# Method Strings
METHOD_STRINGS = {
RT_FILE: {
0: "Signature",
1: "Heuristic",
2: "Behavior",
3: "Cloud",
4: "Other"
},
RT_SECTOR: {
0: "Boot",
1: "MBR",
2: "Partition",
3: "Other"
},
RT_JUMP: {
0: "Jump",
1: "Call",
2: "Other"
},
RT_MEMORY: {
0: "Memory",
1: "Stack",
2: "Other"
}
}
class RecordEdit:
def __init__(self, type=0):
self.type = type
self.subtype = 0
self.method = 0
self.name = ""
self.comment = ""
self.link16_buffer = None
self.link32_buffer = None
self.extra_info = None
self.record = b""
self.modify_flag = 0
def get_type_string(self):
"""Get record type as string"""
if 0 <= self.type < len(TYPE_STRINGS):
return TYPE_STRINGS[self.type]
return f"Unknown ({self.type})"
def get_sub_type_string(self):
"""Get record subtype as string"""
if self.type in [RT_FILE, RT_CA, RT_UNPACK, RT_EXTRACT]:
sub_types = []
for i in range(8):
if self.subtype & (1 << i):
sub_types.append(SUBTYPE_STRINGS[i])
return "".join(sub_types)
elif self.type == RT_SECTOR:
sub_types = []
if self.record:
sector = struct.unpack("<I", self.record[:4])[0]
if sector & 0x01: # ABOOT
sub_types.append(SUBTYPE_SECTOR[0])
if sector & 0x02: # FDBOOT
sub_types.append(SUBTYPE_SECTOR[1])
if sector & 0x04: # HDBOOT
sub_types.append(SUBTYPE_SECTOR[2])
if sector & 0x08: # MBR
sub_types.append(SUBTYPE_SECTOR[3])
return "".join(sub_types)
return ""
def get_method_string(self):
"""Get record method as string"""
if self.type not in METHOD_STRINGS:
return ""
if not self.record:
return ""
try:
if self.type == RT_FILE:
method = struct.unpack("<B", self.record[:1])[0] & 0x7F
elif self.type == RT_SECTOR:
method1 = struct.unpack("<B", self.record[:1])[0] & 0x0F
method2 = struct.unpack("<B", self.record[1:2])[0] & 0x0F
return f"{METHOD_STRINGS[RT_SECTOR].get(method1, 'Unknown')} / {METHOD_STRINGS[RT_SECTOR].get(method2, 'Unknown')}"
elif self.type == RT_JUMP:
method = struct.unpack("<B", self.record[:1])[0]
elif self.type == RT_MEMORY:
method = struct.unpack("<B", self.record[:1])[0]
else:
return ""
return METHOD_STRINGS[self.type].get(method, "Unknown")
except:
return ""
def get_link16_string(self):
"""Get Link16 buffer as string"""
if not self.link16_buffer:
return ""
return " ".join(f"{x:02X}" for x in self.link16_buffer)
def get_link32_string(self):
"""Get Link32 buffer as string"""
if not self.link32_buffer:
return ""
return " ".join(f"{x:02X}" for x in self.link32_buffer)
def get_name(self):
"""Get record name"""
return self.name
def get_comment(self):
"""Get record comment"""
return self.comment
def set_name(self, name):
"""Set record name"""
self.name = name
return True
def set_comment(self, comment):
"""Set record comment"""
self.comment = comment
return True
def set_type(self, type):
"""Set record type"""
if type in TYPE_STRINGS:
self.type = type
return True
return False
def set_subtype(self, subtype):
"""Set record subtype"""
if self.type in [RT_FILE, RT_CA, RT_UNPACK, RT_EXTRACT]:
self.subtype = subtype
return True
return False
def set_method(self, method):
"""Set record method"""
if self.type not in METHOD_STRINGS:
return False
if not self.record:
self.record = b"\x00"
try:
if self.type == RT_FILE:
self.record = struct.pack("<B", method & 0x7F) + self.record[1:]
elif self.type == RT_SECTOR:
self.record = struct.pack("<B", method & 0x0F) + self.record[1:]
elif self.type in [RT_JUMP, RT_MEMORY]:
self.record = struct.pack("<B", method) + self.record[1:]
return True
except:
return False
def set_link16(self, buffer):
"""Set Link16 buffer"""
if buffer is None or isinstance(buffer, bytes):
self.link16_buffer = buffer
return True
return False
def set_link32(self, buffer):
"""Set Link32 buffer"""
if buffer is None or isinstance(buffer, bytes):
self.link32_buffer = buffer
return True
return False
def pack_record(self):
"""Pack record data into bytes"""
data = bytearray()
# Pack type and subtype
data.extend(struct.pack("<BB", self.type, self.subtype))
# Pack name
name_bytes = self.name.encode('utf-8')
data.extend(struct.pack("<H", len(name_bytes)))
data.extend(name_bytes)
# Pack comment
comment_bytes = self.comment.encode('utf-8')
data.extend(struct.pack("<H", len(comment_bytes)))
data.extend(comment_bytes)
# Pack record data
if self.record:
data.extend(struct.pack("<H", len(self.record)))
data.extend(self.record)
else:
data.extend(struct.pack("<H", 0))
# Pack link buffers
if self.link16_buffer:
data.extend(struct.pack("<H", len(self.link16_buffer)))
data.extend(self.link16_buffer)
else:
data.extend(struct.pack("<H", 0))
if self.link32_buffer:
data.extend(struct.pack("<H", len(self.link32_buffer)))
data.extend(self.link32_buffer)
else:
data.extend(struct.pack("<H", 0))
return bytes(data)
def unpack_record(self, data):
"""Unpack record data from bytes"""
offset = 0
# Unpack type and subtype
self.type, self.subtype = struct.unpack("<BB", data[offset:offset+2])
offset += 2
# Unpack name
name_len = struct.unpack("<H", data[offset:offset+2])[0]
offset += 2
self.name = data[offset:offset+name_len].decode('utf-8')
offset += name_len
# Unpack comment
comment_len = struct.unpack("<H", data[offset:offset+2])[0]
offset += 2
self.comment = data[offset:offset+comment_len].decode('utf-8')
offset += comment_len
# Unpack record data
record_len = struct.unpack("<H", data[offset:offset+2])[0]
offset += 2
if record_len > 0:
self.record = data[offset:offset+record_len]
offset += record_len
else:
self.record = b""
# Unpack link buffers
link16_len = struct.unpack("<H", data[offset:offset+2])[0]
offset += 2
if link16_len > 0:
self.link16_buffer = data[offset:offset+link16_len]
offset += link16_len
else:
self.link16_buffer = None
link32_len = struct.unpack("<H", data[offset:offset+2])[0]
offset += 2
if link32_len > 0:
self.link32_buffer = data[offset:offset+link32_len]
offset += link32_len
else:
self.link32_buffer = None
return True
def add_link(self, filename: str) -> bool:
"""Add link from OBJ/COFF file"""
try:
with open(filename, 'rb') as f:
data = f.read()
# Check for special functions
has_cure = b"_cure" in data or b"_Cure" in data
has_decode = b"_decode" in data
has_jmp = b"_jmp" in data
has_link = b"_Link" in data
# Set bits based on found functions
b = 0
if has_cure:
b |= 2
if has_decode or has_jmp or has_link or self.type == RT_KERNEL:
b |= 1
if not b:
return False
# Create new buffer for links
buffer = bytearray()
# Set bits based on found functions
if has_cure:
buffer.append(0x02)
if has_decode or has_jmp or has_link or self.type == RT_KERNEL:
buffer.append(0x01)
# Set Link32 buffer
if self.link32_buffer:
# In original code, it would ask for confirmation here
# For simplicity, we'll just replace it
self.link32_buffer = None
self.link32_buffer = bytes(buffer)
self.modify_flag = 1
return True
except:
return False
def unlink(self) -> bool:
"""Remove link buffers"""
try:
# Check if buffers exist
if not self.link16_buffer and not self.link32_buffer:
return False
# In original code, it would ask for confirmation here
# For simplicity, we'll just remove them
# Delete buffers
self.link16_buffer = None
self.link32_buffer = None
# Set comment if ExtraInfo exists
if self.extra_info:
self.extra_info.set_comment(self.comment)
self.modify_flag = 1
return True
except:
return False
class AvpEditDoc:
def __init__(self):
self.filename = None
self.modified = False
self.compression = False
self.records = []
self.clipboard = []
# Column settings - match original code
self.column_names = ["Name", "*", "Type", "SubType", "Method",
"Link16", "Link32", "Comment"]
self.column_widths = [100, 20, 50, 80, 50, 100, 100, 200]
self.header = AvpBaseHeader()
self.compression_level = 9
def new_document(self):
self.records.clear()
self.header = AvpBaseHeader()
self.filename = ""
self.modified = False
def open_document(self, filename: str) -> bool:
try:
with open(filename, 'rb') as f:
data = f.read()
if not self.header.unpack(data):
return False
offset = len(self.header.pack())
# Read compression flag
self.compression = bool(data[offset])
offset += 1
self.records.clear()
for _ in range(self.header.records_count):
record = RecordEdit(0)
record_size = struct.unpack("<I", data[offset:offset+4])[0]
offset += 4
record_data = data[offset:offset+record_size]
offset += record_size
if self.compression:
# Decompress record data
import zlib
record_data = zlib.decompress(record_data)
if not record.unpack_record(record_data):
return False
self.records.append(record)
self.filename = filename
self.modified = False
return True
except:
return False
def save_document(self, filename: str = None) -> bool:
if filename:
self.filename = filename
if not self.filename:
return False
try:
self.header.records_count = len(self.records)
self.header.modification_date = datetime.now()
data = bytearray(self.header.pack())
# Add compression flag
data.extend(struct.pack("<B", 1 if self.compression else 0))
for record in self.records:
record_data = record.pack_record()
if self.compression:
# Compress record data
import zlib
compressed = zlib.compress(record_data, self.compression_level)
data.extend(struct.pack("<I", len(compressed)))
data.extend(compressed)
else:
data.extend(struct.pack("<I", len(record_data)))
data.extend(record_data)
with open(self.filename, 'wb') as f:
f.write(data)
self.modified = False
return True
except:
return False
def add_record(self, record: RecordEdit) -> bool:
self.records.append(record)
self.modified = True
return True
def delete_record(self, index: int) -> bool:
if 0 <= index < len(self.records):
del self.records[index]
self.modified = True
return True
return False
def edit_record(self, index: int, record: RecordEdit) -> bool:
if 0 <= index < len(self.records):
self.records[index] = record
self.modified = True
return True
return False
def get_record(self, index: int) -> Optional[RecordEdit]:
if 0 <= index < len(self.records):
return self.records[index]
return None
def clipboard_copy(self, index: int) -> bool:
"""Copy record to clipboard"""
if 0 <= index < len(self.records):
self.clipboard.append(self.records[index])
return True
return False
def clipboard_cut(self, index: int) -> bool:
"""Cut record to clipboard"""
if self.clipboard_copy(index):
return self.delete_record(index)
return False
def clipboard_paste(self, index: int) -> bool:
"""Paste record from clipboard"""
if not self.clipboard:
return False
for record in self.clipboard:
self.insert_record(index, record)
index += 1
return True
def insert_record(self, index: int, record: RecordEdit) -> bool:
"""Insert record at specified index"""
if 0 <= index <= len(self.records):
self.records.insert(index, record)
self.modified = True
return True
return False
class AvpBaseHeader:
def __init__(self):
self.signature = b"AVP"
self.version = 1
self.records_count = 0
self.creation_date = datetime.now()
self.modification_date = datetime.now()
self.description = ""
self.checksum = 0
def pack(self) -> bytes:
data = bytearray()
data.extend(self.signature)
data.extend(struct.pack("<I", self.version))
data.extend(struct.pack("<I", self.records_count))
data.extend(struct.pack("<Q", int(self.creation_date.timestamp())))
data.extend(struct.pack("<Q", int(self.modification_date.timestamp())))
data.extend(struct.pack("<I", len(self.description)))
data.extend(self.description.encode())
data.extend(struct.pack("<I", self.checksum))
return bytes(data)
def unpack(self, data: bytes) -> bool:
try:
offset = 0
self.signature = data[offset:offset+3]
offset += 3
self.version = struct.unpack("<I", data[offset:offset+4])[0]
offset += 4
self.records_count = struct.unpack("<I", data[offset:offset+4])[0]
offset += 4
creation_timestamp = struct.unpack("<Q", data[offset:offset+8])[0]
offset += 8
self.creation_date = datetime.fromtimestamp(creation_timestamp)
modification_timestamp = struct.unpack("<Q", data[offset:offset+8])[0]
offset += 8
self.modification_date = datetime.fromtimestamp(modification_timestamp)
desc_len = struct.unpack("<I", data[offset:offset+4])[0]
offset += 4
self.description = data[offset:offset+desc_len].decode()
offset += desc_len
self.checksum = struct.unpack("<I", data[offset:offset+4])[0]
return True
except:
return False
class RecordEditDialog(tk.Toplevel):
def __init__(self, parent, record: RecordEdit):
super().__init__(parent)
self.record = record
self.result = None
self.title("Edit Record")
self.resizable(False, False)
# Create widgets
main_frame = ttk.Frame(self, padding="10")
main_frame.grid(row=0, column=0, sticky="nsew")
# Name
ttk.Label(main_frame, text="Name:").grid(row=0, column=0, sticky="w", pady=5)
self.name_entry = ttk.Entry(main_frame, width=40)
self.name_entry.grid(row=0, column=1, columnspan=2, sticky="ew", pady=5)
self.name_entry.insert(0, record.get_name())
# Type
ttk.Label(main_frame, text="Type:").grid(row=1, column=0, sticky="w", pady=5)
self.type_combo = ttk.Combobox(main_frame, values=TYPE_STRINGS[:-1], state="readonly", width=37)
self.type_combo.grid(row=1, column=1, columnspan=2, sticky="ew", pady=5)
self.type_combo.set(TYPE_STRINGS[record.type])
self.type_combo.bind("<<ComboboxSelected>>", self.on_type_changed)
# SubType
ttk.Label(main_frame, text="SubType:").grid(row=2, column=0, sticky="w", pady=5)
self.subtype_frame = ttk.Frame(main_frame)
self.subtype_frame.grid(row=2, column=1, columnspan=2, sticky="ew", pady=5)
self.subtype_vars = []
self.update_subtype_widgets()
# Method
ttk.Label(main_frame, text="Method:").grid(row=3, column=0, sticky="w", pady=5)
self.method_combo = ttk.Combobox(main_frame, state="readonly", width=37)
self.method_combo.grid(row=3, column=1, columnspan=2, sticky="ew", pady=5)
self.update_method_widgets()
# Comment
ttk.Label(main_frame, text="Comment:").grid(row=4, column=0, sticky="w", pady=5)
self.comment_text = tk.Text(main_frame, width=40, height=5)
self.comment_text.grid(row=4, column=1, columnspan=2, sticky="ew", pady=5)
self.comment_text.insert("1.0", record.get_comment())
# Add Link buttons
link_frame = ttk.Frame(main_frame)
link_frame.grid(row=5, column=0, columnspan=3, pady=5)
ttk.Button(link_frame, text="Link 16-bit", command=self.on_link16).pack(side="left", padx=5)
ttk.Button(link_frame, text="Link 32-bit", command=self.on_link32).pack(side="left", padx=5)
ttk.Button(link_frame, text="Unlink", command=self.on_unlink).pack(side="left", padx=5)
# Move buttons to row 6
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=6, column=0, columnspan=3, pady=10)
ttk.Button(button_frame, text="OK", command=self.on_ok).pack(side="left", padx=5)
ttk.Button(button_frame, text="Cancel", command=self.on_cancel).pack(side="left", padx=5)
# Configure grid weights
main_frame.columnconfigure(1, weight=1)
# Make dialog modal
self.transient(parent)
self.grab_set()
# Set minimum size and update geometry
self.update_idletasks()
min_width = main_frame.winfo_reqwidth() + 40 # Add padding
min_height = main_frame.winfo_reqheight() + 40 # Add padding
self.minsize(min_width, min_height)
# Center dialog
width = self.winfo_width()
height = self.winfo_height()
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}")
parent.wait_window(self)
def update_subtype_widgets(self):
# Clear existing widgets
for widget in self.subtype_frame.winfo_children():
widget.destroy()
self.subtype_vars.clear()
record_type = TYPE_STRINGS.index(self.type_combo.get())
if record_type in [RT_FILE, RT_CA, RT_UNPACK, RT_EXTRACT]:
for i, st in enumerate(SUBTYPE_STRINGS):
if st:
var = tk.BooleanVar(value=bool(self.record.subtype & (1 << i)))
self.subtype_vars.append(var)
ttk.Checkbutton(self.subtype_frame, text=st.strip(), variable=var).pack(side="left")
elif record_type == RT_SECTOR:
for i, st in enumerate(SUBTYPE_SECTOR):
if st:
var = tk.BooleanVar(value=bool(self.record.record and struct.unpack("<I", self.record.record[:4])[0] & (1 << i)))
self.subtype_vars.append(var)
ttk.Checkbutton(self.subtype_frame, text=st, variable=var).pack(side="left")
def update_method_widgets(self):
record_type = TYPE_STRINGS.index(self.type_combo.get())
if record_type in METHOD_STRINGS:
self.method_combo["values"] = list(METHOD_STRINGS[record_type].values())
if self.record.record:
try:
if record_type == RT_FILE:
method = struct.unpack("<B", self.record.record[:1])[0] & 0x7F
elif record_type == RT_SECTOR:
method = struct.unpack("<B", self.record.record[:1])[0] & 0x0F
elif record_type == RT_JUMP:
method = struct.unpack("<B", self.record.record[:1])[0]
elif record_type == RT_MEMORY:
method = struct.unpack("<B", self.record.record[:1])[0]
else:
method = 0
self.method_combo.set(METHOD_STRINGS[record_type].get(method, "Unknown"))
except:
self.method_combo.set("Unknown")
else:
self.method_combo["values"] = []
self.method_combo.set("")
def on_type_changed(self, event):
self.update_subtype_widgets()
self.update_method_widgets()
def on_ok(self):
# Validate and save changes
name = self.name_entry.get()
if not self.record.set_name(name):
messagebox.showerror("Error", "Invalid name format")
return
comment = self.comment_text.get("1.0", "end-1c")
if not self.record.set_comment(comment):
messagebox.showerror("Error", "Invalid comment format")
return
# Update type and subtype
self.record.type = TYPE_STRINGS.index(self.type_combo.get())
if self.record.type in [RT_FILE, RT_CA, RT_UNPACK, RT_EXTRACT]:
self.record.subtype = 0
for i, var in enumerate(self.subtype_vars):
if var.get():
self.record.subtype |= (1 << i)
elif self.record.type == RT_SECTOR:
if not self.record.record:
self.record.record = struct.pack("<I", 0)
sector = list(struct.unpack("<I", self.record.record[:4]))
for i, var in enumerate(self.subtype_vars):
if var.get():
sector[0] |= (1 << i)
else:
sector[0] &= ~(1 << i)
self.record.record = struct.pack("<I", *sector)
# Update method
if self.record.type in METHOD_STRINGS:
method = list(METHOD_STRINGS[self.record.type].keys())[list(METHOD_STRINGS[self.record.type].values()).index(self.method_combo.get())]
if not self.record.record:
self.record.record = struct.pack("<B", 0)
if self.record.type == RT_FILE:
self.record.record = struct.pack("<B", method & 0x7F) + self.record.record[1:]
elif self.record.type == RT_SECTOR:
self.record.record = struct.pack("<B", method & 0x0F) + self.record.record[1:]
elif self.record.type in [RT_JUMP, RT_MEMORY]:
self.record.record = struct.pack("<B", method) + self.record.record[1:]
# Set result and close dialog
self.result = self.record
self.destroy()
def on_cancel(self):
self.destroy()
def on_link16(self):
"""Handle 16-bit link"""
filename = filedialog.askopenfilename(
title="Select 16-bit OBJ file",
filetypes=[
("16-bit OBJ files", "*.o16"),
("All files", "*.*")
]
)
if filename:
if self.record.add_link(filename):
messagebox.showinfo("Success", "16-bit link added successfully")
else:
messagebox.showerror("Error", "Failed to add 16-bit link")
def on_link32(self):
"""Handle 32-bit link"""
filename = filedialog.askopenfilename(
title="Select 32-bit OBJ file",
filetypes=[
("32-bit OBJ files", "*.o32"),
("All files", "*.*")
]
)
if filename:
if self.record.add_link(filename):
messagebox.showinfo("Success", "32-bit link added successfully")
else:
messagebox.showerror("Error", "Failed to add 32-bit link")
def on_unlink(self):
"""Handle unlink"""
if self.record.unlink():
messagebox.showinfo("Success", "Links removed successfully")
else:
messagebox.showerror("Error", "Failed to remove links")
class AboutDialog(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.title("About AVP Edit")
# Make dialog modal
self.transient(parent)
self.grab_set()
self.create_widgets()
self.center_window()
def create_widgets(self):
main_frame = ttk.Frame(self, padding="20")
main_frame.grid(row=0, column=0, sticky="nsew")
# Title
title_label = ttk.Label(main_frame, text="AVP Edit", font=("Helvetica", 16, "bold"))
title_label.grid(row=0, column=0, pady=(0, 10))
# Version
version_label = ttk.Label(main_frame, text="Version 1.0")
version_label.grid(row=1, column=0, pady=(0, 10))
# Description
desc_text = "AVP Edit is a tool for managing virus signature databases.\n\n"
desc_text += "Features:\n"
desc_text += "- View and edit virus signatures\n"
desc_text += "- Add/remove records\n"
desc_text += "- Manage links\n"
desc_text += "- Import/export databases"
desc_label = ttk.Label(main_frame, text=desc_text, justify="left")
desc_label.grid(row=2, column=0, pady=(0, 20))
# OK button
ttk.Button(main_frame, text="OK", command=self.destroy).grid(row=3, column=0)
def center_window(self):
self.update_idletasks()
width = self.winfo_width()
height = self.winfo_height()
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}")
class SettingsDialog(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.title("Settings")
# Make dialog modal
self.transient(parent)
self.grab_set()
self.result = None
self.create_widgets()
self.center_window()
def create_widgets(self):
main_frame = ttk.Frame(self, padding="10")
main_frame.grid(row=0, column=0, sticky="nsew")
# Compression
self.compression_var = tk.BooleanVar(value=False)
ttk.Checkbutton(main_frame, text="Enable Compression",
variable=self.compression_var).grid(row=0, column=0, sticky="w", pady=5)
# Auto-save
self.autosave_var = tk.BooleanVar(value=True)
ttk.Checkbutton(main_frame, text="Auto-save on Exit",
variable=self.autosave_var).grid(row=1, column=0, sticky="w", pady=5)
# Default directory
ttk.Label(main_frame, text="Default Directory:").grid(row=2, column=0, sticky="w", pady=5)
self.dir_entry = ttk.Entry(main_frame, width=40)
self.dir_entry.grid(row=2, column=1, sticky="ew", pady=5)
ttk.Button(main_frame, text="Browse",
command=self.browse_directory).grid(row=2, column=2, padx=5, pady=5)
# Buttons
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=3, column=0, columnspan=3, pady=10)
ttk.Button(button_frame, text="OK", command=self.on_ok).pack(side="left", padx=5)
ttk.Button(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5)
# Configure grid weights
main_frame.columnconfigure(1, weight=1)
def center_window(self):
self.update_idletasks()
width = self.winfo_width()
height = self.winfo_height()
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}")
def browse_directory(self):
directory = filedialog.askdirectory(
title="Select Default Directory"
)
if directory:
self.dir_entry.delete(0, tk.END)
self.dir_entry.insert(0, directory)
def on_ok(self):
self.result = {
"compression": self.compression_var.get(),
"autosave": self.autosave_var.get(),
"default_dir": self.dir_entry.get()
}
self.destroy()
class SearchDialog(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.title("Find")
self.resizable(False, False)
self.result = None
# Create widgets
main_frame = ttk.Frame(self, padding="10")
main_frame.grid(row=0, column=0, sticky="nsew")
# Search text
ttk.Label(main_frame, text="Find what:").grid(row=0, column=0, sticky="w", pady=5)
self.search_text = ttk.Entry(main_frame, width=40)
self.search_text.grid(row=0, column=1, columnspan=2, sticky="ew", pady=5)
# Search type
ttk.Label(main_frame, text="Look in:").grid(row=1, column=0, sticky="w", pady=5)
self.search_type = ttk.Combobox(main_frame,
values=["Name", "Comment", "Type", "SubType", "Method"],
state="readonly")
self.search_type.grid(row=1, column=1, columnspan=2, sticky="ew", pady=5)
self.search_type.set("Name")
# Options
options_frame = ttk.LabelFrame(main_frame, text="Options", padding="5")
options_frame.grid(row=2, column=0, columnspan=3, sticky="ew", pady=5)
self.case_sensitive = tk.BooleanVar()
ttk.Checkbutton(options_frame, text="Match case",
variable=self.case_sensitive).pack(side="left", padx=5)
self.direction = tk.StringVar(value="forward")
ttk.Radiobutton(options_frame, text="Forward", variable=self.direction,
value="forward").pack(side="left", padx=5)
ttk.Radiobutton(options_frame, text="Backward", variable=self.direction,
value="backward").pack(side="left", padx=5)
# Buttons
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=3, column=0, columnspan=3, pady=10)
ttk.Button(button_frame, text="Find Next", command=self.on_ok).pack(side="left", padx=5)
ttk.Button(button_frame, text="Cancel", command=self.on_cancel).pack(side="left", padx=5)
# Center dialog
self.update_idletasks()
width = self.winfo_width()
height = self.winfo_height()
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}")
# Make dialog modal
self.transient(parent)
self.grab_set()
parent.wait_window(self)
def on_ok(self):
"""Handle OK button click"""
self.result = {
"text": self.search_text.get(),
"search_type": self.search_type.get().lower(),
"case_sensitive": self.case_sensitive.get(),
"direction": self.direction.get()
}
self.destroy()
def on_cancel(self):
"""Handle Cancel button click"""
self.destroy()
class AvpEditView(ttk.Frame):
def __init__(self, parent, doc: AvpEditDoc):
super().__init__(parent)
self.doc = doc
self.create_widgets()
def create_widgets(self):
# Create treeview
self.tree = ttk.Treeview(self, columns=self.doc.column_names, show='headings')
for i, name in enumerate(self.doc.column_names):
self.tree.heading(name, text=name)
self.tree.column(name, width=self.doc.column_widths[i])
# Add scrollbars
vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
hsb = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
# Grid layout
self.tree.grid(row=0, column=0, sticky='nsew')
vsb.grid(row=0, column=1, sticky='ns')
hsb.grid(row=1, column=0, sticky='ew')
# Configure grid weights
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
# Bind events
self.tree.bind('<Double-1>', self.on_double_click)
self.tree.bind('<Delete>', self.on_delete)
def refresh_view(self):
# Clear existing items
for item in self.tree.get_children():
self.tree.delete(item)
# Add records
for record in self.doc.records:
values = [
record.get_name(), # Name
"", # Mark (empty for now)
record.get_type_string(), # Type
record.get_sub_type_string(), # SubType
record.get_method_string(), # Method
record.get_link16_string(), # Link16
record.get_link32_string(), # Link32
record.get_comment() # Comment
]
self.tree.insert('', 'end', values=values)
def on_double_click(self, event):
item = self.tree.selection()[0]
index = self.tree.index(item)
record = self.doc.get_record(index)
if record:
dialog = RecordEditDialog(self, record)
if dialog.result:
self.doc.edit_record(index, dialog.result)
self.refresh_view()
def on_delete(self, event):
items = self.tree.selection()
for item in items:
index = self.tree.index(item)
self.doc.delete_record(index)
self.refresh_view()
class AvpEditApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("AVP Edit")
self.geometry("800x600")
self.doc = AvpEditDoc()
self.create_widgets()
self.create_menu()
def create_widgets(self):
self.view = AvpEditView(self, self.doc)
self.view.pack(fill=tk.BOTH, expand=True)
def create_menu(self):
menubar = tk.Menu(self)
self.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="New", command=self.on_new)
file_menu.add_command(label="Open...", command=self.on_open)
file_menu.add_command(label="Save", command=self.on_save)
file_menu.add_command(label="Save As...", command=self.on_save_as)
file_menu.add_separator()
file_menu.add_command(label="Pack/Compress", command=self.on_pack_file)
file_menu.add_command(label="Save and Reload", command=self.on_save_reload)
file_menu.add_separator()
file_menu.add_command(label="Exit", command=self.quit)
# Edit menu
edit_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Edit", menu=edit_menu)
edit_menu.add_command(label="Insert Record", command=self.on_insert_record)
edit_menu.add_command(label="Delete Record", command=self.on_delete_record)
edit_menu.add_command(label="Edit Record", command=self.on_edit_record)
edit_menu.add_separator()
edit_menu.add_command(label="Cut", command=self.on_cut)
edit_menu.add_command(label="Copy", command=self.on_copy)
edit_menu.add_command(label="Paste", command=self.on_paste)
edit_menu.add_separator()
edit_menu.add_command(label="Find...", command=self.on_find)
edit_menu.add_command(label="Find Again", command=self.on_find_again)
# View menu
view_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="View", menu=view_menu)
view_menu.add_command(label="Column Settings...", command=self.on_column_settings)
# Status bar
self.status = ttk.Label(self, text="Ready", relief=tk.SUNKEN)
self.status.pack(side=tk.BOTTOM, fill=tk.X)
def on_new(self):
if self.doc.modified:
if not messagebox.askyesno("Save Changes", "Do you want to save changes?"):
return
if not self.on_save():
return
self.doc.new_document()
self.view.refresh_view()
def on_open(self):
if self.doc.modified:
if not messagebox.askyesno("Save Changes", "Do you want to save changes?"):
return
if not self.on_save():
return
filename = filedialog.askopenfilename(
title="Open AVP Database",
filetypes=[("AVP Database", "*.avp"), ("All files", "*.*")]
)
if filename:
if self.doc.open_document(filename):
self.view.refresh_view()
else:
messagebox.showerror("Error", "Failed to open file")
def on_save(self):
if not self.doc.filename:
return self.on_save_as()
return self.doc.save_document()
def on_save_as(self):
filename = filedialog.asksaveasfilename(
title="Save AVP Database",
filetypes=[("AVP Database", "*.avp"), ("All files", "*.*")],
defaultextension=".avp"
)
if filename:
return self.doc.save_document(filename)
return False
def on_pack_file(self):
"""Pack/compress the current file"""
if not self.doc.filename:
messagebox.showerror("Error", "No file is open")
return
if not self.doc.modified and not messagebox.askyesno("Pack File",
"File is not modified. Pack anyway?"):
return
self.doc.compression = True
if self.on_save():
messagebox.showinfo("Success", "File packed successfully")
else:
messagebox.showerror("Error", "Failed to pack file")
def on_save_reload(self):
"""Save the current file and reload it"""
if not self.doc.filename:
messagebox.showerror("Error", "No file is open")
return
if self.on_save():
self.on_open(self.doc.filename)
messagebox.showinfo("Success", "File saved and reloaded")
else:
messagebox.showerror("Error", "Failed to save file")
def on_insert_record(self):
"""Insert a new record at the selected position"""
items = self.view.tree.selection()
if not items:
index = len(self.doc.records)
else:
index = self.view.tree.index(items[0])
record = RecordEdit(0) # Initialize with type 0
dialog = RecordEditDialog(self, record)
if dialog.result:
self.doc.insert_record(index, dialog.result)
self.view.refresh_view()
def on_delete_record(self):
"""Delete selected records"""
items = self.view.tree.selection()
if not items:
return
if messagebox.askyesno("Confirm Delete",
"Are you sure you want to delete selected records?"):
for item in reversed(items):
index = self.view.tree.index(item)
self.doc.delete_record(index)
self.view.refresh_view()
def on_edit_record(self):
"""Edit selected record"""
items = self.view.tree.selection()
if not items:
return
index = self.view.tree.index(items[0])
record = self.doc.get_record(index)
if record:
dialog = RecordEditDialog(self, record)
if dialog.result:
self.doc.edit_record(index, dialog.result)
self.view.refresh_view()
def on_cut(self):
"""Cut selected records to clipboard"""
items = self.view.tree.selection()
if not items:
return
# Copy to clipboard
self.on_copy()
# Delete selected
self.on_delete_record()
def on_copy(self):
"""Copy selected records to clipboard"""
items = self.view.tree.selection()
if not items:
return
self.doc.clipboard.clear()
for item in items:
index = self.view.tree.index(item)
record = self.doc.get_record(index)
if record:
self.doc.clipboard.append(record)
def on_paste(self):
"""Paste records from clipboard"""
if not self.doc.clipboard:
return
items = self.view.tree.selection()
if not items:
index = len(self.doc.records)
else:
index = self.view.tree.index(items[0])
for record in self.doc.clipboard:
self.doc.insert_record(index, record)
index += 1
self.view.refresh_view()
def on_find(self):
"""Find records matching search criteria"""
dialog = SearchDialog(self)
if dialog.result:
self.last_search = dialog.result
self.find_next_match()
def on_find_again(self):
"""Find next match using last search criteria"""
if not hasattr(self, "last_search"):
self.on_find()
return
self.find_next_match()
def find_next_match(self):
"""Find next record matching search criteria"""
if not self.last_search:
return
items = self.view.tree.get_children()
if not items:
return
# Get start index
current = self.view.tree.selection()
if not current:
start_index = 0
else:
start_index = self.view.tree.index(current[0])
if self.last_search["direction"] == "forward":
start_index += 1
else:
start_index -= 1
if start_index < 0:
start_index = len(items) - 1
elif start_index >= len(items):
start_index = 0
# Search for match
for i in range(len(items)):
index = (start_index + i) % len(items)
if self.match_record(index):
self.view.tree.selection_set(items[index])
self.view.tree.see(items[index])
return
messagebox.showinfo("Search", "No more matches found")
def match_record(self, index):
"""Check if record matches search criteria"""
record = self.doc.get_record(index)
if not record:
return False
search_text = self.last_search["text"]
if not self.last_search["case_sensitive"]:
search_text = search_text.lower()
if self.last_search["search_type"] == "name":
text = record.name
elif self.last_search["search_type"] == "comment":
text = record.comment
elif self.last_search["search_type"] == "type":
text = record.get_type_string()
elif self.last_search["search_type"] == "subtype":
text = record.get_sub_type_string()
elif self.last_search["search_type"] == "method":
text = record.get_method_string()
else:
return False
if not self.last_search["case_sensitive"]:
text = text.lower()
return search_text in text
def on_column_settings(self):
"""Change column settings"""
dialog = ColumnDialog(self)
if dialog.result:
# Update column widths
self.view.tree.column("#0", width=dialog.result["name"])
self.view.tree.column("Mark", width=dialog.result["mark"])
self.view.tree.column("Type", width=dialog.result["type"])
self.view.tree.column("SubType", width=dialog.result["subtype"])
self.view.tree.column("Method", width=dialog.result["method"])
self.view.tree.column("Link16", width=dialog.result["link16"])
self.view.tree.column("Link32", width=dialog.result["link32"])
self.view.tree.column("Comment", width=dialog.result["comment"])
# Save settings
self.settings["column_widths"] = dialog.result
self.save_settings()
if __name__ == "__main__":
app = AvpEditApp()
app.mainloop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment