From 34776398055387c1dcb803ffa2894b1041867b3c Mon Sep 17 00:00:00 2001 From: Nenotriple <70049990+Nenotriple@users.noreply.github.com> Date: Tue, 12 Sep 2023 07:10:30 -0700 Subject: [PATCH] Many changes/fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # v1.7 changes: # If auto-save is enabled, changes to the text file are now saved automatically when the window is closed. # If auto-save isn’t enabled when closing, a warning is given. This allows the user to cancel and go back and save if needed. # The insert_completed_suggestion function now mostly* positions the cursor at the end of multi-word suggestions. # Autocomplete scorer now prioritizes whole word matches, addressing the bias towards longer tags. # Pairs are now logically sorted during loading, fixing the issue where filenames like “10” or “100” loaded before “2” or “9”. # The unused is_modified definition has been removed. # The Pillow library and danbooru.csv file can now be installed and downloaded respectively, with an option to cancel/ignore. # A new “Options” menu has been added: # Adjust font size. # Adjust suggestion quantity, allowing you to set a value between 0-10. # Delete img-txt pairs. A warning popup will confirm the choice or give the user an option to cancel. # UI enhancements include: # Improved formatting and font size for info_label. # Restructured layout for saved_label and auto-save checkbox. # Color and width adjustments for saved_label. # highlight_selected_suggestion now uses a “Black Medium-Small Square” as an additional visual indicator. # An ellipsis has been added to the idle suggestion label. --- img-txt_viewer.py | 310 +++++++++++++++++++++++++++++++++------------- 1 file changed, 227 insertions(+), 83 deletions(-) diff --git a/img-txt_viewer.py b/img-txt_viewer.py index 6ff0af5..19494e5 100644 --- a/img-txt_viewer.py +++ b/img-txt_viewer.py @@ -1,9 +1,25 @@ from tkinter import * from tkinter.filedialog import askdirectory -from tkinter import messagebox, Tk -from PIL import ImageTk, Image +from tkinter import messagebox, Tk, ttk import csv, os, re +try: + print("Pillow already installed... Skipping...") + from PIL import ImageTk, Image +except ImportError: + print("Pillow not found...") + import subprocess, sys + root = Tk() + root.withdraw() + install_pillow = messagebox.askyesno("Pillow not installed!", " Pillow (image viewer) not found. Would you like to install it? ~2.5MB \n\n It's required to display images.") + if install_pillow: + subprocess.check_call(["python", '-m', 'pip', 'install', 'pillow']) + messagebox.showinfo("Pillow Installed", " Successfully installed Pillow. \n\n Please restart the program.") + sys.exit() + +################################################################################################################################################ +################################################################################################################################################ + class Autocomplete: def __init__(self, data_file, max_suggestions=4): self.data = self.load_data(data_file) @@ -11,6 +27,16 @@ def __init__(self, data_file, max_suggestions=4): def load_data(self, data_file): data = {} + if not os.path.isfile(data_file): + import requests + download = messagebox.askyesno("File not found.", " danbooru.csv is required for autocomplete suggestions.\n\n Do you want to download it from the repo? \n\n Yes = Download \n No = Ignore") + if download: + url = "https://raw.githubusercontent.com/Nenotriple/img-txt_viewer/main/danbooru.csv" + response = requests.get(url) + with open(data_file, 'wb') as f: + f.write(response.content) + else: + return data with open(data_file, newline='', encoding='utf-8') as csvfile: reader = csv.reader(csvfile) for row in reader: @@ -35,95 +61,125 @@ def autocomplete(self, text): def get_relevance_score(self, suggestion, text): score = 0 - for i in range(len(text)): - if suggestion[i] == text[i]: - score += 1 - else: - break + if suggestion == text: + score += len(text) * 2 + else: + for i in range(len(text)): + if suggestion[i] == text[i]: + score += 1 + else: + break return score def update_data(self, data_file): self.data = self.load_data(data_file) +################################################################################################################################################ +################################################################################################################################################ + class ImageTextViewer: def __init__(self, master): - self.master = master - master.title("v1.68 - img-txt_viewer --- github.com/Nenotriple/img-txt_viewer") + self.master = master + master.title("v1.7 - img-txt_viewer --- github.com/Nenotriple/img-txt_viewer") master.bind("", lambda event: self.save_text_file()) master.bind("", lambda event: self.prev_pair()) - master.bind("", lambda event: self.next_pair()) - self.master.minsize(1050, 500) + master.bind("", lambda event: self.next_pair()) + self.autocomplete = Autocomplete("danbooru.csv") + self.autocomplete.max_suggestions = 4 + self.master.minsize(905, 550) root.geometry("1050x680") self.max_width = IntVar() self.max_width.set(650) + self.theme_var = StringVar() + self.theme_var.set("Default") + menubar = Menu(self.master) self.master.config(menu=menubar) - filemenu = Menu(menubar, tearoff=0) - sizemenu = Menu(filemenu, tearoff=0) + sizemenu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Image Size", menu=sizemenu) - sizemenu.add_radiobutton(label="Smallest", variable=self.max_width, value=512, command=lambda: self.adjust_window(1050, 540, 1050, 500)) - sizemenu.add_radiobutton(label="Small", variable=self.max_width, value=650, command=lambda: self.adjust_window(1050, 680, 1050, 500)) - sizemenu.add_radiobutton(label="Medium", variable=self.max_width, value=800, command=lambda: self.adjust_window(1200, 830, 1200, 500)) - sizemenu.add_radiobutton(label="Large", variable=self.max_width, value=1000, command=lambda: self.adjust_window(1400, 1030, 1400, 500)) - sizemenu.add_radiobutton(label="Largest", variable=self.max_width, value=1200, command=lambda: self.adjust_window(1800, 1230, 1800, 500)) + sizemenu.add_radiobutton(label="Smallest", variable=self.max_width, value=512, command=lambda: self.adjust_window(1050, 540, 900, 550)) # (setW, setH, minW, minH) + sizemenu.add_radiobutton(label="Small", variable=self.max_width, value=650, command=lambda: self.adjust_window(1050, 680, 905, 550)) + sizemenu.add_radiobutton(label="Medium", variable=self.max_width, value=800, command=lambda: self.adjust_window(1200, 830, 1200, 550)) + sizemenu.add_radiobutton(label="Large", variable=self.max_width, value=1000, command=lambda: self.adjust_window(1400, 1030, 1400, 550)) + sizemenu.add_radiobutton(label="Largest", variable=self.max_width, value=1800, command=lambda: self.adjust_window(1900, 1230, 1800, 550)) + textmenu = Menu(menubar, tearoff=0) + menubar.add_cascade(label="Options", menu=textmenu) + textmenu.add_command(label="Font Size", command=self.set_font_size) + textmenu.add_command(label="Suggestion Quantity", command=self.set_suggestion_quantity) + textmenu.add_command(label="Delete_Pair", command=self.delete_pair) + self.image_dir = StringVar() + self.button_label = StringVar() + self.auto_save_var = BooleanVar() + self.auto_save_var.set(False) + self.text_modified = False self.current_index = 0 + self.selected_suggestion_index = 0 self.image_files = [] self.text_files = [] self.suggestions = [] - self.button_label = StringVar() self.image_dir.set("Choose Directory") self.label_image = Label(master) - self.text_box = Text(master, height=20, width=50, wrap=WORD, undo=True, maxundo=100) + self.text_box = Text(master, height=5, width=5, wrap=WORD, undo=True, maxundo=200) self.suggestion_label = Label(master, text="") + self.directory_button = Button(root, textvariable=self.image_dir, command=self.choose_directory) self.save_button = Button(self.master, text="Save", command=self.save_text_file, fg="blue") - self.auto_save_var = BooleanVar() - self.auto_save_var.set(False) - self.text_modified = False self.directory_button.pack(side=TOP, fill=X) self.label_image.pack(side=LEFT) separator_frame = Frame(root, height=8) separator_frame.pack() - frame = Frame(master) - frame.pack() - self.next_button = Button(frame, text=" Next---> ", command=self.next_pair) - self.prev_button = Button(frame, text="<---Previous", command=self.prev_pair) - self.next_button.pack(side=RIGHT, fill=X, padx=2, expand=True) - self.prev_button.pack(side=LEFT, fill=X, padx=2, expand=True) self.save_button.pack(side=TOP, fill=X, pady=3) - self.auto_save_checkbutton = Checkbutton(master, text="Auto-save", variable=self.auto_save_var) - self.auto_save_checkbutton.pack(side=TOP) - self.create_saved_label() - self.info_label = Label(self.master, text="\n\n" - "CTRL+Z / CTRL+Y: Undo/Redo.\n\n" - "CTRL+S: to save the current text file.\n\n" - "ALT+Left/Right arrow keys quickly moves between img/txt pairs.\n\n" - "\n" - "Get autocomplete suggestions while you type using danbooru tags.\n\n" - "Pressing TAB inserts the selected suggestion.\n\n" - "Presing ALT cycles though suggestions.\n\n" - "\n" - "Select text to see duplicates.\n\n" - "If the auto-save box is checked, text will be saved when moving between img/txt pairs.\n\n" - "Blank text files can be created for images without any matching files when loading a directory.\n\n") - self.info_label.pack(anchor=N) + + button_frame = Frame(master) + button_frame.pack() + self.next_button = Button(button_frame, text="Next--->", command=self.next_pair, width=16) + self.prev_button = Button(button_frame, text="<---Previous", command=self.prev_pair, width=16) + self.next_button.pack(side=RIGHT, padx=2, pady=2) + self.prev_button.pack(side=RIGHT, padx=2, pady=2) + + saved_label_frame = Frame(master) + saved_label_frame.pack(pady=2) + self.auto_save_checkbutton = Checkbutton(saved_label_frame, text="Auto-save", variable=self.auto_save_var, command=self.change_label) + self.auto_save_checkbutton.pack(side=RIGHT) + self.saved_label(saved_label_frame) + + self.info_label = Label(self.master, text= + "Keyboard Shortcuts:\n" + " ▪️ CTRL+Z / CTRL+Y: Undo/Redo.\n" + " ▪️ CTRL+S: Save the current text file.\n" + " ▪️ ALT+Left/Right arrow keys: Quickly move between img/txt pairs.\n\n" + + "Autocomplete Suggestions:\n" + " ▪️ Get autocomplete suggestions while you type using danbooru tags.\n" + " ▪️ Press TAB to insert the selected suggestion.\n" + " ▪️ Press ALT to cycle through suggestions.\n\n" + + "Text Selection:\n" + " ▪️ Select text to see duplicates.\n\n" + + "Auto-save Feature:\n" + " ▪️ If the auto-save box is checked, text will be saved when moving between img/txt pairs.\n\n" + "File Creation:\n" + " ▪️ Blank text files can be created for images without any matching files when loading a directory.\n", + justify=LEFT, font=("Segoe UI", 10)) + self.info_label.pack(anchor=W) + + self.text_box.tag_configure("highlight", background="#5da9be") self.text_box.bind("", lambda event: self.change_label()) - self.text_box.tag_configure("highlight", background="lightblue") self.text_box.bind("", lambda event: self.remove_highlight()) self.text_box.bind("", lambda event: self.remove_highlight()) - self.autocomplete = Autocomplete("danbooru.csv") self.text_box.bind("", self.update_suggestions) - self.selected_suggestion_index = 0 -######################################################################## +################################################################################################################################################ +################################################################################################################################################ def update_suggestions(self, event): if event.keysym == "Tab": if self.selected_suggestion_index < len(self.suggestions): completed_suggestion = self.suggestions[self.selected_suggestion_index] self.insert_completed_suggestion(completed_suggestion) - self.suggestion_label.config(text="") + self.suggestion_label.config(text="...") self.suggestions = [] elif event.keysym in ("Alt_L", "Alt_R"): if self.suggestions: @@ -133,10 +189,10 @@ def update_suggestions(self, event): self.selected_suggestion_index = (self.selected_suggestion_index + 1) % len(self.suggestions) self.highlight_selected_suggestion() elif event.keysym in ("Up", "Down"): - self.suggestion_label.config(text="") + self.suggestion_label.config(text="...") self.suggestions = [] elif event.char == ",": - self.suggestion_label.config(text="") + self.suggestion_label.config(text="...") self.suggestions = [] else: text = self.text_box.get("1.0", "insert") @@ -153,9 +209,9 @@ def update_suggestions(self, event): self.selected_suggestion_index = 0 self.highlight_selected_suggestion() else: - self.suggestion_label.config(text="") + self.suggestion_label.config(text="...") else: - self.suggestion_label.config(text="") + self.suggestion_label.config(text="...") def insert_completed_suggestion(self, completed_suggestion): text = self.text_box.get("1.0", "insert") @@ -171,14 +227,16 @@ def insert_completed_suggestion(self, completed_suggestion): self.text_box.delete("1.0", "end") self.text_box.insert("1.0", updated_text) self.text_box.mark_set("insert", cursor_position) - self.text_box.event_generate("") - + num_words_in_suggestion = len(completed_suggestion.split()) + for _ in range(num_words_in_suggestion): + self.text_box.event_generate("") + def highlight_selected_suggestion(self): if self.suggestions: suggestion_text = "" for i, s in enumerate(self.suggestions): if i == self.selected_suggestion_index: - label_text = f"[{s}]" + label_text = f"◾[{s}]" else: label_text = s suggestion_text += label_text + ", " @@ -207,34 +265,28 @@ def _highlight_duplicates(self, mouse): self.text_box.tag_add("highlight", f"1.0 + {start} chars", f"1.0 + {end} chars") def remove_highlight(self): - self.text_box.tag_remove("highlight", "1.0", "end") + self.text_box.tag_remove("highlight", "1.0", "end") -######################################################################## +################################################################################################################################################ +################################################################################################################################################ - def create_saved_label(self): - self.saved_label = Label(self.master, text="No Changes") - self.saved_label.pack(anchor=N) + def saved_label(self, saved_label_frame): + self.saved_label = Label(saved_label_frame, text="No Changes", width=23) + self.saved_label.pack() self.text_box.bind("", lambda event: self.text_modified()) def change_label(self): if self.auto_save_var.get(): - self.saved_label.config(text="Changes Being autosaved", bg="#4DB6D2", fg="white") + self.saved_label.config(text="Changes are autosaved", bg="#5da9be", fg="white") else: - self.saved_label.config(text="Changes Are Not Saved", bg="red", fg="white") + self.saved_label.config(text="Changes not saved", bg="#FD8A8A", fg="white") - def is_modified(self, *args): - if self.text_files: - text_file = self.text_files[self.current_index] - with open(text_file, "r") as f: - file_content = f.read() - text_content = self.text_box.get("1.0", END) - self.text_modified = file_content != text_content - if self.text_modified: - self.saved_label.config(text="Changes Not Saved", fg="red") - else: - self.saved_label.config(text="No Changes", fg="black") +################################################################################################################################################ +################################################################################################################################################ -######################################################################## + def natural_sort(self, s): + return [int(text) if text.isdigit() else text.lower() + for text in re.split(r'(\d+)', s)] def choose_directory(self): try: @@ -251,7 +303,7 @@ def load_pairs(self): self.image_files = [] self.text_files = [] new_text_files = [] - for filename in sorted(os.listdir(self.image_dir.get())): + for filename in sorted(os.listdir(self.image_dir.get()), key=self.natural_sort): if filename.endswith((".jpg", ".jpeg", ".png", ".webp", ".bmp")): self.image_files.append(os.path.join(self.image_dir.get(), filename)) text_filename = os.path.splitext(filename)[0] + ".txt" @@ -308,10 +360,10 @@ def show_pair(self): self.text_box.insert(END, f.read()) self.text_modified = False if not self.text_modified: - self.saved_label.config(text="No Changes", bg="white", fg="black") + self.saved_label.config(text="No Changes", bg="#f0f0f0", fg="black") self.text_box.bind("", self.highlight_duplicates) self.text_box.bind("", lambda event: self.highlight_duplicates(event, mouse=False)) - self.text_box.bind("", lambda event: self.highlight_duplicates(event, mouse=False)) + self.text_box.bind("", lambda event: self.highlight_duplicates(event, mouse=False)) self.text_box.bind("", lambda event: self.remove_highlight()) self.text_box.bind("", self.disable_tab) self.display_image_index() @@ -321,7 +373,8 @@ def show_pair(self): event = Event() event.height = window_height event.width = window_width - self.scale_image(event, aspect_ratio, image) + self.scale_image(event, aspect_ratio, image) + self.suggestion_label.config(text="...") def scale_image(self, event, aspect_ratio, image): window_height = event.height @@ -336,7 +389,8 @@ def scale_image(self, event, aspect_ratio, image): self.label_image.config(image=photo) self.label_image.image = photo -######################################################################## +################################################################################################################################################ +################################################################################################################################################ def display_image_index(self): if hasattr(self, 'image_index_entry'): @@ -396,7 +450,8 @@ def prev_pair(self): if not self.text_modified: self.saved_label.config(text="No Changes", fg="black") -######################################################################## +################################################################################################################################################ +################################################################################################################################################ def save_text_file(self): if self.text_files: @@ -411,7 +466,26 @@ def save_text_file(self): if text.startswith(","): text = text[1:] f.write(text) - self.saved_label.config(text="Saved", bg="green", fg="white") + self.saved_label.config(text="Saved", bg="#6ca079", fg="white") + + def on_closing(self): + if self.saved_label.cget("text") == "No Changes": + root.destroy() + elif self.auto_save_var.get(): + self.save_text_file() + root.destroy() + else: + if messagebox.askokcancel("Quit", "Are you sure you want to quit without saving?"): + root.destroy() + + def delete_pair(self): + if messagebox.askokcancel("Warning", "Are you sure you want to delete this img-txt pair?\n\nThis can't be undone!!"): + os.remove(self.image_files[self.current_index]) + os.remove(self.text_files[self.current_index]) + del self.image_files[self.current_index] + del self.text_files[self.current_index] + self.total_images_label.config(text=f"/{len(self.image_files)}") + self.show_pair() def disable_tab(self, event): return "break" @@ -424,6 +498,76 @@ def adjust_window(self, new_width, new_height, min_width=None, min_height=None): self.next_pair() self.prev_pair() + def set_font_size(self, event=None): + current_font = self.text_box.cget("font") + current_size = self.text_box.tk.call("font", "actual", current_font, "-size") + dialog = Toplevel(self.master) + dialog.focus_force() + dialog.geometry("180x50") + dialog.title("Font Size") + dialog.attributes('-toolwindow', True) + Label(dialog, text="font size:").pack() + size_var = StringVar() + size_box = ttk.Combobox(dialog, textvariable=size_var) + size_box['values'] = list(range(9, 19)) + size_box.set(current_size) + size_box.bind("<>", lambda event: self.set_font_size_and_destroy(size_var.get(), dialog)) + size_box.bind("", lambda event: size_box.event_generate('')) + size_box.pack() + + def set_font_size_and_destroy(self, size, dialog): + if size: + self.text_box.config(font=("monaco", int(size))) + dialog.destroy() + + def set_suggestion_quantity(self): + dialog = Toplevel(self.master) + dialog.focus_force() + dialog.geometry("200x50") + dialog.title("Autocomplete") + dialog.attributes('-toolwindow', True) + Label(dialog, text="Suggestion Quantity:").pack() + quantity_var = StringVar() + quantity_box = ttk.Combobox(dialog, textvariable=quantity_var) + quantity_box['values'] = list(range(0, 10)) + quantity_box.set(self.autocomplete.max_suggestions) + quantity_box.bind("<>", lambda event: self.set_quantity_and_destroy(quantity_var.get(), dialog)) + quantity_box.bind("", lambda event: quantity_box.event_generate('')) + quantity_box.pack() + + def set_quantity_and_destroy(self, quantity, dialog): + if quantity: + self.autocomplete.max_suggestions = int(quantity) + dialog.destroy() + root = Tk() app = ImageTextViewer(root) +root.protocol("WM_DELETE_WINDOW", app.on_closing) root.mainloop() + +# v1.7 changes: + +# If auto-save is enabled, changes to the text file are now saved automatically when the window is closed. +# If auto-save isn’t enabled when closing, a warning is given. This allows the user to cancel and go back and save if needed. + +# The insert_completed_suggestion function now mostly* positions the cursor at the end of multi-word suggestions. + +# Autocomplete scorer now prioritizes whole word matches, addressing the bias towards longer tags. + +# Pairs are now logically sorted during loading, fixing the issue where filenames like “10” or “100” loaded before “2” or “9”. + +# The unused is_modified definition has been removed. + +# The Pillow library and danbooru.csv file can now be installed and downloaded respectively, with an option to cancel/ignore. + +# A new “Options” menu has been added: +# Adjust font size. +# Adjust suggestion quantity, allowing you to set a value between 0-10. +# Delete img-txt pairs. A warning popup will confirm the choice or give the user an option to cancel. + +# UI enhancements include: +# Improved formatting and font size for info_label. +# Restructured layout for saved_label and auto-save checkbox. +# Color and width adjustments for saved_label. +# highlight_selected_suggestion now uses a “Black Medium-Small Square” as an additional visual indicator. +# An ellipsis has been added to the idle suggestion label.