diff options
author | James Meyer <james.meyer@operamail.com> | 2010-12-02 22:37:23 (GMT) |
---|---|---|
committer | James Meyer <james.meyer@operamail.com> | 2010-12-02 22:37:34 (GMT) |
commit | 8b94d7f39c71234712bead363526a0283efeb9fa (patch) | |
tree | 23f1dbd6458dc39a2c1b08bcdd4cbf768a60d84d /build_tools/larch8/liblarch | |
parent | 338af567e74d08cbd357079941208e494463d61e (diff) | |
download | linhes_dev-8b94d7f39c71234712bead363526a0283efeb9fa.zip |
larch8: first checkin, still needs some work
Diffstat (limited to 'build_tools/larch8/liblarch')
-rw-r--r-- | build_tools/larch8/liblarch/__init__.py | 0 | ||||
-rw-r--r-- | build_tools/larch8/liblarch/docs/README | 1 | ||||
-rw-r--r-- | build_tools/larch8/liblarch/i18n/de/LC_MESSAGES/de.po | 61 | ||||
-rw-r--r-- | build_tools/larch8/liblarch/i18n/de/LC_MESSAGES/liblarch.mo | bin | 0 -> 1241 bytes | |||
-rwxr-xr-x | build_tools/larch8/liblarch/i18n/i18n.py | 61 | ||||
-rwxr-xr-x | build_tools/larch8/liblarch/i18n/i18n2.py | 31 | ||||
-rw-r--r-- | build_tools/larch8/liblarch/i18n/liblarch.pot | 61 | ||||
-rw-r--r-- | build_tools/larch8/liblarch/larcon_base.py | 74 | ||||
-rw-r--r-- | build_tools/larch8/liblarch/larcon_ui.py | 100 | ||||
-rw-r--r-- | build_tools/larch8/liblarch/liblarch_conf.py | 32 | ||||
-rw-r--r-- | build_tools/larch8/liblarch/rootrun.py | 246 | ||||
-rw-r--r-- | build_tools/larch8/liblarch/suim.py | 1365 | ||||
-rw-r--r-- | build_tools/larch8/liblarch/translation.py | 65 | ||||
-rw-r--r-- | build_tools/larch8/liblarch/uim/larcon.uim | 77 |
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 Binary files differnew file mode 100644 index 0000000..a6bf49b --- /dev/null +++ b/build_tools/larch8/liblarch/i18n/de/LC_MESSAGES/liblarch.mo 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") + } + }, + ], +] |