#!/usr/bin/env python3 # Copyright © 2012-13 Qtrac Ltd. All rights reserved. # This program or module is free software: you can redistribute it # and/or modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. It is provided for # educational purposes and is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. import os import multiprocessing import sys import threading import tkinter as tk import tkinter.ttk as ttk import tkinter.filedialog as filedialog import tkinter.messagebox as messagebox Spinbox = ttk.Spinbox if hasattr(ttk, "Spinbox") else tk.Spinbox if __name__ == "__main__": # For stand-alone testing with parallel TkUtil sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) import About import TkUtil import TkUtil.Settings import ImageScale from Globals import * ReportLock = threading.Lock() class Window(ttk.Frame): def __init__(self, master): super().__init__(master, padding=PAD) self.create_variables() self.create_ui() self.sourceEntry.focus() def create_variables(self): settings = TkUtil.Settings.Data self.sourceText = tk.StringVar() self.targetText = tk.StringVar() self.statusText = tk.StringVar() self.statusText.set("Choose or enter folders, then click Scale...") self.dimensionText = tk.StringVar() self.restore = settings.get_bool(GENERAL, RESTORE, True) self.total = self.copied = self.scaled = 0 self.worker = None self.state = multiprocessing.Manager().Value("i", IDLE) def create_ui(self): self.create_widgets() self.layout_widgets() self.create_bindings() settings = TkUtil.Settings.Data if self.restore: position = settings.get_str(GENERAL, POSITION) if position is not None: self.master.geometry(position) self.master.resizable(False, False) def create_widgets(self): self.sourceLabel = ttk.Label(self, text="Source Folder:", underline=-1 if TkUtil.mac() else 1) self.sourceEntry = ttk.Entry(self, width=30, textvariable=self.sourceText) self.sourceButton = TkUtil.Button(self, text="Source...", underline=0, command=lambda *args: self.choose_folder(SOURCE)) self.helpButton = TkUtil.Button(self, text="Help", underline=0, command=self.help) self.targetLabel = ttk.Label(self, text="Target Folder:", underline=-1 if TkUtil.mac() else 1) self.targetEntry = ttk.Entry(self, width=30, textvariable=self.targetText) self.targetButton = TkUtil.Button(self, text="Target...", underline=0, command=lambda *args: self.choose_folder(TARGET)) self.aboutButton = TkUtil.Button(self, text="About", underline=1, command=self.about) self.statusLabel = ttk.Label(self, textvariable=self.statusText) self.scaleButton = TkUtil.Button(self, text="Scale", underline=1, command=self.scale_or_cancel, default=tk.ACTIVE, state=tk.DISABLED) self.quitButton = TkUtil.Button(self, text="Quit", underline=0, command=self.close) self.dimensionLabel = ttk.Label(self, text="Max. Dimension:", underline=-1 if TkUtil.mac() else 6) self.dimensionCombobox = ttk.Combobox(self, textvariable=self.dimensionText, state="readonly", values=("50", "100", "150", "200", "250", "300", "350", "400", "450", "500")) TkUtil.set_combobox_item(self.dimensionCombobox, "400") def layout_widgets(self): pad = dict(padx=PAD, pady=PAD) padWE = dict(sticky=(tk.W, tk.E), **pad) self.sourceLabel.grid(row=0, column=0, sticky=tk.W, **pad) self.sourceEntry.grid(row=0, column=1, columnspan=3, **padWE) self.sourceButton.grid(row=0, column=4, **pad) self.targetLabel.grid(row=1, column=0, sticky=tk.W, **pad) self.targetEntry.grid(row=1, column=1, columnspan=3, **padWE) self.targetButton.grid(row=1, column=4, **pad) self.dimensionLabel.grid(row=2, column=0, sticky=tk.W, **pad) self.dimensionCombobox.grid(row=2, column=1, **padWE) self.helpButton.grid(row=2, column=3, **pad) self.scaleButton.grid(row=2, column=4, **pad) self.aboutButton.grid(row=3, column=3, **pad) self.quitButton.grid(row=3, column=4, **pad) self.statusLabel.grid(row=3, column=0, columnspan=3, **padWE) self.grid() def create_bindings(self): if not TkUtil.mac(): self.master.bind("<Alt-a>", lambda *args: self.targetEntry.focus()) self.master.bind("<Alt-b>", self.about) self.master.bind("<Alt-c>", self.scale_or_cancel) self.master.bind("<Alt-h>", self.help) self.master.bind("<Alt-i>", lambda *args: self.dimensionCombobox.focus()) self.master.bind("<Alt-o>", lambda *args: self.sourceEntry.focus()) self.master.bind("<Alt-q>", self.close) self.master.bind("<Alt-s>", lambda *args: self.choose_folder( SOURCE)) self.master.bind("<Alt-t>", lambda *args: self.choose_folder( TARGET)) self.master.bind("<F1>", self.help) self.sourceEntry.bind("<KeyRelease>", self.update_ui) self.targetEntry.bind("<KeyRelease>", self.update_ui) self.master.bind("<Return>", self.scale_or_cancel) def close(self, event=None): settings = TkUtil.Settings.Data settings.put(GENERAL, RESTORE, self.restore) if self.restore: geometry = TkUtil.geometry_for_str(self.master.geometry()) position = TkUtil.str_for_geometry(x=geometry.x, y= geometry.y) settings.put(GENERAL, POSITION, position) TkUtil.Settings.save() if self.worker is not None and self.worker.is_alive(): self.state.value = TERMINATING self.update_ui() self.worker.join() # Wait for worker to finish self.quit() def help(self, event=None): paras = [ """Reads all the images in the source directory and produces smoothly scaled copies in the target directory."""] messagebox.showinfo("Help — {}".format(APPNAME), "\n\n".join([para.replace("\n", " ") for para in paras]), parent=self) def about(self, event=None): About.Window(self) def update_ui(self, *args): guiState = self.state.value if guiState == WORKING: text = "Cancel" underline = 0 if not TkUtil.mac() else -1 state = "!" + tk.DISABLED elif guiState in {CANCELED, TERMINATING}: text = "Canceling..." underline = -1 state = tk.DISABLED elif guiState == IDLE: text = "Scale" underline = 1 if not TkUtil.mac() else -1 state = ("!" + tk.DISABLED if self.sourceText.get() and self.targetText.get() else tk.DISABLED) self.scaleButton.state((state,)) self.scaleButton.config(text=text, underline=underline) state = tk.DISABLED if guiState != IDLE else "!" + tk.DISABLED for widget in (self.sourceEntry, self.sourceButton, self.targetEntry, self.targetButton): widget.state((state,)) self.master.update() # Make sure the GUI refreshes def choose_folder(self, which): if which == SOURCE: name, mustexist, variable = "Source", True, self.sourceText else: name, mustexist, variable = "Target", False, self.targetText path = filedialog.askdirectory(parent=self, title="Choose {} Folder — {}".format(name, APPNAME), mustexist=mustexist) if path: variable.set(os.path.normpath(os.path.realpath(path))) self.update_ui() def scale_or_cancel(self, event=None): if self.scaleButton.instate((tk.DISABLED,)): return if self.scaleButton.cget("text") == "Cancel": self.state.value = CANCELED self.update_ui() else: self.state.value = WORKING self.update_ui() self.scale() def scale(self): self.total = self.copied = self.scaled = 0 self.configure(cursor="watch") self.statusText.set("Scaling...") self.master.update() # Make sure the GUI refreshes target = self.targetText.get() if not os.path.exists(target): os.makedirs(target) self.worker = threading.Thread(target=ImageScale.scale, args=( int(self.dimensionText.get()), self.sourceText.get(), target, self.report_progress, self.state, self.when_finished)) self.worker.daemon = True self.worker.start() # returns immediately def report_progress(self, future): if self.state.value in {CANCELED, TERMINATING}: return with ReportLock: # Serializes calls to Window.report_progress() self.total += 1 # and accesses to self.total, etc. if future.exception() is None: result = future.result() self.copied += result.copied self.scaled += result.scaled name = os.path.basename(result.name) self.statusText.set("{} {}".format( "Copied" if result.copied else "Scaled", name)) self.master.update() # Make sure the GUI refreshes # Must only be called by the single worker thread when it has finished def when_finished(self): self.state.value = IDLE self.configure(cursor="arrow") self.update_ui() result = "Copied {} Scaled {}".format(self.copied, self.scaled) difference = self.total - (self.copied + self.scaled) if difference: # This will kick in if the user canceled result += " Skipped {}".format(difference) self.statusText.set(result) self.master.update() # Make sure the GUI refreshes if __name__ == "__main__": if sys.stdout.isatty(): application = tk.Tk() application.title("Window") TkUtil.Settings.DOMAIN = "Qtrac" TkUtil.Settings.APPNAME = APPNAME TkUtil.Settings.load() window = Window(application) application.protocol("WM_DELETE_WINDOW", window.close) application.mainloop() else: print("Loaded OK")