diff --git a/gooey/gui/application.py b/gooey/gui/application.py index 47f9b85..ca654ea 100644 --- a/gooey/gui/application.py +++ b/gooey/gui/application.py @@ -3,21 +3,35 @@ Main runner entry point for Gooey. ''' import wx - +import wx.lib.inspection from gooey.gui.lang import i18n -from gooey.gui.controller import Controller from gooey.gui import image_repository - +from gooey.gui.containers.application import GooeyApplication +from gooey.util.functional import merge def run(build_spec): app = wx.App(False) i18n.load(build_spec['language_dir'], build_spec['language']) - image_repository.patch_images(build_spec['image_dir']) - controller = Controller(build_spec) - controller.run() + imagesPaths = image_repository.loadImages(build_spec['image_dir']) + gapp = GooeyApplication(merge(build_spec, imagesPaths)) + # wx.lib.inspection.InspectionTool().Show() + gapp.Show() app.MainLoop() +def build_app(build_spec): + app = wx.App(False) + + i18n.load(build_spec['language_dir'], build_spec['language']) + imagesPaths = image_repository.loadImages(build_spec['image_dir']) + gapp = GooeyApplication(merge(build_spec, imagesPaths)) + # wx.lib.inspection.InspectionTool().Show() + gapp.Show() + return app + + + + diff --git a/gooey/gui/cli.py b/gooey/gui/cli.py new file mode 100644 index 0000000..7234936 --- /dev/null +++ b/gooey/gui/cli.py @@ -0,0 +1,18 @@ +from itertools import chain + +from copy import deepcopy + +from gooey.util.functional import compact + + +def buildCliString(target, cmd, positional, optional): + positionals = deepcopy(positional) + if positionals: + positionals.insert(0, "--") + + cmd_string = ' '.join(compact(chain(optional, positional))) + + if cmd != '::gooey/default': + cmd_string = u'{} {}'.format(cmd, cmd_string) + + return u'{} --ignore-gooey {}'.format(target, cmd_string) diff --git a/gooey/gui/events.py b/gooey/gui/events.py index 5015c1c..beabc8c 100644 --- a/gooey/gui/events.py +++ b/gooey/gui/events.py @@ -1,20 +1,26 @@ """ App wide event registry -Everything in the application is communitcated via pubsub. These are the events that -tie everythign together. +Everything in the application is communicated via pubsub. These are the events +that tie everything together. """ + import wx -new_id = lambda: str(wx.NewId()) +WINDOW_STOP = wx.NewId() +WINDOW_CANCEL = wx.NewId() +WINDOW_CLOSE = wx.NewId() +WINDOW_START = wx.NewId() +WINDOW_RESTART = wx.NewId() +WINDOW_EDIT = wx.NewId() + +WINDOW_CHANGE = wx.NewId() +PANEL_CHANGE = wx.NewId() +LIST_BOX = wx.NewId() + +CONSOLE_UPDATE = wx.NewId() +EXECUTION_COMPLETE = wx.NewId() +PROGRESS_UPDATE = wx.NewId() -WINDOW_STOP = new_id() -WINDOW_CANCEL = new_id() -WINDOW_CLOSE = new_id() -WINDOW_START = new_id() -WINDOW_RESTART = new_id() -WINDOW_EDIT = new_id() +USER_INPUT = wx.NewId() -WINDOW_CHANGE = new_id() -PANEL_CHANGE = new_id() -LIST_BOX = new_id() diff --git a/gooey/gui/processor.py b/gooey/gui/processor.py index d2bcdb6..85ebfdf 100644 --- a/gooey/gui/processor.py +++ b/gooey/gui/processor.py @@ -1,98 +1,102 @@ import os import re import subprocess - +import sys from functools import partial from multiprocessing.dummy import Pool -import sys - +from gooey.gui import events from gooey.gui.pubsub import pub from gooey.gui.util.casting import safe_float -from gooey.gui.util.functional import unit, bind from gooey.gui.util.taskkill import taskkill +from gooey.util.functional import unit, bind class ProcessController(object): - def __init__(self, progress_regex, progress_expr): - self._process = None - self.progress_regex = progress_regex - self.progress_expr = progress_expr - - def was_success(self): - self._process.communicate() - return self._process.returncode == 0 + def __init__(self, progress_regex, progress_expr, encoding): + self._process = None + self.progress_regex = progress_regex + self.progress_expr = progress_expr + self.encoding = encoding + self.wasForcefullyStopped = False - def poll(self): - if not self._process: - raise Exception('Not started!') - self._process.poll() + def was_success(self): + self._process.communicate() + return self._process.returncode == 0 - def stop(self): - if self.running(): - taskkill(self._process.pid) + def poll(self): + if not self._process: + raise Exception('Not started!') + self._process.poll() - def running(self): - return self._process and self.poll() is None + def stop(self): + if self.running(): + self.wasForcefullyStopped = True + taskkill(self._process.pid) - def run(self, command): - env = os.environ.copy() - env["GOOEY"] = "1" - try: - self._process = subprocess.Popen( - command.encode(sys.getfilesystemencoding()), - bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, - stderr=subprocess.STDOUT, shell=True, env=env) - except: - self._process = subprocess.Popen( - command, - bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, - stderr=subprocess.STDOUT, shell=True, env=env) - Pool(1).apply_async(self._forward_stdout, (self._process,)) + def running(self): + return self._process and self.poll() is None - def _forward_stdout(self, process): - ''' - Reads the stdout of `process` and forwards lines and progress - to any interested subscribers - ''' - while True: - line = process.stdout.readline() - if not line: - break - pub.send_message('console_update', msg=line) - pub.send_message('progress_update', progress=self._extract_progress(line)) - pub.send_message('execution_complete') + def run(self, command): + self.wasForcefullyStopped = False + env = os.environ.copy() + env["GOOEY"] = "1" + env["PYTHONIOENCODING"] = self.encoding + try: + self._process = subprocess.Popen( + command.encode(sys.getfilesystemencoding()), + bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, + stderr=subprocess.STDOUT, shell=True, env=env) + except: + self._process = subprocess.Popen( + command, + bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, + stderr=subprocess.STDOUT, shell=True, env=env) + Pool(1).apply_async(self._forward_stdout, (self._process,)) - def _extract_progress(self, text): - ''' - Finds progress information in the text using the - user-supplied regex and calculation instructions - ''' - # monad-ish dispatch to avoid the if/else soup - find = partial(re.search, string=text.strip()) - regex = unit(self.progress_regex) - match = bind(regex, find) - result = bind(match, self._calculate_progress) - return result + def _forward_stdout(self, process): + ''' + Reads the stdout of `process` and forwards lines and progress + to any interested subscribers + ''' + while True: + line = process.stdout.readline() + if not line: + break + pub.send_message(events.CONSOLE_UPDATE, msg=line.decode(self.encoding)) + pub.send_message(events.PROGRESS_UPDATE, + progress=self._extract_progress(line)) + pub.send_message(events.EXECUTION_COMPLETE) - def _calculate_progress(self, match): - ''' - Calculates the final progress value found by the regex - ''' - if not self.progress_expr: - return safe_float(match.group(1)) - else: - return self._eval_progress(match) + def _extract_progress(self, text): + ''' + Finds progress information in the text using the + user-supplied regex and calculation instructions + ''' + # monad-ish dispatch to avoid the if/else soup + find = partial(re.search, string=text.strip().decode(self.encoding)) + regex = unit(self.progress_regex) + match = bind(regex, find) + result = bind(match, self._calculate_progress) + return result - def _eval_progress(self, match): - ''' - Runs the user-supplied progress calculation rule - ''' - _locals = {k: safe_float(v) for k, v in match.groupdict().items()} - if "x" not in _locals: - _locals["x"] = [safe_float(x) for x in match.groups()] - try: - return int(eval(self.progress_expr, {}, _locals)) - except: - return None + def _calculate_progress(self, match): + ''' + Calculates the final progress value found by the regex + ''' + if not self.progress_expr: + return safe_float(match.group(1)) + else: + return self._eval_progress(match) + def _eval_progress(self, match): + ''' + Runs the user-supplied progress calculation rule + ''' + _locals = {k: safe_float(v) for k, v in match.groupdict().items()} + if "x" not in _locals: + _locals["x"] = [safe_float(x) for x in match.groups()] + try: + return int(eval(self.progress_expr, {}, _locals)) + except: + return None diff --git a/gooey/gui/pubsub.py b/gooey/gui/pubsub.py index 15c4a43..983ce9c 100644 --- a/gooey/gui/pubsub.py +++ b/gooey/gui/pubsub.py @@ -1,3 +1,4 @@ +import wx from collections import defaultdict __ALL__ = ['pub'] @@ -5,21 +6,19 @@ __ALL__ = ['pub'] class PubSub(object): ''' A super simplified clone of Wx.lib.pubsub since it doesn't exist on linux - - *grumble grumble* Stupid abandoned wx project... >:( *grumble* ''' def __init__(self): self.registry = defaultdict(list) - def subscribe(self, handler, event): + def subscribe(self, event, handler): self.registry[event].append(handler) def send_message(self, event, **kwargs): for event_handler in self.registry.get(event, []): - event_handler(**kwargs) + wx.CallAfter(event_handler, **kwargs) pub = PubSub() diff --git a/gooey/gui/seeder.py b/gooey/gui/seeder.py new file mode 100644 index 0000000..81d10a2 --- /dev/null +++ b/gooey/gui/seeder.py @@ -0,0 +1,22 @@ +""" +Util for talking to the client program in order to retrieve +dynamic defaults for the UI +""" +import json +import subprocess + + +def fetchDynamicProperties(target, encoding): + """ + Sends a gooey-seed-ui request to the client program it retrieve + dynamically generated defaults with which to seed the UI + """ + cmd = '{} {}'.format(target, 'gooey-seed-ui --ignore-gooey') + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if proc.returncode != 0: + out, _ = proc.communicate() + return json.loads(out.decode(encoding)) + else: + # TODO: useful feedback + return {} + diff --git a/gooey/gui/util/freeze.py b/gooey/gui/util/freeze.py index 8625240..a755aa3 100644 --- a/gooey/gui/util/freeze.py +++ b/gooey/gui/util/freeze.py @@ -1,26 +1,28 @@ -import sys import os +import sys def is_frozen(): - return getattr(sys, 'frozen', False) + return getattr(sys, 'frozen', False) -def get_resource_path(*args): - if is_frozen(): - # MEIPASS explanation: - # https://pythonhosted.org/PyInstaller/#run-time-operation - basedir = getattr(sys, '_MEIPASS', None) - if not basedir: - basedir = os.path.dirname(sys.executable) - resource_dir = os.path.join(basedir, 'gooey') - if not os.path.isdir(resource_dir): - raise IOError( - ("Cannot locate Gooey resources. It seems that the program was frozen, " - "but resource files were not copied into directory of the executable " - "file. Please copy `languages` and `images` folders from gooey module " - "directory into `{}{}` directory. Using PyInstaller, a.datas in .spec " - "file must be specified.".format(resource_dir, os.sep))) - else: - resource_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..')) - return os.path.join(resource_dir, *args) +def getResourcePath(*args): + if is_frozen(): + # MEIPASS explanation: + # https://pythonhosted.org/PyInstaller/#run-time-operation + basedir = getattr(sys, '_MEIPASS', None) + if not basedir: + basedir = os.path.dirname(sys.executable) + resource_dir = os.path.join(basedir, 'gooey') + if not os.path.isdir(resource_dir): + raise IOError( + ( + "Cannot locate Gooey resources. It seems that the program was frozen, " + "but resource files were not copied into directory of the executable " + "file. Please copy `languages` and `images` folders from gooey module " + "directory into `{}{}` directory. Using PyInstaller, a.datas in .spec " + "file must be specified.".format(resource_dir, os.sep))) + else: + resource_dir = os.path.normpath( + os.path.join(os.path.dirname(__file__), '..', '..')) + return os.path.join(resource_dir, *args) diff --git a/gooey/gui/util/functional.py b/gooey/gui/util/functional.py index 8d0c8d4..e6212fa 100644 --- a/gooey/gui/util/functional.py +++ b/gooey/gui/util/functional.py @@ -1,10 +1,6 @@ -''' -Simple monad-ish bindings -''' -def unit(val): - return val -def bind(val, f): - return f(val) if val else None + + + diff --git a/gooey/gui/util/wx_util.py b/gooey/gui/util/wx_util.py index 1b8184b..1f2f6fc 100644 --- a/gooey/gui/util/wx_util.py +++ b/gooey/gui/util/wx_util.py @@ -3,10 +3,26 @@ Collection of Utility methods for creating often used, pre-styled wx Widgets """ import wx +from contextlib import contextmanager from gooey.gui.three_to_four import Constants +@contextmanager +def transactUI(obj): + """ + Coarse grain UI locking to avoid glitchy UI updates + """ + obj.Freeze() + try: + yield + finally: + obj.Layout() + obj.Thaw() + + + + styles = { 'h0': (wx.FONTFAMILY_DEFAULT, Constants.WX_FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False), 'h1': (wx.FONTFAMILY_DEFAULT, Constants.WX_FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False), @@ -15,6 +31,8 @@ styles = { } + + def make_bold(statictext): pointsize = statictext.GetFont().GetPointSize() font = wx.Font(pointsize, *styles['bold']) @@ -22,8 +40,12 @@ def make_bold(statictext): def dark_grey(statictext): - darkgray = (54, 54, 54) - statictext.SetForegroundColour(darkgray) + return withColor(statictext, (54, 54, 54)) + + +def withColor(statictext, hex): + statictext.SetForegroundColour(hex) + return statictext def h0(parent, label): diff --git a/gooey/gui/validators.py b/gooey/gui/validators.py new file mode 100644 index 0000000..c26c2d1 --- /dev/null +++ b/gooey/gui/validators.py @@ -0,0 +1,15 @@ + + +def runValidator(f, value): + """ + Attempt to run the user supplied validation function + + Fall back to False in the even of any errors + """ + try: + return f(value) + except: + return False + + + diff --git a/gooey/util/__init__.py b/gooey/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gooey/util/functional.py b/gooey/util/functional.py new file mode 100644 index 0000000..5120dfd --- /dev/null +++ b/gooey/util/functional.py @@ -0,0 +1,94 @@ +""" +A collection of functional utilities/helpers +""" +from functools import reduce +from copy import deepcopy +from itertools import chain, dropwhile + + +def getin(m, path, default=None): + """returns the value in a nested dict""" + keynotfound = ':com.gooey-project/not-found' + result = reduce(lambda acc, val: acc.get(val, {keynotfound: None}), path, m) + # falsey values like 0 would incorrectly trigger the default to be returned + # so the keynotfound val is used to signify a miss vs just a falesy val + if isinstance(result, dict) and keynotfound in result: + return default + return result + + +def assoc(m, key, val): + """Copy-on-write associates a value in a dict""" + cpy = deepcopy(m) + cpy[key] = val + return cpy + + +def associn(m, path, value): + """ Copy-on-write associates a value in a nested dict """ + def assoc_recursively(m, path, value): + if not path: + return value + p = path[0] + return assoc(m, p, assoc_recursively(m.get(p,{}), path[1:], value)) + return assoc_recursively(m, path, value) + + +def merge(*maps): + """Merge all maps left to right""" + copies = map(deepcopy, maps) + return reduce(lambda acc, val: acc.update(val) or acc, copies) + + +def flatmap(f, coll): + """Applies concat to the result of applying f to colls""" + return list(chain(*map(f, coll))) + + +def indexunique(f, coll): + """Build a map from the collection keyed off of f + e.g. + [{id:1,..}, {id:2, ...}] => {1: {id:1,...}, 2: {id:2,...}} + + Note: duplicates, if present, are overwritten + """ + return zipmap(map(f, coll), coll) + + +def findfirst(f, coll): + """Return first occurrence matching f, otherwise None""" + result = list(dropwhile(f, coll)) + return result[0] if result else None + + +def zipmap(keys, vals): + """Return a map from keys to values""" + return dict(zip(keys, vals)) + + +def compact(coll): + """Returns a new list with all falsy values removed""" + return list(filter(None, coll)) + + +def ifPresent(f): + """Execute f only if value is present and not None""" + def inner(value): + if value: + return f(value) + else: + return True + return inner + + +def identity(x): + """Identity function always returns the supplied argument""" + return x + + +def unit(val): + return val + + +def bind(val, f): + return f(val) if val else None