#!/usr/bin/env python2 # -*- coding: 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 # Queue (queue in Python3) is needed to exchange informations 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 sys,os,threading,subprocess,codecs # sys.version_info checks the interpreter version # this is used to have a script that can run on both Python2 and Python3 # not that useful until the mainline tools are updated, but still... if sys.version_info.major >= 3: import queue # tkinter modules from tkinter import * from tkinter.messagebox import * from tkinter.filedialog import * # ttk must be called last from tkinter.ttk import * else: # we are on Python 2 import Queue # tkinter modules from Tkinter import * from tkMessageBox import * from tkFileDialog import * # ttk must be called last from ttk import * # we need to know in what series we are # so set it in a constant and change it for every new series # it must be a string WESNOTH_SERIES="1.12" # 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])) upper_dir=APP_DIR.split(os.sep) upper_dir.pop() WESNOTH_DATA_DIR=os.sep.join(upper_dir) WESNOTH_CORE_DIR=os.path.normpath(os.path.join(WESNOTH_DATA_DIR,"core")) 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 def run_tool(tool,queue,command): """Runs a maintenance tool with the desired arguments and pushes the output in the supplied queue""" 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,command)) queue.put_nowait(' '.join(wrapped_line)+"\n") si=subprocess.STARTUPINFO() si.dwFlags=subprocess.STARTF_USESHOWWINDOW|subprocess.SW_HIDE # to avoid showing a DOS prompt try: output=subprocess.check_output(' '.join(wrapped_line),stderr=subprocess.STDOUT,startupinfo=si) queue.put_nowait(output) except subprocess.CalledProcessError as error: # post the precise message and the remaining output as a tuple queue.put_nowait((tool,error.returncode,error.output)) else: # STARTUPINFO is not available, nor needed, outside of Windows queue.put_nowait(' '.join(command)+"\n") try: output=subprocess.check_output(command,stderr=subprocess.STDOUT) queue.put_nowait(output) except subprocess.CalledProcessError as error: # post the precise message and the remaining output as a tuple queue.put_nowait((tool,error.returncode,error.output)) 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 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 if sys.version_info.major>=3: super().__init__(parent) else: Toplevel.__init__(self,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, text="{0} is running\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) 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, 80, int(root.winfo_rootx()+(root.winfo_width()-400)/2), int(root.winfo_rooty()+(root.winfo_height()-80)/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 if self.thread.isAlive(): self.after(100,self.check_thread_alive) else: self.after(1,self.destroy) 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""" if sys.version_info.major>=3: super().__init__(None,tearoff=0) # otherwise Tk allows splitting it in a new window else: Menu.__init__(self,None,tearoff=0) 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""" if sys.version_info.major>=3: super().__init__(parent,**kwargs) else: Entry.__init__(self,parent,**kwargs) # 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 # finally, bind the "select all" key shortcut # again, Mac uses Command instead of Control windowingsystem = self.tk.call('tk', 'windowingsystem') if windowingsystem == "win32": # Windows, both 32 and 64 bit self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) # for some weird reason, using a KeyPress binding to set the selection on # a readonly Entry or disabled Text doesn't work, but a KeyRelease does self.bind("", self.on_select_all) elif windowingsystem == "aqua": # MacOS with Aqua self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("", self.on_select_all) elif windowingsystem == "x11": # Linux, FreeBSD, Darwin with X11 self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("", 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""" if sys.version_info.major>=3: super().__init__(parent,**kwargs) else: Spinbox.__init__(self,parent,**kwargs) # see the above widget for an explanation of this block windowingsystem = self.tk.call('tk', 'windowingsystem') if windowingsystem == "win32": self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("", self.on_select_all) elif windowingsystem == "aqua": self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("", self.on_select_all) elif windowingsystem == "x11": self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("", 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""" if sys.version_info.major>=3: super().__init__(*args,**kwargs) else: Text.__init__(self,*args,**kwargs) # see descriptions of above widgets windowingsystem = self.tk.call('tk', 'windowingsystem') if windowingsystem == "win32": # Windows, both 32 and 64 bit self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("", self.on_select_all) elif windowingsystem == "aqua": # MacOS with Aqua self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("", self.on_select_all) elif windowingsystem == "x11": # Linux, FreeBSD, Darwin with X11 self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("",self.on_context_menu) self.bind("", 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""" if sys.version_info.major>=3: super().__init__(parent,text="Directory",**kwargs) else: LabelFrame.__init__(self,parent,text="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, 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: # 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"): # we're on Linux; usually this string is 'linux2' 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="." if os.path.exists(userdata): # we may have gotten it wrong directory=askdirectory(initialdir=userdata) else: directory=askdirectory(initialdir=".") 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) if sys.version_info.major>=3: super().__init__(parent) else: Frame.__init__(self,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, text="Normal", variable=self.mode_variable, value=0) self.radio_normal.grid(row=0, column=0, sticky=W, padx=10) self.radio_dryrun=Radiobutton(self.mode_frame, text="Dry run\nDo not perform changes", variable=self.mode_variable, value=1) self.radio_dryrun.grid(row=1, column=0, sticky=W, padx=10) self.radio_clean=Radiobutton(self.mode_frame, text="Clean\nDelete *.bak files", variable=self.mode_variable, value=2) self.radio_clean.grid(row=2, column=0, sticky=W, padx=10) self.radio_diff=Radiobutton(self.mode_frame, text="Diff\nShow differences in converted files", variable=self.mode_variable, value=3) self.radio_diff.grid(row=3, column=0, sticky=W, padx=10) self.radio_revert=Radiobutton(self.mode_frame, text="Revert\nRevert conversions using *.bak files", variable=self.mode_variable, value=4) self.radio_revert.grid(row=4, column=0, sticky=W, padx=10) 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, 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, text="List 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, text="Name files\nbefore 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, 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, text="Convert EOL characters to Unix style", 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, text="Don't 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.freeze_variable=BooleanVar() self.freeze_check=Checkbutton(self.options_frame, text="Ignore newlines in messages", variable=self.freeze_variable) self.freeze_check.grid(row=4, 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=5, 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): if sys.version_info.major>=3: super().__init__(parent) else: Frame.__init__(self,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 files matching regexp:", 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 files not matching regexp:", 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, text="Report only on macros referenced\nin at least 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="List actual & formal argtypes\nfor calls in fname", 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="Ignore refcount 0 on names\nmatching regexp:", 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): if sys.version_info.major>=3: super().__init__(parent) else: Frame.__init__(self,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.radio_dryrun=Radiobutton(self.mode_frame, text="Dry run\nDo not perform changes", variable=self.mode_variable, value=1) self.radio_dryrun.grid(row=1, column=0, sticky=W, padx=10) 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, 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, 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, text="Also report\nunchanged 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 files\nmatching regexp:", variable=self.exclude_variable, command=self.exclude_callback) self.exclude_check.grid(row=1, column=0, columnspan=2, 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, text="Do not generate output", variable=self.quiet_variable) self.quiet_check.grid(row=2, column=0, sticky=W, padx=10) 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 MainFrame(Frame): def __init__(self,parent): self.parent=parent if sys.version_info.major>=3: self.queue=queue.Queue() super().__init__(parent) else: self.queue=Queue.Queue() Frame.__init__(self,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, text="Run wmllint", image=ICONS['run'], compound=LEFT, width=15, # to avoid changing size when callback is called command=self.on_run_wmllint) self.run_button.pack(side=LEFT, padx=5, pady=5) self.save_button=Button(self.buttonbox, text="Save as text...", image=ICONS['save'], compound=LEFT, command=self.on_save) self.save_button.pack(side=LEFT, padx=5, pady=5) self.clear_button=Button(self.buttonbox, text="Clear output", image=ICONS['clear'], compound=LEFT, command=self.on_clear) self.clear_button.pack(side=LEFT, padx=5, pady=5) self.about_button=Button(self.buttonbox, text="About...", image=ICONS['about'], compound=LEFT, command=self.on_about) self.about_button.pack(side=LEFT, padx=5, pady=5) self.exit_button=Button(self.buttonbox, text="Exit", image=ICONS['exit'], compound=LEFT, command=parent.destroy) self.exit_button.pack(side=RIGHT, padx=5, pady=5) 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.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 (Mac OS) 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) # this allows using the mouse wheel even on the disabled Text widget # without the need to clic on said widget self.tk_focusFollowsMouse() 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_button.configure(text="Run wmllint",command=self.on_run_wmllint) elif active_tab==1: self.run_button.configure(text="Run wmlscope",command=self.on_run_wmlscope) elif active_tab==2: self.run_button.configure(text="Run wmlindent",command=self.on_run_wmlindent) 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","""wmllint cannot run because there is no directory selected. Please select a directory or disable the "Skip core directory" option""") return # build the command line wmllint_command_string=[] # get the path of the Python interpreter wmllint_command_string.append(sys.executable) # get the path of the desired tool (wmllint, in this case) wmllint_command_string.append(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 self.wmllint_tab.freeze_variable.get(): wmllint_command_string.append("--stringfreeze") 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): showwarning("Warning","""You selected the core directory or one of its subdirectories in the add-on selection box. wmllint will be run only on the Wesnoth core directory""") else: wmllint_command_string.append(umc_dir) elif not umc_dir: # path does not exists because the box was left empty showwarning("Warning","""You didn't select a directory. wmllint will be run only on the Wesnoth core directory""") else: # path doesn't exist and isn't empty showerror("Error","""The selected directory does not exists""") return # stop here # start thread and wmllint subprocess wmllint_thread=threading.Thread(target=run_tool,args=("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=[] wmlscope_command_string.append(sys.executable) wmlscope_command_string.append(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="{0}"'.format(self.wmlscope_tab.exclude_regexp.get())) if self.wmlscope_tab.from_variable.get(): wmlscope_command_string.append('--from="{0}"'.format(self.wmlscope_tab.from_regexp.get())) if self.wmlscope_tab.refcount_variable.get(): try: wmlscope_command_string.append("--refcount=" + str(self.wmlscope_tab.refcount_number.get())) except ValueError as error: # normally it should be impossible to raise this exception # due to the fact that the Spinbox is read-only showerror("Error","""You typed an 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="{0}"'.format(self.wmlscope_tab.typelist_string.get())) if self.wmlscope_tab.force_variable.get(): wmlscope_command_string.append('--force-used="{0}"'.format(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): showwarning("Warning","""You selected the core directory or one of its subdirectories in the add-on selection box. wmlscope will be run only on the Wesnoth core directory""") else: wmlscope_command_string.append(umc_dir) elif not umc_dir: # path does not exists because the box was left empty showwarning("Warning","""You didn't select a directory. wmlscope will be run only on the Wesnoth core directory""") else: # path doesn't exist and isn't empty showerror("Error","""The selected directory does not exists""") return # stop here # start thread and wmlscope subprocess wmlscope_thread=threading.Thread(target=run_tool,args=("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=[] wmlindent_command_string.append(sys.executable) wmlindent_command_string.append(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="{0}"'.format(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 exists because the box was left empty showwarning("Warning","""You didn't select a directory. wmlindent will be run on the Wesnoth core directory""") wmlindent_command_string.append(WESNOTH_CORE_DIR) else: # path doesn't exist and isn't empty showerror("Error","""The selected directory does not exists""") return # stop here # start thread and wmllint subprocess wmlindent_thread=threading.Thread(target=run_tool,args=("wmlindent",self.queue,wmlindent_command_string)) wmlindent_thread.start() # build popup dialog=Popup(self.parent,"wmlindent",wmlindent_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() # I tried posting directly the error in the queue, but for some reason # isinstance(queue_item,subprocess.CalledProcessError) is ignored if isinstance(queue_item,tuple): showerror("Error","""There was an error while executing {0}. Error code: {1}""".format(queue_item[0],queue_item[1])) self.text.configure(state=NORMAL) self.text.insert(END,queue_item[2]) self.text.configure(state=DISABLED) 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: out=codecs.open(fn,"w","utf-8") out.write(self.text.get(1.0,END)[:-1]) # exclude the double endline at the end out.close() 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) def on_about(self): showinfo("About Maintenance tools GUI","""© Elvish_Hunter, 2014-2015 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""") 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=''') } ROOT_W,ROOT_H=800,480 # 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) # use a better style on Linux instead of the Motif-like one style=Style() if sys.platform.startswith("linux") and "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)