#!/usr/bin/env python3 # encoding: utf-8 # By Elvish_Hunter, April 2014 # You may notice that this script, unlike all our other Python mainline scripts, # has a .pyw extension, instead of .py. *This is deliberate*. On Windows, .pyw # scripts are started directly in GUI mode, without opening a prompt. # This is, after all, the behavior that we want. # threading and subprocess are needed to run wmllint without freezing the window # codecs is used to save files as UTF8 # locale and gettext provides internationalization and localization (i18n, l10n) # queue is needed to exchange information between threads # if we use the run_tool thread to do GUI stuff we obtain weird crashes # This happens because Tk is a single-thread GUI import argparse import codecs import gettext import locale import os import queue import subprocess import sys import threading # tkinter modules import tkinter.font as font from tkinter import * from tkinter.filedialog import * from tkinter.messagebox import * from tkinter.messagebox import WARNING # for Python >= 3.9 # ttk must be called last from tkinter.ttk import * from wesnoth import version # check if additional themes are available and set a flag # use pip or your package manager to install the "ttkthemes" package try: from ttkthemes import ThemedStyle additional_themes = True except ImportError: additional_themes = False # we need to know in what series we are # so set it in a constant string WESNOTH_SERIES = "{0}.{1}".format(version.major, version.minor) # get the location where the script is placed # we'll check later if this is a Wesnoth directory # and use it to generate the command lines # os.path.realpath gets the full path of this script, # while removing any symlink # This allows users to create a link to the app on their desktop # os.path.normpath allows Windows users to see their standard path separators APP_DIR, APP_NAME = os.path.split(os.path.realpath(sys.argv[0])) WESNOTH_ROOT_DIR = os.sep.join(APP_DIR.split(os.sep)[:-2]) # pop out "data" and "tools" WESNOTH_DATA_DIR = os.path.join(WESNOTH_ROOT_DIR, "data") WESNOTH_CORE_DIR = os.path.normpath(os.path.join(WESNOTH_DATA_DIR, "core")) WESNOTH_TRAN_DIR = os.path.join(WESNOTH_ROOT_DIR, "translations") _ = lambda x: x def set_global_locale(): """ Attempts to set locale for the application session based on user-input at the command-line. If no input is given, fall back to the system locale. """ global _ # TODO: Replace CLI args for a proper locale selection GUI. # More importantly, code to dynamically update the text/layout is missing. parser = argparse.ArgumentParser( description=_("Open a Graphical User Interface (GUI) to WML Tools") ) parser.add_argument( '--lang', help=_("Launch GUI.pyw in the specified language. Language code is expected as a POSIX locale name, refer to " "gettext.wesnoth.org for a full list.") ) if not os.path.isdir(WESNOTH_TRAN_DIR): showerror( _("Error"), # TRANSLATORS: {0} is "translations", the directory where compiled translation files (.mo) are stored. _("‘{0}’ directory not found. Please run the GUI.pyw program packaged with the Wesnoth installation."). format("translations") ) # User-specified locale opts = parser.parse_args(sys.argv[1:]) if opts.lang is not None: try: _ = gettext.translation("wesnoth-tools", WESNOTH_TRAN_DIR, languages=[opts.lang], fallback=False).gettext except OSError: showerror( _("Error"), # TRANSLATORS: {0} is the language argument entered by the user. _("Locale {0} not recognized.").format(opts.lang) ) return # System locale # On POSIX systems, getlocale() should provide the POSIX locale name that gettext uses for finding translations. # However, on Windows, getlocale() returns strings likely not suitable for gettext, although getdefaultlocale() # does. try: system_locale = locale.getlocale()[0] _ = gettext.translation("wesnoth-tools", WESNOTH_TRAN_DIR, languages=[system_locale], fallback=False).gettext except OSError: # Needed for compatibility with Python <3.10, and/or Windows 7/8. # TODO: Note that getdefaultlocale() is deprecated in Python 3.11 so an alternative arrangement needs to be # implemented for Windows. system_locale = locale.getdefaultlocale()[0] _ = gettext.translation("wesnoth-tools", WESNOTH_TRAN_DIR, languages=[system_locale], fallback=False).gettext def on_update_locale(value): if value is None: try: set_global_locale() except: # _ defaults to identity lambda. pass else: pass on_update_locale(None) def wrap_elem(line): """If the supplied line contains spaces, return it wrapped between double quotes""" if ' ' in line: return "\"{0}\"".format(line) return line class ToolThread(threading.Thread): def __init__(self, tool, tool_queue, command): super().__init__() self.tool = tool self.command = command self.queue = tool_queue self.subproc = None def run(self): # we can't use check_output, because it doesn't support # performing operations on the subprocess # so we'll have to use subprocess.Popen() instead # set the encoding for the subprocess # otherwise, with the new Unicode literals used by the wml tools, # we may get UnicodeDecodeErrors env = os.environ env['PYTHONIOENCODING'] = "utf8" if sys.platform == "win32": # Windows wants a string, Linux wants a list and Polly wants a cracker # Windows wants also strings flavoured with double quotes # since maps return iterators, we must cast them as lists, otherwise join won't work # not doing this causes an OSError: [WinError 87] # this doesn't happen on Python 2.7, because here map() returns a list wrapped_line = list(map(wrap_elem, self.command)) self.queue.put_nowait(' '.join(wrapped_line) + "\n") si = subprocess.STARTUPINFO() si.dwFlags = subprocess.STARTF_USESHOWWINDOW | subprocess.SW_HIDE # to avoid showing a DOS prompt self.subproc = subprocess.Popen(' '.join(wrapped_line), stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=si, env=env) else: # STARTUPINFO is not available, nor needed, outside of Windows self.queue.put_nowait(' '.join(self.command) + "\n") self.subproc = subprocess.Popen(self.command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) out, err = self.subproc.communicate() # waits for the subprocess to finish and gets the output self.queue.put_nowait(str(out, "utf8")) self.queue.put_nowait(str(err, "utf8")) # unlike check_output, Popen never raises if a subprocess terminates abnormally # the documentation writes about the chance of OSError being raised # but I tested by deleting wmllint and it doesn't happen # perhaps because it's a Python script and the Python interpreter # certainly exists if self.subproc.returncode != 0: # post a tuple into the queue to allow showing the error self.queue.put_nowait((self.tool, self.subproc.returncode)) def terminate(self): self.subproc.kill() def is_wesnoth_tools_path(path): """Checks if the supplied path may be a wesnoth/data/tools directory""" lower_path = path.lower() if "wesnoth" in lower_path and \ "data" in lower_path and \ "tools" in lower_path: return True return False def get_addons_directory(): """Returns a string containing the path of the add-ons directory""" # os.path.expanduser gets the current user's home directory on every platform if sys.platform == "win32": # get userdata directory on Windows # it assumes that you choose to store userdata in the My Games directory # while installing Wesnoth userdata = os.path.join(os.path.expanduser("~"), "Documents", "My Games", "Wesnoth" + WESNOTH_SERIES, "data", "add-ons") elif sys.platform.startswith("linux") or "bsd" in sys.platform: # we're on Linux or on a BSD system like FreeBSD userdata = os.path.join(os.path.expanduser("~"), ".local", "share", "wesnoth", WESNOTH_SERIES, "data", "add-ons") elif sys.platform == "darwin": # we're on macOS # bear in mind that I don't have a Mac, so this point may be bugged userdata = os.path.join(os.path.expanduser("~"), "Library", "Application Support", "Wesnoth_" + WESNOTH_SERIES, "data", "add-ons") else: # unknown system; if someone else wants to add other rules, be my guest userdata = "." return userdata if os.path.exists(userdata) else "." # fallback in case the directory doesn't exist def attach_context_menu(widget, function): # on Mac the right button fires a Button-2 event, or so I'm told # some mice don't even have two buttons, so the user is forced # to use Command + the only button # bear in mind that I don't have a Mac, so this point may be bugged # bind also the context menu key, for those keyboards that have it # that is, most of the Windows and Linux ones (however, in Win it's # called App, while on Linux is called Menu) # Mac doesn't have a context menu key on its keyboards, so no binding # bind also the Shift+F10 shortcut (same as Menu/App key) # the call to tk windowingsystem is justified by the fact # that it is possible to install X11 over Darwin windowingsystem = widget.tk.call('tk', 'windowingsystem') if windowingsystem == "win32": # Windows, both 32 and 64 bit widget.bind("", function) widget.bind("", function) widget.bind("", function) elif windowingsystem == "aqua": # MacOS with Aqua widget.bind("", function) widget.bind("", function) elif windowingsystem == "x11": # Linux, FreeBSD, Darwin with X11 widget.bind("", function) widget.bind("", function) widget.bind("", function) def attach_select_all(widget, function): # bind the "select all" key shortcut # again, Mac uses Command instead of Control windowingsystem = widget.tk.call('tk', 'windowingsystem') if windowingsystem == "win32": widget.bind("", function) elif windowingsystem == "aqua": widget.bind("", function) elif windowingsystem == "x11": widget.bind("", function) class Tooltip(Toplevel): def __init__(self, widget, text, tag=None): """A tooltip, or balloon. Displays the specified help text when the mouse pointer stays on the widget for more than 500 ms.""" # the master attribute retrieves the window where our "parent" widget is super().__init__(widget.master) self.widget = widget self.preshow_id = None self.show_id = None self.label = Label(self, text=text, background="#ffffe1", # background color used on Windows borderwidth=1, relief=SOLID, padding=1, # Tk has a bunch of predefined fonts # use the one specific for tooltips font=font.nametofont("TkTooltipFont")) self.label.pack() self.overrideredirect(True) # allow binding the tooltips to tagged elements of a widget # only Text, Canvas and Treeview support tags # and as such, they have a tag_bind method # if the widget doesn't support tags, bind directly to it if tag and hasattr(widget, "tag_bind") and callable(widget.tag_bind): self.widget.tag_bind(tag, "", self.preshow) self.widget.tag_bind(tag, "", self.hide) else: self.widget.bind("", self.preshow) self.widget.bind("", self.hide) self.widget.bind("", self.hide) self.widget.bind("", self.hide) self.withdraw() def preshow(self, event=None): self.after_cleanup() # 500 ms and 5000 ms are the default values used on Windows self.preshow_id = self.after(500, self.show) def show(self): self.after_cleanup() # check if the tooltip will end up out of the screen # and handle this case if so screen_width = self.winfo_screenwidth() tooltip_width = self.winfo_reqwidth() if tooltip_width + self.winfo_pointerx() > screen_width: # unfortunately, it seems like Tkinter doesn't have a way to check the pointer's size # so I'm using a value of 20px, which is enough for the usual 16px pointers self.geometry("+%d+%d" % (screen_width - tooltip_width, self.winfo_pointery() + 20)) else: self.geometry("+%d+%d" % (self.winfo_pointerx(), self.winfo_pointery() + 20)) # update_idletasks forces a geometry update self.update_idletasks() self.state("normal") self.lift() self.show_id = self.after(5000, self.hide) def hide(self, event=None): self.after_cleanup() self.withdraw() def after_cleanup(self): # each event should clean up after itself, # to avoid having two .after() calls conflicting # for example, one previously scheduled .after() may # try to hide the tooltip before five seconds are passed if self.show_id: self.after_cancel(self.show_id) self.show_id = None if self.preshow_id: self.after_cancel(self.preshow_id) self.preshow_id = None def set_text(self, text): self.label.configure(text=text) self.update_idletasks() class Popup(Toplevel): def __init__(self, parent, tool, thread): """Creates a popup that informs the user that the desired tool is running. Self destroys when the tool thread is over""" self.thread = thread super().__init__(parent) self.transient(parent) self.grab_set() self.protocol("WM_DELETE_WINDOW", lambda: None) # disable close button self.resizable(width=False, height=False) frame = Frame(self) frame.pack(fill=BOTH, expand=YES) wait_label = Label(frame, # TRANSLATORS: {0} is the name of command being executed. text=_("Running: {0}\nPlease wait...").format(tool), justify=CENTER) wait_label.grid(row=0, column=0, padx=5, pady=5) wait_progress = Progressbar(frame, mode="indeterminate") wait_progress.grid(row=1, column=0, sticky=E + W, padx=5, pady=5) terminate_button = Button(frame, text=_("Terminate script"), image=ICONS["process-stop"], compound=LEFT, command=self.terminate) terminate_button.grid(row=2, column=0, padx=5, pady=5) frame.columnconfigure(0, weight=1) # place the popup in the middle of the main window # get the main window position and dimension self.geometry("{0}x{1}+{2}+{3}".format(400, 140, int(root.winfo_rootx() + (root.winfo_width() - 400) / 2), int(root.winfo_rooty() + (root.winfo_height() - 140) / 2))) wait_progress.start(10) self.check_thread_alive() def check_thread_alive(self): """Checks if the thread is still alive, and destroys the window if it isn't""" # placing this in a for or while cycle freezes the app # so we need to use the .after method and recursively call the function # that's one of the many quirks of Tkinter try: # Python <= 3.8 is_alive = self.thread.isAlive() except AttributeError: # Python >= 3.9 is_alive = self.thread.is_alive() finally: if is_alive: self.after(100, self.check_thread_alive) else: self.after(1, self.destroy) def terminate(self): self.thread.terminate() class ContextMenu(Menu): def __init__(self, x, y, widget): """A subclass of Menu, used to display a context menu in Text and Entry widgets If the widget isn't active, some options do not appear""" super().__init__(None, tearoff=0) # otherwise Tk allows splitting it in a new window self.widget = widget # MacOS uses a key called Command, instead of the usual Control used by Windows and Linux # so prepare the accelerator strings accordingly # For future reference, Mac also uses Option instead of Alt # also, a little known fact about Python is that it *does* support using the ternary operator # like in this case control_key = "Command" if self.tk.call('tk', 'windowingsystem') == "aqua" else "Ctrl" # str is necessary because in some instances a Tcl_Obj is returned instead of a string if str(widget.cget('state')) in (ACTIVE, NORMAL): # do not add if state is readonly or disabled self.add_command(label=_("Cut"), image=ICONS['cut'], compound=LEFT, accelerator='%s+X' % control_key, command=lambda: self.widget.event_generate("<>")) self.add_command(label=_("Copy"), image=ICONS['copy'], compound=LEFT, accelerator='%s+C' % control_key, command=lambda: self.widget.event_generate("<>")) if str(widget.cget('state')) in (ACTIVE, NORMAL): self.add_command(label=_("Paste"), image=ICONS['paste'], compound=LEFT, accelerator='%s+V' % control_key, command=lambda: self.widget.event_generate("<>")) self.add_separator() self.add_command(label=_("Select All"), image=ICONS['select_all'], compound=LEFT, accelerator='%s+A' % control_key, command=self.on_select_all) self.tk_popup(x, y) # self.post does not destroy the menu when clicking out of it def on_select_all(self): # disabled Text widgets have a different way to handle selection if isinstance(self.widget, Text): # adding a SEL tag to a chunk of text causes it to be selected self.widget.tag_add(SEL, "1.0", END) elif isinstance(self.widget, Entry) or \ isinstance(self.widget, Combobox): # apparently, the <> event doesn't fire correctly if the widget is readonly self.widget.select_range(0, END) elif isinstance(self.widget, Spinbox): self.widget.selection("range", 0, END) class EntryContext(Entry): def __init__(self, parent, **kwargs): """An enhanced Entry widget that has a right-click menu Use like any other Entry widget""" super().__init__(parent, **kwargs) attach_context_menu(self, self.on_context_menu) attach_select_all(self, self.on_select_all) def on_context_menu(self, event): if str(self.cget('state')) != DISABLED: ContextMenu(event.x_root, event.y_root, event.widget) def on_select_all(self, event): self.select_range(0, END) class SpinboxContext(Spinbox): def __init__(self, parent, **kwargs): """An enhanced Spinbox widget that has a right-click menu Use like any other Spinbox widget""" super().__init__(parent, **kwargs) attach_context_menu(self, self.on_context_menu) attach_select_all(self, self.on_select_all) def on_context_menu(self, event): if str(self.cget('state')) != DISABLED: ContextMenu(event.x_root, event.y_root, event.widget) def on_select_all(self, event): self.selection("range", 0, END) class EnhancedText(Text): def __init__(self, *args, **kwargs): """A subclass of Text with a context menu Use it like any other Text widget""" super().__init__(*args, **kwargs) attach_context_menu(self, self.on_context_menu) attach_select_all(self, self.on_select_all) def on_context_menu(self, event): # the disabled state in a Text widget is pretty much # like the readonly state in Entry, hence no state check ContextMenu(event.x_root, event.y_root, event.widget) def on_select_all(self, event): self.tag_add(SEL, "1.0", END) class SelectDirectory(LabelFrame): def __init__(self, parent, textvariable=None, **kwargs): """A subclass of LabelFrame sporting a readonly Entry and a Button with a folder icon. It comes complete with a context menu and a directory selection screen""" super().__init__(parent, text=_("Working directory"), **kwargs) self.textvariable = textvariable self.dir_entry = EntryContext(self, width=40, textvariable=self.textvariable, state="readonly") self.dir_entry.pack(side=LEFT, fill=BOTH, expand=YES) self.dir_button = Button(self, image=ICONS['browse'], compound=LEFT, text=_("Browse..."), command=self.on_browse) self.dir_button.pack(side=LEFT) self.clear_button = Button(self, image=ICONS['clear16'], compound=LEFT, # TRANSLATORS: Clear button for clearing the directory text box. text=_("Clear"), command=self.on_clear) self.clear_button.pack(side=LEFT) def on_browse(self): # if the user already selected a directory, try to use it current_dir = self.textvariable.get() if os.path.exists(current_dir): directory = askdirectory(initialdir=current_dir) # otherwise attempt to detect the user's userdata folder else: directory = askdirectory(initialdir=get_addons_directory()) if directory: # use os.path.normpath, so on Windows the usual backwards slashes are correctly shown self.textvariable.set(os.path.normpath(directory)) def on_clear(self): self.textvariable.set("") class SelectOutputPath(Frame): def __init__(self, parent, textvariable, filetypes=None, **kwargs): """A subclass of Frame with a readonly Entry and a Button with a browse icon. It has a context menu and a save file selection dialog.""" super().__init__(parent, **kwargs) self.textvariable = textvariable self.file_entry = EntryContext(self, width=40, textvariable=self.textvariable, state="readonly") self.file_entry.pack(side=LEFT, fill=BOTH, expand=YES) self.file_button = Button(self, image=ICONS['browse'], compound=LEFT, text=_("Browse..."), command=self.on_browse) self.file_button.pack(side=LEFT) self.clear_button = Button(self, image=ICONS['clear16'], compound=LEFT, # TRANSLATORS: Clear button for clearing the directory text box. text=_("Clear"), command=self.on_clear) self.clear_button.pack(side=LEFT) self.filetypes = filetypes def on_browse_file(self): # if the user already selected a file, try to use its directory current_dir, current_file = os.path.split(self.textvariable.get()) if os.path.exists(current_dir): return asksaveasfilename(filetypes=self.filetypes, initialdir=current_dir, initialfile=current_file, confirmoverwrite=False) # the GUI will ask later if the file should be overwritten, so disable it for now # otherwise attempt to detect the user's userdata folder else: return asksaveasfilename(filetypes=self.filetypes, initialdir=get_addons_directory(), confirmoverwrite=False) def on_browse_dir(self): current_dir = self.textvariable.get() if os.path.exists(current_dir): return askdirectory(initialdir=current_dir) # otherwise attempt to detect the user's userdata folder else: return askdirectory(initialdir=get_addons_directory()) def on_browse(self): if self.filetypes is None: directory = self.on_browse_dir() else: directory = self.on_browse_file() if directory: # use os.path.normpath, so on Windows the usual backwards slashes are correctly shown self.textvariable.set(os.path.normpath(directory)) def on_clear(self): self.textvariable.set("") class WmllintTab(Frame): def __init__(self, parent): # it means super(WmllintTab,self), that in turn means # Frame.__init__(self,parent) super().__init__(parent) self.mode_variable = IntVar() self.mode_frame = LabelFrame(self, text=_("wmllint mode")) self.mode_frame.grid(row=0, column=0, sticky=N + E + S + W) self.radio_normal = Radiobutton(self.mode_frame, # TRANSLATORS: Normal run mode for the WML tool. text=_("Normal"), variable=self.mode_variable, value=0) self.radio_normal.grid(row=0, column=0, sticky=W, padx=10) self.tooltip_normal = Tooltip(self.radio_normal, # TRANSLATORS: Tooltip explanation for normal run mode. _("Perform conversion and save changes to file")) self.radio_dryrun = Radiobutton(self.mode_frame, text=_("Dry run"), variable=self.mode_variable, value=1) self.radio_dryrun.grid(row=1, column=0, sticky=W, padx=10) self.tooltip_dryrun = Tooltip(self.radio_dryrun, # TRANSLATORS: Tooltip explanation for dry run mode. _("Perform conversion without saving changes to file")) self.radio_clean = Radiobutton(self.mode_frame, text=_("Clean"), variable=self.mode_variable, value=2) self.radio_clean.grid(row=2, column=0, sticky=W, padx=10) self.tooltip_clean = Tooltip(self.radio_clean, # TRANSLATORS: Tooltip explanation for clean mode. _("Delete back-up files")) self.radio_diff = Radiobutton(self.mode_frame, text=_("Diff"), variable=self.mode_variable, value=3) self.radio_diff.grid(row=3, column=0, sticky=W, padx=10) self.tooltip_diff = Tooltip(self.radio_diff, # TRANSLATORS: Tooltip explanation for diff run mode. _("Show differences in converted files")) self.radio_revert = Radiobutton(self.mode_frame, text=_("Revert"), variable=self.mode_variable, value=4) self.radio_revert.grid(row=4, column=0, sticky=W, padx=10) self.tooltip_revert = Tooltip(self.radio_revert, # TRANSLATORS: Tooltip explanation for revert run mode. _("Revert conversions using back-up files")) self.verbosity_frame = LabelFrame(self, text=_("Verbosity level")) self.verbosity_frame.grid(row=0, column=1, sticky=N + E + S + W) self.verbosity_variable = IntVar() self.radio_v0 = Radiobutton(self.verbosity_frame, # TRANSLATORS: Verbosity level. text=_("Terse"), variable=self.verbosity_variable, value=0) self.radio_v0.grid(row=0, column=0, sticky=W, padx=10) self.radio_v1 = Radiobutton(self.verbosity_frame, # TRANSLATORS: Verbosity level. text=_("Show changes"), variable=self.verbosity_variable, value=1) self.radio_v1.grid(row=1, column=0, sticky=W, padx=10) self.radio_v2 = Radiobutton(self.verbosity_frame, # TRANSLATORS: Verbosity level. text=_("Name files before processing"), variable=self.verbosity_variable, value=2) self.radio_v2.grid(row=2, column=0, sticky=W, padx=10) self.radio_v3 = Radiobutton(self.verbosity_frame, # TRANSLATORS: Verbosity level. text=_("Show parse details"), variable=self.verbosity_variable, value=3) self.radio_v3.grid(row=3, column=0, sticky=W, padx=10) self.options_frame = LabelFrame(self, text=_("wmllint options")) self.options_frame.grid(row=0, column=2, sticky=N + E + S + W) self.stripcr_variable = BooleanVar() self.stripcr_check = Checkbutton(self.options_frame, # TRANSLATORS: EOL = Special characters marking 'end-of-line'. text=_("Convert EOL characters to Unix format"), variable=self.stripcr_variable) self.stripcr_check.grid(row=0, column=0, sticky=W, padx=10) self.missing_variable = BooleanVar() self.missing_check = Checkbutton(self.options_frame, # TRANSLATORS: 'side=' in this context refers to WML and should not be # translated. text=_("Do not warn about tags without side= keys"), variable=self.missing_variable) self.missing_check.grid(row=1, column=0, sticky=W, padx=10) self.known_variable = BooleanVar() self.known_check = Checkbutton(self.options_frame, text=_("Disable checks for unknown units"), variable=self.known_variable) self.known_check.grid(row=2, column=0, sticky=W, padx=10) self.spell_variable = BooleanVar() self.spell_check = Checkbutton(self.options_frame, text=_("Disable spellchecking"), variable=self.spell_variable) self.spell_check.grid(row=3, column=0, sticky=W, padx=10) self.skip_variable = BooleanVar() self.skip_core = Checkbutton(self.options_frame, text=_("Skip core directory"), variable=self.skip_variable, command=self.skip_core_dir_callback) self.skip_core.grid(row=4, column=0, sticky=W, padx=10) self.columnconfigure(0, weight=1) self.columnconfigure(1, weight=1) self.columnconfigure(2, weight=1) def skip_core_dir_callback(self): # if Skip core directory is enabled # avoid checking for unknown unit types if self.skip_variable.get(): self.known_variable.set(True) self.known_check.configure(state=DISABLED) else: self.known_variable.set(False) self.known_check.configure(state=NORMAL) class WmlscopeTab(Frame): def __init__(self, parent): super().__init__(parent) self.options_frame = LabelFrame(self, text=_("wmlscope options")) self.options_frame.grid(row=0, column=0, sticky=N + E + S + W) self.normal_options = Frame(self.options_frame) self.normal_options.grid(row=0, column=0, sticky=N + E + S + W) self.crossreference_variable = BooleanVar() # equivalent to warnlevel 1 self.crossreference_check = Checkbutton(self.normal_options, text=_("Check for duplicate macro definitions"), variable=self.crossreference_variable) self.crossreference_check.grid(row=0, column=0, sticky=W, padx=10) self.collisions_variable = BooleanVar() self.collisions_check = Checkbutton(self.normal_options, text=_("Check for duplicate resource files"), variable=self.collisions_variable) self.collisions_check.grid(row=1, column=0, sticky=W, padx=10) self.definitions_variable = BooleanVar() self.definitions_check = Checkbutton(self.normal_options, text=_("Make definition list"), variable=self.definitions_variable) self.definitions_check.grid(row=2, column=0, sticky=W, padx=10) self.listfiles_variable = BooleanVar() self.listfiles_check = Checkbutton(self.normal_options, text=_("List files that will be processed"), variable=self.listfiles_variable) self.listfiles_check.grid(row=3, column=0, sticky=W, padx=10) self.unresolved_variable = BooleanVar() self.unresolved_check = Checkbutton(self.normal_options, text=_("Report unresolved macro references"), variable=self.unresolved_variable) self.unresolved_check.grid(row=4, column=0, sticky=W, padx=10) self.extracthelp_variable = BooleanVar() self.extracthelp_check = Checkbutton(self.normal_options, text=_("Extract help from macro definition comments"), variable=self.extracthelp_variable) self.extracthelp_check.grid(row=5, column=0, sticky=W, padx=10) self.unchecked_variable = BooleanVar() self.unchecked_check = Checkbutton(self.normal_options, text=_("Report all macros with untyped formals"), variable=self.unchecked_variable) self.unchecked_check.grid(row=6, column=0, sticky=W, padx=10) self.progress_variable = BooleanVar() self.progress_check = Checkbutton(self.normal_options, text=_("Show progress"), variable=self.progress_variable) self.progress_check.grid(row=7, column=0, sticky=W, padx=10) self.separator = Separator(self.options_frame, orient=VERTICAL) self.separator.grid(row=0, column=1, sticky=N + S) self.options_with_regexp = Frame(self.options_frame) self.options_with_regexp.grid(row=0, column=2, sticky=N + E + S + W) self.exclude_variable = BooleanVar() self.exclude_check = Checkbutton(self.options_with_regexp, text=_("Exclude file names matching regular expression:"), variable=self.exclude_variable, command=self.exclude_callback) self.exclude_check.grid(row=0, column=0, sticky=W, padx=10) self.exclude_regexp = StringVar() self.exclude_entry = EntryContext(self.options_with_regexp, textvariable=self.exclude_regexp, state=DISABLED) self.exclude_entry.grid(row=0, column=1, sticky=E + W, padx=10) self.from_variable = BooleanVar() self.from_check = Checkbutton(self.options_with_regexp, text=_("Exclude file names not matching regular expression:"), variable=self.from_variable, command=self.from_callback) self.from_check.grid(row=1, column=0, sticky=W, padx=10) self.from_regexp = StringVar() self.from_entry = EntryContext(self.options_with_regexp, textvariable=self.from_regexp, state=DISABLED) self.from_entry.grid(row=1, column=1, sticky=E + W, padx=10) self.refcount_variable = BooleanVar() self.refcount_check = Checkbutton(self.options_with_regexp, # TRANSLATORS: 'n' in this context refers to number, as in 'n number of # files'. text=_("Report only on macros referenced in exactly n files:"), variable=self.refcount_variable, command=self.refcount_callback) self.refcount_check.grid(row=2, column=0, sticky=W, padx=10) self.refcount_number = IntVar() self.refcount_spin = SpinboxContext(self.options_with_regexp, from_=0, to=999, textvariable=self.refcount_number, width=3, state=DISABLED) self.refcount_spin.grid(row=2, column=1, sticky=E + W, padx=10) self.typelist_variable = BooleanVar() self.typelist_check = Checkbutton(self.options_with_regexp, text=_("Report macro definitions and usages in file:"), variable=self.typelist_variable, command=self.typelist_callback) self.typelist_check.grid(row=3, column=0, sticky=W, padx=10) self.typelist_string = StringVar() self.typelist_entry = EntryContext(self.options_with_regexp, textvariable=self.typelist_string, state=DISABLED) self.typelist_entry.grid(row=3, column=1, sticky=E + W, padx=10) self.force_variable = BooleanVar() self.force_check = Checkbutton(self.options_with_regexp, text=_("Allow unused macros with names matching regular expression:"), variable=self.force_variable, command=self.force_callback) self.force_check.grid(row=4, column=0, sticky=W, padx=10) self.force_regexp = StringVar() self.force_entry = EntryContext(self.options_with_regexp, textvariable=self.force_regexp, state=DISABLED) self.force_entry.grid(row=4, column=1, sticky=E + W, padx=10) self.columnconfigure(0, weight=1) self.options_frame.columnconfigure(0, weight=1) self.options_frame.columnconfigure(2, weight=1) self.options_with_regexp.columnconfigure(1, weight=1) # uniform= makes the options in the options_with_regexp frame evenly sized # please note that "regexp" is an ID of the widget group, not a special value # you can replace "regexp" with "bacon" and it still works... for row in range(5): self.options_with_regexp.rowconfigure(row, uniform="regexp") def exclude_callback(self): if self.exclude_variable.get(): self.exclude_entry.configure(state=NORMAL) else: self.exclude_entry.configure(state=DISABLED) def from_callback(self): if self.from_variable.get(): self.from_entry.configure(state=NORMAL) else: self.from_entry.configure(state=DISABLED) def refcount_callback(self): if self.refcount_variable.get(): self.refcount_spin.configure(state="readonly") else: self.refcount_spin.configure(state=DISABLED) def typelist_callback(self): if self.typelist_variable.get(): self.typelist_entry.configure(state=NORMAL) else: self.typelist_entry.configure(state=DISABLED) def force_callback(self): if self.force_variable.get(): self.force_entry.configure(state=NORMAL) else: self.force_entry.configure(state=DISABLED) class WmlindentTab(Frame): def __init__(self, parent): super().__init__(parent) self.mode_variable = IntVar() self.mode_frame = LabelFrame(self, text=_("wmlindent mode")) self.mode_frame.grid(row=0, column=0, sticky=N + E + S + W) self.radio_normal = Radiobutton(self.mode_frame, text=_("Normal"), variable=self.mode_variable, value=0) self.radio_normal.grid(row=0, column=0, sticky=W, padx=10) self.tooltip_normal = Tooltip(self.radio_normal, # TRANSLATORS: Tooltip explanation for normal run mode. _("Perform conversion and save changes to file")) self.radio_dryrun = Radiobutton(self.mode_frame, text=_("Dry run"), variable=self.mode_variable, value=1) self.radio_dryrun.grid(row=1, column=0, sticky=W, padx=10) self.tooltip_dryrun = Tooltip(self.radio_dryrun, # TRANSLATORS: Tooltip explanation for dry run mode. _("Perform conversion without saving changes to file")) self.verbosity_frame = LabelFrame(self, text=_("Verbosity level")) self.verbosity_frame.grid(row=0, column=1, sticky=N + E + S + W) self.verbosity_variable = IntVar() self.radio_v0 = Radiobutton(self.verbosity_frame, # TRANSLATORS: Verbosity level. text=_("Terse"), variable=self.verbosity_variable, value=0) self.radio_v0.grid(row=0, column=0, sticky=W, padx=10) self.radio_v1 = Radiobutton(self.verbosity_frame, # TRANSLATORS: Verbosity level. text=_("Verbose"), variable=self.verbosity_variable, value=1) self.radio_v1.grid(row=1, column=0, sticky=W, padx=10) self.radio_v2 = Radiobutton(self.verbosity_frame, # TRANSLATORS: Verbosity level. text=_("Report unchanged files"), variable=self.verbosity_variable, value=2) self.radio_v2.grid(row=2, column=0, sticky=W, padx=10) self.options_frame = LabelFrame(self, text=_("wmlindent options")) self.options_frame.grid(row=0, column=2, sticky=N + E + S + W) self.exclude_variable = BooleanVar() self.exclude_check = Checkbutton(self.options_frame, text=_("Exclude file names matching regular expression:"), variable=self.exclude_variable, command=self.exclude_callback) self.exclude_check.grid(row=1, column=0, sticky=W, padx=10) self.regexp_variable = StringVar() self.regexp_entry = EntryContext(self.options_frame, textvariable=self.regexp_variable, state=DISABLED) self.regexp_entry.grid(row=1, column=1, sticky=E + W, padx=10) self.quiet_variable = BooleanVar() self.quiet_check = Checkbutton(self.options_frame, # TRANSLATORS: Option to run 'quietly'. text=_("Quiet mode"), variable=self.quiet_variable) self.quiet_check.grid(row=2, column=0, sticky=W, padx=10) self.tooltip_quiet = Tooltip(self.quiet_check, # TRANSLATORS: Tooltip explanation for quiet option. _("Do not generate output messages")) self.columnconfigure(0, weight=1) self.columnconfigure(1, weight=1) self.columnconfigure(2, weight=1) self.options_frame.columnconfigure(1, weight=1) def exclude_callback(self): if self.exclude_variable.get(): self.regexp_entry.configure(state=NORMAL) else: self.regexp_entry.configure(state=DISABLED) class WmlxgettextTab(Frame): def __init__(self, parent): super().__init__(parent) self.output_wrapper_frame = Frame(self) self.output_wrapper_frame.grid(row=0, column=0, columnspan=2, sticky=N + E + S + W) self.output_label = Label(self.output_wrapper_frame, text=_("Output directory:")) self.output_label.grid(row=0, column=0, sticky=W) self.output_variable = StringVar() self.output_frame = SelectOutputPath(self.output_wrapper_frame, textvariable=self.output_variable) self.output_frame.grid(row=0, column=1, sticky=N + E + S + W) self.options_labelframe = LabelFrame(self, text=_("Options")) self.options_labelframe.grid(row=1, column=0, sticky=N + E + S + W) self.recursive_variable = BooleanVar() self.recursive_variable.set(True) self.recursive_check = Checkbutton(self.options_labelframe, text=_("Scan subdirectories"), variable=self.recursive_variable) self.recursive_check.grid(row=0, column=0, sticky=W) self.warnall_variable = BooleanVar() self.warnall_check = Checkbutton(self.options_labelframe, text=_("Show optional warnings"), variable=self.warnall_variable) self.warnall_check.grid(row=1, column=0, sticky=W) self.fuzzy_variable = BooleanVar() self.fuzzy_check = Checkbutton(self.options_labelframe, # TRANSLATORS: Also called "Needs work". text=_("Mark all strings as fuzzy"), variable=self.fuzzy_variable) self.fuzzy_check.grid(row=2, column=0, sticky=W) self.advanced_labelframe = LabelFrame(self, text=_("Advanced options")) self.advanced_labelframe.grid(row=1, column=1, sticky=N + E + S + W) self.package_version_variable = BooleanVar() self.package_version_check = Checkbutton(self.advanced_labelframe, text=_("Package version"), variable=self.package_version_variable) self.package_version_check.grid(row=0, column=0, sticky=W) self.initialdomain_variable = BooleanVar() self.textdomain_variable = BooleanVar() self.textdomain_check = Checkbutton(self.advanced_labelframe, text="Filter textdomains:", variable=self.textdomain_variable, command=self.textdomain_callback) self.textdomain_check.grid(row=1, column=0, sticky=W) self.textdomain_name = StringVar() self.textdomain_entry = Entry(self.advanced_labelframe, state=DISABLED, width=40, textvariable=self.textdomain_name) self.textdomain_entry.grid(row=1, column=1, sticky=E + W) self.initialdomain_check = Checkbutton(self.advanced_labelframe, text=_("Initial textdomain:"), variable=self.initialdomain_variable, command=self.initialdomain_callback) self.initialdomain_check.grid(row=2, column=0, sticky=W) self.initialdomain_name = StringVar() self.initialdomain_entry = Entry(self.advanced_labelframe, state=DISABLED, width=40, textvariable=self.initialdomain_name) self.initialdomain_entry.grid(row=2, column=1, sticky=E + W) self.output_wrapper_frame.columnconfigure(1, weight=1) self.output_wrapper_frame.rowconfigure(0, uniform="group") self.advanced_labelframe.columnconfigure(1, weight=1) self.advanced_labelframe.columnconfigure(2, weight=1) self.columnconfigure(0, weight=2) self.columnconfigure(1, weight=1) def textdomain_callback(self, event=None): if self.textdomain_variable.get(): self.textdomain_entry.configure(state=NORMAL) else: self.textdomain_entry.configure(state=DISABLED) def initialdomain_callback(self, event=None): if self.initialdomain_variable.get(): self.initialdomain_entry.configure(state=NORMAL) else: self.initialdomain_entry.configure(state=DISABLED) class MainFrame(Frame): def __init__(self, parent): self.parent = parent self.queue = queue.Queue() super().__init__(parent) self.grid(sticky=N + E + S + W) self.buttonbox = Frame(self) self.buttonbox.grid(row=0, column=0, sticky=E + W) self.run_button = Button(self.buttonbox, image=ICONS['run'], command=self.on_run_wmllint) self.run_button.pack(side=LEFT, padx=5, pady=5) self.run_tooltip = Tooltip(self.run_button, _("Run wmllint")) self.save_button = Button(self.buttonbox, image=ICONS['save'], command=self.on_save) self.save_button.pack(side=LEFT, padx=5, pady=5) self.save_tooltip = Tooltip(self.save_button, _("Save as text...")) self.clear_button = Button(self.buttonbox, image=ICONS['clear'], command=self.on_clear) self.clear_button.pack(side=LEFT, padx=5, pady=5) self.clear_tooltip = Tooltip(self.clear_button, _("Clear output")) self.about_button = Button(self.buttonbox, image=ICONS['about'], command=self.on_about) self.about_button.pack(side=LEFT, padx=5, pady=5) self.about_tooltip = Tooltip(self.about_button, _("About...")) self.exit_button = Button(self.buttonbox, image=ICONS['exit'], command=self.on_quit) self.exit_button.pack(side=RIGHT, padx=5, pady=5) self.exit_tooltip = Tooltip(self.exit_button, _("Exit")) self.dir_variable = StringVar() self.dir_frame = SelectDirectory(self, textvariable=self.dir_variable) self.dir_frame.grid(row=1, column=0, sticky=E + W) # Notebook is one of the new widgets introduced by ttk # it isn't available on Python 2.6 and lower, like the rest of ttk widgets # please note that the Frames that become tabs don't need to be packed or gridded self.notebook = Notebook(self) self.notebook.grid(row=2, column=0, sticky=E + W) self.wmllint_tab = WmllintTab(None) self.notebook.add(self.wmllint_tab, text=_("wmllint"), sticky=N + E + S + W) self.wmlscope_tab = WmlscopeTab(None) self.notebook.add(self.wmlscope_tab, text=_("wmlscope"), sticky=N + E + S + W) self.wmlindent_tab = WmlindentTab(None) self.notebook.add(self.wmlindent_tab, text=_("wmlindent"), sticky=N + E + S + W) self.wmlxgettext_tab = WmlxgettextTab(None) self.notebook.add(self.wmlxgettext_tab, text=_("wmlxgettext"), sticky=N + E + S + W) self.output_frame = LabelFrame(self, text=_("Output")) self.output_frame.grid(row=3, column=0, sticky=N + E + S + W) # in former versions of this script, I disabled the text widget at its creation # it turned out that doing so on Aqua (macOS) causes the widget to ignore # any additional binding set after its disabling # the subclass EnhancedText first calls the constructor of the original Text widget # and only later it creates its own bindings # so first create the widget, and disable it later self.text = EnhancedText(self.output_frame, wrap=WORD, takefocus=True) self.text.configure(state=DISABLED) self.text.grid(row=0, column=0, sticky=N + E + S + W) self.update_text() self.yscrollbar = Scrollbar(self.output_frame, command=self.text.yview) self.yscrollbar.grid(row=0, column=1, sticky=N + S) self.text["yscrollcommand"] = self.yscrollbar.set self.xscrollbar = Scrollbar(self.output_frame, orient=HORIZONTAL, command=self.text.xview) self.xscrollbar.grid(row=1, column=0, sticky=E + W) self.text["xscrollcommand"] = self.xscrollbar.set self.grip = Sizegrip(self.output_frame) self.grip.grid(row=1, column=1) self.output_frame.rowconfigure(0, weight=1) self.output_frame.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.rowconfigure(3, weight=1) self.notebook.bind("<>", self.tab_callback) parent.protocol("WM_DELETE_WINDOW", self.on_quit) def tab_callback(self, event): # we check the ID of the active tab and ask its position # the order of the tabs is pretty obvious active_tab = self.notebook.index(self.notebook.select()) if active_tab == 0: self.run_tooltip.set_text(_("Run wmllint")) self.run_button.configure(command=self.on_run_wmllint) elif active_tab == 1: self.run_tooltip.set_text(_("Run wmlscope")) self.run_button.configure(command=self.on_run_wmlscope) elif active_tab == 2: self.run_tooltip.set_text(_("Run wmlindent")) self.run_button.configure(command=self.on_run_wmlindent) elif active_tab == 3: self.run_tooltip.set_text(_("Run wmlxgettext")) self.run_button.configure(command=self.on_run_wmlxgettext) def on_run_wmllint(self): # first of all, check if we have something to run wmllint on it # if not, stop here umc_dir = self.dir_variable.get() if not umc_dir and self.wmllint_tab.skip_variable.get(): showerror(_("Error"), _("""No directory selected. Please select a directory or disable the "Skip core directory" option.""")) return # build the command line from Python interpreter path and wmllint tool path wmllint_command_string = [sys.executable, os.path.join(APP_DIR, "wmllint")] mode = self.wmllint_tab.mode_variable.get() if mode == 0: pass elif mode == 1: wmllint_command_string.append("--dryrun") elif mode == 2: wmllint_command_string.append("--clean") elif mode == 3: wmllint_command_string.append("--diff") elif mode == 4: wmllint_command_string.append("--revert") verbosity = self.wmllint_tab.verbosity_variable.get() for n in range(verbosity): wmllint_command_string.append("-v") if self.wmllint_tab.stripcr_variable.get(): wmllint_command_string.append("--stripcr") if self.wmllint_tab.missing_variable.get(): wmllint_command_string.append("--missing") if self.wmllint_tab.known_variable.get(): wmllint_command_string.append("--known") if self.wmllint_tab.spell_variable.get(): wmllint_command_string.append("--nospellcheck") if not self.wmllint_tab.skip_variable.get(): wmllint_command_string.append(WESNOTH_CORE_DIR) if os.path.exists(umc_dir): # add-on exists # the realpaths are here just in case that the user # attempts to fool the script by feeding it a symlink if os.path.realpath(WESNOTH_CORE_DIR) in os.path.realpath(umc_dir): answer = askokcancel(_("Warning"), _("""Core directory or one of its subdirectories selected in the add-on selection box. The tool will be run only on the Wesnoth core directory."""), icon=WARNING) if not answer: return else: wmllint_command_string.append(umc_dir) elif not umc_dir: # path does not exist because the box was left empty answer = askokcancel(_("Warning"), _("""No directory selected. The tool will be run only on the Wesnoth core directory."""), icon=WARNING) if not answer: return else: # path doesn't exist and isn't empty showerror(_("Error"), _("""The selected directory does not exist.""")) return # stop here # start thread and wmllint subprocess wmllint_thread = ToolThread("wmllint", self.queue, wmllint_command_string) wmllint_thread.start() # build popup dialog = Popup(self.parent, _("wmllint"), wmllint_thread) def on_run_wmlscope(self): # build the command line wmlscope_command_string = [sys.executable, os.path.join(APP_DIR, "wmlscope")] if self.wmlscope_tab.crossreference_variable.get(): wmlscope_command_string.append("--crossreference") if self.wmlscope_tab.collisions_variable.get(): wmlscope_command_string.append("--collisions") if self.wmlscope_tab.definitions_variable.get(): wmlscope_command_string.append("--definitions") if self.wmlscope_tab.listfiles_variable.get(): wmlscope_command_string.append("--listfiles") if self.wmlscope_tab.unresolved_variable.get(): wmlscope_command_string.append("--unresolved") if self.wmlscope_tab.extracthelp_variable.get(): wmlscope_command_string.append("--extracthelp") if self.wmlscope_tab.unchecked_variable.get(): wmlscope_command_string.append("--unchecked") if self.wmlscope_tab.progress_variable.get(): wmlscope_command_string.append("--progress") if self.wmlscope_tab.exclude_variable.get(): wmlscope_command_string.append("--exclude") wmlscope_command_string.append(self.wmlscope_tab.exclude_regexp.get()) if self.wmlscope_tab.from_variable.get(): wmlscope_command_string.append("--from") wmlscope_command_string.append(self.wmlscope_tab.from_regexp.get()) if self.wmlscope_tab.refcount_variable.get(): try: wmlscope_command_string.append("--refcount") wmlscope_command_string.append(str(self.wmlscope_tab.refcount_number.get())) except ValueError: # normally it should be impossible to raise this exception # due to the fact that the Spinbox is read-only showerror(_("Error"), _("""Invalid value. Value must be an integer in the range 0-999.""")) return if self.wmlscope_tab.typelist_variable.get(): wmlscope_command_string.append("--typelist") wmlscope_command_string.append(self.wmlscope_tab.typelist_string.get()) if self.wmlscope_tab.force_variable.get(): wmlscope_command_string.append("--force-used") wmlscope_command_string.append(self.wmlscope_tab.force_regexp.get()) wmlscope_command_string.append(WESNOTH_CORE_DIR) umc_dir = self.dir_variable.get() if os.path.exists(umc_dir): # add-on exists # the realpaths are here just in case that the user # attempts to fool the script by feeding it a symlink if os.path.realpath(WESNOTH_CORE_DIR) in os.path.realpath(umc_dir): answer = askokcancel(_("Warning"), _("""Core directory or one of its subdirectories selected in the add-on selection box. The tool will be run only on the Wesnoth core directory."""), icon=WARNING) if not answer: return else: wmlscope_command_string.append(umc_dir) elif not umc_dir: # path does not exist because the box was left empty answer = askokcancel(_("Warning"), _("""No directory selected. The tool will be run only on the Wesnoth core directory."""), icon=WARNING) if not answer: return else: # path doesn't exist and isn't empty showerror(_("Error"), _("""The selected directory does not exist.""")) return # stop here # start thread and wmlscope subprocess wmlscope_thread = ToolThread("wmlscope", self.queue, wmlscope_command_string) wmlscope_thread.start() # build popup dialog = Popup(self.parent, _("wmlscope"), wmlscope_thread) def on_run_wmlindent(self): # build the command line wmlindent_command_string = [sys.executable, os.path.join(APP_DIR, "wmlindent")] mode = self.wmlindent_tab.mode_variable.get() if mode == 0: pass elif mode == 1: wmlindent_command_string.append("--dryrun") verbosity = self.wmlindent_tab.verbosity_variable.get() for n in range(verbosity): wmlindent_command_string.append("-v") if self.wmlindent_tab.exclude_variable.get(): wmlindent_command_string.append("--exclude") wmlindent_command_string.append(self.wmlindent_tab.regexp_variable.get()) if self.wmlindent_tab.quiet_variable.get(): wmlindent_command_string.append("--quiet") umc_dir = self.dir_variable.get() if os.path.exists(umc_dir): # add-on exists wmlindent_command_string.append(umc_dir) elif not umc_dir: # path does not exist because the box was left empty answer = askokcancel(_("Warning"), _("""No directory selected. The tool will be run on the Wesnoth core directory."""), icon=WARNING) if not answer: return wmlindent_command_string.append(WESNOTH_CORE_DIR) else: # path doesn't exist and isn't empty showerror(_("Error"), _("""The selected directory does not exist.""")) return # stop here # start thread and wmllint subprocess wmlindent_thread = ToolThread("wmlindent", self.queue, wmlindent_command_string) wmlindent_thread.start() # build popup dialog = Popup(self.parent, _("wmlindent"), wmlindent_thread) def on_run_wmlxgettext(self): # build the command line and add the path of the Python interpreter and wmlxgettext wmlxgettext_command_string = [sys.executable, os.path.join(APP_DIR, "wmlxgettext")] if self.wmlxgettext_tab.textdomain_variable.get(): wmlxgettext_command_string.extend(["--domain", self.wmlxgettext_tab.textdomain_entry.get()]) wmlxgettext_command_string.append("--directory") umc_dir = self.dir_variable.get() if os.path.exists(umc_dir): # add-on exists wmlxgettext_command_string.append(umc_dir) elif not umc_dir: # path does not exist because the box was left empty showwarning(_("Warning"), _("""No directory selected. The tool will not be run.""")) return else: # path doesn't exist and isn't empty showerror(_("Error"), _("""The selected directory does not exist.""")) return if self.wmlxgettext_tab.recursive_variable.get(): wmlxgettext_command_string.append("--recursive") output_file = self.wmlxgettext_tab.output_variable.get() if os.path.exists(output_file): # TRANSLATORS: Dialogue box title. answer = askyesno(title=_("Overwrite Confirmation"), # TRANSLATORS: {0} is a placeholder for a file name, and not meant to be modified. message=_("""File {0} already exists. Do you want to overwrite it?""").format(output_file)) if not answer: return elif not output_file: showwarning(_("Warning"), _("""No output file selected. The tool will not be run.""")) return wmlxgettext_command_string.extend(["-o", self.wmlxgettext_tab.output_variable.get()]) if self.wmlxgettext_tab.warnall_variable.get(): wmlxgettext_command_string.append("--warnall") if self.wmlxgettext_tab.fuzzy_variable.get(): wmlxgettext_command_string.append("--fuzzy") if self.wmlxgettext_tab.package_version_variable.get(): wmlxgettext_command_string.append("--package-version") wmlxgettext_command_string.append("--no-text-colors") if self.wmlxgettext_tab.initialdomain_variable.get(): wmlxgettext_command_string.extend(["--initialdomain", self.wmlxgettext_tab.initialdomain_entry.get()]) # start thread and wmlxgettext subprocess wmlxgettext_thread = ToolThread("wmlxgettext", self.queue, wmlxgettext_command_string) wmlxgettext_thread.start() # build popup dialog = Popup(self.parent, _("wmlxgettext"), wmlxgettext_thread) def update_text(self): """Checks periodically if the queue is empty. If it contains a string, pushes it into the Text widget. If it contains an error in form of a tuple, displays a message and pushes the remaining output in the Text widget""" if not self.queue.empty(): queue_item = self.queue.get_nowait() # if there's a tuple in the queue, it's because a tool exited with # non-zero status if isinstance(queue_item, tuple): showerror(_("Error"), _("""There was an error while executing {0}. Error code: {1}""".format(queue_item[0], queue_item[1]))) # otherwise it's just the output elif isinstance(queue_item, str): self.text.configure(state=NORMAL) self.text.insert(END, queue_item) self.text.configure(state=DISABLED) self.after(100, self.update_text) def on_save(self): fn = asksaveasfilename(defaultextension=".txt", filetypes=[(_("Text file"), "*.txt")], initialdir=".") if fn: try: with codecs.open(fn, "w", "utf-8") as out: out.write(self.text.get(1.0, END)[:-1]) # exclude the double endline at the end # the output is saved, if we close we don't lose anything self.text.edit_modified(False) except IOError as error: # in case that we attempt to write without permissions showerror(_("Error"), _("""Error while writing to: {0} Error code: {1} {2}""".format(fn, error.errno, error.strerror))) def on_clear(self): self.text.configure(state=NORMAL) self.text.delete(1.0, END) self.text.configure(state=DISABLED) # the edit_modified flag is set to True every time that the content # of the text widget is altered # since there's nothing useful inside of it, set it to False self.text.edit_modified(False) def on_about(self): showinfo(_("About Maintenance Tools GUI"), # TRANSLATORS: {0} is a placeholder for Wesnoth's current version, and not meant to be modified. _("""© Elvish_Hunter, 2014-2024 Version: {0} Part of The Battle for Wesnoth project and released under the GNU GPL v2 license Icons are taken from the Tango Desktop Project (http://tango.freedesktop.org), and are released in the Public Domain.""").format( version.as_string)) def on_quit(self): # check if the text widget contains something # and ask for a confirmation if so if self.text.edit_modified(): # TRANSLATORS: Dialogue box title. answer = askyesno(_("Exit Confirmation"), _("Do you really want to quit?"), icon=WARNING) if answer: ICONS.clear() self.parent.destroy() else: ICONS.clear() self.parent.destroy() root = Tk() if is_wesnoth_tools_path(APP_DIR): # a dictionary with all the icons # they're saved in GIF format (the only one supported by Tkinter) # and then encoded in base64 # this is done to avoid having small files floating around ICONS = { "about": PhotoImage(data=b''' R0lGODlhIAAgAOf/AExOK05QLVRRNFRWMlhZL1lYQF1eNFpcWWRhRF9jQ19hXmdnPWdoVWNndGdn cGVodmhpZ2ttamtsdXNwOmpufGhwd25wbWxwfm1xf3Rydm10fG9zgXB0gnd6QnJ2hHZ4dXt7T3t9 P3Z9kXl9i3SAmHyBhIWGR4WEaoCEkomJXX+GmnuHoIWIeXmIp4iJdIaIhYSIl36Ko42Lj5CPYn+O oIuPnoWRno+QmZCSj4uTp22ZzHSXzHOax3Kb1YqZrHedypWbkXie36KgWp2clJyem36h1p6eqIKh 3Z+hnoykz3+p0IWn3KSnhZemuaWmkKemi4yoy6qpe4qr1KansZapzpCsz42u16qsqZOt5Iyx05Ov 07GxcJywyKevt7Gzd5Sx4pay1quvv5ez16+vuZqy3qizwa6ztZS42rK0saG12ra3jqW11Z624aC3 1re0uZq615253by5hLW3tJy56ru6i7W5qZy82aO62ba3waC84Lq4vLi6t6W925+/3KXA16++0ru9 ur6/qanB4K3B2sC+wq3B58PAxMDGlcDCvq3E477DxcLEwbLG37rHx8TGw67K4bXJ4sbMm7PK6brJ 6sTI2MrIzMjKx8jJ08vKwbzL7LbO4L3M4LfP4cTN1cfOw8fNz7zP6b/P4svOytHSqMHR5MvQ0rzT 5cnR2s/Rzs/Tw8PT59DWpM3S1dDSz77W6MzT6cbV6dLU0crW5dPW0sjX69LX2cnY7NXX1Mba5s/Y 4MrZ7dbY1c3Z587a6Mvb7s/b6szc8Nnb19zfp9zdxtfc39Dd69rc2d7b4Nvhr9Le7Nne4dff6NDg 9N3f3NTg7t7g3dXh7+Hg19zg8Nni6tzi5ODi39fj8drj6+LntePmwtzk7eHk4Nnl9ODk9OPl4uDm 6OrouN7n7+Ln6uXn5OPo6+bo5eDp8efp5uHq8uXq7eXp+ejq5+jp8+Ps9Ofs7+rs6Ovxvu/s8evu 6unu8Orv8uru/u3u+Ovw8+7w7PXzwu/w+vL4xfL3+fT5/Pz78v///yH5BAEKAP8ALAAAAAAgACAA AAj+AP8JHEiwoMGDCA+uI3SDw4MHHG4QWpewokA9FGJQKTQp06Q1OS7osXhwXg0RZOYs6bFDxw8r kkCpqDGP5EB8NVrMOVIEDiRSsEIxAmPFl48a+Gz+w0NiTpA8sGjZ4tWLly1ab6Qwo4HHprgLX4Ik ikq1V7JeumSZqiLGFwZxJN20wPIFFixcvpLp1Yv2jRZYTdyQRJHkiCRSsqgmcwYN2t5BbQS9QkGS ApkeoUzZMgttGrRr1qD9YuTnjjoKJBuw0fEIFq9fzqZpC6dN2zRYmkjba0DyQhoeZ2DpSgbNWjhz 6MItk0WKE6N6F0jCgKJECiSzs5GHm7YsHC9SpKT+UbZIaAWfLHb6XYN9TbY2Vp9+yeJWxgxJfCO4 JOoTjh8/cdQQI+A2pSwjTTceUETSMRj8AUoo9yxDjjvufHPMN8exM0IlSv1TywabwHINbenMEw89 97RjzwiGdChQJTYkw4sy2qQTjzyz0GPPJVO4KNA3GEDDSye5uHPPLLHcY08YivgoEAqUJNMIEZ6k MkQ09+jjwTZOesjBKZ8AUUcgTwyTzhRGdCkQCxV0Ucs34pBTiwwZvKPmPx1sMUYJEjigwQcMIHDn PwaYkA042IxCRxwzDDCoExMwAc8+8AgDwgInDEqAEP7ksw84akQxAwGDDhCCF4cgs0okKSyQwKAm /7ggAAABBABAAZnC+g8OCuyxBwQv6DqQBQccEIGwBAGyB7J3BgQAOw=='''), "run": PhotoImage(data=b''' R0lGODlhIAAgAOeSAEZHQ0pLR0pMR1BRTVNUUFVWUldYVVhZVVtcV15gXWBhXGFhXmVlYmZmYmZn YmdpY2tsZ21va3BwbXBxbXBybHN0cHh4d3p8eX1/e35/en+BfH+BfYGBfoGBf4GDfIaIhoyNiJOU kpOVkpaXk5aXlJqamZqbmZyemqKioKqrqaysq66urLO0srS1tLa2tbe3tbi5trm6t7q6ucDBvsDB wMPDw8XFw8XFxcfHx8vLycvLys3Nzc/Pz8/QztHR0dLS0tLT0dPT09TU0tXV1dbW1tfX19fY1tra 2tvb29zc3N3d3d/f3+Dg4OHh3+Li4uTk5OLl4OXl5ePm4eTm5Obm5ufn5+bo5ejo6Obp5efp5enp 6efq5ujq5urq6unr5+rr6uvr6+ns6Ors6Ozs7Ort6evt6evt6uvt6+3t7evu6uzu6uzu6+3u7e7u 7u3v6+3v7O7v7O7v7e/v7+3w7O7w7e7w7vDw8O/x7u/x7/Dx7/Hx8PHx8fDy7/Hy8PLy8vHz8PL0 8fP08vT09PT18/T19PT29PX29PX29fb39fb39vf49vf49/j4+Pj5+Pn5+Pn6+fr7+v3+/f////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////yH5BAEKAP8ALAAAAAAgACAA AAj+AP8JHEiwoMGDCBMqXMiwocOHECNKnKgQwwuKCgEw6FAD40EAkGgYCLHDI0EAg9KsOZFAhUmB KL14mWPlggQZJmPKFFMHyIIPNzDqlOmFzJ0YB0rwmAigEJkyZqKaKQPnDIkGLYZEBIDoDZ2vYL/2 mTLBAg4kDwEs4tPnDx88b9SEwSIFSpweBEwccch1i8wwYsSE8cIlSxg9LgKseChgkRk3dO7k4ZPn zh1CRh6IQPtwQCM6fQIVQpQo0aMvGzgIkVjAUR5AhhZBiiQoRQUbFBF8Dq2IkY4ILDwqWFSGzqEm GVC8dIDoDpsRIJK8/AfhEQwPOaYLpKDBhfaBM74Kix9Pvrz58wUDAgA7'''), "save": PhotoImage(data=b''' R0lGODlhIAAgAOf8AAABACJKhSVOgydOiSJUjyxSjiNVkCZXkjBWjDFXjShZlFJUUSpalVRVU0lX aFVWVDVakFZYVTdck1dZVjlelVpcWTtgl1tdWkJfkTRjmS1kpjxhmD5imV1fXDBmqEFlnGBiX2Fj YEFomWJkYT1ppWNlYmRlYz1roURqm0VrnGZnZUFsqWdoZkdsnmhpZ0htnzpwrENuq0JvpTtxrWlr aEtwomttak1ypE5zpUt3rld2o014sF12nU55sVp4pVZ6rVh8r2B+q1t/smF/rFyAs3t9el2BtHx+ e2GCqWiAqWOBrl6CtX1/fGSCsF+Dtn6AfWqCq2WDsWGFuICCf2CHtGKGuWOHumeIr12LvWSKt4SG g2uJt2WLuG6KrIaIhWuMtGeNunCMrmyNtWiOu2mPvHCOvGqQvXyPs2yTwG2UwY+RjniUtm+Ww3yU sZCSj3GYxXeYwJOVknmZwnqaw5WXlHubxHydxYWcuX2exn6fx3+gyIifvYChyZyem4ahxIGiyoqh v52fnIeixoOkzKGjoIqmyZKlvYunyqOlooyoy42pzKWnpKaopY+rzpCsz5Gt0KmrqKCsuZKu0pqt xpOv05evzautqpiwzqyuq5mxz6Gww62vrJuy0K6wrbCyrrGzr6y0vLK0sa21vaa2ybO1srS2s7W3 tKm5zLa4tbe5tri6t628z7m7uLu9ur2/vLjAyLLB1bjD0bvDy8HDv7/ExsLEwcPFwsTGw8XHxLzI 1sbIxcfJxr7K2MjKx8nLyMDM2srMycHN28vOysnO0MrP0c3Py87QzM/RzsrS29DSz9HT0NLU0dPV 0tTW09XX1NbY1dLa4tXa3Nja1tbb3dnb19rc2dXd5tvd2tne4dze29zd593f3Nvg497g3d/h3tzi 5ODi3+Hk4ODl6OPl4eTm4+Xn5Obo5efp5uXq7ejq5+nr6Ors6eft7+vu6u3v6+7w7e/x7u3y9fDy 7/Hz8PL08fP18vT38/f59vn7+Pr8+fv9+vz/+////////////////yH5BAEKAP8ALAAAAAAgACAA AAj+AP8JHEiwoMGDCAkGO4Piw4s1vxJKJCjOh4Qgcgr5ycKhBraJCa1ZEPPoUKNHjzhx+oIAGUiD 7F4AWqPjhpKTig49kgNB20uChmpsCBMLVhIJghT94dOIypCfAtG1IJCL4KgMjfLUmeNog8ufvJDE MgiFy583bAYpWQP11B5qRSZcmHvBgYhCacy8wSPgQoW/EbyoQxgM2pNN+fbp05cvnoJDZMCMUVTA Xj168+bFYYSQW7oO9VYFGD16gCIsVqo8Ij26yzYX7xCmonNv3CQKcv4MOqRIipMllS6pVJJiWjsm uBAW6bauW7g7G+ZgWWJEiJAeMEiU2QJhmLNwtbz+HKTGAp+3cebMXUFRR0iOGB40aFiS5oAsatKk nQPhzWCfVvCcN8443/hwwhgkyKfBDnUwoAk1EEozDiGLFNROCfAMOE443WQTTQoyoJEgDHxw0EZ+ EEaoQmwDuaJGPdSY0w1+zTRDDAU/oLECHjXwYIwxyjDTzDbMjPPELARNIY051HDYDDNQGvNKAkoU AgQGtPQCjDDGSEONMtS0osVA27AAozTOdNONM8wYU8wuoBBwgwGi2IJLL8Lkx+Uy3YzQjUCImKJO NjQ2uY0zxviySyQBRHKnMc58WQyQ0mTTByEC2XBOJ1Mc4emnoIYq6hFTWLIMDQI9YM8TzWTj6qu9 sMYqazbNPHFOAwItAM8R5jDi66/AmgNAesQWm94RxiyQ6zpHyAPJs9BGKw8A8lRr7bXyHKGKsv8s kM4R+nQi7rjk6gPAYuimu9gRqHC7wDngkitvJ+aqa+8RpLhrzhPz2Ovvv/rM88Qn7o4TSKejJizq FG5gwq0Jt3jTzTYRsrnMj8Ioqosts7jSCiuqmELKJ55sEocLApUSwgIst+zyyzC/DIIlAzWDiyqh bKLzzjz33DMpnqiSHFRECxQQADs='''), "clear": PhotoImage(data=b''' R0lGODlhIAAgAOf/AHAFAHsGCJcAAHMKC3QLBHYMAKwAAYkLA3kRCbgAAIIQB8EAAHsUEbEGDIgX DIAaG5sYF4MkDYYmB80TDrIcFr8gHpE8A8EuLtIqKZVAGOMzM5lNEKBXA+BDQZdgEJVkEu5ISaBn D5xpDvZITZtpGJ9sHJxvJKRwIKBzKKV3Jat2HqV4NKZ5Lrd6EcB7CK5/NK2AQrKDOMKDEcWGIcKJ KrqKP7OLSr2PGLyPI7SNU7WPWsiPOMOSP8KSTb+VTsWUScOeCcWfAMChAMiiAMahIMWlAMOkEsuk AsenAb2mM8ifXb2gdMqpCNOgTceoJcmqG8GqN72pTc6sAMysDsysH8Smesaio8+mas2uLNOxAdWo WcyxI9WpYcmuU9C0BM6zGMyyL8WxVNOyJ9OvSNKyMceqqbmxl9m2D9G2Kda5ALa0mdO4H9KweNa6 E9S9ANi7Fc2vrtu9AMm1ks65XNi8JdO+Jde9OtS8Ude9Q9rCCdjBHLm7uNq/PdjCKuHCENi/TdW/ Wt3FENy8b8W/mOHIANXBacHAt9nFSdzGOeHIGNG/odnDZdzKF8DCvtHDhNzHQ83Cot/IMufIG+DJ KN6/ksfCweHBjdnGdc/GmeXMHt/NKt7KTcTGw97NNd3KVujOD+nLLeLMP97MXujPI+TFl8fJxuPO St3Lf+TGn+XPQ+jQMefQO+HRQ9HLtsrMyeXQVO7UHO3TKd7QdeLTTd7QkOLScfHWIOfWP+fTXuzY H/DWLdrNzs/Rzt/SmObVbebYYd/Rvt3Up+/bMejZW+fXduDVodPV0vbbNPLbT+7aZencZNXX1PXc SOvdXu7bbeHZq+ndbOnbgPneN+/fWfjiLvXhQtnb2OPctPjgVNvd2uPfvPrlPe7jePDhhvflT93f 3Obd1vflV+7ilN7g3fvkYPvnSPXkg+7km+rhxuDi3ubh3/noYvLml+Hk4Ovkz+vj2/fojOPm4urm 1+bo5f3udurp4P/uhenr6PrwjO3s4/Dr6f/wjevu6u3v7P31sv///yH5BAEKAP8ALAAAAAAgACAA AAj+AP8J1Jfjwwcd7wQqXMiwYcMlMVBR8mHinsOLGP+VoCTiA4sXcjKKXOiBDYp+JK6Q6DdyJAwl JUiEIHVCUUuRwD5Q4uGiyQ8WN0VWSWFphgwuHoAFxdhvRQ0tLnaosLEoCMulDMF94EGjxY0gj4q4 w9pQ0QccQMSYY7eBgRWyC90BOZIMXw8EECgMUAf336kgdZqNieCgAwgMAeD0vRaESAYCDTSMuADA ApZnffMhELBgQgUFEgTtc7YF0FWsDwwkOFCghz965bxx4wOmHtkyAwBwgGePHjlv1GIp80RFHtld XUytK7cOmbdjuRLNqvUEHVlApsJtY4ZN1bFRmdLgzIpGRRvWS4imUdNVDVQsSZkmeTElbkuvpb3Q NINlK5a0M7G8QUgkUiByDh6A5HMTOlNAkwgsflCTSBvyBRKKFH2IQ10xLfVTxC95fBIHLNUUkcka XjDCyhRfiHNOEbS0dAcikeSRRxzCELgKE25oMgsfTJBRyE3BfIGLF4kw8ck0U+gRViDd+DLEHEs9 8QofbtCRxS2pFMHKJkygYQQkWDmyBTFedFJEHsPYYYcnQ0BhCFxOPCJLKLgUQYwnQSRhRl//tDKE KH8c8oQQUagBqEKYPOFEGINUsuiklFYaVEAAOw=='''), "exit": PhotoImage(data=b''' R0lGODlhIAAgAOfxAKQBAKMCBKUEAKUEBaYHBqcJB80AAKcKDs4ABM8ABKgMB9AAD6gMD6oOCKkP EKsRCdMHEckMFKUYFcERD68ZG7AaHLMdHrIeI7QfJMgdHNIcGrAmJLInJcohHdMeItQgIrQqLM4m JrcuLtEqL9UuLNcwLNkzNFZXVdM2MlZYVdo0NVdZVtU4NFhaV9U5OVlbWNY6OlpcWdc7O1tdWtg8 PMtAPFxeW15gXds/Pto/Q19hXmBiX91BRWJkYctJRWNlYtlGRWVmZNtHRmZnZYdfXWdoZtxKTWhq Z2lraN5LTmpsac1ST2ttas5UVm1vbNxRUMpXVW5wbd5SUdBWV29xbt9TUcxZV3Byb99UWHFzcHN1 ct1ZWXR2c95aWnV3dHZ4dXd5dnh6d3l7eHp8ed5iY3x+e31/fH6AfeJlZn+BfoCCf+RnZ4GDgIKE gYOFgoSGg4WHhIaIhYeJhoiKh+FzcomLiMl6eIqMiYuNioyOi42PjI6QjY+RjpCSj5GTkJ6Qi5KU kZOVkpSWk+iAgZWXlJaYlZeZlpial+aFg5qbmJudmZyem52fnJ6gnZ+hnueOjqCin6GjoOmQkMaa l6KkoaOloqSmo8Ken6WnpKaopaeppqiqp+uYlamrqM+in6utquidnqyuq+men62vrM2oqeyhobCy ru2iorGzr+qmpLO1stqsquGtrLe5tri6t7m7uLq8ubu9ury+u72/vO6ytL7BvcDCvsHDv9u9vMLE wcPFwsTGw8XHxN/Bv8bIxcjKx8nLyMrMycvOys3Py87QzM/RztDSz+vNy9LU0dPV0tTW09XX1NbY 1dnb19rc2dvd2tze293f3N7g3eDi3+Hk4OPl4eTm4+vl5OXn5PLk5ezm5ebo5efp5vPm5ujq5/Tn 5+nr6Ors6evu6u3v6+7w7e/x7vDy7/Hz8PL08fP18vT38/b49Pf59vj69/n7+Pr8+fv9+v////// /////////////////////////////////////////////////////yH5BAEKAP8ALAAAAAAgACAA AAj+AP8JHEiwoMGDBSGdWMiwYYoWOo5c8QIGDJconRAKPKEqj5szYLIwKdLjxgwdSJqhS8cSnTcv Gv+daLVHjpoxXqIgGdJjxxAw1Naxa9eOnbo4MU/IApTnTRoxXJzsDMKkzbZ27rK6Ywco6a5EfOq8 MRNGSxQlSLLM4ebunduthpICc0RoT52PYbxQifJFD7i28N65a5coaTBKiwjpmePmqRctY/iEAyyY sMBrvQyeEFapUSJBdt+cGRPmDCBxlAcXviaigOZhmBwlItRn8ccybQiNS01YGwgoFTQTywRpUSFA eurAUZPmjaHd7wIP/rPBCq3gBU8Q0xSJkaFAe/D+5OnUSE8i6NK/SZgiKhUFBfAVbJBJbFPnz3zy 3BmlChUm9PB8g0ETkqDBSSmnnFIKKALQ14klshHiR3JeuPGKK+h9c8ESg2DxxBZddLFFFZw0qF0n mERiHHJ3wJFILpVAl40FPtABBA9AGJFEEkYA8YiJxHSSCSWMJBIIH3gAAswmfpDjTjcV1ECGCibI QAMOMsCAAw2IANmJfZ4J0gcfubRyRiJOHqOACzR88IEGCyBggAEILLCGlw9CkkghfrwyixlvQOLk O6woMAIJcs6pqAFV4JniIoY08sodbORByaDvrKJACIjSicCnCDyBpyaUeHZkHXfwYUk5baEDjif+ BXSAqAw45IAjEAE4+GUlEdaGRx+YcMMOO+Vs08wkA2RAAgsFCOCsAA7oiqKefO6hhyCb7FLNOuZs Aw0xRBAQAQoMCNfJuZl0Z4gggACSSCawMHNOOuFQs4walxAwQbnZBXmuJrwmIvBnmvBSjTnonAOO NG78Q0oBCph77iaYUFKJJYEEogoy1YhDzsficPOGQLjYIfG5nYwClinENEPNNjDDXA01ZiTl7yev VNIHKsEs00w00UgjtNDRNMNFUsOEEosphqjiyzDIJLPM1FRPrQwyTCA9Syaq1OLLL8GELczYZI8d DDA/JOVKK7DMYsstueSiy9x0152LLTbEpFAYCiu08EIMMcwg+OAz2EB4DJHEpPjiCAUEADs='''), "browse": PhotoImage(data=b''' R0lGODlhEAAQAMZ6AFpaWlxcXFxcXV1dXTNkpDRlpGBgYDZmpTdmo2FhYTdnpThopThopjlopDlo pWNjYztppjtqpjxqpWZmZmdnZ0BtqGpqamtra2xsbElxpW1tbUhzq29vb013r0d5tHNzc095rnZ2 dlR9snp6emp/mWiAn35+fn9/f4CAgH+FjlmMw4aGhlyOxGCPw2iPvY+Um5WVlXuawJmZmZqampub m5ycnJ2dnZ6enp+fn6CgoKGhoX6n1H+o1KOjo4Gp1aSkpIKq1aWlpKWlpYSr1qampoar1IWs1qen p4au2Iiu2Kmpqaqqqqurq4uw2Yyw2aysrI2x2q2trY6z2q6urpK027CwsJG125K125O125K225O2 25S23LKyspS33LOzs5W43Ju73re3t52937q6uqK/3729vcDAwMHBwMTExK7I5MXFxbHK5MfHx7PL 5sjIyLTM5snJybXN5srKyrfO5rfO57fP57jP577T6cHV6sTX6/Dw8PDw8PDw8PDw8PDw8PDw8CH5 BAEKAH8ALAAAAAAQABAAAAfOgH+Cg4SFf0QoiYkrhoMmcGxqaGhcMI1/I2pVUUxLSycaGBcXFox/ IWdPS0dCP0pjam5yZhOCH2VKRD06OTg4OTo9PQmCHGG6OTY1NDM0NTc3A4IWXj0ZDtjZ2QgQIhRT OQtvdHR1c+foawQPSjcNd1lWV1n0WVpfbQcGQTUSeF0AA3aRgiSOggA6aFTIA6Zhwy1NkkCxwwAA jRcg0mCh4sQIjx0ggZCJMEBGCRdimgDxwbLlkCIdBJBw0IKFips4cXqIkaKAz59Af274EwgAOw=='''), "clear16": PhotoImage(data=b''' R0lGODlhEAAQAOeIAKsbDbg4HX9QCaxDFKxDJYRUCoJUEKhJGYpYDIlZDIZaGLtKKIdbGothIoxi IqZpCqlrCqttC7FsHbJyDLJ2C8htM7x6D5uHAKGIAcF9EJyLAKCQCKGRC6KRDaGSEKOSD6SSEKOT EaOTEqOTE6SUEaWUFKWWF6aWF6qOY6eXG6qZLqydIa2eKK2fKrChJLOjJ7CjNrynL72tMbqodrum h7mtULmuUbuwVsGyNryyW72yWsS0NMa1M8W1OMW2NdexYs29QcS7dcy+R9rCA9vDBM7ASNzEB9zF ENHCT9zGENzGFcnBgNXHVc7Brd/KJtjIT+PLEdDFltDDsOXNFNDJlufQGNzNWNPIuNLMm93PXuTS SeTTTOHTW+XUTu7XI+LUZ+bWVufXW9jUsOfYX/PbKu3aRtrVs+fZbdrWtOjabNvXtvbYYfbeL+zc aOrccuvdd+ved/vkN/PjZfzlPPvlRP3mPOPgzOTh0OTi0v3pUubk1v3qX/3qY/3rYf3rbPjqiPvq hP3sa+jn3P3sbfvti/3uff3vhP3vivDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8CH5BAEKAP8ALAAAAAAQABAA AAjBAK84YNDkn8GDCA0qeBBBgJSEEA1MyAChwb8gEA/SQGAhQwIVGjIeRFGAAgYZL9SINDjjAhA4 Eg60WCIIIpYNT9pUAPDDjRAQOvQgpCIixoIAa/j4KfRHCIcbZhASGAAokKFBeejIeVPExYiDUXAc 2tMnTx0yVZJo+VIC4QlCc+Kw8TKFyBAwPmwgZJGlTBUoRoYM2WKlQ0IxK9IcEawkDBMSaCDC6DHG SZczO0zYEZkCCRceH2qs/IfHQ4gcdxIGBAA7'''), "cut": PhotoImage(data=b''' R0lGODlhEAAQAMZ5AKYBAaYCAqcDA6YFBacFBagFBKYGBqgHB6gKCqkLC6sREbYPDqsUFK0ZGa4a Gq8bG6wcHK4dHcAZF60fH8AaGccaGckaGrAiIrAjI7AkJMwdHM0dHbAlJcofH8wgIM4gINAgINEg ILEpKc8hIdAhIdIhIdMiItAjI84kJMslJNQjI9UjI7MuLp42M9onJ7UxMbc3N7pBQaJJRLtERLtF Rb5YVs5qas5ubomJhMt4eIqMh4uNiMx7e42Pis1+fo+RjI+Rjc2AgJCSjpGTjs+GhpWXkpiZlpia lZqbl5qcl5udmZ6fm6Gjn6aopKeopKippamqpqmqp6qsp9ehodiioq2uqrCxrrGyr7Oyr7KzsLO0 sbO1sLS1srW2srW2s7a4s7e5tLm6tr6/vMPEwcXGw8bIw8vLyuPFxczOyc3Oy9HSz9PT0tLU0NbW 1djZ2Nvc2dvc2+Hh3+ri4ufo5u7v7vT19Pb29fb29vf39/Dw8PDw8PDw8PDw8PDw8PDw8PDw8CH5 BAEKAH8ALAAAAAAQABAAAAe7gH+Ca0NMgoJaRYeLQndNY4dGak6LgjpsdFaCbVJlP5V/Q1t2SJA/ aFVKoG47b3RRXF9pOmKgf0ddeEBLXmBQtn9wOnF1SU89cMB/V2FzUj1Zyn9mOGRYLcnAORMGMgEV GDygRAchGws1BRYgHQhTh1QEIzMUAH9yGhI0HgNEgiIqfPxBkUCQgBR/goAQIUjBhz83SrwQBGOF jT8kGAhi4eLEhghnBJ15YGLEBg6CqFxoACEkvBgOMoQMBAA7'''), "copy": PhotoImage(data=b''' R0lGODlhEAAQAKUeAIiKhYmLhoyOiZialZialpyemqGjn6mqp62uq6+wrbu8ury9ur2+u8PEw8fH xs/QzdDRz9TU1NnZ2dra2tvb2+Pj4uPk4uzs7O3t7e7u7e7u7u/v7u/v7/Dw7/Dw8PHx8PHx8fPz 8/T09Pb29fb29vf39vf39/j49/r6+fr6+vv7+/z8+/7+/f////Dw8PDw8PDw8PDw8PDw8PDw8PDw 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8CH5BAEKAD8ALAAAAAAQABAAAAaKQIhi SCw+fsgfw8RsOhVJpYmzqVY5HBM0ueRQDoCweIzocgKttHoN6FIArY58Lm+1pxt0x8Hvd+xLGxNw dHSAJhoZen18dW0kGBKEhR0pIW0iGBeLjBolDW0fIB56dAKnAgAQC6wALHQpAyknFgBRAShzKbEp FQAFtyIlIysqEQS1W0kJAWNhBltBADs='''), "paste": PhotoImage(data=b''' R0lGODlhEAAQAMZZAGpDAmtEA2xEAXBJB3BKB3FKB3FKC3JLC3JNDnNNDnNOEHROEHRPEHVPEFxc W1xcXF5eXmZoZGdpZGpsaG5sZG5tZHBtY3BtZHFvZHNvZH5+e39/fKF8QKN8PYCAfbN7Iqd9O6R+ Prl/I7p/I4WFhMCEJMKGKMWHJsWHJ8aIJ5WViZeXirqrkburkb6wmL+wmLGysrK0tLO1tbe3tLi5 tbm5trm6tru7u8HCvszNys3Oy9jY1dnZ1tra2Nvb2eDg4Obk4Ofn5Ofn5ejo5unp5+rq6Ovq6Ovr 6evr6uzs6uzs6+3t6+3t7O3u7e7u7e7u7u/v7e/v7u/v7/Dw7/Hx8fLy8v7+/f7+/v////Dw8PDw 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8CH5 BAEKAH8ALAAAAAAQABAAAAe6gH+Cf1IkD4cPJFKDgi0NAgEOKisrKg4BAgwvf0wHJSEXEBseHhoQ FBwiCU9ABSkYVD83szc/VRUoAEatKRFYU8DBWBImuq0nvlMyyzJRw8W7Bci/wVBLz8YE08oyMTBF 2LvayVFMSEU+4UDjWE5KR0Q9OersSUVDOjY06gPTREI8amAZSMxYP19BduCYQRAFNCD9LDS5QjGK FQkoMupiYmAEiAwTQobsQPIDgid/WCwQwLKlSwUu/gQCADs='''), "select_all": PhotoImage(data=b''' R0lGODlhEAAQAKUZAAAAAIeJhIiKhYqMh4uNiIGXr4KYsLS1s7W2s7W2tKi+1qm/16rA2KvB2azC 2q3D267E3K/F3bDG3rHH37LI4LPJ4evr6+zs7O7u7vDw8PLy8vT09PX19fb29vf39/j4+Pn5+fr6 +vv7+/z8/P39/f7+/vDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8CH5BAEKAD8ALAAAAAAQABAAAAaWwN9B QCwaiYjfL0AqlUik0UgkCoVAA6WApFgsGIyGWPwhaEuLgrrgWDvKWhJjXXjY7fDfdr5+QP4WeQIj DXQQahEXgiMODn4RERKSGIuOEJCSExMZgiJtdGoUGh5meiKPkgAUABUbpFohqBOrHB0dr3ogDwa8 BqseHx64ArqXErMAHiDBpQEfz9AAH9LPWT8IR9kCCT9BADs='''), "window_icon": PhotoImage(data=b''' R0lGODlhEAAQAMZqAAogQA0lRA4nQBMmQBAoQRUoQRYoQRYpQhcpQxcqQhgqQRgrRBYtQhctQh8w SSEwRSMxQiIyRSAyTTE9RTlCSjtDSjRFYDVFYDdFXUBFSjVGYzdGZDpJYkRKWFZKQ1dKQ1ROSlRO S1FQSVZQQFNSTlZST1tSQWpTRVdYVUxZcWxURmVZRVNcbWdbSndgQlhkfXVjVYVjOY5jNIxjPY9l NGdrcIZnPY9lPoBpSI5oO4RuPJBrP3pwWIZxQYpvT4RxT5RxRaJ2KJ55SYt9apx8UYSCgaSAUpmC Y62BNaCCVpmEbaGEWYmJiKOResqQL56XfaKWgc2XOKSfm6yghdidIqyoj92lK6yurq6vsN6xW7m5 turIeO7MeevMhu3Nfe7SjevWn+zZq+3aou7aofjdj/bfmfLgo/zim/fkmfzurP////////////// /////////////////////////////////////////////////////////////////////////yH5 BAEKAH8ALAAAAAAQABAAAAeOgH+Cg4SFgx+GhB6EIVqDQzCDWiCEVypHNSkcDggZPidYhUYWL0xV TygGDDKGSxtTaWJjZz8EO4ZCGlBmYWBlPAI0hkAXRWhfXWQkDTmGNh0sXF5bWRATMYklGEhRTkEK Iol/LRImVFYjAyviTRUBOj0AFEqJUn9JEQcFD0R/9uI4EixwIa7QjRkFExIKBAA7='''), "process-stop": PhotoImage(data=b''' R0lGODlhIAAgAOf/AIgAAZAID7gAALkAAL8AAMEAAMIAALsCAZwKD8MAAMQAAMMABsYBAMwAArwF AsYCCMcEAI4UD6AREsgHAL4LBKIUFKEUGZEZGJoXFsoLAZsYHLURF8kMFJUdIJ0bHrgVGaYaHcIT EJ8dGp4dH5AiJM0TDqAfILoZGpMkIM0UFqkeJIwnKcQYGc4WHr4eHcccG9EaGacmJZIsLckfHdMe G9UgHMsjJdcjJMcqKtglK9knJsotLNspJ80wLdUuLN4tKc0xNNYwMs8yNdgxM9A0NtE1N9kzNNI2 N9o0NdQ3ONU5OdY6OuY2Nd04Pdc7O9g8POA6Oeg4N+k5N9o9PZlPT9s/Pto/Q+s7PtxAP9xARN1B QJhUUd1BRd9CQN5CRqBVVdlGRelDQOFFSNtHRtxIR+tGR+VIRd5KSPVFRuZJS+5ISa1aXKhcW+FN S+lLSOBNUP5GSv9HRapeXeJPUqtfXuRQU+tPUK1hYPtNUfBSU/dRUKtmaPlSUa5oau1ZWudbXvdZ WfZZXuteW+RgXrBwcOZiYPpdYe5hY7JyculkYuhkaLR0dPdiYvJlZu1nZbd2dvpkZO9pZ/BqaO9q bbV7d/5nZ/dpa7V9f/lrbPJtb+5wb7iAgvZxc/5vcLWDg/Fzcvlyb/Z3du95evl5ePN8ffZ/gPOD gPqCg/OEh/WFgvyEhfaGg/iIi/KKiqCin/+Iiaahn/qKjfWMjPCOjPaNjaKkofiPj/GRlP+OjPmQ kKWnpKaopfaUkamrqPmWk6qsqfyZlrKtq/uZnK6wrfednbCyrrK0sbO1ssqzsO6qqLe5trm7uLy+ u8i7u72/vO+ytMu+v8DCvsHDwO23t/O2uM/Cw/C6uve6vMbIxfK9vMfKxvW/v8vNyvLDwM3PzPLF yM/RzvbHxNLU0dPV0vnMz/XQ0PfR0ufa2urd3ebh3/vc2+3g4ePl4eTm4/Dj5Ovl5OXn5Obo5e3n 5ufq5u/p6Ors6Ovu6u3v7PPu7PXv7u/x7vDy7/Hz8PL08fP18tbW1iH5BAEKAP8ALAAAAAAgACAA AAj+AP8JHDjQE4CDCBMeZEOwoUOBmzC04kWRYi5btmixWhXjzsOPlzSYSlSlZJUnKFE6mUNKxZ6P DSl5MKXIik0rJlM+WSkKRB+YAmWiSmSTC5ebJXWu/ASCEMxHI4YaBUMVzFGcJ1U6eaNJxKKHi6IW 4lK17NWsKpe8mSTiUUNEJlAVskIWTBo8Z6raLBkGzZMlSo68cWTh0kBCcQcVtcsomR4yVPeWwTQL DeAjR84kwuBJIABaf0rSNWPoHLtnerxcnZyO3S00gY8QOeMIgOdcKKvYhJOMXbx4qLVYUcPp3e9z cZocKUIEiCTb/wDgfiJai55p87JTA2Qn1Lvs6CD+aVlOpHkk6NJTmuQCyFr2etZGycu+DpMYzMyB ADl/Wyf1Klgwso099xRYoDugnIFfc/o1gl4uTjjh3xNYYLLNPfpkKM8oCpK3ww444CAIerYsEeGE fqiyDz8smtPJETZwsMACCSRQgANXkIiEiRGe6Ecu+LDYTz/8bKMHBDXWeOMAUqBHCxJQLiGlE4L4 kg8//uCjDpH7bBMIjTYOIGYU6MlihBFQQimIMEH6k88plYTjDz/7dNPIAgWIOYAATKDHShBnnnkI MW3mU8oNE2BCzpz6fMMJBWIKwCd6qbQRxKVBnJLNiviccoMCCSwQijn97LPOKyHsKcAAfQrERgz0 oliKKSnX0BNLDqDWuEAp5rhjywuqDqBDC9D9s0YMmUABQwkTTJAJLj7kmuQCsgADrKQD8DBDAMgQ xIYFedDQAATkpiCtkg5QkCq2PLwQQTMO0VGBHTRAwAAD59rogJ7ssnABNB/dIQG9+CaJLr8COMCD Cx1UA5QcErhBg8H6IuzADyegADBM3ETzhQRlwJDkkpEm/MMHJATDDDbjPDSONs4cQwUCaqhRRhlh hHHFFVJEwQQTG6wASzHLsPyRONgwY8wWCjUNgAxDF90yUOJwI40yxfyySy2udO2KLr0Mo4w03EwN VEPjiOPN2tys7Y04Zj8UEAA7''') } ROOT_W, ROOT_H = 1024, 600 # the following string may be confusing, so here there's an explanation # Python supports two ways to perform string interpolation # the first one is the C-like style # the second one is the following, where each number enclosed in brackets points to an argument of the format method root.geometry("{0}x{1}+{2}+{3}".format(ROOT_W, ROOT_H, int((root.winfo_screenwidth() - ROOT_W) / 2), int((root.winfo_screenheight() - ROOT_H) / 2))) root.title("Maintenance Tools GUI") root.rowconfigure(0, weight=1) root.columnconfigure(0, weight=1) # set the window icon # for now, it's just a grayscale Wesnoth icon # also, this line shouldn't have effect on macOS root.tk.call("wm", "iconphoto", root, "-default", ICONS["window_icon"]) # use a better style on X11 systems instead of the Motif-like one if root.tk.call('tk', 'windowingsystem') == "x11": if additional_themes: # if ttkthemes is available try using a new theme # some of them have issues, but this one seems to work fine # TODO: at some point, add a preferences dialog to allow changing theme # and a preferences file to keep track of it and other settings style = ThemedStyle() if "keramik" in style.theme_names(): style.set_theme("keramik") else: # no ttkthemes, clam is a built-in theme style = Style() if "clam" in style.theme_names(): style.theme_use("clam") app = MainFrame(root) root.mainloop() sys.exit(0) else: root.withdraw() # avoid showing a blank Tk window showerror(_("Error"), _("This application must be placed into the wesnoth/data/tools directory.")) sys.exit(1)