[GRASS-SVN] r67972 - in grass/trunk/gui/wxpython: gui_core xml

svn_grass at osgeo.org svn_grass at osgeo.org
Sun Feb 28 20:25:16 PST 2016


Author: wenzeslaus
Date: 2016-02-28 20:25:16 -0800 (Sun, 28 Feb 2016)
New Revision: 67972

Added:
   grass/trunk/gui/wxpython/gui_core/pyedit.py
   grass/trunk/gui/wxpython/xml/menudata_pyedit.xml
Log:
wxGUI: add simple Python editor [news]

Provides Open, Save and Run plus examples and templates.
Always shows a simple template which can run right away
with simple text output. Links online resources related
to Python. Focused on absolute beginners.


Added: grass/trunk/gui/wxpython/gui_core/pyedit.py
===================================================================
--- grass/trunk/gui/wxpython/gui_core/pyedit.py	                        (rev 0)
+++ grass/trunk/gui/wxpython/gui_core/pyedit.py	2016-02-29 04:25:16 UTC (rev 67972)
@@ -0,0 +1,554 @@
+import sys
+import os
+import stat
+from StringIO import StringIO
+import time
+
+import wx
+
+import grass.script as gscript
+from grass.script.utils import try_remove
+
+# just for testing
+if __name__ == '__main__':
+    from grass.script.setup import set_gui_path
+    set_gui_path()
+
+from core.utils import _
+from core.gcmd import EncodeString, GError
+from core.giface import StandaloneGrassInterface
+from gui_core.pystc import PyStc
+from core import globalvar
+from core.menutree import MenuTreeModelBuilder
+from gui_core.menu import Menu
+from gui_core.toolbars import BaseToolbar, BaseIcons
+from icons.icon import MetaIcon
+
+# TODO: add validation: call/import pep8 (error message if not available)
+# TODO: run with parameters
+# TODO: run with overwrite (in process env, not os.environ)
+# TODO: add more examples (separate file)
+# TODO: add test for templates and examples
+# TODO: add pep8 test for templates and examples
+# TODO: add snippets?
+
+
+def script_template():
+    """The most simple script which runs and gives something"""
+    return r"""#!/usr/bin/env python
+
+import grass.script as gscript
+
+
+def main():
+    gscript.run_command('g.region', flags='p')
+
+
+if __name__ == '__main__':
+    main()
+"""
+
+
+def module_template():
+    """Template from which to start writing GRASS module"""
+    import getpass
+    author = getpass.getuser()
+
+    properties = {}
+    properties['name'] = 'module name'
+    properties['author'] = author
+    properties['description'] = 'Module description'
+
+    output = StringIO()
+    # header
+    output.write(
+        r"""#!/usr/bin/env python
+#
+#%s
+#
+# MODULE:       %s
+#
+# AUTHOR(S):    %s
+#
+# PURPOSE:      %s
+#
+# DATE:         %s
+#
+#%s
+""" % ('#' * 72,
+       EncodeString(properties['name']),
+       EncodeString(properties['author']),
+       EncodeString('\n# '.join(properties['description'].splitlines())),
+       time.asctime(),
+       '#' * 72))
+
+    # UI
+    output.write(
+        r"""
+#%%module
+#%% description: %s
+#%%end
+""" % (EncodeString(' '.join(properties['description'].splitlines()))))
+
+    # import modules
+    output.write(
+        r"""
+import sys
+import os
+import atexit
+
+import grass.script as gscript
+""")
+
+    # cleanup()
+    output.write(
+        r"""
+RAST_REMOVE = []
+
+def cleanup():
+""")
+    output.write(
+        r"""    gscript.run_command('g.remove', flags='f', type='raster',
+                          name=RAST_REMOVE)
+""")
+    output.write("\ndef main():\n")
+    output.write(
+        r"""    options, flags = gscript.parser()
+    gscript.run_command('g.remove', flags='f', type='raster',
+                        name=RAST_REMOVE)
+""")
+
+    output.write("\n    return 0\n")
+
+    output.write(
+        r"""
+if __name__ == "__main__":
+    atexit.register(cleanup)
+    sys.exit(main())
+""")
+    return output.getvalue()
+
+
+def script_example():
+    """Example of a simple script"""
+    return r"""#!/usr/bin/env python
+
+import grass.script as gscript
+
+def main():
+    input_raster = 'elevation'
+    output_raster = 'high_areas'
+    stats = gscript.parse_command('r.univar', map='elevation', flags='g')
+    raster_mean = float(stats['mean'])
+    raster_stddev = float(stats['stddev'])
+    raster_high = raster_mean + raster_stddev
+    gscript.mapcalc('{r} = {a} > {m}'.format(r=output_raster, a=input_raster,
+                                             m=raster_high))
+
+if __name__ == "__main__":
+    main()
+"""
+
+
+def module_example():
+    """Example of a GRASS module"""
+    return r"""#!/usr/bin/env python
+
+#%module
+#% description: Adds the values of two rasters (A + B)
+#% keyword: raster
+#% keyword: algebra
+#% keyword: sum
+#%end
+#%option G_OPT_R_INPUT
+#% key: araster
+#% description: Name of input raster A in an expression A + B
+#%end
+#%option G_OPT_R_INPUT
+#% key: braster
+#% description: Name of input raster B in an expression A + B
+#%end
+#%option G_OPT_R_OUTPUT
+#%end
+
+
+import sys
+
+import grass.script as gscript
+
+
+def main():
+    options, flags = gscript.parser()
+    araster = options['araster']
+    braster = options['braster']
+    output = options['output']
+
+    gscript.mapcalc('{r} = {a} + {b}'.format(r=output, a=araster, b=braster))
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
+"""
+
+
+class PyEditController(object):
+    # using the naming GUI convention, change for controller?
+    # pylint: disable=invalid-name
+
+    def __init__(self, panel, guiparent, giface):
+        """Simple editor, this class could be a pure controller"""
+        self.guiparent = guiparent
+        self.giface = giface
+        self.body = panel
+        self.filename = None
+        self.tempfile = None  # bool, make them strings for better code
+        self.running = False
+
+    def OnRun(self, event):
+        """Run Python script"""
+        if self.running:
+            # ignore when already running
+            return
+
+        if not self.filename:
+            self.filename = gscript.tempfile()
+            self.tempfile = True
+            try:
+                fd = open(self.filename, "w")
+                fd.write(self.body.GetText())
+            except IOError as e:
+                GError(_("Unable to launch Python script. %s") % e,
+                       parent=self.guiparent)
+                return
+            finally:
+                fd.close()
+                mode = stat.S_IMODE(os.lstat(self.filename)[stat.ST_MODE])
+                os.chmod(self.filename, mode | stat.S_IXUSR)
+        else:
+            fd = open(self.filename, "w")
+            try:
+                fd.write(self.body.GetText())
+            finally:
+                fd.close()
+            # set executable file
+            # (not sure if needed every time but useful for opened files)
+            os.chmod(self.filename, stat.S_IRWXU | stat.S_IWUSR)
+
+        # TODO: add overwrite to toolbar, needs env in GConsole
+        # run in console as other modules, avoid Python shell which
+        # carries variables over to the next execution
+        self.giface.RunCmd([fd.name], skipInterface=True, onDone=self.OnDone)
+        self.running = True
+
+    def OnDone(self, event):
+        """Python script finished"""
+        if self.tempfile:
+            try_remove(self.filename)
+            self.filename = None
+        self.running = False
+
+    def SaveAs(self):
+        """Save python script to file"""
+        filename = ''
+        dlg = wx.FileDialog(parent=self.guiparent,
+                            message=_("Choose file to save"),
+                            defaultDir=os.getcwd(),
+                            wildcard=_("Python script (*.py)|*.py"),
+                            style=wx.FD_SAVE)
+
+        if dlg.ShowModal() == wx.ID_OK:
+            filename = dlg.GetPath()
+
+        if not filename:
+            return
+
+        # check for extension
+        if filename[-3:] != ".py":
+            filename += ".py"
+
+        if os.path.exists(filename):
+            dlg = wx.MessageDialog(
+                parent=self.guiparent,
+                message=_("File <%s> already exists. "
+                          "Do you want to overwrite this file?") % filename,
+                caption=_("Save file"),
+                style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
+            if dlg.ShowModal() == wx.ID_NO:
+                dlg.Destroy()
+                return
+
+            dlg.Destroy()
+
+        self.filename = filename
+        self.tempfile = False
+        self.Save()
+
+    def Save(self):
+        """Save current content to a file and set executable permissions"""
+        assert self.filename
+        fd = open(self.filename, "w")
+        try:
+            fd.write(self.body.GetText())
+        finally:
+            fd.close()
+
+        # executable file
+        os.chmod(self.filename, stat.S_IRWXU | stat.S_IWUSR)
+
+    def OnSave(self, event):
+        """Save python script to file
+
+        Just save if file already specified, save as action otherwise.
+        """
+        if self.filename:
+            self.Save()
+        else:
+            self.SaveAs()
+
+    # TODO: it should be probably used with replacing, when this gives what we want?
+    def IsModified(self):
+        """Check if python script has been modified"""
+        return self.body.modified
+
+    def Open(self):
+        """Ask for a filename and load its content"""
+        filename = ''
+        dlg = wx.FileDialog(parent=self.guiparent,
+                            message=_("Open file"),
+                            defaultDir=os.getcwd(),
+                            wildcard=_("Python script (*.py)|*.py"),
+                            style=wx.OPEN)
+
+        if dlg.ShowModal() == wx.ID_OK:
+            filename = dlg.GetPath()
+
+        if not filename:
+            return
+
+        fd = open(filename, "r")
+        try:
+            self.body.SetText(fd.read())
+        finally:
+            fd.close()
+
+        self.filename = filename
+        self.tempfile = False
+
+    def OnOpen(self, event):
+        if self.CanReplaceContent('file'):
+            self.Open()
+
+    def IsEmpty(self):
+        """Check if python script is empty"""
+        return len(self.body.GetText()) == 0
+
+    def SetScriptTemplate(self, event):
+        if self.CanReplaceContent('template'):
+            self.body.SetText(script_template())
+
+    def SetModuleTemplate(self, event):
+        if self.CanReplaceContent('template'):
+            self.body.SetText(module_template())
+
+    def SetScriptExample(self, event):
+        if self.CanReplaceContent('template'):
+            self.body.SetText(script_example())
+
+    def SetModuleExample(self, event):
+        if self.CanReplaceContent('template'):
+            self.body.SetText(module_example())
+
+    def CanReplaceContent(self, by_message):
+        if by_message == 'template':
+            message = _("Replace the content by the template?")
+        elif by_message == 'file':
+            message = _("Replace the current content by the file content?")
+        else:
+            message = by_message
+        if not self.IsEmpty():
+            dlg = wx.MessageDialog(
+                parent=self.guiparent, message=message,
+                caption=_("Replace content"),
+                style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
+            if dlg.ShowModal() == wx.ID_NO:
+                dlg.Destroy()
+                return False
+            dlg.Destroy()
+        return True
+
+    def OnHelp(self, event):
+        import webbrowser
+
+        # inspired by g.manual but simple not using GRASS_HTML_BROWSER
+        # not using g.manual because it does not show
+        entry = 'libpython/script_intro.html'
+        major, minor, patch = gscript.version()['version'].split('.')
+        url = 'http://grass.osgeo.org/grass%s%s/manuals/%s' % (
+            major, minor, entry)
+        webbrowser.open(url)
+
+    def OnPythonHelp(self, event):
+        import webbrowser
+
+        url = 'https://docs.python.org/%s/tutorial/' % sys.version_info[0]
+        webbrowser.open(url)
+
+    def OnModulesHelp(self, event):
+        self.giface.Help('full_index')
+
+    def OnAddonsHelp(self, event):
+        import webbrowser
+
+        url = 'https://grass.osgeo.org/development/code-submission/'
+        webbrowser.open(url)
+
+    def OnSupport(self, event):
+        import webbrowser
+
+        url = 'https://grass.osgeo.org/support/'
+        webbrowser.open(url)
+
+
+class PyEditToolbar(BaseToolbar):
+    # GUI class
+    # pylint: disable=too-many-ancestors
+    # pylint: disable=too-many-public-methods
+    """PyEdit toolbar"""
+    def __init__(self, parent):
+        BaseToolbar.__init__(self, parent)
+
+        # workaround for http://trac.wxwidgets.org/ticket/13888
+        if sys.platform == 'darwin':
+            parent.SetToolBar(self)
+
+        self.InitToolbar(self._toolbarData())
+
+        # realize the toolbar
+        self.Realize()
+
+    def _toolbarData(self):
+        """Toolbar data"""
+        icons = {
+            'open': MetaIcon(img='open',
+                             label=_('Open (Ctrl+O)')),
+            'save': MetaIcon(img='save',
+                             label=_('Save (Ctrl+S)')),
+            'run': MetaIcon(img='execute',
+                            label=_('Run (Ctrl+R')),
+        }
+
+        return self._getToolbarData((('open', icons['open'],
+                                      self.parent.OnOpen),
+                                     ('save', icons['save'],
+                                      self.parent.OnSave),
+                                     (None, ),
+                                     ('run', icons['run'],
+                                      self.parent.OnRun),
+                                     (None, ),
+                                     ("help", BaseIcons['help'],
+                                      self.parent.OnHelp),
+                                    ))
+
+
+class PyEditFrame(wx.Frame):
+    # GUI class and a lot of trampoline methods
+    # pylint: disable=missing-docstring
+    # pylint: disable=too-many-public-methods
+    # pylint: disable=invalid-name
+    def __init__(self, parent, giface, id=wx.ID_ANY,
+                 title=_("GRASS GIS Simple Python Editor"),
+                 **kwargs):
+        wx.Frame.__init__(self, parent=parent, id=id, title=title, **kwargs)
+        self.parent = parent
+
+        filename = os.path.join(
+            globalvar.WXGUIDIR, 'xml', 'menudata_pyedit.xml')
+        self.menubar = Menu(
+            parent=self,
+            model=MenuTreeModelBuilder(filename).GetModel(separators=True))
+        self.SetMenuBar(self.menubar)
+
+        self.toolbar = PyEditToolbar(parent=self)
+        # workaround for http://trac.wxwidgets.org/ticket/13888
+        # TODO: toolbar is set in toolbar and here
+        if sys.platform != 'darwin':
+            self.SetToolBar(self.toolbar)
+
+        self.panel = PyStc(parent=self)
+        self.controller = PyEditController(
+            panel=self.panel, guiparent=self, giface=giface)
+
+        # don't start with an empty page
+        self.panel.SetText(script_template())
+
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(item=self.panel, proportion=1,
+                  flag=wx.EXPAND)
+        sizer.Fit(self)
+        sizer.SetSizeHints(self)
+        self.SetSizer(sizer)
+        self.Fit()
+        self.SetAutoLayout(True)
+        self.Layout()
+
+    # TODO: it would be nice if we can pass the controller to the menu
+    # might not be possible on the side of menu
+    # here we use self and self.controller which might make it harder
+    def OnOpen(self, *args, **kwargs):
+        self.controller.OnOpen(*args, **kwargs)
+
+    def OnSave(self, *args, **kwargs):
+        self.controller.OnSave(*args, **kwargs)
+
+    def OnClose(self, *args, **kwargs):
+        # saves without asking if we have an open file
+        self.controller.OnSave(*args, **kwargs)
+        self.Destroy()
+
+    def OnRun(self, *args, **kwargs):
+        # save without asking
+        self.controller.OnRun(*args, **kwargs)
+
+    def OnHelp(self, *args, **kwargs):
+        # save without asking
+        self.controller.OnHelp(*args, **kwargs)
+
+    def OnSimpleScriptTemplate(self, *args, **kwargs):
+        self.controller.SetScriptTemplate(*args, **kwargs)
+
+    def OnGrassModuleTemplate(self, *args, **kwargs):
+        self.controller.SetModuleTemplate(*args, **kwargs)
+
+    def OnSimpleScriptExample(self, *args, **kwargs):
+        self.controller.SetScriptExample(*args, **kwargs)
+
+    def OnGrassModuleExample(self, *args, **kwargs):
+        self.controller.SetModuleExample(*args, **kwargs)
+
+    def OnPythonHelp(self, *args, **kwargs):
+        self.controller.OnPythonHelp(*args, **kwargs)
+
+    def OnModulesHelp(self, *args, **kwargs):
+        self.controller.OnModulesHelp(*args, **kwargs)
+
+    def OnAddonsHelp(self, *args, **kwargs):
+        self.controller.OnAddonsHelp(*args, **kwargs)
+
+    def OnSupport(self, *args, **kwargs):
+        self.controller.OnSupport(*args, **kwargs)
+
+
+def main():
+    """Test application (potentially useful as g.gui.pyedit)"""
+    app = wx.App()
+    giface = StandaloneGrassInterface()
+    simple_editor = PyEditFrame(parent=None, giface=giface)
+    simple_editor.SetSize((600, 800))
+    simple_editor.Show()
+    app.MainLoop()
+
+
+if __name__ == '__main__':
+    main()

Added: grass/trunk/gui/wxpython/xml/menudata_pyedit.xml
===================================================================
--- grass/trunk/gui/wxpython/xml/menudata_pyedit.xml	                        (rev 0)
+++ grass/trunk/gui/wxpython/xml/menudata_pyedit.xml	2016-02-29 04:25:16 UTC (rev 67972)
@@ -0,0 +1,98 @@
+<menudata>
+  <menubar>
+    <menu>
+      <label>&File</label>
+      <items>
+        <menuitem>
+          <label>Open</label>
+          <help>Load model from file</help>
+          <handler>OnOpen</handler>
+          <shortcut>Ctrl+O</shortcut>
+        </menuitem>
+        <menuitem>
+          <label>Save</label>
+          <help>Save model</help>
+          <handler>OnSave</handler>
+          <shortcut>Ctrl+S</shortcut>
+        </menuitem>
+        <menuitem>
+          <label>Quit editor</label>
+          <help>Close editor window</help>
+          <handler>OnClose</handler>
+          <shortcut>Ctrl+W</shortcut>
+        </menuitem>
+      </items>
+    </menu>
+    <menu>
+      <label>&Run</label>
+      <items>
+        <menuitem>
+          <label>Run</label>
+          <help>Run Python script</help>
+          <handler>OnRun</handler>
+          <shortcut>Ctrl+R</shortcut>
+        </menuitem>
+      </items>
+    </menu>
+    <menu>
+      <label>&Templates</label>
+      <items>
+        <menuitem>
+          <label>Load script &template</label>
+          <help>Load simple script template</help>
+          <handler>OnSimpleScriptTemplate</handler>
+        </menuitem>
+        <menuitem>
+          <label>Load GRASS &module template</label>
+          <help>Load full GRASS module template</help>
+          <handler>OnGrassModuleTemplate</handler>
+        </menuitem>
+      </items>
+    </menu>
+    <menu>
+      <label>&Examples</label>
+      <items>
+        <menuitem>
+          <label>Load script &example</label>
+          <help>Load simple script example</help>
+          <handler>OnSimpleScriptExample</handler>
+        </menuitem>
+        <menuitem>
+          <label>Load GRASS &module example</label>
+          <help>Load full GRASS module example</help>
+          <handler>OnGrassModuleExample</handler>
+        </menuitem>
+      </items>
+    </menu>
+    <menu>
+      <label>&Help</label>
+      <items>
+        <menuitem>
+          <label>GRASS Python Help</label>
+          <help></help>
+          <handler>OnHelp</handler>
+        </menuitem>
+        <menuitem>
+          <label>Python help</label>
+          <help>Display the HTML man pages of Graphical modeler</help>
+          <handler>OnPythonHelp</handler>
+        </menuitem>
+        <menuitem>
+          <label>GRASS Modules help</label>
+          <help>Display the HTML man pages of Graphical modeler</help>
+          <handler>OnModulesHelp</handler>
+        </menuitem>
+        <menuitem>
+          <label>Getting your code to GRASS Addons</label>
+          <help>Display the HTML man pages of Graphical modeler</help>
+          <handler>OnAddonsHelp</handler>
+        </menuitem>
+        <menuitem>
+          <label>Get support</label>
+          <help>Display the HTML man pages of Graphical modeler</help>
+          <handler>OnSupport</handler>
+        </menuitem>
+      </items>
+    </menu>
+  </menubar>
+</menudata>



More information about the grass-commit mailing list