Add Tooltips for sprites

This commit is contained in:
Kevin Cathcart 2017-12-08 17:33:59 -05:00
parent 5dbc21ce3a
commit 911737b84f
3 changed files with 157 additions and 13 deletions

17
Gui.py
View File

@ -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 argparse import Namespace
from Rom import Sprite
from glob import glob from glob import glob
import json import json
import random import random
import os import os
import shutil 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.parse import urlparse
from urllib.request import urlopen 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): def guiMain(args=None):
@ -318,9 +320,11 @@ class SpriteSelector(object):
i = 0 i = 0
for file in glob(output_path(path)): 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 if image is None: continue
button = Button(frame, image=image, command=lambda file=file: self.select_sprite(file)) 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.image = image
button.grid(row=i // 16, column=i % 16) button.grid(row=i // 16, column=i % 16)
i += 1 i += 1
@ -394,8 +398,7 @@ class SpriteSelector(object):
return local_path("data/sprites/unofficial") return local_path("data/sprites/unofficial")
def get_image_for_sprite(filename): def get_image_for_sprite(sprite):
sprite = Sprite(filename)
if not sprite.valid: if not sprite.valid:
return None return None
height = 24 height = 24

116
GuiUtils.py Normal file
View File

@ -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, "<Enter>", cls.enter)
widget.bind_class(cls.tag, "<Leave>", cls.leave)
widget.bind_class(cls.tag, "<Motion>", 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

37
Rom.py
View File

@ -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 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 from Utils import local_path
import random import random
import io
import json import json
import hashlib import hashlib
import logging import logging
import os
import struct import struct
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
@ -100,6 +102,8 @@ class Sprite(object):
def __init__(self, filename): def __init__(self, filename):
with open(filename, 'rb') as file: with open(filename, 'rb') as file:
filedata = bytearray(file.read()) filedata = bytearray(file.read())
self.name = os.path.basename(filename)
self.author_name = None
self.valid = True self.valid = True
if len(filedata) == 0x7000: if len(filedata) == 0x7000:
# sprite file with graphics and without palette data # sprite file with graphics and without palette data
@ -123,11 +127,10 @@ class Sprite(object):
self.glove_palette = filedata[0xDEDF5:0xDEDF9] self.glove_palette = filedata[0xDEDF5:0xDEDF9]
elif filedata.startswith(b'ZSPR'): elif filedata.startswith(b'ZSPR'):
result = self.parse_zspr(filedata,1) result = self.parse_zspr(filedata,1)
print(result)
if result is None: if result is None:
self.valid = False self.valid = False
return return
(sprite, palette) = result (sprite, palette, self.name, self.author_name) = result
if len(sprite) != 0x7000: if len(sprite) != 0x7000:
self.valid = False self.valid = False
return return
@ -174,26 +177,48 @@ class Sprite(object):
return arr return arr
def parse_zspr(self, filedata, expected_kind): def parse_zspr(self, filedata, expected_kind):
logger = logging.getLogger('')
headerstr = "<4xBHHIHIHH6x" headerstr = "<4xBHHIHIHH6x"
headersize = struct.calcsize(headerstr) headersize = struct.calcsize(headerstr)
if len(filedata) < headersize: if len(filedata) < headersize:
return None return None
(version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind) = struct.unpack_from(headerstr,filedata) (version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind) = struct.unpack_from(headerstr,filedata)
if version not in [1]: if version not in [1]:
#ZSPR Version not supported logger.error('Error parsing ZSPR file: Version %g not supported', version)
return None return None
if kind != expected_kind: if kind != expected_kind:
return None 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 real_csum = sum(filedata) % 0x10000
if real_csum != csum or real_csum ^ 0xFFFF != icsum: if real_csum != csum or real_csum ^ 0xFFFF != icsum:
#invalid checksum logger.warning('ZSPR file has incorrect checksum. It may be corrupted.')
pass pass
sprite = filedata[sprite_offset:sprite_offset + sprite_size] sprite = filedata[sprite_offset:sprite_offset + sprite_size]
palette = filedata[palette_offset:palette_offset + palette_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): def decode_palette(self):
"Returns the palettes as an array of arrays of 15 colors" "Returns the palettes as an array of arrays of 15 colors"