From f44d78d82fc2b7186d044877f03ca065bc3ff9a9 Mon Sep 17 00:00:00 2001 From: Kevin Cathcart Date: Sun, 10 Dec 2017 11:10:04 -0500 Subject: [PATCH] Made sprite updating a background task Also make it use the correct folders for bundled builds --- Gui.py | 90 +++++++++++++++++++++++++++++++++++++++-------------- GuiUtils.py | 75 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 23 deletions(-) diff --git a/Gui.py b/Gui.py index 80262b28..9780e561 100644 --- a/Gui.py +++ b/Gui.py @@ -8,7 +8,7 @@ from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, T from urllib.parse import urlparse from urllib.request import urlopen -from GuiUtils import ToolTips +from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress from Main import main, __version__ as ESVersion from Rom import Sprite from Utils import is_bundled, local_path, output_path, open_file @@ -292,12 +292,6 @@ def guiMain(args=None): mainWindow.mainloop() -def set_icon(window): - er16 = PhotoImage(file=local_path('data/ER16.gif')) - er32 = PhotoImage(file=local_path('data/ER32.gif')) - er48 = PhotoImage(file=local_path('data/ER32.gif')) - window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48) - class SpriteSelector(object): def __init__(self, parent, callback): if is_bundled(): @@ -326,6 +320,7 @@ class SpriteSelector(object): button.pack(side=LEFT) set_icon(self.window) + self.window.focus() def icon_section(self, frame_label, path, no_results_label): frame = LabelFrame(self.window, text=frame_label, padx=5, pady=5) @@ -349,23 +344,72 @@ class SpriteSelector(object): def update_official_sprites(self): # need to wrap in try catch. We don't want errors getting the json or downloading the files to break us. - sprites_arr = json.loads(temp_sprites_json) - current_sprites = [os.path.basename(file) for file in glob('data/sprites/official/*')] - official_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr] - needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in official_sprites if filename not in current_sprites] - - for (sprite_url, filename) in needed_sprites: - target = os.path.join('data/sprites/official',filename) - with urlopen(sprite_url) as response, open(target, 'wb') as out: - shutil.copyfileobj(response, out) - - official_filenames = [filename for (_, filename) in official_sprites] - obsolete_sprites = [sprite for sprite in current_sprites if sprite not in official_filenames] - for sprite in obsolete_sprites: - os.remove(os.path.join('data/sprites/official', sprite)) - self.window.destroy() - SpriteSelector(self.parent, self.callback) + self.parent.update() + def work(task): + resultmessage="" + successful = True + + def finished(): + task.close_window() + if successful: + messagebox.showinfo("Sprite Updater", resultmessage) + else: + messagebox.showerror("Sprite Updater", resultmessage) + SpriteSelector(self.parent, self.callback) + + try: + task.update_status("Downloading official sprites list") + sprites_arr = json.loads(temp_sprites_json) + except Exception as e: + resultmessage = "Error getting list of official sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e) + successful = False + task.queue_event(finished) + return + + try: + task.update_status("Determining needed sprites") + current_sprites = [os.path.basename(file) for file in glob(self.official_sprite_dir+'/*')] + official_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr] + needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in official_sprites if filename not in current_sprites] + bundled_sprites=[] + + official_filenames = [filename for (_, filename) in official_sprites] + obsolete_sprites = [sprite for sprite in current_sprites if sprite not in official_filenames] + except Exception as e: + resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e) + successful = False + task.queue_event(finished) + return + + updated = 0 + for (sprite_url, filename) in needed_sprites: + try: + task.update_status("Downloading needed sprite %g/%g" % (updated + 1, len(needed_sprites))) + target = os.path.join(self.official_sprite_dir, filename) + with urlopen(sprite_url) as response, open(target, 'wb') as out: + shutil.copyfileobj(response, out) + except Exception as e: + resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (type(e).__name__, e) + successful = False + updated += 1 + + deleted = 0 + for sprite in obsolete_sprites: + try: + task.update_status("Removing obsolete sprite %g/%g" % (deleted + 1, len(obsolete_sprites))) + os.remove(os.path.join(self.official_sprite_dir, sprite)) + except Exception as e: + resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (type(e).__name__, e) + successful = False + deleted += 1 + + if successful: + resultmessage = "official sprites updated sucessfully" + + task.queue_event(finished) + + BackgroundTaskProgress(self.parent, work, "Updating Sprites") def browse_for_sprite(self): diff --git a/GuiUtils.py b/GuiUtils.py index 722b6ff7..ded1b3b2 100644 --- a/GuiUtils.py +++ b/GuiUtils.py @@ -1,5 +1,79 @@ +import queue +import threading import tkinter as tk +from Utils import local_path + +def set_icon(window): + er16 = tk.PhotoImage(file=local_path('data/ER16.gif')) + er32 = tk.PhotoImage(file=local_path('data/ER32.gif')) + er48 = tk.PhotoImage(file=local_path('data/ER32.gif')) + window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48) + +# Although tkinter is intended to be thread safe, there are many reports of issues +# some which may be platform specific, or depend on if the TCL library was compiled without +# multithreading support. Therefore I will assume it is not thread safe to avoid any possible problems +class BackgroundTask(object): + def __init__(self, window, code_to_run): + self.window = window + self.queue = queue.Queue() + self.running = True + self.process_queue() + self.task=threading.Thread(target=code_to_run , args=(self,)) + self.task.start() + + def stop(self): + self.running = False + + #safe to call from worker + def queue_event(self, event): + self.queue.put(event) + + def process_queue(self): + try: + while True: + if not self.running: + return + event = self.queue.get_nowait() + event() + if self.running: + #if self is no longer running self.window may no longer be valid + self.window.update_idletasks() + except queue.Empty: + pass + if self.running: + self.window.after(100, self.process_queue) + +class BackgroundTaskProgress(BackgroundTask): + def __init__(self, parent, code_to_run, title): + self.parent = parent + self.window = tk.Toplevel(parent) + self.window['padx'] = 5 + self.window['pady'] = 5 + + self.window.attributes("-toolwindow",1) + + self.window.wm_title(title) + self.labelVar = tk.StringVar() + self.labelVar.set("") + self.label = tk.Label(self.window, textvariable = self.labelVar, width=50) + self.label.pack() + self.window.resizable(width=False, height=False) + + set_icon(self.window) + self.window.focus() + super().__init__(self.window, code_to_run) + + #safe to call from worker thread + def update_status(self, text): + self.queue_event(lambda text=text: self.labelVar.set(text)) + + # only call this in an event callback + def close_window(self): + self.stop() + self.window.destroy() + + class ToolTips(object): # This class derived from wckToolTips which is available under the following license: @@ -41,6 +115,7 @@ class ToolTips(object): widget.bind_class(cls.tag, "", cls.enter) widget.bind_class(cls.tag, "", cls.leave) widget.bind_class(cls.tag, "", cls.motion) + widget.bind_class(cls.tag, "", cls.leave) # pick suitable colors for tooltips try: