From 911737b84f20f94b5ce9ee5234ca77dcdb9a0187 Mon Sep 17 00:00:00 2001 From: Kevin Cathcart Date: Fri, 8 Dec 2017 17:33:59 -0500 Subject: [PATCH] Add Tooltips for sprites --- Gui.py | 17 ++++---- GuiUtils.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Rom.py | 37 ++++++++++++++--- 3 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 GuiUtils.py diff --git a/Gui.py b/Gui.py index 14f8adba..13cf98af 100644 --- a/Gui.py +++ b/Gui.py @@ -1,15 +1,17 @@ -from Main import main, __version__ as ESVersion -from Utils import is_bundled, local_path, output_path, open_file from argparse import Namespace -from Rom import Sprite from glob import glob import json import random import os import shutil +from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, StringVar, IntVar, Frame, Label, W, E, X, Y, Entry, Spinbox, Button, filedialog, messagebox from urllib.parse import urlparse from urllib.request import urlopen -from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, StringVar, IntVar, Frame, Label, W, E, X, Y, Entry, Spinbox, Button, filedialog, messagebox + +from GuiUtils import ToolTips +from Main import main, __version__ as ESVersion +from Rom import Sprite +from Utils import is_bundled, local_path, output_path, open_file def guiMain(args=None): @@ -318,9 +320,11 @@ class SpriteSelector(object): i = 0 for file in glob(output_path(path)): - image = get_image_for_sprite(file) + sprite = Sprite(file) + image = get_image_for_sprite(sprite) if image is None: continue button = Button(frame, image=image, command=lambda file=file: self.select_sprite(file)) + ToolTips.register(button, sprite.name + ("\nBy: %s" % sprite.author_name if sprite.author_name is not None else "")) button.image = image button.grid(row=i // 16, column=i % 16) i += 1 @@ -394,8 +398,7 @@ class SpriteSelector(object): return local_path("data/sprites/unofficial") -def get_image_for_sprite(filename): - sprite = Sprite(filename) +def get_image_for_sprite(sprite): if not sprite.valid: return None height = 24 diff --git a/GuiUtils.py b/GuiUtils.py new file mode 100644 index 00000000..722b6ff7 --- /dev/null +++ b/GuiUtils.py @@ -0,0 +1,116 @@ +import tkinter as tk + + +class ToolTips(object): + # This class derived from wckToolTips which is available under the following license: + + # Copyright (c) 1998-2007 by Secret Labs AB + # Copyright (c) 1998-2007 by Fredrik Lundh + # + # By obtaining, using, and/or copying this software and/or its + # associated documentation, you agree that you have read, understood, + # and will comply with the following terms and conditions: + # + # Permission to use, copy, modify, and distribute this software and its + # associated documentation for any purpose and without fee is hereby + # granted, provided that the above copyright notice appears in all + # copies, and that both that copyright notice and this permission notice + # appear in supporting documentation, and that the name of Secret Labs + # AB or the author not be used in advertising or publicity pertaining to + # distribution of the software without specific, written prior + # permission. + # + # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO + # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + # FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR + # ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + label = None + window = None + active = 0 + tag = None + + @classmethod + def getcontroller(cls, widget): + if cls.tag is None: + + cls.tag = "ui_tooltip_%d" % id(cls) + widget.bind_class(cls.tag, "", cls.enter) + widget.bind_class(cls.tag, "", cls.leave) + widget.bind_class(cls.tag, "", cls.motion) + + # pick suitable colors for tooltips + try: + cls.bg = "systeminfobackground" + cls.fg = "systeminfotext" + widget.winfo_rgb(cls.fg) # make sure system colors exist + widget.winfo_rgb(cls.bg) + except: + cls.bg = "#ffffe0" + cls.fg = "black" + + return cls.tag + + @classmethod + def register(cls, widget, text): + widget.ui_tooltip_text = text + tags = list(widget.bindtags()) + tags.append(cls.getcontroller(widget)) + widget.bindtags(tuple(tags)) + + @classmethod + def unregister(cls, widget): + tags = list(widget.bindtags()) + tags.remove(cls.getcontroller(widget)) + widget.bindtags(tuple(tags)) + + # event handlers + @classmethod + def enter(cls, event): + widget = event.widget + if not cls.label: + # create and hide balloon help window + cls.popup = tk.Toplevel(bg=cls.fg, bd=1) + cls.popup.overrideredirect(1) + cls.popup.withdraw() + cls.label = tk.Label( + cls.popup, fg=cls.fg, bg=cls.bg, bd=0, padx=2, justify=tk.LEFT + ) + cls.label.pack() + cls.active = 0 + cls.xy = event.x_root + 16, event.y_root + 10 + cls.event_xy = event.x, event.y + cls.after_id = widget.after(200, cls.display, widget) + + @classmethod + def motion(cls, event): + widget = event.widget + cls.xy = event.x_root + 16, event.y_root + 10 + cls.event_xy = event.x, event.y + + @classmethod + def display(cls, widget): + if not cls.active: + # display balloon help window + text = widget.ui_tooltip_text + if callable(text): + text = text(widget, cls.event_xy) + cls.label.config(text=text) + cls.popup.deiconify() + cls.popup.lift() + cls.popup.geometry("+%d+%d" % cls.xy) + cls.active = 1 + cls.after_id = None + + @classmethod + def leave(cls, event): + widget = event.widget + if cls.active: + cls.popup.withdraw() + cls.active = 0 + if cls.after_id: + widget.after_cancel(cls.after_id) + cls.after_id = None diff --git a/Rom.py b/Rom.py index 08aea9c4..14f6c60a 100644 --- a/Rom.py +++ b/Rom.py @@ -4,9 +4,11 @@ from Text import Uncle_texts, Ganon1_texts, PyramidFairy_texts, TavernMan_texts, from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts from Utils import local_path import random +import io import json import hashlib import logging +import os import struct JAP10HASH = '03a63945398191337e896e5771f77173' @@ -100,6 +102,8 @@ class Sprite(object): def __init__(self, filename): with open(filename, 'rb') as file: filedata = bytearray(file.read()) + self.name = os.path.basename(filename) + self.author_name = None self.valid = True if len(filedata) == 0x7000: # sprite file with graphics and without palette data @@ -123,11 +127,10 @@ class Sprite(object): self.glove_palette = filedata[0xDEDF5:0xDEDF9] elif filedata.startswith(b'ZSPR'): result = self.parse_zspr(filedata,1) - print(result) if result is None: self.valid = False return - (sprite, palette) = result + (sprite, palette, self.name, self.author_name) = result if len(sprite) != 0x7000: self.valid = False return @@ -174,26 +177,48 @@ class Sprite(object): return arr def parse_zspr(self, filedata, expected_kind): + logger = logging.getLogger('') headerstr = "<4xBHHIHIHH6x" headersize = struct.calcsize(headerstr) if len(filedata) < headersize: return None (version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind) = struct.unpack_from(headerstr,filedata) if version not in [1]: - #ZSPR Version not supported + logger.error('Error parsing ZSPR file: Version %g not supported', version) return None if kind != expected_kind: return None + stream = io.BytesIO(filedata) + stream.seek(headersize) + + def read_utf16le(stream): + "Decodes a null-terminated UTF-16_LE string of unknown size from a stream" + raw = bytearray() + while True: + char = stream.read(2) + if char in [b'', b'\x00\x00']: + break + raw += char + return raw.decode('utf-16_le') + + sprite_name = read_utf16le(stream) + author_name = read_utf16le(stream) + + # Ignoring the Author Rom name for the time being. + real_csum = sum(filedata) % 0x10000 if real_csum != csum or real_csum ^ 0xFFFF != icsum: - #invalid checksum + logger.warning('ZSPR file has incorrect checksum. It may be corrupted.') pass sprite = filedata[sprite_offset:sprite_offset + sprite_size] palette = filedata[palette_offset:palette_offset + palette_size] - #FIXME: Check lengths of those byte arrays against the _size values - return (sprite, palette) + if len(sprite) != sprite_size or len(palette) != palette_size: + logger.error('Error parsing ZSPR file: Unexpected end of file') + return None + + return (sprite, palette, sprite_name, author_name) def decode_palette(self): "Returns the palettes as an array of arrays of 15 colors"