summaryrefslogtreecommitdiffstats
path: root/build_tools/larch8/liblarch
diff options
context:
space:
mode:
Diffstat (limited to 'build_tools/larch8/liblarch')
-rw-r--r--build_tools/larch8/liblarch/__init__.py0
-rw-r--r--build_tools/larch8/liblarch/docs/README1
-rw-r--r--build_tools/larch8/liblarch/i18n/de/LC_MESSAGES/de.po61
-rw-r--r--build_tools/larch8/liblarch/i18n/de/LC_MESSAGES/liblarch.mobin0 -> 1241 bytes
-rwxr-xr-xbuild_tools/larch8/liblarch/i18n/i18n.py61
-rwxr-xr-xbuild_tools/larch8/liblarch/i18n/i18n2.py31
-rw-r--r--build_tools/larch8/liblarch/i18n/liblarch.pot61
-rw-r--r--build_tools/larch8/liblarch/larcon_base.py74
-rw-r--r--build_tools/larch8/liblarch/larcon_ui.py100
-rw-r--r--build_tools/larch8/liblarch/liblarch_conf.py32
-rw-r--r--build_tools/larch8/liblarch/rootrun.py246
-rw-r--r--build_tools/larch8/liblarch/suim.py1365
-rw-r--r--build_tools/larch8/liblarch/translation.py65
-rw-r--r--build_tools/larch8/liblarch/uim/larcon.uim77
14 files changed, 2174 insertions, 0 deletions
diff --git a/build_tools/larch8/liblarch/__init__.py b/build_tools/larch8/liblarch/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/build_tools/larch8/liblarch/__init__.py
diff --git a/build_tools/larch8/liblarch/docs/README b/build_tools/larch8/liblarch/docs/README
new file mode 100644
index 0000000..1e90f74
--- /dev/null
+++ b/build_tools/larch8/liblarch/docs/README
@@ -0,0 +1 @@
+TODO!
diff --git a/build_tools/larch8/liblarch/i18n/de/LC_MESSAGES/de.po b/build_tools/larch8/liblarch/i18n/de/LC_MESSAGES/de.po
new file mode 100644
index 0000000..eccec24
--- /dev/null
+++ b/build_tools/larch8/liblarch/i18n/de/LC_MESSAGES/de.po
@@ -0,0 +1,61 @@
+# German translations for liblarch version 1 package.
+# Copyright (C) 2010 Michael Towers
+# Automatically generated, 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: liblarch-1\n"
+"POT-Creation-Date: 2010-08-15 21:52+CEST\n"
+"PO-Revision-Date: 2010-08-01 09:59+CEST\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: rootrun.py:121
+msgid "Please enter (sudo) password:"
+msgstr "Bitte (sudo) Passwort eingeben:"
+
+#: rootrun.py:128
+msgid "Please enter root password:"
+msgstr "Bitte root-Passwort eingeben:"
+
+#: rootrun.py:148
+msgid "Operation cancelled"
+msgstr "Operation abgebrochen"
+
+#: rootrun.py:161
+msgid "Incorrect password, try again:"
+msgstr "Falsches Passwort, noch einmal versuchen"
+
+#: translation.py:60
+msgid "Document '%s' not found"
+msgstr "DOkument '%s' nicht gefunden"
+
+#: uim/larcon.uim:46
+msgid "Quit"
+msgstr "Schließen"
+
+#: uim/larcon.uim:47
+msgid "Exit the application, make no (further) changes"
+msgstr "Schließe die Anwendung, keine (weiteren) Änderungen durchführen"
+
+#: uim/larcon.uim:70
+msgid "Help"
+msgstr "Hilfe"
+
+#: uim/larcon.uim:71
+msgid "Show the help page"
+msgstr "Zeige die Hilfsseite an"
+
+#: uim/larcon.uim:72
+msgid "Hide help"
+msgstr "Hilfsseite schließen"
+
+#: uim/larcon.uim:73
+msgid "Hide the help page, return to main view"
+msgstr "Verstecke die Hilfsseite, kehre zur Hauptansicht zurück"
diff --git a/build_tools/larch8/liblarch/i18n/de/LC_MESSAGES/liblarch.mo b/build_tools/larch8/liblarch/i18n/de/LC_MESSAGES/liblarch.mo
new file mode 100644
index 0000000..a6bf49b
--- /dev/null
+++ b/build_tools/larch8/liblarch/i18n/de/LC_MESSAGES/liblarch.mo
Binary files differ
diff --git a/build_tools/larch8/liblarch/i18n/i18n.py b/build_tools/larch8/liblarch/i18n/i18n.py
new file mode 100755
index 0000000..28b60ef
--- /dev/null
+++ b/build_tools/larch8/liblarch/i18n/i18n.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python2
+# -*- coding: UTF-8 -*-
+
+#2010.08.15
+# Copyright 2010 Michael Towers
+
+"""
+1) Generally something like: pygettext.py -p i18n -o liblarch.pot *.py
+
+I think poedit can do most of the processing, but the steps are:
+
+2) cd i18n ; msginit -i liblarch.pot -l de
+
+OR:
+2a) to update a po file:
+
+cd i18n ; msgmerge -U liblarch.po liblarch.pot
+
+3) edit po file
+
+4) generate binary file:
+cd i18n ; msgfmt -c -v -o liblarch.mo liblarch.po
+
+5) move the .mo file to i18n/de/LC_MESSAGES
+"""
+
+import sys, os, shutil
+from subprocess import call
+
+thisdir = os.path.dirname(os.path.realpath(__file__))
+basedir = os.path.dirname(thisdir)
+os.chdir(basedir)
+
+if (len(sys.argv) < 2):
+ lang = "de"
+else:
+ lang = sys.argv[1]
+print "Generating internationalization for language '%s'\n" % lang
+print " If you wanted a different language run 'i18n.py <language>'"
+print " For example 'i18n.py fr'\n"
+
+dirs = [""]
+allpy = [os.path.join(d, "*.py") for d in dirs]
+alluim = [os.path.join('uim', "*.uim")]
+call(["pygettext.py", "-p", thisdir, "-o", "liblarch.pot"] + allpy + alluim)
+
+os.chdir(thisdir)
+langfile = lang + ".po"
+pofile = os.path.join(lang, "LC_MESSAGES", langfile)
+if os.path.isfile(pofile):
+ shutil.copy(pofile, ".")
+ call(["msgmerge", "-U", langfile, "liblarch.pot"])
+else:
+ call(["sed", "-i", "s|CHARSET|utf-8|", "liblarch.pot"])
+ call(["msginit", "--no-translator", "-i", "liblarch.pot", "-l", lang])
+
+lf = open("lang", "w")
+lf.write(lang)
+lf.close()
+
+print "Now edit '%s' and then run 'i18n2.py'" % langfile
diff --git a/build_tools/larch8/liblarch/i18n/i18n2.py b/build_tools/larch8/liblarch/i18n/i18n2.py
new file mode 100755
index 0000000..486e0d4
--- /dev/null
+++ b/build_tools/larch8/liblarch/i18n/i18n2.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python2
+# -*- coding: UTF-8 -*-
+
+#2010.08.01
+# Copyright 2010 Michael Towers
+
+"""This is part 2 of the internationalization helper.
+After editing liblarch.po, run this to compile it and copy it to the
+correct location.
+"""
+
+import os
+from subprocess import call
+
+thisdir = os.path.dirname(os.path.realpath(__file__))
+os.chdir(thisdir)
+lf = open("lang", "r")
+lang = lf.read()
+lf.close()
+langfile = lang + ".po"
+
+print "Compiling internationalization for language '%s'\n" % lang
+call(["msgfmt", "-c", "-v", "-o", "liblarch.mo", langfile])
+
+podir = os.path.join(lang, "LC_MESSAGES")
+if not os.path.isdir(podir):
+ os.makedirs(podir)
+os.rename(langfile, os.path.join(podir, langfile))
+os.rename("liblarch.mo", os.path.join(podir, "liblarch.mo"))
+
+print "DONE!"
diff --git a/build_tools/larch8/liblarch/i18n/liblarch.pot b/build_tools/larch8/liblarch/i18n/liblarch.pot
new file mode 100644
index 0000000..ef84c64
--- /dev/null
+++ b/build_tools/larch8/liblarch/i18n/liblarch.pot
@@ -0,0 +1,61 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2010-08-15 21:52+CEST\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: ENCODING\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: rootrun.py:121
+msgid "Please enter (sudo) password:"
+msgstr ""
+
+#: rootrun.py:128
+msgid "Please enter root password:"
+msgstr ""
+
+#: rootrun.py:148
+msgid "Operation cancelled"
+msgstr ""
+
+#: rootrun.py:161
+msgid "Incorrect password, try again:"
+msgstr ""
+
+#: translation.py:60
+msgid "Document '%s' not found"
+msgstr ""
+
+#: uim/larcon.uim:46
+msgid "Quit"
+msgstr ""
+
+#: uim/larcon.uim:47
+msgid "Exit the application, make no (further) changes"
+msgstr ""
+
+#: uim/larcon.uim:70
+msgid "Help"
+msgstr ""
+
+#: uim/larcon.uim:71
+msgid "Show the help page"
+msgstr ""
+
+#: uim/larcon.uim:72
+msgid "Hide help"
+msgstr ""
+
+#: uim/larcon.uim:73
+msgid "Hide the help page, return to main view"
+msgstr ""
+
diff --git a/build_tools/larch8/liblarch/larcon_base.py b/build_tools/larch8/liblarch/larcon_base.py
new file mode 100644
index 0000000..9d343f5
--- /dev/null
+++ b/build_tools/larch8/liblarch/larcon_base.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python2
+#
+# larcon_base.py -- Basic backend framework for larcon applications
+#
+# (c) Copyright 2010 Michael Towers (larch42 at googlemail dot com)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#-------------------------------------------------------------------
+# 2010.08.14
+
+import os, threading
+from liblarch_conf import liblarchdir
+from rootrun import init_rootrun
+from translation import i18n_liblarch, _
+
+class Backend:
+ def __init__(self, appname, env, basedir=None):
+ """The env(ironment), i.e. the global dictionary, is needed for
+ fetching uim files.
+ If the basedir is not provided it can be derived from the appname.
+ """
+ if basedir:
+ self.basedir = basedir
+ else:
+ self.basedir = os.path.dirname(liblarchdir) + '/' + appname
+ self.appenv = env
+
+ def fss_fetch_layout(self, uim):
+ fp = os.path.join(self.basedir, 'uim', uim)
+ fh = open(fp)
+ r = fh.read()
+ fh.close()
+ return eval(r, self.appenv)
+
+ def fss_uim_fetch(self, uim):
+ fp = os.path.join(liblarchdir, 'uim', uim)
+ fh = open(fp)
+ r = fh.read()
+ fh.close()
+ return eval(r)
+
+ def setuisig(self, fn):
+ self.ui_signal = fn
+
+ def rootcall(self, cmd, fn_done):
+ self.rcall = init_rootrun(self._pwget)
+ self.rcall.run(cmd, fn_done)
+
+ def _pwget(self, cb, prompt):
+ self.pw_event = threading.Event()
+ self.ui_signal('get_password', prompt)
+ self.pw_event.wait()
+ self.rcall.cb_done(cb, *self.pw_returned)
+
+ def fss_sendpassword(self, ok, pw):
+ # The result needs to be passed to the waiting _pwget method
+ # (which is running in a different thread).
+ self.pw_returned = (ok, pw)
+ self.pw_event.set()
+ return None
+
diff --git a/build_tools/larch8/liblarch/larcon_ui.py b/build_tools/larch8/liblarch/larcon_ui.py
new file mode 100644
index 0000000..faddab7
--- /dev/null
+++ b/build_tools/larch8/liblarch/larcon_ui.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python2
+#
+# larcon_self.py -- Frame for a single larcon tool
+
+# (c) Copyright 2009-2010 Michael Towers (larch42 at googlemail dot com)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#-------------------------------------------------------------------
+# 2010.08.14
+
+from suim import Suim
+
+class LarconGui(Suim):
+ def __init__(self, appname, backend):
+ self.appname = appname
+ self.backend = backend
+ self._running = False
+ Suim.__init__(self, appname, [appname])
+ self.widgetlist(self.fss('uim_fetch', 'larcon.uim'))
+ self.connect('$$$uiquit$$$', self.quit)
+ self.command('larcon.title', appname)
+ self.command('larcon.icon', appname + '.png')
+ self.command('larcon:main.layout', ['VBOX', appname])
+ self.connect('larcon:docs*clicked', self._showdocs)
+ self._showdocs(init=True)
+
+
+ def fss(self, func, *args):
+ """Supply backend (file-system) services to the gui
+ """
+ if func:
+ if self._running and (func[0] != '_'):
+ self.busy(True)
+ # (Repeated setting or unsetting of the busy state is just ignored)
+ result = self.backend(func, *args)
+ # When the function is not finished, it returns None, otherwise (ok, val)
+ else:
+ result = True
+ if self._running and result != None:
+ self.busy(False)
+ return result
+
+
+ def data(self, key):
+ return self.command('larcon_data.get', key)
+
+
+ def _showdocs(self, init=False):
+ if init:
+ self.command('larcon:docview.html', self.fss('about'))
+ self.helpstate = False
+ else:
+ self.helpstate = not self.helpstate
+ self.command('larcon:stack.set', 1 if self.helpstate else 0)
+ self.command('larcon:docs.text', self.data('hidetext')
+ if self.helpstate else self.data('showtext'))
+ self.command('larcon:docs.tt', self.data('hidett')
+ if self.helpstate else self.data('showtt'))
+
+
+ def go(self):
+ self.command('larcon.pack')
+ self.command('larcon.show')
+ self.run()
+
+
+ def sigin(self, signal, *args):
+ self.idle_add(getattr(self, 'sig_' + signal), *args)
+
+
+ def sig_get_password(self, message):
+ """This is a callback, triggered by signal 'get_password'
+ to ask the user to input the password.
+ """
+ self.fss('sendpassword', *self.command('textLineDialog', message,
+ "%s: pw" % self.appname, "", True))
+
+
+ def sig_showcompleted(self, ok, message):
+ """This is a callback, triggered by signal 'showcompleted'
+ to display an info dialog.
+ """
+ self.command('infoDialog' if ok else 'warningDialog', message)
+ self.fss(None) # Tell 'fss' that the command has terminated
+
+
+
diff --git a/build_tools/larch8/liblarch/liblarch_conf.py b/build_tools/larch8/liblarch/liblarch_conf.py
new file mode 100644
index 0000000..68b64ef
--- /dev/null
+++ b/build_tools/larch8/liblarch/liblarch_conf.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python2
+#
+# liblarch_conf.py -- Configuration for liblarch modules
+#
+# (c) Copyright 2010 Michael Towers (larch42 at googlemail dot com)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#-------------------------------------------------------------------
+# 2010.08.01
+
+import os
+
+liblarchdir = os.path.dirname(os.path.realpath(__file__))
+
+
+###### su / sudo for running commands as root
+sudo = False
+sudoprompt = '_PW_'
+
diff --git a/build_tools/larch8/liblarch/rootrun.py b/build_tools/larch8/liblarch/rootrun.py
new file mode 100644
index 0000000..240f2cd
--- /dev/null
+++ b/build_tools/larch8/liblarch/rootrun.py
@@ -0,0 +1,246 @@
+#!/usr/bin/env python2
+#
+# rootrun.py -- Running commands as root, using pexpect with su or sudo
+#
+# (c) Copyright 2010 Michael Towers (larch42 at googlemail dot com)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#-------------------------------------------------------------------
+# 2010.08.08
+
+
+import os, threading, pexpect
+from subprocess import Popen, PIPE, STDOUT
+from collections import deque
+
+from liblarch_conf import sudo, sudoprompt
+
+from translation import i18n_liblarch
+_ = i18n_liblarch()
+
+
+class FnChain:
+ """Implements a simple dynamic function chain.
+ The supplied function call is executed in a new thread. Further
+ (pending) function calls can be added to the fifo list, self.fnlist,
+ which is a queue for execution when the current function returns.
+ When the list is empty, the chain is complete.
+ """
+ def __init__(self, fn, *args):
+ self.fnlist = deque()
+ self.step(fn, *args)
+ self.pending = {}
+ self.flagcount = 0
+ threading.Thread(target=self._fnloop, args=()).start()
+
+ def _fnloop(self):
+ while True:
+ try:
+ fn, args = self.fnlist.popleft()
+ except IndexError:
+ return
+ fn(*args)
+
+ def step(self, fn, *args):
+ self.fnlist.append((fn, args))
+
+ def callback(self, cb, continuation, *args):
+ if continuation:
+ self.flagcount += 1
+ myflag = self.flagcount
+ if cb:
+ self.pending[myflag] = continuation
+ else:
+ myflag = None
+ if cb:
+ cb(myflag, *args)
+ # When the callback completes it needs to call the
+ # callback_done method, passing the flag and the results,
+ # unless the flag is None.
+ elif continuation:
+ self.step(continuation, *args)
+
+ def callback_done(self, flag, *args):
+ self.step(self.pending[flag], *args)
+ del(self.pending[flag])
+
+
+
+class _RootCommand:
+ """This class allows shell commands to be run as root by non-root users.
+ This is achieved using pexpect to negotiate with either su or sudo
+ (selectable in the configuration file).
+ The run method allows interaction with the running process on a
+ line-by-line basis, if desired, using callbacks.
+ The call method is a non-interactive wrapper to allow simple running
+ of shell scripts as root, returning completion code and output text.
+ """
+ def __init__(self, callback_pw):
+ """Initialize the instance
+ """
+ self.password = None
+ self.callback_pw = callback_pw
+ self.fnchain = None
+
+
+ def cb_done(self, flag, *args):
+ """Called by a callback to pass its result back.
+ """
+ if flag:
+ self.fnchain.callback_done(flag, *args)
+
+
+ def run(self, cmd, end_callback, line_callback=None, cwd=None):
+ self.cmd = cmd
+ self.line_callback = line_callback
+ self.end_callback = end_callback
+ self.cwd = cwd
+ if self.fnchain:
+ debug("Previous FnChain instance not terminated")
+ self.fnchain = FnChain(self._run_start)
+
+ def _run_start(self, message=None):
+ if sudo:
+ self.process = pexpect.spawn('sudo -p "%s" bash -c "echo ___ && %s"'
+ % (sudoprompt, self.cmd), cwd=self.cwd, timeout=None)
+ self.password_prompt = sudoprompt
+ if not message:
+ message = _("Please enter (sudo) password:")
+
+ else: # use su
+ self.process = pexpect.spawn('su -c "echo ___ && %s"' % self.cmd,
+ cwd=self.cwd, timeout=None)
+ self.password_prompt = ':'
+ if not message:
+ message = _("Please enter root password:")
+
+ if self.process.expect([self.password_prompt, '\r\n']) == 0:
+ if self.password:
+ self.fnchain.step(self._run_sendpw)
+ else:
+ self.fnchain.callback(self.callback_pw, self._run_pw, message)
+
+ else:
+ self.fnchain.step(self._run_readloop)
+
+ def _run_pw(self, ok, pw):
+ if ok:
+ self.password = pw
+ self.fnchain.step(self._run_sendpw)
+ else:
+ self.process.close(force=True)
+ # Avoid calling the end callback before self.fnchain is reset (threads!)
+ _fnchain = self.fnchain
+ self.fnchain = None
+ _fnchain.callback(self.end_callback, None, False, _("Operation cancelled"))
+
+ def _run_sendpw(self):
+ self.process.sendline(self.password)
+ while True:
+ line1 = self.process.readline().strip()
+ if line1:
+ break
+ if line1 == '___':
+ self.fnchain.step(self._run_readloop)
+ else:
+ self.process.close(force=True)
+ self.password = None
+ self.fnchain.step(self._run_start, _("Incorrect password, try again:"))
+
+ def _run_readloop(self):
+ """The read loop collects output from the subprocess line by line
+ and passes it on via the line callback. When the process has finished
+ the end callback is passed the complete output text.
+ """
+ lines = ""
+ while True:
+ line = self.process.readline()
+ if not line: # Process finished
+ break
+ line = line.rstrip()
+ lines += line + '\n'
+ self.fnchain.callback(self.line_callback, None, line)
+
+ # From the pexpect docs:
+ # If you wish to get the exit status of the child you must call the
+ # close() method. The exit or signal status of the child will be stored
+ # in self.exitstatus or self.signalstatus. If the child exited normally
+ # then exitstatus will store the exit return code and signalstatus will
+ # be None. If the child was terminated abnormally with a signal then
+ # signalstatus will store the signal value and exitstatus will be None.
+ self.process.close()
+ # Avoid calling the end callback before self.fnchain is reset (threads!)
+ _fnchain = self.fnchain
+ self.fnchain = None
+ _fnchain.callback(self.end_callback, None, self.process.exitstatus == 0, lines)
+
+
+ def interrupt(self):
+ # Need to start a second pexpect to interrupt as root
+ if sudo:
+ pexpect.run('sudo -p "%s" kill -2 %d"' % (self.password_prompt, self.pid),
+ events={self.password_prompt: self.password+'\n'})
+ else:
+ pexpect.run('su -c "kill -2 %d"' % self.pid,
+ events={':': self.password+'\n'})
+
+# def kill(self):
+# self.process.close(force=True)
+
+ def setpid(self, pid):
+ """For kill to work as desired the pid to which the signal should
+ be sent must first be set.
+ """
+ self.pid = pid
+
+
+ def send(self, cmd):
+ self.process.sendline(cmd)
+
+
+ def call(self, cmd, cwd=None):
+ self.event = threading.Event()
+ self.run(cmd, self._end_callback0)
+ self.event.wait()
+ return self.retv
+
+
+ def _end_callback0(self, ok, text):
+ self.retv = (ok, text)
+ self.event.set()
+
+
+
+_rc = None
+def init_rootrun(callback_pw):
+ global _rc
+ if not _rc:
+ _rc = _RootCommand(callback_pw)
+ return _rc
+
+
+def runasroot(cmd, cwd=None, pwget=None):
+ """Wrap RootCommand within a function, which waits for completion and
+ returns the result as (ok (bool), output-text).
+ In addition to the command to be executed it is also necessary to pass
+ the callback for fetching the password, unless this has been supplied
+ to a previous call, or is known to be unnecessary (unlikely, but possible).
+ The cwd parameter allows the current working directory to be changed
+ for the call.
+ """
+ if not _rc:
+ init_rootrun(pwget)
+ return _rc.call(cmd, cwd)
diff --git a/build_tools/larch8/liblarch/suim.py b/build_tools/larch8/liblarch/suim.py
new file mode 100644
index 0000000..78e4354
--- /dev/null
+++ b/build_tools/larch8/liblarch/suim.py
@@ -0,0 +1,1365 @@
+#!/usr/bin/env python2
+# -*- coding: UTF-8 -*-
+#
+# suim.py
+#
+# (c) Copyright 2010 Michael Towers (larch42 at googlemail dot com)
+#
+# This file is part of the larch project.
+#
+# larch is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# larch is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with larch; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#----------------------------------------------------------------------------
+# 2010.10.24
+
+#++++++++++++++++++++++++++++++++++++++++++++++++++++
+#TODO
+# Add more widgets
+# Add more attribute handling
+# Add more signal handling
+
+# Fetching of image and icon files via a sort of url-like mechanism, I
+# suppose initially using the 'base:' prefix might be ok.
+# Then the cwd of the gui script would be irrelevant.
+
+#New file dialog for accessing the 'server' end.
+
+# I suspect the DialogButtonBox stuff needs changing to cope with
+# translations.
+
+#----------------------------------------------------
+
+
+"""SUIM - Simple User Interface Module
+
+The aim is to provide a means of creating graphical user interfaces of
+moderate complexity while abstracting the interface to the actual underlying
+toolkit in such a way that (at least potentially) an alternative toolkit
+could be used.
+[At present this aspect is rather theoretical since only a pyqt based
+version has been written.]
+
+The gui layout is specified as a python data structure, using widget types,
+parameter and signal names independent of the underlying toolkit. All
+widgets are accessible by their tag, which must be specified.
+
+A widget is defined by a call to the 'widget' method of the GuiApp instance.
+The first argument is the widget type, the second is the widget tag, the
+remaining ones must be named, they form the parameters to the constructor.
+If the widget is a 'container' (i.e. if it contains other widgets), it will
+need a 'layout' parameter defining the layout of its contents.
+
+There is also a 'widgetlist' method which accepts a list of widget
+definitions, each definition being itself a list. The first entry in a
+definition is the widget type, the second is the widget tag, the
+third is a dictionary containing all the parameters. For convenience (I'm not
+sure if I will keep this, though) any entries after the dictionary will be
+treated as signal names. These are just added to the parameter dictionary
+with value '' (enabling the signal with its default tag).
+
+Signals have signatures/keys comprising the tag of the emitting widget and
+the signal name (separated by '*'), and this will by default also be the tag
+by which the signal is known for connection purposes. But this can be
+overridden, for example to allow several widgets to emit the same signal.
+In the latter case the widget tag can (optionally) be passed as the first
+argument to the signal handler.
+
+Passing signal names as parameters to a widget constructor enables these
+signals. They can later be disabled, if desired.
+
+Connect and disconnect methods are available, to associate (or dissociate)
+handler functions with (/from) signals.
+"""
+
+import os, sys, traceback, threading
+from PyQt4 import QtGui, QtCore, QtWebKit
+from collections import deque
+
+
+def debug(text):
+ sys.stderr.write("GUI: %s\n" % text)
+ sys.stderr.flush()
+
+# Either I need to wrap all text input with this or I need to ensure that
+# I get unicode from outside ...
+import locale
+encoding = locale.getdefaultlocale()[1]
+def convert(text):
+ """Try to handle encoding.
+ """
+ if isinstance(text, str):
+ return text.decode(encoding) if encoding else text.decode()
+ else:
+ return text
+
+
+# Widget Base Classes - essentially used as 'Mixins' >>>>>>>>>>>>>>>>
+class WBase:
+ def x__tt(self, text):
+ """Set tooltip.
+ """
+ self.setToolTip(text) #qt
+
+ def x__text(self, text=""):
+ """Set widget text.
+ """
+ self.setText(convert(text)) #qt
+
+ def x__enable(self, on):
+ """Enable/Disable widget. on should be True to enable the widget
+ (display it in its normal, active state), False to disable it
+ (which will normally be paler and non-interactive).
+ """
+ self.setEnabled(on) #qt
+
+ def x__focus(self):
+ self.setFocus() #qt
+
+ def x__width(self, w):
+ """Set the minimum width for the widget.
+ """
+ self.setMinimumWidth(w) #qt
+
+ def x__typewriter(self, on):
+ """Use a typewriter (fixed spacing) font.
+ """
+ if on:
+ f = QtGui.QFont(self.font()) #qt
+ f.setFamily("Courier") #qt
+ self.setFont(f) #qt
+
+ def x__busycursor(self, on):
+ """Set/clear the busy-cursor for this widget.
+ """
+ if on:
+ self.setCursor(QtCore.Qt.BusyCursor) #qt
+ else:
+ self.unsetCursor() #qt
+
+
+class BBase:
+ """Button mixin.
+ """
+ def x__icon(self, icon):
+ self.setIcon(self.style().standardIcon(icondict[icon])) #qt
+
+#qt
+icondict = { "left" : QtGui.QStyle.SP_ArrowLeft,
+ "right" : QtGui.QStyle.SP_ArrowRight,
+ "down" : QtGui.QStyle.SP_ArrowDown,
+ "up" : QtGui.QStyle.SP_ArrowUp,
+ "reload" : QtGui.QStyle.SP_BrowserReload,
+ }
+
+class Container:
+ """This just adds layout management for widgets which contain
+ other widgets.
+ """
+ def x__layout(self, layout, immediate=False):
+ """A layout specifies and organizes the contents of a widget.
+ Note that the layouting is not immediately performed by default as
+ it is unlikely that all the contained widgets have been defined yet.
+ """
+ self._layout = layout
+ if immediate:
+ self.x__pack()
+
+ def x__pack(self):
+ """A layout call specifies and organizes the contents of a widget.
+ The layout can be a layout manager list, or a single widget name
+ (or an empty string, which will cause a warning to be issued, but
+ may be useful during development).
+
+ There are three sorts of thing which can appear in layout manager
+ lists (apart from the layout type at the head of the list and an
+ optional attribute dict as second item). There can be named
+ widgets, there can be further layout managers (specified as lists,
+ nested as deeply as you like) and there can be layout widgets,
+ like spacers and separators.
+
+ A layout widget can have optional arguments, which are separated
+ by commas, e.g. 'VLINE,3' passes the argument '3' to the VLINE
+ constructor.
+ """
+ # getattr avoids having to have an __init__() for Container.
+ if getattr(self, '_layout', None):
+ if self._layout != '$':
+ self.setLayout(self.getlayout(self._layout))
+ self._layout = '$'
+ else:
+ debug("No layout set on '%s'" % self.w_name)
+
+ def getlayout(self, item):
+ if isinstance(item, list):
+ try:
+ # Create a layout manager instance
+ layoutmanager = layout_table[item[0]]()
+ assert isinstance(layoutmanager, Layout)
+ except:
+ gui_error("Unknown layout type: %s" % item[0])
+ if (len(item) > 1) and isinstance(item[1], dict):
+ dictarg = item[1]
+ ilist = item[2:]
+ else:
+ dictarg = {}
+ ilist = item[1:]
+ # Build up the list of objects to lay out
+ # If the layout manager is a GRID, accept only grid rows ('+')
+ if isinstance(layoutmanager, GRID):
+ args = []
+ rowlen = None
+ for i in ilist:
+ if isinstance(i, list) and (i[0] == '+'):
+ args.append(self.getlayoutlist(i[1:], grid=True))
+ if rowlen == None:
+ rowlen = len(i)
+ elif len(i) != rowlen:
+ gui_error("Grid (%s) row lengths unequal"
+ % self.w_name)
+ else:
+ gui_error("Grid (%s) layouts must consist of grid"
+ " rows ('+')" % self.w_name)
+ else:
+ # Otherwise the elements of the argument list can be:
+ # A sub-layout
+ # A widget
+ # A SPACE
+ args = self.getlayoutlist(ilist)
+ layoutmanager.do_layout(args)
+ # Attributes
+ for key, val in dictarg:
+ handler = "x__" + key
+ if hasattr(layoutmanager, handler):
+ getattr(layoutmanager, handler)(val)
+ return layoutmanager
+
+ else:
+ # It must be a widget, which will need to be put in a box (qt)
+ return self.getlayout(['VBOX', item])
+
+ def getlayoutlist(self, items, grid=False):
+ objects = []
+ for i in items:
+ if isinstance(i, list):
+ obj = self.getlayout(i)
+ else:
+ parts = i.split(',')
+ i = parts[0]
+ args = parts[1:]
+ try:
+ obj = layout_table[i](*args)
+ if not (isinstance(obj, SPACE) # or a separator line
+ or isinstance(obj, QtGui.QWidget)): #qt
+ assert (grid and isinstance(obj, Span))
+ except:
+ obj = guiapp.getwidget(i)
+ if obj != None:
+ if isinstance(obj, Container):
+ obj.x__pack()
+ else:
+ gui_error("Bad item in layout of '%s': '%s'"
+ % (self.w_name, i))
+ objects.append(obj)
+ return objects
+
+
+class XContainer(Container):
+ """This is a mixin class for containers which can contain more than
+ one layout.
+ """
+ def x__layout(self, layout):
+ gui_error("An extended container (%s) has no 'layout' method"
+ % self.w_name)
+
+
+class TopLevel(Container):
+ def x__show(self):
+ self.set_visible()
+
+ def set_visible(self, on=True):
+ self.setVisible(on) #qt
+
+ def x__size(self, w_h):
+ w, h = [int(i) for i in w_h.split("_")]
+ self.resize(w, h) #qt
+
+ def x__icon(self, iconpath):
+ guiapp.setWindowIcon(QtGui.QIcon(iconpath)) #qt
+
+ def x__title(self, text):
+ if text == None:
+ text = guiapp.appname
+ self.setWindowTitle(text) #qt
+
+ def x__getSize(self):
+ s = self.size() #qt
+ return "%d_%d" % (s.width(), s.height()) #qt
+
+ def x__getScreenSize(self):
+ dw = guiapp.desktop() #qt
+ geom = dw.screenGeometry(self) #qt
+ return "%d_%d" % (geom.width(), geom.height()) #qt
+
+#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+
+class Window(QtGui.QWidget, TopLevel): #qt
+ """This is needed to trap window closing events.
+ """
+ def __init__(self):
+ QtGui.QWidget.__init__(self) #qt
+ self.closesignal = ""
+
+ def closeEvent(self, event): #qt
+ if self.closesignal:
+ guiapp.sendsignal(self.closesignal)
+ event.ignore() #qt
+ return
+ QtGui.QWidget.closeEvent(self, event) #qt
+
+ def x__closesignal(self, text):
+ self.closesignal = text
+
+
+class Dialog(QtGui.QDialog, TopLevel):
+ def __init__(self):
+ QtGui.QDialog.__init__(self) #qt
+
+ def x__showmodal(self):
+ return self.exec_() == QtGui.QDialog.Accepted #qt
+
+
+class DialogButtons(QtGui.QDialogButtonBox): #qt
+ def __init__(self):
+ return
+
+ def x__buttons(self, args):
+ """This keyword argument MUST be present.
+ """
+ buttons = 0
+ for a in args:
+ try:
+ b = getattr(QtGui.QDialogButtonBox, a) #qt
+ assert isinstance(b, int) #qt
+ buttons |= b #qt
+ except:
+ gui_warning("Unknown Dialog button: %s" % a)
+ QtGui.QDialogButtonBox.__init__(self, buttons) #qt
+
+ def x__dialog(self, dname):
+ """This must be set or else the dialog buttons won't do anything.
+ """
+ self._dialog = guiapp.getwidget(dname)
+ self.connect(self, QtCore.SIGNAL("clicked(QAbstractButton *)"), #qt
+ self._clicked) #qt
+
+ def _clicked(self, button): #qt
+ if self.buttonRole(button) == self.AcceptRole: #qt
+ self._dialog.accept() #qt
+ else:
+ self._dialog.reject() #qt
+
+
+def textLineDialog(label="???:", title=None, text="", pw=False):
+ if title == None:
+ title = guiapp.appname
+ echo = QtGui.QLineEdit.Password if pw else QtGui.QLineEdit.Normal #qt
+ res, ok = QtGui.QInputDialog.getText(None, title, label, echo, text) #qt
+ return (ok, unicode(res))
+
+
+def listDialog(label="???", title=None, items=[], current=0):
+ if title == None:
+ title = guiapp.appname
+ res, ok = QtGui.QInputDialog.getItem(None, title, label, items, current)
+ return (ok, unicode(res))
+
+
+def confirmDialog(message, title=None):
+ if title == None:
+ title = guiapp.appname
+ return (QtGui.QMessageBox.question(None, title, convert(message),
+ QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel)
+ == QtGui.QMessageBox.Ok)
+
+
+def infoDialog(message, title=None):
+ if title == None:
+ title = guiapp.appname
+ QtGui.QMessageBox.information(None, title, message)
+
+
+#+++++++++++++++++++++++++++
+# Error handling
+def gui_error(message, title=None):
+ if title == None:
+ title = "!!! %s !!!" % guiapp.appname
+ QtGui.QMessageBox.critical(None, title, message)
+ guiapp.quit()
+
+def gui_warning(message, title=None):
+ if title == None:
+ title = guiapp.appname
+ QtGui.QMessageBox.warning(None, title, message)
+
+def onexcept(text):
+ debug(traceback.format_exc())
+ gui_error(text, "Exception")
+#---------------------------
+
+
+fileDialogDir = ''
+# In tests with the gtk dialog the options were not respected,
+# with ro you could still create new directories
+# and all files were shown when only-directories was specified.
+# So I am using the non-native dialogs.
+
+def fileDialog_getdir(caption = None, ro = True, startdir=None):
+ global fileDialogDir
+ if caption == None:
+ caption = ''
+ if startdir == None:
+ startdir = fileDialogDir
+ options = QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog
+ if ro:
+ options |= QtGui.QFileDialog.ReadOnly
+ d = QtGui.QFileDialog.getExistingDirectory(None, caption, startdir, options)
+ if d:
+ d = unicode(d)
+ fileDialogDir = d
+ return d
+
+def fileDialog_open(caption = None, startdir=None, filter=None):
+ global fileDialogDir
+ if caption == None:
+ caption = ''
+ if startdir == None:
+ startdir = fileDialogDir
+ if filter:
+ filter = '%s (%s)' % (filter[0], ' '.join(filter[1:])) #qt
+ else:
+ filter = ''
+ d = QtGui.QFileDialog.getOpenFileName(None, caption, startdir, filter, #qt
+ options=QtGui.QFileDialog.DontUseNativeDialog | QtGui.QFileDialog.ReadOnly) #qt
+ if d:
+ d = unicode(d)
+ fileDialogDir = os.path.dirname(d)
+ return d
+
+def fileDialog_save(caption = None, startdir=None, filter=None):
+ global fileDialogDir
+ if caption == None:
+ caption = ''
+ if startdir == None:
+ startdir = fileDialogDir
+ if filter:
+ filter = '%s (%s)' % (filter[0], ' '.join(filter[1:])) #qt
+ else:
+ filter = ''
+ d = QtGui.QFileDialog.getSaveFileName(None, caption, startdir, filter, #qt
+ options=QtGui.QFileDialog.DontUseNativeDialog) #qt
+ if d:
+ d = unicode(d)
+ fileDialogDir = os.path.dirname(d)
+ return d
+
+
+class Stack(QtGui.QStackedWidget, XContainer): #qt
+ def __init__(self):
+ QtGui.QStackedWidget.__init__(self) #qt
+ self.x_twidgets = []
+
+ def x__pages(self, pages):
+ self.x_twidgets = pages
+
+ def x__pack(self):
+ for name in self.x_twidgets:
+ w = guiapp.getwidget(name)
+ w.x__pack()
+ self.addWidget(w) #qt
+
+ def x__set(self, index=0):
+ self.setCurrentIndex(index) #qt
+
+
+class Notebook(QtGui.QTabWidget, XContainer): #qt
+ def __init__(self):
+ QtGui.QTabWidget.__init__(self) #qt
+ self.x_tabs = []
+ self.x_twidgets = []
+
+ def x__changed(self, name=''):
+ guiapp.signal(self, 'changed', name, 'currentChanged(int)') #qt
+
+ def x__tabs(self, tabs):
+ self.x_twidgets = tabs
+
+ def x__pack(self):
+ for name, title in self.x_twidgets:
+ w = guiapp.getwidget(name)
+ w.x__pack()
+ self.addTab(w, title) #qt
+ self.x_tabs.append([name, w])
+
+ def x__set(self, index=0):
+ self.setCurrentIndex(index) #qt
+
+ def x__enableTab(self, index, on):
+ self.setTabEnabled(index, on) #qt
+
+
+class Page(QtGui.QWidget, Container): #qt
+ def __init__(self):
+ QtGui.QWidget.__init__(self) #qt
+
+ def x__enable(self, on):
+ """Enable/Disable widget. on should be True to enable the widget
+ (display it in its normal, active state), False to disable it
+ (which will normally be paler and non-interactive).
+ """
+ self.setEnabled(on) #qt
+
+
+class Frame(QtGui.QGroupBox, WBase, Container): #qt
+ def __init__(self):
+ QtGui.QGroupBox.__init__(self) #qt
+ self._text = None
+
+ def x__text(self, text):
+ self._text = text
+ self.setTitle(text) #qt
+
+# A hack to improve spacing
+ def setLayout(self, layout):
+ topgap = 10 if self._text else 0
+ layout.setContentsMargins(0, topgap, 0, 0) #qt
+ QtGui.QGroupBox.setLayout(self, layout)
+
+
+class OptionalFrame(Frame): #qt
+ def __init__(self): #qt
+ Frame.__init__(self) #qt
+ self.setCheckable(True) #qt
+ self.setChecked(False) #qt
+
+ def x__toggled(self, name=''):
+ guiapp.signal(self, 'toggled', name, 'toggled(bool)') #qt
+
+ def x__opton(self, on):
+ self.setChecked(on) #qt
+
+#TODO: Is this still needed? (I think it's a qt bug)
+ def x__enable_hack(self): #qt
+ if not self.isChecked(): #qt
+ self.setChecked(True) #qt
+ self.setChecked(False) #qt
+
+ def x__active(self):
+ return self.isChecked() #qt
+
+
+def read_markup(markup):
+ def read_markup0(mlist):
+ text = ''
+ for i in mlist:
+ text += read_markup(i) if isinstance(i, list) else i
+ return text
+ tag = markup[0]
+ if tag == '':
+ return read_markup0(markup[1:])
+ elif tag in ('h1', 'h2', 'h3', 'h4', 'p', 'em', 'strong'):
+ return '<%s>%s</%s>' % (tag, read_markup0(markup[1:]), tag)
+ elif tag == 'color':
+ return '<span style="color:%s;">%s</span>' % (markup[1],
+ read_markup0(markup[2:]))
+ return "Markup parse error"
+
+
+class Label(QtGui.QLabel, WBase): #qt
+ def __init__(self):
+ QtGui.QLabel.__init__(self) #qt
+
+ def x__markup(self, markup):
+ self.setText(read_markup(markup)) #qt
+
+ def x__image(self, path):
+ self.setPixmap(QtGui.QPixmap(path)) #qt
+
+ def x__align(self, pos):
+ if pos == "center":
+ a = QtCore.Qt.AlignCenter #qt
+ else:
+ a = QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter #qt
+ self.setAlignment(a) #qt
+
+
+class Button(QtGui.QPushButton, WBase, BBase): #qt
+ def __init__(self):
+ QtGui.QPushButton.__init__(self) #qt
+
+ def x__clicked(self, name=''):
+ guiapp.signal(self, 'clicked', name, 'clicked()') #qt
+
+
+class ToggleButton(QtGui.QPushButton, WBase, BBase): #qt
+ def __init__(self):
+ QtGui.QPushButton.__init__(self) #qt
+ self.setCheckable(True) #qt
+
+ def x__toggled(self, name=''):
+ guiapp.signal(self, 'toggled', name, 'toggled(bool)') #qt
+
+ def x__set(self, on):
+ self.setChecked(on) #qt
+
+
+class CheckBox(QtGui.QCheckBox, WBase): #qt
+ def __init__(self):
+ QtGui.QCheckBox.__init__(self) #qt
+
+ def x__toggled(self, name=''):
+ # A bit of work is needed to get True/False state #qt
+ # instead of 0/1/2 #qt
+ guiapp.signal(self, 'toggled', name,
+ 'toggled(bool)', self.s_toggled) #qt
+
+ def s_toggled(self, state): #qt
+ """Convert the argument to True/False.
+ """ #qt
+ return (state != QtCore.Qt.Unchecked,) #qt
+
+ def x__set(self, on):
+ self.setCheckState(2 if on else 0) #qt
+
+ def x__active(self):
+ return self.checkState() != QtCore.Qt.Unchecked #qt
+
+
+class RadioButton(QtGui.QRadioButton, WBase): #qt
+ def __init__(self):
+ QtGui.QPushButton.__init__(self) #qt
+
+ def x__toggled(self, name=''):
+ guiapp.signal(self, 'toggled', name, 'toggled(bool)') #qt
+
+ def x__set(self, on):
+ self.setChecked(on) #qt
+
+ def x__active(self):
+ return self.isChecked() #qt
+
+
+class ComboBox(QtGui.QComboBox, WBase): #qt
+ def __init__(self):
+ QtGui.QComboBox.__init__(self) #qt
+
+ def x__changed(self, name=''):
+ guiapp.signal(self, 'changed', name, 'currentIndexChanged(int)') #qt
+
+ def x__changedstr(self, name=''):
+ guiapp.signal(self, 'changedstr', name,
+ 'currentIndexChanged(const QString &)') #qt
+
+ def x__set(self, items, index=0):
+ self.blockSignals(True)
+ self.clear() #qt
+ if items:
+ self.addItems(items) #qt
+ self.setCurrentIndex(index) #qt
+ self.blockSignals(False)
+
+ def x__index(self):
+ return self.currentIndex() #qt
+
+ def x__setindex(self, index):
+ return self.setCurrentIndex(index) #qt
+
+
+class ListChoice(QtGui.QListWidget, WBase): #qt
+ def __init__(self):
+ QtGui.QListWidget.__init__(self) #qt
+
+ def x__changed(self, name=''):
+ guiapp.signal(self, 'changed', name, 'currentRowChanged(int)') #qt
+
+ def x__set(self, items, index=0):
+ self.blockSignals(True)
+ self.clear() #qt
+ if items:
+ self.addItems(items) #qt
+ self.setCurrentRow(index) #qt
+ self.blockSignals(False)
+
+
+class List(QtGui.QTreeWidget, WBase): #qt
+ # Only using top-level items of the tree
+ def __init__(self):
+ QtGui.QTreeWidget.__init__(self) #qt
+ self.mode = ""
+ self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) #qt
+ self.setRootIsDecorated(False) #qt
+ self._hcompact = False # used for scheduling header-compaction
+
+ def x__select(self, name=''):
+ guiapp.signal(self, 'select', name,
+ 'itemSelectionChanged()', self.s_select) #qt
+
+ def x__clicked(self, name=''):
+ guiapp.signal(self, 'clicked', name,
+ 'itemClicked(QTreeWidgetItem *,int)', self.s_clicked) #qt
+
+ def s_select(self):
+ # Signal a selection change, passing the new selection list (indexes)
+ s = [self.indexOfTopLevelItem(i) for i in self.selectedItems()] #qt
+ if self.mode == "Single":
+ return s
+ else:
+ return (s,)
+
+ def s_clicked(self, item, col): #qt
+ """This is intended for activating a user-defined editing function.
+ Tests showed that this is called after the selection is changed, so
+ if using this signal, use it only in 'Single' selection mode and
+ use this, not 'select' to record selection changes. Clicking on the
+ selected row should start editing the cell, otherwise just change
+ the selection.
+ """
+ ix = self.indexOfTopLevelItem(item) #qt
+ return (ix, col)
+
+ def x__selectionmode(self, sm):
+ self.mode = sm
+ if sm == "None":
+ self.setSelectionMode(QtGui.QAbstractItemView.NoSelection) #qt
+ elif sm == "Single":
+ self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) #qt
+ else:
+ self.mode = ""
+ self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) #qt
+
+ def x__headers(self, headers): #qt
+ self.setHeaderLabels(headers) #qt
+ if self._hcompact:
+ self._compact()
+
+ def x__set(self, items, index=0): #qt
+ # Note that each item must be a tuple/list containing
+ # entries for each column.
+ self.clear() #qt
+ c = 0
+ for i in items:
+ item = QtGui.QTreeWidgetItem(self, i) #qt
+ self.addTopLevelItem(item) #qt
+ if c == index:
+ self.setCurrentItem(item)
+ c += 1
+ if self._hcompact:
+ self._compact()
+
+ def x__compact(self, on=True):
+ self._hcompact = on
+ if on:
+ self._compact()
+
+ def _compact(self):
+ for i in range(self.columnCount()): #qt
+ self.resizeColumnToContents(i) #qt
+
+
+class LineEdit(QtGui.QLineEdit, WBase): #qt
+ def __init__(self):
+ QtGui.QLineEdit.__init__(self) #qt
+
+ def x__enter(self, name=''):
+ guiapp.signal(self, 'enter', name, 'returnPressed()') #qt
+
+ def x__changed(self, name=''):
+ guiapp.signal(self, 'changed', name, 'textEdited(const QString &)') #qt
+
+ def x__get(self):
+ return unicode(self.text()) #qt
+
+ def x__ro(self, ro):
+ self.setReadOnly(ro) #qt
+
+ def x__pw(self, star):
+ self.setEchoMode(QtGui.QLineEdit.Password if star == "+" #qt
+ else QtGui.QLineEdit.NoEcho if star == "-" #qt
+ else QtGui.QLineEdit.Normal) #qt
+
+
+class CheckList(QtGui.QWidget, WBase): #qt
+ def __init__(self):
+ QtGui.QWidget.__init__(self) #qt
+ self.box = QtGui.QVBoxLayout(self) #qt
+ self.title = None
+ if text: #qt
+ l.addWidget(QtGui.QLabel(text)) #qt
+ self.widget = QtGui.QListWidget() #qt
+ l.addWidget(self.widget) #qt
+
+ def x__title(self, text):
+ if self.title:
+ self.title.setText(text) #qt
+ else:
+ self.title = QtGui.QLabel(text) #qt
+ self.box.insertWidget(0, self.title) #qt
+
+ def x__checked(self, index):
+ return (self.widget.item(index).checkState() == #qt
+ QtCore.Qt.Checked) #qt
+
+ def x__set(self, items):
+ self.widget.blockSignals(True) #qt
+ self.widget.clear() #qt
+ if items:
+ for s, c in items:
+ wi = QtGui.QListWidgetItem(s, self.widget) #qt
+ wi.setCheckState(QtCore.Qt.Checked if c #qt
+ else QtCore.Qt.Unchecked) #qt
+ self.blockSignals(False) #qt
+
+
+class TextEdit(QtGui.QTextEdit, WBase): #qt
+ def __init__(self):
+ QtGui.QTextEdit.__init__(self) #qt
+
+ def x__ro(self, ro):
+ self.setReadOnly(ro) #qt
+
+ def x__append_and_scroll(self, text):
+ self.append(text) #qt
+ self.ensureCursorVisible() #qt
+
+ def x__get(self):
+ return unicode(self.toPlainText()) #qt
+
+ def x__undo(self):
+ QtGui.QTextEdit.undo(self) #qt
+
+ def x__redo(self):
+ QtGui.QTextEdit.redo(self) #qt
+
+ def x__copy(self):
+ QtGui.QTextEdit.copy(self) #qt
+
+ def x__cut(self):
+ QtGui.QTextEdit.cut(self) #qt
+
+ def x__paste(self):
+ QtGui.QTextEdit.paste(self) #qt
+
+
+class HtmlView(QtWebKit.QWebView, WBase): #qt
+ def __init__(self):
+ QtWebKit.QWebView.__init__(self) #qt
+
+ def x__html(self, content):
+ self.setHtml(content) #qt
+
+ def x__setUrl(self, url):
+ self.load(QtCore.QUrl(url)) #qt
+
+ def x__prev(self):
+ self.back() #qt
+
+ def x__next(self):
+ self.forward() #qt
+
+
+class SpinBox(QtGui.QDoubleSpinBox, WBase): #qt
+ def __init__(self):
+ QtGui.QDoubleSpinBox.__init__(self) #qt
+ self.step = None
+
+ def x__changed(self, name=''):
+ guiapp.signal(self, 'changed', name, 'valueChanged(double)') #qt
+
+ def x__min(self, min):
+ self.setMinimum(min)
+
+ def x__max(self, max):
+ self.setMaximum(max)
+
+ def x__decimals(self, dec):
+ self.setDecimals(dec)
+ if not self.step:
+ self.setSingleStep(10**(-dec))
+
+ def x__step(self, step):
+ self.setSingleStep(step)
+
+ def x__value(self, val):
+ self.setValue(val)
+
+
+class ProgressBar(QtGui.QProgressBar, WBase): #qt
+ def __init__(self):
+ QtGui.QProgressBar.__init__(self) #qt
+
+ def x__set(self, value):
+ self.setValue(value) #qt
+
+ def x__max(self, max):
+ self.setMaximum(max) #qt
+
+
+
+# Layout classes
+class Layout:
+ """A mixin base class for all layout widgets.
+ """
+ pass
+
+boxmargin=3
+class _BOX(Layout):
+ def do_layout(self, items):
+ self.setContentsMargins(boxmargin, boxmargin, boxmargin, boxmargin) #qt
+ for wl in items:
+ if isinstance(wl, QtGui.QWidget): #qt
+ self.addWidget(wl) #qt
+ elif isinstance(wl, SPACE): #qt
+ if wl.size: #qt
+ self.addSpacing(wl.size) #qt
+ self.addStretch() #qt
+ elif isinstance(wl, Layout): #qt
+ self.addLayout(wl) #qt
+ else: #qt
+ gui_error("Invalid Box entry: %s" % repr(wl))
+
+
+class VBOX(QtGui.QVBoxLayout, _BOX): #qt
+ def __init__(self):
+ QtGui.QVBoxLayout.__init__(self) #qt
+
+
+class HBOX(QtGui.QHBoxLayout, _BOX): #qt
+ def __init__(self):
+ QtGui.QHBoxLayout.__init__(self) #qt
+
+
+class GRID(QtGui.QGridLayout, Layout): #qt
+ def __init__(self):
+ QtGui.QGridLayout.__init__(self) #qt
+
+ def do_layout(self, rows):
+ y = -1
+ for row in rows:
+ y += 1
+ x = -1
+ for wl in row:
+ x += 1
+ if isinstance(wl, Span):
+ continue
+ # Determine the row and column spans
+ x1 = x + 1
+ while (x1 < len(row)) and isinstance(row[x1], CSPAN):
+ x1 += 1
+ y1 = y + 1
+ while (y1 < len(rows)) and isinstance(rows[y1][x], RSPAN):
+ y1 += 1
+
+ if isinstance(wl, QtGui.QWidget): #qt
+ self.addWidget(wl, y, x, y1-y, x1-x) #qt
+ elif isinstance(wl, Layout):
+ self.addLayout(wl, y, x, y1-y, x1-x) #qt
+ elif isinstance(wl, SPACE):
+ self.addItem(QtGui.QSpacerItem(wl.size, wl.height),
+ y, x, y1-y, x1-x) #qt
+ else:
+ gui_error("Invalid entry in Grid layout: %s" % repr(wl))
+
+
+class SPACE:
+ """Can be used in boxes and grids. In boxes only size is of interest,
+ and it also means vertical size in the case of a vbox. In grids size
+ is the width.
+ """
+ def __init__(self, size_width='0', height='0'): #qt
+ self.size = int(size_width) #qt
+ self.height = int(height) #qt
+
+
+class Span:
+ """Class to group special grid layout objects together - it doesn't
+ actually do anything itself, but is used for checking object types.
+ """
+ pass
+
+
+class CSPAN(Span):
+ """Column-span layout item. It doesn't do anything itself, but it is used
+ by the Grid layout constructor.
+ """
+ pass
+
+
+class RSPAN(Span):
+ """Row-span layout item. It doesn't do anything itself, but it is used
+ by the Grid layout constructor.
+ """
+ pass
+
+
+class HLINE(QtGui.QFrame): #qt
+ def __init__(self, pad=None):
+ # pass the pad argument thus: 'HLINE,3'
+ QtGui.QFrame.__init__(self) #qt
+ self.setFrameShape(QtGui.QFrame.HLine) #qt
+ if pad:
+ self.setFixedHeight(1 + 2*int(pad)) #qt
+
+
+class VLINE(QtGui.QFrame): #qt
+ def __init__(self, pad=None):
+ # pass the pad argument thus: 'VLINE,3'
+ QtGui.QFrame.__init__(self) #qt
+ self.setFrameShape(QtGui.QFrame.VLine) #qt
+ if pad:
+ self.setFixedWidth(1 + 2*int(pad)) #qt
+
+
+class DATA:
+ """This is not really a widget, it just holds a dictionary of
+ potentially internationalized messages.
+ """
+ def x__messages(self, mdict):
+ self.messages = mdict
+
+ def x__get(self, key):
+ return self.messages.get(key)
+
+
+class Suim(QtGui.QApplication):
+ """This class represents an application gui, possibly with more than
+ one top level window.
+ """
+ timers = [] # timer objects
+
+ def __init__(self, appname='suim', busywidgets = []):
+ global guiapp, T_
+ guiapp = self
+ QtGui.QApplication.__init__(self, []) #qt
+ self.appname = appname
+ self.eno = QtCore.QEvent.registerEventType() #qt
+ # This overcomplicated looking bit should deal with translating the built in dialogs
+ _translator = QtCore.QTranslator(self)
+ if (_translator.load('qt_'+ QtCore.QLocale.system().name(),
+ QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath))):
+ self.installTranslator(_translator)
+
+ # list of widgets to disable while 'busy'
+ self.busywidgets = busywidgets
+
+ self.busystate = False
+ self.busy_lock = threading.Lock()
+
+ self.setQuitOnLastWindowClosed(False) #qt
+
+ self.widgets = {} # all widgets, key = widget tag
+ self.signal_dict = {} # signal connections, key = signature
+
+ # callback list for event loop: (callback, arglist) pairs:
+ self.idle_calls = deque()
+
+
+ def event(self, e):
+ if e.type() == self.eno:
+ # Process item from list
+ cb, a = self.idle_calls.popleft()
+ cb(*a)
+ return True
+ else:
+ return QtGui.QApplication.event(self, e) #qt
+
+
+ def run(self):
+ self.exec_() #qt
+
+
+ def quit(self, cc=0):
+ # QCoreApplication provides the static function 'exit'
+ self.exit(cc) #qt
+
+
+ def addwidget(self, fullname, wo):
+ if self.widgets.has_key(fullname):
+ gui_error("Attempted to define widget '%s' twice." % fullname)
+ self.widgets[fullname] = wo
+
+
+ def getwidget(self, w):
+ widget = self.widgets.get(w)
+ if widget == None:
+ gui_warning("Unknown widget: %s" % w)
+ return widget
+
+
+ def show(self, windowname):
+ self.getwidget(windowname).setVisible()
+
+
+ def command(self, cmdtext, *args):
+ cmd = specials_table.get(cmdtext)
+ if not cmd:
+ w, m = cmdtext.split(".")
+ wo = self.getwidget(w)
+ cmd = getattr(wo, 'x__' + m)
+ return cmd(*args)
+
+
+ def widget(self, wtype, wname, args):
+ wobj = widget_table[wtype]()
+ wobj.w_name = wname
+
+ # Attributes
+ for key, val in args.iteritems():
+ handler = "x__" + key
+ if hasattr(wobj, handler):
+ getattr(wobj, handler)(val)
+# Unrecognized attributes are ignored ...
+
+ self.addwidget(wname, wobj)
+
+
+ def widgetlist(self, wlist):
+ for w in wlist:
+ # Add simple signals
+ for s in w[3:]:
+ w[2][s] = ''
+ self.widget(w[0], w[1], w[2])
+
+
+ def signal(self, source, signal, name=None, xsignal=None, convert=None):
+ """Enable or disable a signal.
+ Signal.signals is a dictionary of enabled signals.
+ The key is constructed from the widget name and the formal signal name.
+ The name of the signal which actually gets generated will be the
+ same as the key unless the 'name' parameter is set. See the
+ 'Signal' class for further details.
+ If 'name' is None (not ''!), the signal will be disabled.
+ """
+ widsig = source.w_name + '*' + signal
+ if name == None:
+ s = Signal.signals.get(widsig)
+ if not s:
+ gui_error("Can't disable signal '%s' - it's not enabled"
+ % widsig)
+ s.disconnect() # Probably not necessary in qt
+ del(Signal.signals[widsig])
+ else:
+ if Signal.signals.has_key(widsig):
+ gui_error("Signal already connected: %s" % widsig)
+ Signal.signals[widsig] = Signal(source, signal, name, xsignal,
+ convert)
+
+
+ def connect(self, signal, function):
+ if self.signal_dict.has_key(signal):
+ self.signal_dict[signal].append(function)
+ else:
+ self.signal_dict[signal] = [function]
+
+
+ def connectlist(self, *slotlist):
+ for s in slotlist:
+ self.connect(*s)
+
+
+ def disconnect(self, signal, function):
+ try:
+ l = self.signal_dict[signal]
+ l.remove(function)
+ except:
+ gui_error("Slot disconnection for signal '%s' failed"
+ % signal)
+
+
+ def sendsignal(self, name, *args):
+ # When there are no slots a debug message is output.
+ slots = self.signal_dict.get(name)
+ if slots:
+ try:
+ for slot in slots:
+ slot(*args)
+ except:
+ gui_error("Signal handling error:\n %s"
+ % traceback.format_exc())
+ else:
+ debug("Unhandled signal: %s %s" % (name, repr(args)))
+
+
+ def idle_add(self, callback, *args):
+ self.idle_calls.append((callback, args))
+ e = QtCore.QEvent(self.eno) #qt
+ self.postEvent(self, e) #qt
+
+
+ def timer(self, callback, period):
+ """Start a timer which calls the callback function on timeout.
+ Only if the callback returns True will the timer be retriggered.
+ """
+ Suim.timers.append(Timer(callback, period))
+
+
+ def busy(self, on, busycursor=True):
+ """This activates (or deactivates, for on=False) a 'busy' mechanism,
+ which can be one or both of the following:
+ Make the application's cursor change to the 'busy cursor'.
+ Disable a group of widgets.
+ There is a lock to prevent the busy state from being set when it
+ is already active, or unset it when already unset.
+ """
+ self.busy_lock.acquire()
+ if on:
+ if self.busystate:
+ self.busy_lock.release()
+ return
+ self.busycursor = busycursor
+ if busycursor:
+ self.setOverrideCursor(QtCore.Qt.BusyCursor) #qt
+ else:
+ if not self.busystate:
+ self.busy_lock.release()
+ return
+ if self.busycursor:
+ self.restoreOverrideCursor() #qt
+ self.busystate = on
+ self.busy_lock.release()
+ for wn in self.busywidgets:
+ w = self.getwidget(wn)
+ if w:
+ w.setEnabled(not on) #qt
+ else:
+ debug("*ERROR* No widget '%s'" % wn)
+
+
+class Timer(QtCore.QTimer): #qt
+ def __init__(self, timers, callback, period):
+ QtCore.QTimer.__init__(self) #qt
+ self.x_callback = callback
+ self.connect(self, QtCore.SIGNAL("timeout()"), #qt
+ self.x_timeout)
+ self.start(int(period * 1000)) #qt
+
+ def x_timeout(self):
+ if not self.x_callback():
+ self.stop() #qt
+ Suim.timers.remove(self)
+
+
+
+class Signal:
+ """Each instance represents a single connection.
+ """
+ signals = {} # Enabled signals
+
+ def __init__(self, source, signal, name, xsignal, convert):
+ """'source' is the widget object which initiates the signal.
+ 'signal' is the signal name.
+ If 'name' is given (not empty), the signal will get this as its name,
+ and this name may be used for more than one connection.
+ Otherwise the name is built from the name of the source widget and
+ the signal type as 'source*signal' and this is unique.
+ If 'name' begins with '+' an additional argument, the source
+ widget name, will be inserted at the head of the argument list.
+ 'xsignal' is a toolkit specific signal descriptor.
+ 'convert' is an optional function (default None) - toolkit specific -
+ to perform signal argument conversions.
+ """
+ self.widsig = '%s*%s' % (source.w_name, signal)
+ #+ For disconnect?
+ self.xsignal = xsignal
+ #-
+ self.convert = convert # Argument conversion function
+ self.tag = name if name else self.widsig
+ self.wname = source.w_name if self.tag[0] == '+' else None
+ if not source.connect(source, QtCore.SIGNAL(xsignal), #qt
+ self.signal):
+ gui_error("Couldn't enable signal '%s'" % self.widsig)
+
+ def signal(self, *args):
+ if self.convert:
+ args = self.convert(*args)
+ if self.wname:
+ guiapp.sendsignal(self.tag, self.wname, *args)
+ else:
+ guiapp.sendsignal(self.tag, *args)
+
+ def disconnect(self):
+ w = guiapp.getwidget(self.widsig.split('*')[0])
+ w.disconnect(w, QtCore.SIGNAL(self.xsignal), self.signal) #qt
+
+
+
+#+++++++++++++++++++++++++++
+# Catch all unhandled errors.
+def errorTrap(type, value, tb):
+ etext = "".join(traceback.format_exception(type, value, tb))
+ debug(etext)
+ gui_error(etext, "This error could not be handled.")
+
+sys.excepthook = errorTrap
+#---------------------------
+
+widget_table = {
+ "DATA": DATA,
+ "Window": Window,
+ "Dialog": Dialog,
+ "DialogButtons": DialogButtons,
+ "Notebook": Notebook,
+ "Stack": Stack,
+ "Page": Page,
+ "Frame": Frame,
+ "Button": Button,
+ "ToggleButton": ToggleButton,
+ "RadioButton": RadioButton,
+ "CheckBox": CheckBox,
+ "Label": Label,
+ "CheckList": CheckList,
+ "List": List,
+ "OptionalFrame": OptionalFrame,
+ "ComboBox": ComboBox,
+ "ListChoice": ListChoice,
+ "LineEdit": LineEdit,
+ "TextEdit": TextEdit,
+ "HtmlView": HtmlView,
+ "SpinBox": SpinBox,
+ "ProgressBar": ProgressBar,
+}
+
+specials_table = {
+ "textLineDialog": textLineDialog,
+ "infoDialog": infoDialog,
+ "confirmDialog": confirmDialog,
+ "errorDialog": gui_error,
+ "warningDialog": gui_warning,
+ "fileDialog_getdir": fileDialog_getdir,
+ "fileDialog_open": fileDialog_open,
+ "fileDialog_save": fileDialog_save,
+ "listDialog": listDialog,
+}
+
+layout_table = {
+ "VBOX": VBOX,
+ "HBOX": HBOX,
+ "GRID": GRID,
+# "+": GRIDROW,
+ "-": CSPAN,
+ "|": RSPAN,
+ "*": SPACE,
+ "VLINE": VLINE,
+ "HLINE": HLINE,
+}
+
diff --git a/build_tools/larch8/liblarch/translation.py b/build_tools/larch8/liblarch/translation.py
new file mode 100644
index 0000000..114a8a2
--- /dev/null
+++ b/build_tools/larch8/liblarch/translation.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python2
+#
+# translation.py -- Translation services
+#
+# (c) Copyright 2010 Michael Towers (larch42 at googlemail dot com)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#-------------------------------------------------------------------
+# 2010.08.14
+
+import os, gettext
+from liblarch_conf import liblarchdir
+
+def i18n_module(basedir, appname):
+ """This doesn't use gettext.install in order not to affect the whole
+ application, it allows per module translation services.
+ """
+ return gettext.translation(appname, basedir + '/i18n', fallback=True).ugettext
+
+
+def i18n_liblarch():
+ """Provides translation for liblarch itself.
+ """
+ return i18n_module(liblarchdir, 'liblarch')
+_ = i18n_liblarch()
+
+
+lang = (os.environ.get('LANGUAGE') or os.environ.get('LC_ALL')
+ or os.environ.get('LC_MESSAGES') or os.environ.get('LANG'))
+
+
+def i18nurl(doc):
+ docbase, docfile = doc.rsplit('/', 1)
+ docbase += '/'
+ i18npath = docbase + lang.split('.')[0] + '/' + docfile
+ if not os.path.isfile(i18npath):
+ i18npath = docbase + lang.split('_')[0] + '/' + docfile
+ if not os.path.isfile(i18npath):
+ i18npath = doc
+ if not os.path.isfile(i18npath):
+ return None
+ return i18npath
+
+def i18ndoc(doc):
+ p = i18nurl(doc)
+ if not p:
+ return _("Document '%s' not found") % i18npath
+ fh = open(p)
+ data = fh.read().decode('utf-8')
+ fh.close()
+ return data
+
diff --git a/build_tools/larch8/liblarch/uim/larcon.uim b/build_tools/larch8/liblarch/uim/larcon.uim
new file mode 100644
index 0000000..44bd030
--- /dev/null
+++ b/build_tools/larch8/liblarch/uim/larcon.uim
@@ -0,0 +1,77 @@
+# larcon.uim - The basic layout for the larcon parser
+#
+# (c) Copyright 2010 Michael Towers (larch42 at googlemail dot com)
+#
+# This file is part of the larch project.
+#
+# larch is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# larch is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with larch; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#----------------------------------------------------------------------------
+# 2010.08.13
+
+# The variable <mainwidget> should be set before running this.
+
+[
+ ['Window', 'larcon',
+ { 'size': '500_300',
+
+# needs title and icon
+
+ 'closesignal': '$$$uiquit$$$',
+ 'layout':
+ ['VBOX',
+ 'larcon:stack',
+ 'HLINE',
+ ['HBOX', 'larcon:docs', '*', 'larcon:quit']
+ ]
+ }
+ ],
+ ['Stack', 'larcon:stack',
+ { 'pages': ['larcon:main', 'larcon:docpage']
+ }
+ ],
+ ['Button', 'larcon:quit',
+ { 'text': _("Quit"),
+ 'tt': _("Exit the application, make no (further) changes"),
+ 'clicked': '$$$uiquit$$$'
+ },
+ ],
+ ['Button', 'larcon:docs',
+ {
+ },
+ 'clicked'
+ ],
+ ['HtmlView', 'larcon:docview',
+ {
+ }
+ ],
+ ['Page', 'larcon:main',
+ {# 'layout': ['VBOX', mainwidget]
+ }
+ ],
+ ['Page', 'larcon:docpage',
+ { 'layout': ['VBOX', 'larcon:docview']
+ }
+ ],
+ ['DATA', 'larcon_data',
+ { 'messages':
+ { 'showtext': _("Help"),
+ 'showtt': _("Show the help page"),
+ 'hidetext': _("Hide help"),
+ 'hidett': _("Hide the help page, return to main view")
+ }
+ },
+ ],
+]