diff options
Diffstat (limited to 'build_tools/l7/larch0/gui/front')
-rw-r--r-- | build_tools/l7/larch0/gui/front/docviewer.py | 68 | ||||
-rw-r--r-- | build_tools/l7/larch0/gui/front/editor.py | 115 | ||||
-rw-r--r-- | build_tools/l7/larch0/gui/front/logview.py | 94 | ||||
-rw-r--r-- | build_tools/l7/larch0/gui/front/mainwindow.py | 132 | ||||
-rw-r--r-- | build_tools/l7/larch0/gui/front/page_installation.py | 188 | ||||
-rw-r--r-- | build_tools/l7/larch0/gui/front/page_larchify.py | 248 | ||||
-rw-r--r-- | build_tools/l7/larch0/gui/front/page_medium.py | 320 | ||||
-rw-r--r-- | build_tools/l7/larch0/gui/front/page_mediumprofile.py | 87 | ||||
-rw-r--r-- | build_tools/l7/larch0/gui/front/page_project.py | 203 | ||||
-rw-r--r-- | build_tools/l7/larch0/gui/front/uim.py | 1327 |
10 files changed, 2782 insertions, 0 deletions
diff --git a/build_tools/l7/larch0/gui/front/docviewer.py b/build_tools/l7/larch0/gui/front/docviewer.py new file mode 100644 index 0000000..a0af083 --- /dev/null +++ b/build_tools/l7/larch0/gui/front/docviewer.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# +# docviewer.py +# +# (c) Copyright 2009-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.06.24 + + +import os + + +class DocViewer: + def __init__(self): + self.index = self._getPage('index.html') + self.homepath = None + ui.widgetlist(fss('fetch_layout', 'docviewer.uim')) + + ui.connectlist( + ('doc:hide*clicked', self._hide), + ('doc:back*clicked', self._back), + ('doc:forward*clicked', self._forward), + ('doc:home*clicked', self.gohome), + ('doc:parent*clicked', self.goto), + (':docs*clicked', self._show), + ) + + def _show(self): + ui.runningtab(3) + + def _hide(self): + ui.runningtab() + + def _back(self): + ui.command('doc:content.prev') + + def _forward(self): + ui.command('doc:content.next') + + def _getPage(self, page): + return fss('get_docs_url', page) + + def gohome(self, home=None): + if home: + self.homepath = self._getPage(home) + self.goto(self.homepath) + + def goto(self, path=None): + if not path: + path = self.index + ui.command('doc:content.setUrl', path) diff --git a/build_tools/l7/larch0/gui/front/editor.py b/build_tools/l7/larch0/gui/front/editor.py new file mode 100644 index 0000000..649f55c --- /dev/null +++ b/build_tools/l7/larch0/gui/front/editor.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# +# editor.py +# +# (c) Copyright 2009-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.06.24 + +class Editor: + def __init__(self): + ui.widgetlist(fss('fetch_layout', 'editor.uim')) + ui.connectlist( + ('edit:ok*clicked', self.ok), + ('edit:cancel*clicked', self.cancel), + ('edit:revert*clicked', self.dorevert), + ('edit:copy*clicked', self.copy), + ('edit:cut*clicked', self.cut), + ('edit:paste*clicked', self.paste), + ('edit:undo*clicked', self.undo), + ('edit:redo*clicked', self.redo), + ) + + def start(self, title, endcall, text='', revert=None): + ui.command('edit:title.markup', ['h3', title]) + self.endcall = endcall + self.revert = revert + try: + self.text0 = revert() if text == None else text + except: + run_error("BUG: Editor - no revert function?") + ui.command('edit:content.text', self.text0) + ui.runningtab(4) + + def ok(self): + self.endcall(ui.command('edit:content.get')) + ui.runningtab() + + def cancel(self): + ui.runningtab() + + def dorevert(self): + if self.revert: + self.text0 = self.revert() + ui.command('edit:content.text', self.text0) + + def copy(self): + ui.command('edit:content.copy') + + def cut(self): + ui.command('edit:content.cut') + + def paste(self): + ui.command('edit:content.paste') + + def undo(self): + ui.command('edit:content.undo') + + def redo(self): + ui.command('edit:content.redo') + + def edit(self, fname, source=None, label=None, filter=None): + """Files (<fname> and <source>) can be either an absolute path or else + relative to the profile directory, the application base directory + or the working directory. Relative paths are determined by the + prefixes 'profile:', 'base:' or 'working:'. + If the file <fname> already exists its contents will be taken as the + starting point, otherwise the file <source>, which may also be an + empty string, will be read in. + Whichever file is available its contents can be filtered by an + optional 'filter' function, which takes the file contents as a + string as argument and returns the transformed contents as another + string. + """ + def revert(): + """If a file is addressed by 'source' revert to its contents, + if source is "", clear the contents, otherwise revert to the + contents as they were before entering the editor. + """ + return textsrc if source != None else text0 + + def endfile(text): + t = text.encode("utf8") + if t and (t[-1] != "\n"): + t += "\n" + fss('savefile', fname, text) + + if source != None: + textsrc = "" if source == "" else fss('readfile', source, + filter=filter) + # Read the file, if it exists, else return None + text0 = fss('readfile', fname, filter=filter, trap=False) + if text0 == None: + assert source != None # The file must be present + text0 = textsrc + if not label: + label = ui.command('editor_data.get', 'msg_dflt') % fname + self.start(label, endfile, text0, revert) + diff --git a/build_tools/l7/larch0/gui/front/logview.py b/build_tools/l7/larch0/gui/front/logview.py new file mode 100644 index 0000000..b9d2ac3 --- /dev/null +++ b/build_tools/l7/larch0/gui/front/logview.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# +# logview.py +# +# (c) Copyright 2009-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.07.09 + +import locale +# Try to work around problems when the system encoding is not utf8 +encoding = locale.getdefaultlocale()[1] +if encoding == "UTF8": + encoding = None + +#TODO: progress bar? +class Progress: + def __init__(self): + self.active = False + ui.widgetlist(fss('fetch_layout', 'progress.uim')) + ui.connect('progress:done*clicked', self._done) + + def _done(self): + self.active = False + ui.runningtab(0) + + def start(self): + # Set busy cursor on the progress area + ui.command('progress:page.busycursor', True) + # Initialize widgets + ui.command("progress:text.text") + ui.command("progress:progress.text") + ui.command("progress:done.enable", False) + ui.command("progress:cancel.enable", True) + self.active = True + ui.runningtab(1) + + def end(self, auto=False): + ui.command("progress:cancel.enable", False) + ui.command("progress:done.enable", True) + # Clear busy cursor on the progress area + ui.command('progress:page.busycursor', True) + ui.command('progress:page.busycursor', False) + if auto: + self._done() + + def addLine(self, line): + # Try to work around problems when the system encoding is not utf8 + if encoding: + line = line.decode(self.encoding, "replace").encode("UTF8") + ui.command("progress:text.append_and_scroll", line) + logger.addLine(line) + + def set(self, text=""): + ui.command("progress:progress.text", text) + + +class Logger: + def __init__(self): + ui.widgetlist(fss('fetch_layout', 'logger.uim')) + ui.connectlist( + ('log:clear*clicked', self.clear), + ('log:hide*clicked', self._hide), + (':showlog*clicked', self._show), + ) + + def clear(self): + ui.command('log:text.text') + + def addLine(self, line): + # Try to work around problems when the system encoding is not utf8 + ui.command('log:text.append_and_scroll', line) + + def _show(self): + ui.runningtab(2) + + def _hide(self): + ui.runningtab() diff --git a/build_tools/l7/larch0/gui/front/mainwindow.py b/build_tools/l7/larch0/gui/front/mainwindow.py new file mode 100644 index 0000000..407b46a --- /dev/null +++ b/build_tools/l7/larch0/gui/front/mainwindow.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# +# (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.07.13 + +import __builtin__ +from uim import Uim, debug +__builtin__.debug = debug + + +_running = False +def fss(*args, **kargs): + while True: + if _running: + ui.command(':larch.busy', [], True) + ok, result = filesystem(*args) + if _running: + ui.command(':larch.busy', [], False) + if ok: + filter = kargs.get('filter') + return filter(result) if filter else result + + if kargs.get('trap', True): + ui.command('errorDialog', result) + # That might return, but the gui should quit (soon?). + return None + +__builtin__.fss = fss + + + +class Ui(Uim): + def __init__(self): + Uim.__init__(self) + + def runningtab(self, i=None): + if (i == None): + i = 1 if progress.active else 0 + if (i == 0) and hasattr(stage, 'reenter'): + stage.reenter() + self.command(':tabs.set', i) + + def fileDialog(self, message, startdir=None, create=False, + dirsonly=False, file=None, bookmarks=None, filter=None): + return self.command('fileDialog', message, startdir, None, + dirsonly, create, file, bookmarks, filter) + + def enable_installation_page(self, on): + self.command(':notebook.enableTab', 1, on) + + def quit(self): + """Do any tidying up which may be necessary. + """ + larchscripts.close() + Uim.quit() + + def data(self, key): + return self.command('main_page_data.get', key) + +__builtin__.ui = Ui() + + +def tab_changed(index): + __builtin__.stage = pages[index] + stage.enter() +ui.connect(':notebook*changed', tab_changed) + + +from page_project import ProjectSettings +from page_installation import Installation +from page_larchify import Larchify +from page_mediumprofile import MediumProfile +from page_medium import Medium + + +from docviewer import DocViewer + +from editor import Editor + +from logview import Logger, Progress + + +def run_error(message, title=None): + ui.command('warningDialog', message, title) +__builtin__.run_error = run_error + +pages = [] +def start(): + pages.append(ProjectSettings()) + pages.append(Installation()) + pages.append(Larchify()) + pages.append(MediumProfile()) + pages.append(Medium()) + + __builtin__.docviewer = DocViewer() + __builtin__.edit = Editor().edit + + __builtin__.progress = Progress() + __builtin__.logger = Logger() + + MainWindow = fss('fetch_layout', 'page_main.uim') + ui.widgetlist(MainWindow) + + ui.connect('$$$uiclose$$$', ui.quit) + ui.connect('$$$uiquit$$$', ui.quit) + ui.connect('$$$cancel$$$', larchscripts.interrupt) + + ui.command(':larch.pack') + # Set up the first gui page (project settings) + pages[0].setup() + ui.command(':larch.show') + _running = True + ui.run() + diff --git a/build_tools/l7/larch0/gui/front/page_installation.py b/build_tools/l7/larch0/gui/front/page_installation.py new file mode 100644 index 0000000..111e247 --- /dev/null +++ b/build_tools/l7/larch0/gui/front/page_installation.py @@ -0,0 +1,188 @@ +# page_installation.py - Handler for the installation page +# +# (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.07.19 + +class Installation: + def __init__(self): + ui.widgetlist(fss('fetch_layout', 'page_installation.uim')) + + ui.connectlist( + (':addedpacks*clicked', self.edit_addedpacks), + (':vetopacks*clicked', self.edit_vetopacks), + (':pacmanconf*clicked', self.edit_pacmanconf), + (':repos*clicked', self.edit_repos), + (':editmirrorlist*clicked', self.edit_mirrorlist), + (':cache_change*clicked', self.change_cache), + (':editrepolist*clicked', self.edit_repolist), + (':install*clicked', self.install), + (':sync*clicked', self.dosync), + (':update*clicked', self.doupdate), + (':add*clicked', self.doadd), + (':remove*clicked', self.doremove), + (':installrepos*toggled', self.installrepo), + ) + + + def enter(self): + """This is called when the page is entered/selected/shown. + It performs initializations which depend on the state. + """ + docviewer.gohome('gui_installation.html') + # Set package cache display + ui.command(':cache_show.text', fss('getitem', 'pacman_cache')) + ui.command(':installrepos.opton', fss('getbool', 'installrepo')) + + + def data(self, key): + return ui.command('install_page_data.get', key) + + + def edit_addedpacks(self): + edit('profile:addedpacks') # In profile dir + + + def edit_vetopacks(self): + # If there is no vetopacks file, start an empty one + edit('profile:vetopacks', "") # In profile dir + + + def edit_pacmanconf(self): + edit('profile:pacman.conf.options', # In profile dir + 'base:data/pacman.conf', # Relative to base_dir + label=self.data('edit_pc'), + filter=pacmanoptions) + + + def edit_repos(self): + """This edits the repository list file for the live system. + It will be used to construct the /etc/pacman.conf file. + If the option to specify a different file for the installation + stage is not enabled (the default), this file will also be used + for the installation. + """ + edit('profile:pacman.conf.repos', # In profile dir + 'base:data/pacman.conf.repos', # Relative to base_dir + label=self.data('edit_pr')) + + + def edit_mirrorlist(self): + ml = '/etc/pacman.d/mirrorlist' + if not fss('isfile', ml): + ml = 'base:data/mirrorlist' # Relative to base_dir + edit('working:mirrorlist', ml, + label=self.data('edit_mli')) + + + def change_cache(self): + # Is anything more necessary? Do I need to test the path? + # Would a directory browser be better? + ok, path = ui.command('textLineDialog', + self.data('prompt_ncp'), + None, fss('getitem', 'pacman_cache')) + if ok: + self.set_pacman_cache(path) + + + def set_pacman_cache(self, path): + path = path.strip().rstrip('/') + fss('setitem', 'pacman_cache', path) + ui.command(':cache_show.text', path) + + + def edit_repolist(self): + """This edits the repository list file used for installation, + if the corresponding option is enabled. + """ + # Should it be based on the default or on the profile? + rf = 'profile:pacman.conf.repos' + if not fss('isfile', rf): + rf = 'base:data/pacman.conf.repos' # Relative to base_dir + edit('working:pacman.conf.repos', rf, + label=self.data('edit_pri')) + + + def installrepo(self, on): + fss('setbool', 'installrepo', on) + + + def install(self): + """Start the installation. + """ + self.archin('install') + + + def dosync(self): + self.archin('refresh') + + + def doupdate(self): + f = ui.fileDialog(message=self.data('msg_pu'), + filter=(self.data('filter_pu'), '*.pkg.tar.*')) + if f: + self.archin('update ' + f) + + + def doadd(self): + ok, plist = ui.command('textLineDialog', + self.data('prompt_pi'), + 'pacman -S') + if ok: + self.archin('sync ' + plist.strip()) + + + def doremove(self): + ok, plist = ui.command('textLineDialog', + self.data('prompt_pr'), + 'pacman -Rs') + if ok: + self.archin('remove ' + plist.strip()) + + + def archin(self, cmd): + """This runs the 'archin' script (as root). It doesn't wait for + completion because the output must be collected and displayed + while it is running. The display switches the view to the + progress reporting page. It should probably activate the busy + cursor too. + """ + larchscripts.archin(cmd, ui.command(':installrepos.active')) + + + +def pacmanoptions(text): + """A filter for pacman.conf to remove the repository info. + """ + texto = "" + block = "" + for line in text.splitlines(): + block += line + "\n" + if line.startswith("#["): + break + if line.startswith("[") and not line.startswith("[options]"): + break + if not line.strip(): + texto += block + block = "" + return texto + + + diff --git a/build_tools/l7/larch0/gui/front/page_larchify.py b/build_tools/l7/larch0/gui/front/page_larchify.py new file mode 100644 index 0000000..9868727 --- /dev/null +++ b/build_tools/l7/larch0/gui/front/page_larchify.py @@ -0,0 +1,248 @@ +# page_larchify.py - Handler for the project settings page +# +# (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.07.13 + +USERINFO = ['pw', 'maingroup', 'uid', 'skel', 'xgroups', 'expert'] + +class Larchify: + def __init__(self): + ui.widgetlist(fss('fetch_layout', 'page_larchify.uim')) + + ui.connectlist( + (':build*clicked', self.build), + (':ssh*toggled', self.sshtoggle), + (':locales*clicked', self.locales), + (':rcconf*clicked', self.rcconf), + (':initcpio*clicked', self.initcpio), + (':overlay*clicked', self.overlay), + (':utable*clicked', self.uedit), + (':useradd*clicked', self.useradd), + (':userdel*clicked', self.userdel), + (':rootpwb*clicked', self.rootpw), + ) + + self.userheaders = self.data('uheaders') + + + def data(self, key): + return ui.command('larchify_page_data.get', key) + + + def enter(self): + """This is called when the page is entered/selected/shown. + It performs initializations which depend on the state. + """ + docviewer.gohome('gui_larchify.html') + + # Check that it could possibly be an Arch installation + idir = fss('get_installation_dir') + couldBeArch = fss('isdir', ':var/lib/pacman/local') + ui.command(':build.enable', couldBeArch) + if not couldBeArch: + run_error(_("No Arch installation at %s") % idir) + + # ssh keys + sshon = fss('isfile', ':usr/bin/ssh-keygen') + ui.command(':ssh.enable', sshon) + + if fss('isfile', 'profile:nosshkeys'): + sshon = False + ui.command(":ssh.set", sshon) + + # users table + idir_normal = idir != '/' + ui.command(':users.enable', idir_normal) + if idir_normal: + # Fetch users information + fss('newUserinfo') + self.readuserinfo() + + # Root password + self.showrootpw() + + self.reenter() + + + def reenter(self): + """These also need resetting after a build run. + """ + # Whether there is an old system.sqf to reuse? + ossqf = fss('oldsqf') + if not ossqf: + ui.command(':oldsquash.set', False) + ui.command(':oldsquash.enable', ossqf) + + # Whether there is a set of old glibc locales to reuse? + olcl = fss('oldlocales') + if not olcl: + ui.command(':oldlocales.set', False) + ui.command(':oldlocales.enable', olcl) + +#TODO: Remove hack if the underlying bug gets fixed + ui.command(":larchify_advanced.enable_hack") + + + def readuserinfo(self, select=None): + """'select' should be a username, defaulting to the first entry. + """ + self.usersel = 0 + self.userlist = [] + i = 0 + for u in fss('allusers'): + self.userlist.append(self.userinfolist(u)) + if u == select: + self.usersel = i + i += 1 + ui.command(':utable.set', self.userlist, self.usersel) + + + def userinfolist(self, user): + return [user] + fss('getuserinfo', user, USERINFO) + + + def uedit(self, row, column): + if self.usersel == row: + uname = self.userlist[row][0] + ulcell = self.userlist[row][column] + if column == 4: + ok, text = self.select_skel(ulcell) + else: + ok, text = ui.command('textLineDialog', + self.userheaders[column] + ':', 'larchify', ulcell) + text = text.strip() + if ok: + try: + if (column == 0) and (text != ''): + # Rename the user, by adding a new one and deleting + # the old + uname = text + fss('newuser', uname) + i = 0 + for f in USERINFO: + i += 1 + fss('userset', uname, f, self.userlist[row][i]) + if not fss('deluser', ulcell): + run_error(self.data('rn_error')) + + else: + fss('userset', uname, USERINFO[column-1], text) + fss('saveusers') + + except: + run_error(self.data('ud_error')) + self.readuserinfo(uname) + + else: + self.usersel = row + + + def select_skel(self, current): + # Present a list of available 'skel' folders + self.skellist = [self.data('def_skel')] + for f in fss('listskels'): + self.skellist.append(f.rsplit('/skel_', 1)[1]) + try: + i = self.skellist.index(current) + except: + i = 0 + ok, skeli = ui.command('listDialog', self.data('skel_lbl'), + self.data('skel_ttl'), self.skellist, i) + if ok: + return (True, '' if skeli == self.skellist[0] + else skeli.split()[0]) + return (False, '') + + + def useradd(self): + ok, name = ui.command('textLineDialog', self.data('newlogin')) + if ok: + name = name.strip() + if name != '' and fss('newuser', name): + self.userlist.append(self.userinfolist(name)) + self.usersel = len(self.userlist) -1 + ui.command(':utable.set', self.userlist, self.usersel) + + + def userdel(self): + if self.usersel >= 0: + user = self.userlist[self.usersel][0] + if fss('deluser', user): + del(self.userlist[self.usersel]) + lu = len(self.userlist) + if lu: + if lu <= self.usersel: + self.usersel -= 1 + ui.command(':utable.set', self.userlist, self.usersel) + + + def showrootpw(self): + self.rootpw = fss('readfile', 'profile:rootpw', trap=False) + if self.rootpw == None: + self.rootpw = "" + ui.command(':rootpwe.text', self.rootpw) + + + def rootpw(self): + ok, pw = ui.command('textLineDialog', self.data('newrootpw'), + "larchify", self.rootpw) + if ok: + pw = pw.strip() + if pw: + fss('savefile', 'profile:rootpw', pw) + else: + fss('rm_rf', 'profile:rootpw') + self.showrootpw() + + + def sshtoggle(self, on): + """Whether the system ssh keys are pregenerated + depends on the presence of the profile file 'nosshkeys' + (and of course on openssh being installed). + """ + sshoff = fss('isfile', 'profile:nosshkeys') + if on: + if sshoff: + fss('rm_rf', 'profile:nosshkeys') + elif not sshoff: + fss('savefile', 'profile:nosshkeys', "Don't pregenerate ssh keys") + + + def locales(self): + edit('profile:rootoverlay/etc/locale.gen', 'install:etc/locale.gen') + + + def rcconf(self): + edit('profile:rootoverlay/etc/rc.conf', 'install:etc/rc.conf') + + + def initcpio(self): + edit('profile:rootoverlay/etc/mkinitcpio.conf.larch0', + 'install:etc/mkinitcpio.conf.larch0') + + + def overlay(self): + fss('browse', 'profile:rootoverlay') + + + def build(self): + larchscripts.larchify(ui.command(':oldsquash.active'), + ui.command(':oldlocales.active')) diff --git a/build_tools/l7/larch0/gui/front/page_medium.py b/build_tools/l7/larch0/gui/front/page_medium.py new file mode 100644 index 0000000..75f0efe --- /dev/null +++ b/build_tools/l7/larch0/gui/front/page_medium.py @@ -0,0 +1,320 @@ +# page_medium.py - Handler for the project settings page +# +# (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.07.13 + +"""This page can also handle sources other than the normal larch +installation. It should convert any larch image into +another, with different medium and/or different bootloader, so long as the +bootloader is supported by the source image. +""" + +import os +BOOTLOADERS = ('grub', 'syslinux') + +class Medium: + def __init__(self): + ui.widgetlist(fss('fetch_layout', 'page_medium.uim')) + + ui.connectlist( + (':source_larch*toggled', self.src_larch), + (':source_dev*toggled', self.src_dev), + (':source_iso*toggled', self.src_iso), + (':source_path*toggled', self.src_path), + (':source_select*clicked', self.src_select), + (':vlabelb*clicked', self.newlabel), + (':grub*toggled', self.grub), + (':syslinux*toggled', self.syslinux), + (':selectpart*clicked', self.choosepartition), + (':search*toggled', self.searchtoggled), + (':destination*toggled', self.activate_actions), + (':make_medium*clicked', self.make), + (':bootcd*clicked', self.makeboot), + ) + ui.command(':bootcd.enable', False) + + + def enter(self): + """This is called when the page is entered/selected/shown. + It performs initializations which depend on the state. + """ + self.source = '' + self.destination = '' + ui.command(':larchpart.text') + docviewer.gohome('gui_medium.html') + if ui.command(':source_larch.active'): + self.src_larch(True) + else: + ui.command(':source_larch.set', True) + ui.command(':%s.set' % fss('getitem', 'medium_search'), True) + ui.command(':vlabele.text', fss('get_mediumlabel')) + + + def data(self, key): + return ui.command('medium_page_data.get', key) + + + def setupbootloader(self, init=False): + if init: + self.bootloaders = [] + self.nosave = False + self.source = '' + for b in BOOTLOADERS: + ui.command(':%s.set' % b, False) + ui.command(':%s.enable' % b, False) + pbl = fss('getitem', 'medium_btldr') + for b in self.bootloaders: + ui.command(':%s.enable' % b, True) + if self.bootloaders: + if pbl not in self.bootloaders: + pbl = self.bootloaders[0] + ui.command(':%s.set' % pbl, True) + ui.command(':dosave.set', not self.nosave) + ui.command(':source_show.text', self.source) + self.activate_actions() + + + def activate_actions(self, *args): + # There needs to be a bootloader + bl = self.get_bootloader() + bcdok = (ui.command(':source_dev.active') + and bool(self.source) + and bool(bl)) + ui.command(':bootcd.enable', bcdok) + # If using a destination partition, that needs to be available. + wp = ui.command(':destination.active') + if wp: + # If writing to a partition without installing a bootloader + if ui.command(':nombr.active'): + bl = True # bootloader not necessary + dp = bool(self.destination) + else: + dp = True + ui.command(':make_medium.enable', bool(self.source) and bl and dp) + ui.command(':detection.enable', bcdok or wp) + + + def get_bootloader(self): + if ui.command(':grub.active'): + return 'grub' + if ui.command(':syslinux.active'): + return 'syslinux' + return None + + + def _testmedium(self, cc): + # Runs in background thread ... + self.bootloaders = [] + if not (cc & 8): + self.bootloaders.append('syslinux') + if not (cc & 16): + self.bootloaders.append('grub') + self.nosave = bool(cc & 2) + ui.idle_add(self.setupbootloader, bool(cc & 1)) + return True # return immediately to normal view + + + def src_larch(self, on): + if on: + mpath, ok, self.bootloaders, self.nosave = fss('testmedium') + if ok: + self.source = mpath + else: + self.source = '' + run_error(self.data('msg_med') % mpath) + self.setupbootloader() + + ui.command(':source_select.enable', not on) + ui.command(':chroot.set', on) + + + def src_dev(self, on): + """The source system is on a mountable device, which must be + selected from a list of available devices (via the Choose button) + and tested for validity, maybe the presence of larch/system.sqf. + """ + if on: + self.setupbootloader(init=True) + + + def src_iso(self, on): + """The source system is in an 'iso' file, which must be + selected by browsing the file system (via the Choose button) + and tested for validity, maybe the presence of larch/system.sqf. + """ + if on: + self.setupbootloader(init=True) + + + def src_path(self, on): + """The source system is in a directory, which must be + selected by browsing the file system (via the Choose button) + and tested for validity, maybe the presence of larch/system.sqf. + """ + if on: + self.setupbootloader(init=True) + + + def src_select(self): + """The function of this button varies according to the selected + source ... + """ + src = None + if ui.command(':source_dev.active'): + part = self.selectpart() + if part: + src = part + + elif ui.command(':source_iso.active'): + iso = ui.fileDialog(self.data('iso_src'), + filter=(self.data('iso_type'), '*.iso')) + if iso: + src = iso + + elif ui.command(':source_path.active'): + medium = ui.fileDialog(self.data('medium_src'), dirsonly=True) + if medium: + src = medium + + if src: + self.source = src + larchscripts.testmedium(src, self._testmedium) + + + def grub(self, on): + if on: + fss('setitem', 'medium_btldr', 'grub') + + def syslinux(self, on): + if on: + fss('setitem', 'medium_btldr', 'syslinux') + + + def newlabel(self): + ok, l = ui.command('textLineDialog', + self.data('prompt_label'), + None, fss('getitem', 'medium_label')) + if ok: + ui.command(':vlabele.text', fss('set_mediumlabel', l)) + + + def choosepartition(self): + p = self.selectpart(True) + if p: + ui.command(':larchpart.text', p) + self.destination = p + self.activate_actions() + + + def selectpart(self, write=False): + # Present a list of available partitions (only unmounted ones + # are included) + self.partlist = fss('get_partitions') + ok, choice = ui.command('listDialog', + self.data('parts_dst') if write else self.data('parts_src'), + self.data('parts_t'), + self.partlist, len(self.partlist) - 1) + # The partition to be used is fetched from the gui, so there is no + # need to save it anywhere else. + if ok: + return choice.split()[0] + else: + return None + + + def searchtoggled(self, on): + ui.command(':nolarchboot.enable', not on) + + + def make(self): + """Write the larch medium. + """ + args = ['-l', ui.command(':vlabele.get')] + if ui.command(':syslinux.active'): + args.append('-b') + + # Is it standard (using profile, etc.) or copying? + if ui.command(':source_larch.active'): + # Normal larch build + path = '' + if not ui.command(':chroot.active'): + args.append('-c') + + else: + # Copying from another larch medium + path = self.source + if ui.command(':chroot.active'): + args.append('-C') + + # Write to iso file or to partition? + if ui.command(':destination.active'): + # Write to partition + for db in ('label', 'uuid', 'device', 'search'): + if ui.command(':%s.active' % db): + detect = db + args += ['-d', detect] + if (detect != 'search') and ui.command(':nolarchboot.active'): + args.append('-n') + args.append('-a' if ui.command(':dosave.active') else '-A') + if ui.command(':noformat.active'): + args.append('-x') + if ui.command(':nombr.active'): + args.append('-m') + larchscripts.writemedium(path, args, + os.path.basename(self.destination)) + + else: + # Write an 'iso' file + df = self.isopath() + if df: + args += ['-D', df[0], '-o', df[1]] + larchscripts.writemedium(path, args) + + + def makeboot(self): + return + args = ['-l', fss('getbootisolabel')] + if ui.command(':syslinux.active'): + args.append('-b') + path = os.path.basename(self.source) + if ui.command(':chroot.active'): + args.append('-C') + df = self.isopath(bootiso=True) + if df: + args += ['-D', df[0], '-o', df[1]] + larchscripts.writemedium(path, args, 'BOOTISO') + + + def isopath(self, bootiso=False): + sdir = fss('getisosavedir') + ifname = fss('getbootisofile' if bootiso else 'getisofile') + path = ui.fileDialog(self.data('isopath'), startdir=sdir, + create=True, file=ifname, filter=(self.data('iso_type'), '*.iso')) + if path: + f = os.path.basename(path) + d = os.path.dirname(path) + if d != sdir: + fss('setitem', 'isosavedir', d) + if f != ifname: + fss('setitem', 'bootisofile' if bootiso else 'isofile', f) + return (d, f) + + return None diff --git a/build_tools/l7/larch0/gui/front/page_mediumprofile.py b/build_tools/l7/larch0/gui/front/page_mediumprofile.py new file mode 100644 index 0000000..0ebb769 --- /dev/null +++ b/build_tools/l7/larch0/gui/front/page_mediumprofile.py @@ -0,0 +1,87 @@ +# page_mediumprofile.py - Handler for the project settings page +# +# (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.07.14 + + +class MediumProfile: + def __init__(self): + ui.widgetlist(fss('fetch_layout', 'page_mediumprofile.uim')) + + ui.connectlist( + (':bootlines*clicked', self.editbootlines), + (':grubtemplate*clicked', self.editgrub), + (':syslinuxtemplate*clicked', self.editsyslin), + (':cdroot*clicked', self.browsecdroot), + (':nosessionsave*toggled', self.nosessionsave), + ) + + + def enter(self): + """This is called when the page is entered/selected/shown. + It performs initializations which depend on the state. + """ + docviewer.gohome('gui_mediumprofile.html') + ui.command(':nosessionsave.set', fss('isfile', 'profile:nosave')) + + +# def data(self, key): +# return ui.command('mediumprofile_page_data.get', key) + + + def editbootlines(self): + edit('profile:bootlines', 'base:data/bootlines') + + + def editgrub(self): + f0 = 'profile:cd-root/grub0/menu.lst' + if not fss('isfile', f0): + f0 = 'base:cd-root/grub0/menu.lst' + edit('profile:cd-root/grub/menu.lst', f0) + + + def editsyslin(self): + f0 = 'profile:cd-root/isolinux0/isolinux.cfg' + if not fss('isfile', f0): + f0 = 'base:cd-root/isolinux0/isolinux.cfg' + edit('profile:cd-root/isolinux/isolinux.cfg', f0) + + + def browsecdroot(self): + fss('browse', 'profile:cd-root') + + + def nosessionsave(self, on): + """Whether session saving is available is firstly determined by + the writability of the boot device (assuming the extension + feature to allow the use of other devices is not being used). + The standard scripts will also not offer session saving if the + file larch/nosave is present on the boot medium. + """ + ns = fss('isfile', 'profile:nosave') + if on: + if not ns: + fss('savefile', 'profile:nosave', + "Suggestion to disable session saving" + " (can be overridden)") + else: + fss('rm_rf', 'profile:nosave') + diff --git a/build_tools/l7/larch0/gui/front/page_project.py b/build_tools/l7/larch0/gui/front/page_project.py new file mode 100644 index 0000000..e9b902d --- /dev/null +++ b/build_tools/l7/larch0/gui/front/page_project.py @@ -0,0 +1,203 @@ +# page_project.py - Handler for the project settings page +# +# (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.07.13 + +import os + +class ProjectSettings: + def __init__(self): + ui.widgetlist(fss('fetch_layout', 'page_project.uim')) + + ui.connectlist( + (':choose_profile_combo*changed', self.switch_profile), + (':profile_rename*clicked', self.rename_profile), + (':profile_browse*clicked', self.browse_profile), + (':profile_delete*clicked', self.delete_profile), + (':profile_save*clicked', self.save_profile), + (':installation_path_change*clicked', self.new_build_path), + (':choose_project_combo*changed', self.switch_project), + (':new_project*clicked', self.get_new_project_name), + (':project_delete*clicked', self.delete_project), + ) + + + def setup(self): + # Initialize project combobox + self.projects = fss('get_projects') + self.project_name = fss('get_project') + try: + pix = self.projects.index(self.project_name) + except: + self.switch_project(0) + return + ui.command(':choose_project_combo.set', self.projects, pix) + # Initialize profile combobox + self.profiles = fss('get_profiles') + self.profile_name = fss('get_profile') + try: + pfix = self.profiles.index(self.profile_name) + except: + self.switch_profile(0) + pfix = 0 + ui.command(':choose_profile_combo.set', self.profiles, pfix) + # Initialize installation_dir display + self.set_build_dir(fss('get_installation_dir')) + + + def enter(self): + """This is called when the page is entered/selected/shown. + It performs initializations which depend on the state. + """ + docviewer.gohome('gui_project_settings.html') + + + def data(self, key): + return ui.command('project_page_data.get', key) + + + def set_build_dir(self, path): + self.build_dir = path + ui.command(':installation_path_show.text', self.build_dir) + ui.enable_installation_page(self.build_dir != '/') + + + def switch_profile(self, index): + """This has no effect on the display! + It is assumed that the display is already updated, or will be + updated later, and that the index is valid, so that the operation + cannot fail. + """ + self.profile_name = self.profiles[index] + fss('set_profile', self.profile_name) + + + def browse_profile(self): + source = ui.fileDialog(self.data('file_ps'), dirsonly=True, + bookmarks=fss('get_profile_bookmarks'), + startdir=fss('getitem', 'profile_browse_dir')) + if source: + fss('setitem', 'profile_browse_dir', os.path.dirname(source)) + if os.path.basename(source) in self.profiles: + if not ui.command('confirmDialog', self.data('prompt_pr')): + return + if fss('get_new_profile', source): + self.setup() + else: + run_error(self.data('msg_npd') % source) + + + def rename_profile(self): + if fss('can_rename_profile'): + ok, new = ui.command('textLineDialog', + self.data('prompt_pn'), + None, self.profile_name) + if ok: + new = new.strip() + if new in self.profiles: + ui.command('warningDialog', self.data('prompt_pe') % new) + else: + fss('rename_profile', new) + self.setup() + else: + ui.command('infoDialog', self.data('msg_pu')) + + + def save_profile(self): + bookmarks = fss('get_profile_bookmarks') + startdir = fss('getitem', 'profile_browse_dir') + path = ui.fileDialog(self.data('file_sp'), + create=True, file=self.profile_name, + bookmarks=bookmarks, + startdir=startdir if startdir else bookmarks[0][0]) + if path: + fss('setitem', 'profile_browse_dir', os.path.dirname(path)) + ok = fss('save_profile', path, False) + if ok == False: + if ui.command('confirmDialog', self.data('prompt_dr')): + # Force overwrite + fss('save_profile', path, True) + elif ok == None: + run_error(self.data('msg_piu')) + else: + self.setup() + + + def delete_profile(self): + plist = fss('list_free_profiles') + if plist: + ok, item = ui.command('listDialog', self.data('prompt_dp'), + self.data('delprof'), plist) + if ok: + if fss('delete_profile', item): + self.setup() + else: + ui.command('infoDialog', self.data('msg_dpff') % item) + else: + ui.command('infoDialog', self.data('msg_npf')) + + + def new_build_path(self): + # Is anything more necessary? Do I need to test or create the path? + # I don't think so, the installation code does that. + # If the path is "/", the installation page should be inhibited, + # but that is handled by 'setup'. + ok, path = ui.command('textLineDialog', + self.data('prompt_ip'), + None, self.build_dir) + if ok: + path = fss('set_installation_dir', path) + if path: + self.set_build_dir(path) + + + def switch_project(self, index): + fss('set_project', self.projects[index]) + self.setup() + + + def get_new_project_name(self): + ok, name = ui.command('textLineDialog', + self.data('prompt_np'), + None, self.project_name) + if ok: + if name in self.projects: + run_error(self.data('msg_pe') % name) + else: + fss('set_project', name) + self.setup() + + + def delete_project(self): + """Pop up a list of eligible project names, the selected one + will be deleted. + """ + plist = fss('list_free_projects') + if plist: + ok, item = ui.command('listDialog', self.data('prompt_pd'), + self.data('delproj'), plist) + if ok: + fss('delete_project', item) + self.setup() + else: + ui.command('infoDialog', self.data('msg_np')) + + diff --git a/build_tools/l7/larch0/gui/front/uim.py b/build_tools/l7/larch0/gui/front/uim.py new file mode 100644 index 0000000..71e106b --- /dev/null +++ b/build_tools/l7/larch0/gui/front/uim.py @@ -0,0 +1,1327 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# uim.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.18.07 + +#TODO? +# 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. + + +"""UIM - 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 +#try: +# import json +#except: +# import simplejson as json + +#++++++++++++++++++++++++++++++++++++++++++++++++++++ +#TODO +# Add more widgets +# Add more attribute handling +# Add more signal handling + +#---------------------------------------------------- + +def debug(text): + sys.stderr.write("GUI: %s\n" % text) + sys.stderr.flush() + + +# 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(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): + 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. It also supports + a 'busy' mechanism. + """ + def __init__(self): + QtGui.QWidget.__init__(self) #qt + self.closesignal = "" + self.busystate = False + self.busy_lock = threading.Lock() + + 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 + + def x__busy(self, widgets, 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. + """ + # I couldn't get the following calls to work: + # w.setCursor(QtCore.Qt.BusyCursor) + # w.unsetCursor() + self.busy_lock.acquire() + if on: + if self.busystate: + debug("*ERROR* Attempt to set busy state twice") + self.busy_lock.release() + return + self.busycursor = busycursor + if busycursor: + guiapp.setOverrideCursor(QtCore.Qt.BusyCursor) #qt + else: + if not self.busystate: + debug("*ERROR* Attempt to release busy state twice") + self.busy_lock.release() + return + if self.busycursor: + guiapp.restoreOverrideCursor() #qt + self.busystate = on + self.busy_lock.release() + for wn in widgets: + w = guiapp.getwidget(wn) + if w: + w.setEnabled(not on) #qt + else: + debug("*ERROR* No widget '%s'" % wn) + + +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=None, title=None, text="", pw=False): + if label == None: + label = "Enter the value here:" + if title == None: + title = "Enter Information" + if pw: + echo = QtGui.QLineEdit.Password #qt + else: + echo = QtGui.QLineEdit.Normal #qt + result, ok = QtGui.QInputDialog.getText(None, #qt + title, label, echo, text) #qt + return (ok, unicode(result)) + + +def listDialog(label=None, title=None, items=[], current=0): + if label == None: + label = "Choose one of the entries" + if title == None: + title = "Select an item" + item, ok = QtGui.QInputDialog.getItem(None, title, label, items, + current, editable=False) #qt + return (ok, unicode(item)) + + +def confirmDialog(message, title=None): + if title == None: + title = "Confirmation" + return (QtGui.QMessageBox.question(None, title, message, #qt + QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel) == #qt + QtGui.QMessageBox.Yes) #qt + + +def infoDialog(message, title=None): + if title == None: + title = "Information" + QtGui.QMessageBox.information(None, title, message) #qt + + +#+++++++++++++++++++++++++++ +# Error handling +def gui_error(message, title=None): + if title == None: + title = "Error" + QtGui.QMessageBox.critical(None, title, message) #qt + guiapp.exit(1) #qt + +def gui_warning(message, title=None): + if title == None: + title = "Warning" + QtGui.QMessageBox.warning(None, title, message) #qt + +def onexcept(text): + debug(traceback.format_exc()) + gui_error(text, "Exception") +#--------------------------- + +fileDialogDir = "/" +def fileDialog(message, start=None, title=None, dir=False, create=False, + file=None, urls=None, filter=None): + # filter is a list: first a textual description, then acceptable glob filenames + global fileDialogDir + if not start: + start = fileDialogDir + dlg = QtGui.QFileDialog(None, message, start) #qt + if title: + dlg.setWindowTitle(title) #qt + dlg.setReadOnly(not create) #qt + if dir: + dlg.setFileMode(dlg.Directory) #qt + elif not create: + dlg.setFileMode(dlg.ExistingFile) #qt + if filter: + dlg.setNameFilter("%s (%s)" % (filter[0], " ".join(filter[1:]))) #qt + + if urls: + urlsqt = [ QtCore.QUrl.fromLocalFile(u[0]) for u in urls ] #qt + urlsqt.append(QtCore.QUrl.fromLocalFile( + QtGui.QDesktopServices.storageLocation( + QtGui.QDesktopServices.HomeLocation))) + + dlg.setSidebarUrls(urlsqt) #qt + + if file: + dlg.selectFile(file) + + if dlg.exec_(): + path = str(dlg.selectedFiles()[0]).strip() + if os.path.isdir(path): + fileDialogDir = path + elif os.path.isfile(path): + fileDialogDir = os.path.dirname(path) + return path + else: + return "" + + +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) + + +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): + 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): + 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 Uim(QtGui.QApplication): + """This class represents an application gui, possibly with more than + one top level window. + """ + timers = [] # timer objects + + def __init__(self): + global guiapp + guiapp = self + self.eno = QtCore.QEvent.registerEventType() #qt + QtGui.QApplication.__init__(self, []) #qt + 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): +# self.quit() #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. + """ + Uim.timers.append(Timer(callback, period)) + + +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 + Uim.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": fileDialog, + "listDialog": listDialog, +} + +layout_table = { + "VBOX": VBOX, + "HBOX": HBOX, + "GRID": GRID, +# "+": GRIDROW, + "-": CSPAN, + "|": RSPAN, + "*": SPACE, + "VLINE": VLINE, + "HLINE": HLINE, +} + |