diff options
author | James Meyer <james.meyer@operamail.com> | 2010-11-04 18:06:25 (GMT) |
---|---|---|
committer | James Meyer <james.meyer@operamail.com> | 2010-11-04 18:06:27 (GMT) |
commit | 429f14eea9f4c00ddd8d82f25612213df8d4af84 (patch) | |
tree | d8441dd4ef5ef539c0b263b138d36fb1995c38d8 /build_tools/larch7/larch0/gui | |
parent | 11ef4af01d6e197a54d0759e688ab5cbd336be4b (diff) | |
download | linhes_dev-429f14eea9f4c00ddd8d82f25612213df8d4af84.zip |
add profiles for larch 7
Diffstat (limited to 'build_tools/larch7/larch0/gui')
27 files changed, 5336 insertions, 0 deletions
diff --git a/build_tools/larch7/larch0/gui/MEDIUM_README b/build_tools/larch7/larch0/gui/MEDIUM_README new file mode 100644 index 0000000..ede0975 --- /dev/null +++ b/build_tools/larch7/larch0/gui/MEDIUM_README @@ -0,0 +1,14 @@ +Medium building + +In the cli, when -S--source is passed, no chroot is used for building unless +explicitly requested via the -C/--chroot option. When no -S--source is passed, +chroot will be used unless the -c/--nochroot option is passed. + +In the gui the default source is the medium directory within the installation +dir (as built by the previous stages). +If this is selected chroot commands will be used for the +medium building unless explicitly overridden (uncheck the chroot CheckBox). +If another source is selected chroot commands will only be used if explicitly +requested (check the chroot CheckBox). The chroot dir would be the project's +installation dir (so if this is '/' chroot is never available). + diff --git a/build_tools/larch7/larch0/gui/askpass.py b/build_tools/larch7/larch0/gui/askpass.py new file mode 100755 index 0000000..ac65325 --- /dev/null +++ b/build_tools/larch7/larch0/gui/askpass.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# +""" +# One possibility: +from PyQt4 import QtGui + +app = QtGui.QApplication([]) +result, ok = QtGui.QInputDialog.getText(None, "sudo", + "Please enter the password to run as administrator", + QtGui.QLineEdit.Password) + + +print result +#exit(0 if ok else 1) +""" + +# This version connects via a socket to the main application +import socket +import sys + +port = '\0larch-sudopw' +data = 'pw-get' + +# Create a socket (SOCK_STREAM means a TCP socket) +sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + +# Connect to server and send data +sock.connect(port) +sock.send(data + '\n') + +# Receive data from the server and shut down +received = sock.recv(1024) +sock.close() + +print received diff --git a/build_tools/larch7/larch0/gui/controller.py b/build_tools/larch7/larch0/gui/controller.py new file mode 100644 index 0000000..2025301 --- /dev/null +++ b/build_tools/larch7/larch0/gui/controller.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python +# +# controller.py - Manages file-system access and calling of larch scripts +# +# (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 + +import sys, os, pwd, traceback, __builtin__ +try: + import json as serialize +except: + import simplejson as serialize +from subprocess import Popen, PIPE, call +import threading +import re +import SocketServer + +from config import * +start_translator(switchC=False) + + +exports = {} +def add_exports(elist): + for key, method in elist: + exports[key] = method + +__builtin__.add_exports = add_exports + + +def error0(message): + sys.stderr.write('>>ERROR>>' + message + '\n') + sys.stderr.flush() +__builtin__.error0 = error0 + + +class Fs: + """Collect file system access methods in one class. + """ + def __init__(self): + add_exports( ( + ('fetch_layout', self.fetch_layout), + ('isfile', self.isfile), + ('isdir', self.isdir), + ('rm_rf', self.rm_rf), + ('get_partitions', self.get_partitions), + ('readfile', self.readfile), + ('savefile', self.savefile), + ('get_docs_url', self.get_docs_url), + ('oldsqf', self.oldsqf), + ('oldlocales', self.oldlocales), +# ('getpath', self.getpath), + ('browse', self.browse)) + ) + + def fetch_layout(self, layout_file): + fh = open(base_dir + '/gui/layouts/' + layout_file) + r = fh.read() + fh.close() + return (True, eval(r)) + + def rm_rf(self, path): + call(['rm', '-rf', self._getpath(path)]) + return (True, None) + + +# def exists(self, path): +# return os.path.exists(self._getpath(path)) + +# def copy(self, src, dst): +# shutil.copytree(self._getpath(src), self._getpath(dst)) + + + def oldsqf(self): + return (True, + os.path.isfile(self._getpath('install:' + CHROOT_SYSTEMSQF)) + or os.path.isfile(self._getpath('install:' + CHROOT_DIR_MEDIUM + + '/larch/system.sqf'))) + + def oldlocales(self): + return (True, os.path.isdir(self._getpath('install:%s/locale' + % CHROOT_DIR_BUILD))) + + def isfile(self, path): + return (True, os.path.isfile(self._getpath(path))) + + def isdir(self, path): + return (True, os.path.isdir(self._getpath(path))) + + + def browse(self, path): + fpath = self._getpath(path) + if call(['mkdir', '-p', fpath]) == 0: + # Start file browser at fpath + call(project_manager.appget('filebrowser').replace('$', fpath) + + ' &', shell=True) + return (True, None) + else: + return (False, None) + + +# def makedirs(self, path): +# os.makedirs(self._getpath(path)) +# return (True, None) + + def readfile(self, f): + f = self._getpath(f) + try: + fh = open(f) + r = fh.read() + fh.close() + return (True, r) + except: + return (False, _("Couldn't read file '%s'") % f) + + def savefile(self, f, d): + f = self._getpath(f) + dir = os.path.dirname(f) + if not os.path.isdir(dir): + os.makedirs(dir) + try: + fh = open(f, "w") + fh.write(d) + fh.close() + return (True, None) + except: + return (False, _("Couldn't save file '%s'") % f) + + def _getpath(self, f): + if f[0] != "/": + base, f = f.split(':') + f = '/' + f + if base == 'base': + f = base_dir + f + elif base == 'profile': + f = project_manager.profile_path + f + elif base == 'working': + f = project_manager.project_dir + f + else: + f = project_manager.get_ipath()[1] + f + return f + + def get_docs_url(self, page): + if lang and (len(lang) > 1): + p = base_dir + ('/docs/%s/html/' % lang[0:2]) + page + if os.path.isfile(p): + return (True, p) + return (True, base_dir + '/docs/html/' + page) + + + def get_partitions(self): + """Get a list of available partitions (only unmounted ones + are included). + """ + # First get a list of mounted devices + mounteds = [] + fh = open('/etc/mtab') + for l in fh: + dev = l.split()[0] + if dev.startswith('/dev/sd'): + mounteds.append(dev[5:]) + fh.close() + # Get a list of partitions + partlist = [] + fh = open('/proc/partitions') + for l in fh: + fields = l.split() + if len(fields) == 4: + dev = fields[3] + if dev.startswith('sd') and (dev[-1] in '0123456789'): + size = (int(fields[2]) + 512) / 1024 + if (size > 200) and (dev not in mounteds): + # Keep a tuple (partition, size in MiB) + partlist.append("%-12s %12s MiB" + % ('/dev/' + dev, size)) + fh.close() + return (True, partlist) + + + +class LarchScripts: + """This class deals with calling the larch scripts. + As they must be run as root a separate dispatcher process, running as + root, is used to call the actual scripts. The dispatcher is started + using 'sudo'. + A call will be initiated from the gui, and is sent to the dispatcher, + which starts the process and returns the output when it is available. + If the script is interactive, it might also require input, which can be + passed via the dispatcher. + While reading output from the dispatcher the gui must remain responsive, + so that the view can be switched and the subprocess aborted, if desired. + To achieve this a separate thread is used for reading input from the + dispatcher, together with a mechanism for activating a handler in a + thread-safe way, 'ui.idle_add'. + """ + def __init__(self): + self.larch_dispatcher = None # dispatcher subprocess + self.progress_waiting = None # used by progress widget + + + def call(self, cmd, arg=[], atend=None): + self.cmd = cmd + self.arg = arg + # Callback on completion: + self.atend = atend # returns True for automatic return to normal view + if self.larch_dispatcher: + self.runcmd() + + else: + # Start a socket server to handle password requests + # Use a unix domain server in the abstract namespace + port = '\0larch-sudopw' + # Create the server + self.sserver = SocketServer.UnixStreamServer(port, MyHandler) + self.sst = threading.Thread(target=self.sserver.serve_forever, + args=()) + self.sst.start() + # Handle one request + #self.sserver.handle_request() + + # Start the larch dispatcher script + os.environ['SUDO_ASKPASS'] = base_dir + '/gui/askpass.py' + self._sudo_wait() + dispatcher = Popen(['sudo', '-A', '-k', + base_dir + '/gui/dispatcher.py'], + stdin=PIPE, + stdout=PIPE, + stderr=PIPE) + + # And a thread to read its output + self.istream = dispatcher.stdin + self.estream = dispatcher.stderr + self.t = threading.Thread(target=self.readinput, args=(dispatcher,)) + self.t.start() + + + def runcmd(self): + progress.start() # initialize progress widget + ui.runningtab(1) # switch view to progress widget + # Run command: + cx = '%s %s:%s\n' % (self.cmd, project_manager.project_dir, + serialize.dumps(self.arg)) + logger.addLine('++' + cx) + self.istream.write(cx) + self.istream.flush() + r = self.geterr() + if r: + ui.command('infoDialog', r, 'BUG') + + + def geterr(self): + r = "" + while True: + rx = self.estream.readline() + if rx: + rx = rx.strip() + else: + break + if rx == '!+': + break + if r: + r +='\n' + r += rx + return r + + + def readinput(self, dispatcher): + ostream = dispatcher.stdout + while True: + line = ostream.readline() + if not line: + break + id, line = line.rstrip().split(':', 1) + try: + if line[0] == '=': + self._stop_server() + # The dispatcher has just started, make it available + self.larch_dispatcher = dispatcher + # Reenable the gui, and queue the command + ui.idle_add(self._dispatcher_started, self.runcmd) + continue + elif line[0] != '/': + line = serialize.loads(line) + except: + line = '[[%s]]' % line + ui.idle_add(self.addline, line) # cross-thread call + + if self.larch_dispatcher == None: + self._stop_server() + ui.idle_add(self._dispatcher_started, None) + + + + def _sudo_wait(self): + ui.command(':larch.busy', [':larch'], True) + + + def _dispatcher_started(self, cmd): + if cmd: + cmd() + else: + ui.command('infoDialog', + ("%s:\n %s" % (ui.data('authfail'), self.geterr())), + 'sudo') + ui.command(':larch.busy', [':larch'], False) + + + def _stop_server(self): + # Stop the password socket-server + self.sserver.shutdown() + self.sserver = None + + + def close(self): + if self.larch_dispatcher: + self.istream.write('quit\n') + self.istream.flush() + self.larch_dispatcher.wait() + + + def interrupt(self): + logger.addLine('--Terminate--') + self.istream.write('kill\n') + self.istream.flush() + + + def addline(self, message): + """A line has been received from the script. + This must be handled in the main thread. + The input lines are filtered for pacman, mksquashfs and mkisofs + progress output so that appropriate progress reports can be given. + """ + if 'pacman:' in message: + progress.set(message[2:]) + if message.endswith('|100'): + progress.set() + message = message.rsplit('|', 1)[0].rstrip() + else: + return + + if 'mksquashfs:' in message: + progress.set(message[2:]) + self.progress_waiting = message + return + + if 'mkisofs:' in message: + progress.set(message[2:]) + self.progress_waiting = ">_mkisofs: completed" + return + + if self.progress_waiting: + progress.set() + progress.addLine(self.progress_waiting) + self.progress_waiting = None + + progress.addLine(message) + if message[0] == '/': + # process finished: + auto = False + if self.atend: + auto = self.atend(int(message[1:])) + progress.end(auto) + self.cmd = None + elif message.startswith('?>'): + # a query (yes/no) + # Pop up the query + reply = '??YES' if ui.command('confirmDialog', + message[2:], self.cmd) else '??NO' + self.istream.write(reply + '\n') + self.istream.flush() + + + def archin(self, cmd, installrepos): + args = ['-p', project_manager.profile_path, + '-i', project_manager.get_ipath()[1], + '-c', project_manager.get('pacman_cache')] + rf = project_manager.project_dir + '/pacman.conf.repos' + if installrepos and os.path.isfile(rf): + args += ['-r', rf] + self.call('archin', args + cmd.split()) + + + def larchify(self, oldsyssqf, oldlocales): + args = ['-p', project_manager.profile_path, + '-i', project_manager.get_ipath()[1],] + if oldsyssqf: + args.append('-o') + if oldlocales: + args.append('-l') + self.call('larchify', args) + + + def testmedium(self, path, atend): + self.call('live_iso', ['-T', '-S', path,], atend=atend) + + + def writemedium(self, path, args, dest=None): + if dest == 'BOOTISO': + args.append(path) + cmd = 'boot_iso' + else: + if path: + args += ['-S', path] + else: + args += ['-p', project_manager.profile_path] + args += ['-i', project_manager.get_ipath()[1]] + if dest != None: + args.append(dest) + cmd = 'live_part' + else: + cmd = 'live_iso' + self.call(cmd, args) + + + +class MyHandler(SocketServer.StreamRequestHandler): + def handle(self): + self._event = threading.Event() + # self.rfile is a file-like object created by the handler; + # we can now use e.g. readline() instead of raw recv() calls + data = self.rfile.readline().strip() + if data == 'pw-get': + ui.idle_add(self.dialog) + self._event.wait() + # Likewise, self.wfile is a file-like object used to write back + # to the client + self.wfile.write(self.pw) + + def dialog(self): + ok, self.pw = ui.command('textLineDialog', + ui.data('getpw'), + 'sudo', '', True) + if not ok: + self.pw = '' + self._event.set() + + + +fs = Fs() + +def filesystem(key, *args): + return exports[key](*args) + +__builtin__.filesystem = filesystem +__builtin__.larchscripts = LarchScripts() + +import project +project_manager.init() + diff --git a/build_tools/larch7/larch0/gui/dirview.py b/build_tools/larch7/larch0/gui/dirview.py new file mode 100755 index 0000000..303f9b6 --- /dev/null +++ b/build_tools/larch7/larch0/gui/dirview.py @@ -0,0 +1,172 @@ +#!/usr/bin/python + + +# Next look at switching to a selected directory from the path (DONE), +# switching to a directory in the list, and removing/changing toolbar +# buttons (and their actions). +# Have a checkbutton for hidden files / directories somewhere. + + +import os +from PyQt4 import QtGui, QtCore + +def clicked(r, c): + print r, c + +def iclicked(item): + print item + + +class DirListing(QtGui.QTreeWidget): #qt + # Only using top-level items of the tree + def __init__(self): + QtGui.QTreeWidget.__init__(self) #qt + self._hcompact = False # used for scheduling header-compaction + self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + self.headers(['Name']) #qt + self.setRootIsDecorated(False) #qt + + self.connect(self, QtCore.SIGNAL('itemSelectionChanged()'), + self.s_select) + self.connect(self, QtCore.SIGNAL('itemClicked(QTreeWidgetItem *,int)'), + self.s_clicked) + + + def s_select(self): + # Signal a selection change, passing the new selection list (indexes) + s = [self.indexOfTopLevelItem(i) for i in self.selectedItems()] #qt + print "Sel", s + + + def s_clicked(self, item, col): #qt + # I guess I should use this for selection if using single + # click actions, because setting a list up might cause the + # first item to be selected (it doesn't, actually, so select + # could be used), and it should + # only change directory if actually clicked. + + + """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 + print ix, col + + + + def headers(self, headers): #qt + self.setHeaderLabels(headers) #qt + if self._hcompact: + self._compact() + + def set(self, items, index=-1): #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 + + + +def dirsel(action): + print action.xtag + i = 0 + if action.xindex == 0: + print '/' + else: + path = '' + while i < action.xindex: + i += 1 + path += '/' + dirs[i] + print path + setlisting(path) +# the toolbuttons should stay the same until a different lower directory +# is chosen (one not in the old list?) + + +def setlisting(path): + dlist = os.listdir(path) + dldir = [] + dlfile = [] + for f in dlist: + if os.path.isdir(path + '/' + f): + dldir.append('d:' + f) + else: + dlfile.append('f:' + f) + dldir.sort() + dlfile.sort() + listing.set([d] for d in (dldir + dlfile)) + + +if __name__ == '__main__': + + import sys + + app = QtGui.QApplication(sys.argv) + app.setStyleSheet(""" + QToolButton { + border: 2px solid #8f8f91; + border-radius: 6px; + background-color: yellow; + } + + QToolButton:checked { + background-color: #f0c080; + } +""") + + window = QtGui.QWidget() + listing = DirListing() + bar = QtGui.QToolBar() + bar.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly) + actg = QtGui.QActionGroup(bar) + QtCore.QObject.connect(actg, QtCore.SIGNAL('triggered (QAction *)'), dirsel) + actg.setExclusive(True) + + layout = QtGui.QVBoxLayout() + layout.addWidget(bar) + layout.addWidget(listing) + window.setLayout(layout) + window.resize(600, 480) + + + + path = '/home/mt/DATA/pyjamas' + + + dirs = path.split('/') +# dirs = ['', 'home', 'mt', 'DATA', 'software-verylong', 'DOCS', 'python_qt'] + butix = 0 + for but in dirs: + bw = bar.addAction(but+'/') + bw.setCheckable(True) + actw = actg.addAction(bw) + actw.xtag = but + actw.xindex = butix + butix += 1 + + setlisting(path) + + window.show() + + sys.exit(app.exec_()) diff --git a/build_tools/larch7/larch0/gui/dispatcher.py b/build_tools/larch7/larch0/gui/dispatcher.py new file mode 100755 index 0000000..bcdc94c --- /dev/null +++ b/build_tools/larch7/larch0/gui/dispatcher.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# +# dispatcher.py - Subprocess running as root to call larch scripts +# +# (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 + +import os, sys, threading, signal +from subprocess import Popen, PIPE, STDOUT +try: + import json as serialize +except: + import simplejson as serialize + +module_dir = os.path.dirname(os.path.realpath(__file__)) +base_dir = os.path.dirname(module_dir) + + +def out(key, line): + sys.stdout.write(str(key) + ':' + line + '\n') + sys.stdout.flush() + +# Signal dispatcher started +out(0, '==') + +def input_reader(): + global process + id = process.pid + while True: + line = process.stdout.readline() + if not line: + break + + out(id, line.rstrip()) + + process.wait() + out(id, '/%d' % process.returncode) + process = None + +def kill(): + """Kill subprocesses (well, interrupt actually). + """ + if process: + os.killpg(process.pid, signal.SIGINT) + + +process = None +while True: + line = sys.stdin.readline() + if line: + line = line.rstrip() + else: + # Controlling process died + break + cmdx = line.split(None, 1) + cmd = cmdx[0] + response = '' + if cmd in ('archin', 'larchify', 'live_iso', 'live_part', 'boot_iso', + 'test'): + # Start a larch script, together with a thread to read from it. + # The arguments are passed as a serialized list + if process: + response = ("!*** Bug (Dispatcher ERROR):" + " process already running (%d)" % process.pid) + else: + cmdx2 = cmdx[1].split(':', 1) + cmds = ([base_dir + '/cli/%s.py' % cmd, '-s'] + + serialize.loads(cmdx2[1])) + process = Popen(cmds, + preexec_fn=os.setpgrp, # to facilitate interruption + cwd=cmdx2[0], # set to project directory + stdin=PIPE, + stdout=PIPE, + stderr=STDOUT) + t = threading.Thread(target=input_reader, args=()) + t.start() + + elif cmd.startswith('??'): + process.stdin.write(cmd + '\n') + process.stdin.flush() + elif cmd == 'kill': + kill() + elif cmd != 'quit': + response = "!*** Bug (Dispatcher command): " + line + sys.stderr.write(response + '\n!+\n') + sys.stderr.flush() + if cmd == 'quit': + break + +kill() diff --git a/build_tools/larch7/larch0/gui/front/docviewer.py b/build_tools/larch7/larch0/gui/front/docviewer.py new file mode 100644 index 0000000..a0af083 --- /dev/null +++ b/build_tools/larch7/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/larch7/larch0/gui/front/editor.py b/build_tools/larch7/larch0/gui/front/editor.py new file mode 100644 index 0000000..649f55c --- /dev/null +++ b/build_tools/larch7/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/larch7/larch0/gui/front/logview.py b/build_tools/larch7/larch0/gui/front/logview.py new file mode 100644 index 0000000..b9d2ac3 --- /dev/null +++ b/build_tools/larch7/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/larch7/larch0/gui/front/mainwindow.py b/build_tools/larch7/larch0/gui/front/mainwindow.py new file mode 100644 index 0000000..407b46a --- /dev/null +++ b/build_tools/larch7/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/larch7/larch0/gui/front/page_installation.py b/build_tools/larch7/larch0/gui/front/page_installation.py new file mode 100644 index 0000000..111e247 --- /dev/null +++ b/build_tools/larch7/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/larch7/larch0/gui/front/page_larchify.py b/build_tools/larch7/larch0/gui/front/page_larchify.py new file mode 100644 index 0000000..9868727 --- /dev/null +++ b/build_tools/larch7/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/larch7/larch0/gui/front/page_medium.py b/build_tools/larch7/larch0/gui/front/page_medium.py new file mode 100644 index 0000000..75f0efe --- /dev/null +++ b/build_tools/larch7/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/larch7/larch0/gui/front/page_mediumprofile.py b/build_tools/larch7/larch0/gui/front/page_mediumprofile.py new file mode 100644 index 0000000..0ebb769 --- /dev/null +++ b/build_tools/larch7/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/larch7/larch0/gui/front/page_project.py b/build_tools/larch7/larch0/gui/front/page_project.py new file mode 100644 index 0000000..e9b902d --- /dev/null +++ b/build_tools/larch7/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/larch7/larch0/gui/front/uim.py b/build_tools/larch7/larch0/gui/front/uim.py new file mode 100644 index 0000000..71e106b --- /dev/null +++ b/build_tools/larch7/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, +} + diff --git a/build_tools/larch7/larch0/gui/larch.py b/build_tools/larch7/larch0/gui/larch.py new file mode 100755 index 0000000..bddc78c --- /dev/null +++ b/build_tools/larch7/larch0/gui/larch.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# +# larch.py - GUI for the larch scripts +# +# (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.06.27 + +import sys, os +dirname = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(dirname + '/front') +sys.path.append(os.path.dirname(dirname) + '/cli') + +import controller +from mainwindow import start + +# Note that the gui module must have a reference point for relative +# paths, so the current directory must be set to the larch base directory +# before starting the gui: +os.chdir(controller.base_dir) +start() diff --git a/build_tools/larch7/larch0/gui/layouts/docviewer.uim b/build_tools/larch7/larch0/gui/layouts/docviewer.uim new file mode 100644 index 0000000..8e85a0f --- /dev/null +++ b/build_tools/larch7/larch0/gui/layouts/docviewer.uim @@ -0,0 +1,72 @@ +# docviewer.uim - The layout for the documentation viewer widget +# +# (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.05.21 + +[ + ['Frame', 'doc:page', + { 'layout': + ['VBOX', + ['HBOX', 'doc:header', '*', 'doc:back', 'doc:forward', + 'doc:home', 'doc:parent', 'doc:hide'], + 'doc:content' + ] + } + ], + ['Label', 'doc:header', + { 'markup': ['h2', _("Documentation")] + } + ], + ['HtmlView', 'doc:content', {}], + ['Button', 'doc:hide', + { 'text': _("Hide"), + 'tt': _("Return to the larch controls"), + 'clicked': '' + }, + ], + ['Button','doc:back', + { 'icon': 'left', + 'tt': _("Go back in the viewing history"), + 'clicked': '' + }, + ], + ['Button','doc:forward', + { 'icon': 'right', + 'tt': _("Go forward in the viewing history"), + 'clicked': '' + }, + ], + + ['Button','doc:home', + { 'icon': 'reload', + 'tt': _("Reload the documentation for the current larch tab"), + 'clicked': '' + }, + ], + + ['Button','doc:parent', + { 'icon': 'up', + 'tt': _("Go to the general larch documentation index"), + 'clicked': '' + }, + ], + +] diff --git a/build_tools/larch7/larch0/gui/layouts/editor.uim b/build_tools/larch7/larch0/gui/layouts/editor.uim new file mode 100644 index 0000000..2338f6e --- /dev/null +++ b/build_tools/larch7/larch0/gui/layouts/editor.uim @@ -0,0 +1,92 @@ +# editor.uim - The layout for the editor widget +# +# (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.05.21 + +[ + ['Frame', 'edit:page', + { 'layout': + ['VBOX', + ['HBOX', 'edit:header', '*', 'edit:title'], + ['HBOX', 'edit:content', + ['VBOX', 'edit:copy', 'edit:cut', 'edit:paste', + 'edit:undo', 'edit:redo', 'edit:revert', + '*', 'edit:cancel', 'edit:ok' + ] + ] + ] + } + ], + ['Label', 'edit:header', + { 'markup': ['h2', _("Editor")] + } + ], + ['Label', 'edit:title', {}], + ['TextEdit', 'edit:content', {}], + ['Button', 'edit:ok', + { 'text': _('OK'), + 'clicked': '' + } + ], + ['Button', 'edit:cancel', + { 'text': _('Cancel'), + 'clicked': '' + } + ], + ['Button', 'edit:revert', + { 'text': _('Revert'), + 'tt': _('Restore the text to its initial/default state'), + 'clicked': '' + } + ], + ['Button', 'edit:copy', + { 'text': _('Copy'), + 'clicked': '' + } + ], + ['Button', 'edit:cut', + { 'text': _('Cut'), + 'clicked': '' + } + ], + ['Button', 'edit:paste', + { 'text': _('Paste'), + 'clicked': '' + } + ], + ['Button', 'edit:undo', + { 'text': _('Undo'), + 'clicked': '' + } + ], + ['Button', 'edit:redo', + { 'text': _('Redo'), + 'clicked': '' + } + ], + + ['DATA', 'editor_data', + { 'messages': + { 'msg_dflt': _("Editing '%s'") + } + }, + ], +] diff --git a/build_tools/larch7/larch0/gui/layouts/logger.uim b/build_tools/larch7/larch0/gui/layouts/logger.uim new file mode 100644 index 0000000..0ecb7bf --- /dev/null +++ b/build_tools/larch7/larch0/gui/layouts/logger.uim @@ -0,0 +1,57 @@ +# logger.uim - The layout for the logging widget +# +# (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.05.22 + +[ + ['Frame', 'log:page', + { 'layout': + ['VBOX', + 'log:header', + ['HBOX', + 'log:text', + ['VBOX', 'log:clear', '*', 'log:hide'] + ] + ] + } + ], + ['Label', 'log:header', + { 'markup': ['', ['h2', _("Low-level Command Logging")], + ['p', _("Here you can follow the detailed, low-level" + " progress of the commands.")]] + } + ], + ['TextEdit', 'log:text', + { 'ro': True + } + ], + ['Button', 'log:clear', + { 'text': _("Clear"), + 'clicked': '' + } + ], + ['Button', 'log:hide', + { 'text': _("Hide"), + 'tt': _("Go back to the larch controls"), + 'clicked': '' + } + ] +] diff --git a/build_tools/larch7/larch0/gui/layouts/page_installation.uim b/build_tools/larch7/larch0/gui/layouts/page_installation.uim new file mode 100644 index 0000000..0949dcd --- /dev/null +++ b/build_tools/larch7/larch0/gui/layouts/page_installation.uim @@ -0,0 +1,179 @@ +# page_installation.uim - The layout 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.06.24 + +[ + ['Page', ':page_installation', + { 'layout': + ['VBOX', + ['HBOX', ':edit_profile', '*', ':pacmanops'], + '*', ':editmirrorlist', + ':settings_advanced', + 'HLINE', + ['HBOX', '*', ':install'] + ] + } + ], + # - - - - The profile editing frame + ['Frame', ':edit_profile', + { 'text': _("Edit Profile"), + 'layout': + ['VBOX', + ':addedpacks', + ':vetopacks', + ':pacmanconf', + ':repos' + ] + } + ], + ['Button', ':addedpacks', + { 'text': _("Edit 'addedpacks'"), + 'tt': _("Edit the list of packages to be installed") + }, + 'clicked' + ], + ['Button', ':vetopacks', + { 'text': _("Edit 'vetopacks'"), + 'tt': _("Edit the list of packages NOT to install") + }, + 'clicked' + ], + ['Button', ':pacmanconf', + { 'text': _("Edit pacman.conf options"), + 'tt': _("Edit pacman.conf options - not the repositories") + }, + 'clicked' + ], + ['Button', ':repos', + { 'text': _("Edit pacman.conf repositories"), + 'tt': _("Edit the repository entries for pacman.conf") + }, + 'clicked' + ], + + # - - - - The installed package tweaking frame + ['OptionalFrame', ':pacmanops', + { 'text': _("Tweak Installed Packages"), + 'layout': + ['VBOX', ':sync', ':update', ':add', ':remove'] + } + ], + ['Button', ':sync', + { 'text': _("Synchronize db"), + 'tt': _("Synchronize the pacman db on the target (pacman -Sy)") + }, + 'clicked' + ], + ['Button', ':update', + { 'text': _("Update / Add package [-U]"), + 'tt': _("Update / Add a package from a package file" + " using pacman -U") + }, + 'clicked' + ], + ['Button', ':add', + { 'text': _("Add package(s) [-S]"), + 'tt': _("Add one or more packages (space separated)" + " using pacman -S") + }, + 'clicked' + ], + ['Button', ':remove', + { 'text': _("Remove package(s) [-Rs]"), + 'tt': _("Remove one or more packages (space separated)" + " using pacman -Rs") + }, + 'clicked' + ], + + # - - - - The advanced installation options frame + ['OptionalFrame', ':settings_advanced', + { 'text': _("Advanced Installation Options"), + 'layout': ['HBOX', ':installrepos', 'VLINE,3', ':cache'] + } + ], + + ['OptionalFrame', ':installrepos', + { 'text': _("Use project repository list"), + 'tt': _("Enables use of an alternative pacman.conf" + " for installation only"), + 'layout': + ['HBOX', ':editrepolist'] + }, + 'toggled' + ], + ['Button', ':editrepolist', + { 'text': _("Edit repository list"), + 'tt': _("Edit repository list file used for installation") + }, + 'clicked' + ], + ['Button', ':editmirrorlist', + { 'text': _("Edit mirror list used for installation only"), + 'tt': _("A mirror list for the live system should be placed" + " in the overlay") + }, + 'clicked' + ], + + ['Frame', ':cache', + { 'text': _("Package Cache"), + 'layout': + ['HBOX', ':cache_show', ':cache_change'] + } + ], + ['LineEdit', ':cache_show', + { 'ro': True, + 'tt': _("The path to the (host's) package cache") + } + ], + ['Button', ':cache_change', + { 'text': _("Change"), + 'tt': _("Change the package cache path") + }, + 'clicked' + ], + + ['Button', ':install', + { 'text': _("Install"), + 'tt': _("This will start the installation to the set path") + }, + 'clicked' + ], + + ['DATA', 'install_page_data', + { 'messages': + { 'edit_pc': _("Editing pacman.conf options only"), + 'edit_pr': _("Editing pacman repositories"), + 'edit_mli': _("Editing mirror list for installation"), + 'prompt_ncp': _("Enter new package cache path:"), + 'edit_pri': _("Editing pacman repositories for installation"), + 'msg_pu': _("Package to add/update"), + 'filter_pu': _("Packages"), + 'prompt_pi': _("Enter the names of packages to install -" + "\n separated by spaces:"), + 'prompt_pr': _("Enter the names of packages to remove -" + "\n separated by spaces:"), + } + }, + ], +] diff --git a/build_tools/larch7/larch0/gui/layouts/page_larchify.uim b/build_tools/larch7/larch0/gui/layouts/page_larchify.uim new file mode 100644 index 0000000..df7be8b --- /dev/null +++ b/build_tools/larch7/larch0/gui/layouts/page_larchify.uim @@ -0,0 +1,177 @@ +# page_larchify.uim - The layout for the larchify 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.02.07 + +(lambda USERHEADERS: +[ + ['Page', ':page_larchify', + { 'layout': + ['VBOX', + ':larchify', + ':users', + 'HLINE', + ['HBOX', ':overlay', ':locales', ':rcconf'], + '*', + ':larchify_advanced', + 'HLINE', + ['HBOX', ':oldsquash', '*', ':build'] + ] + } + ], + # - - - - The profile editing frame + ['Label', ':larchify', + { 'markup': ['', " *** ", ['strong', _("The system to be" + " compressed must be installed and ready.")], " *** "] + } + ], + + ['Button', ':locales', + { 'text': _("Edit supported locales"), + 'tt': _("Edit the /etc/locale.gen file to select" + " supported glibc locales") + }, + 'clicked' + ], + ['Button', ':rcconf', + { 'text': _("Edit Arch configuration"), + 'tt': _("Edit the /etc/rc.conf file to configure the" + " live system") + }, + 'clicked' + ], + ['Button', ':overlay', + { 'text': _("Edit overlay"), + 'tt': _("Open a file browser on the profile's 'rootoverlay'") + }, + 'clicked' + ], + + ['OptionalFrame', ':larchify_advanced', + { 'text': _("Advanced Options"), + 'layout': + ['HBOX', ':initcpio', '*', ':oldlocales', ':ssh'] + } + ], + ['Button', ':initcpio', + { 'text': _("Edit mkinitcpio.conf"), + 'tt': _("Edit the configuration file for generating" + " the initramfs via mkinitcpio") + }, + 'clicked' + ], + ['CheckBox', ':ssh', + { 'text': _("Generate ssh keys"), + 'tt': _("The ssh host keys will be pre-generated") + }, + 'toggled' + ], + ['CheckBox', ':oldlocales', + { 'text': _("Reuse existing locales"), + 'tt': _("To save time it may be possible to reuse glibc" + " locales from a previous run") + }, +# 'toggled' + ], + + ['CheckBox', ':oldsquash', + { 'text': _("Reuse existing system.sqf"), + 'tt': _("Reuse existing system.sqf, to save time if the" + " base system hasn't changed") + }, +# 'toggled' + ], + ['Button', ':build', + { 'text': _("Larchify"), + 'tt': _("Build the main components of the larch system") + }, + 'clicked' + ], + +#Note that this should be disabled if installation directory is '/' + ['Frame', ':users', + { 'text': _("User accounts"), + 'layout': + ['VBOX', + ':utable', + ['HBOX', ':useradd', ':userdel', '*', + ':rootpwl', ':rootpwe', ':rootpwb' + ] + ] + } + ], + ['List', ':utable', + { 'selectionmode': 'Single', + 'headers': USERHEADERS, + 'compact': True, + 'tt': _("Click on a row to select, click on a selected" + " cell to edit") + }, +# 'select', + 'clicked' + ], + ['Button', ':useradd', + { 'text': _("Add user"), + 'tt': _("Create a new user-name") + }, + 'clicked' + ], + ['Button', ':userdel', + { 'text': _("Delete user"), + 'tt': _("Remove the selected user-name") + }, + 'clicked' + ], + ['Label', ':rootpwl', + { 'text': _("Root password:") + } + ], + ['LineEdit', ':rootpwe', + { 'ro': True, + 'tt': _("The unencrypted root password for the live system") + } + ], + ['Button', ':rootpwb', + { 'text': _("Change"), + 'tt': _("Enter a new password for the 'root' user") + }, + 'clicked' + ], + + + ['DATA', 'larchify_page_data', + { 'messages': + { 'uheaders': USERHEADERS, + 'rn_error': _("Renaming failed, see log"), + 'ud_error': _("Couldn't adjust user definition"), + 'def_skel': _("Default (/etc/skel)"), + 'skel_lbl': _("This folder will be copied\n" + "to build the user's home folder:"), + 'skel_ttl': _("Choose 'skel' Folder"), + 'newlogin': _("Enter login-name for new user:"), + 'newrootpw': _("Enter root password for live system:"), + } + }, + ], +] +)([_("User-Name"), _("Password"), _("Group"), + "UID", _("'skel' directory"), + _("Additional Groups"), _("Expert options")]) diff --git a/build_tools/larch7/larch0/gui/layouts/page_main.uim b/build_tools/larch7/larch0/gui/layouts/page_main.uim new file mode 100644 index 0000000..3ab4a9e --- /dev/null +++ b/build_tools/larch7/larch0/gui/layouts/page_main.uim @@ -0,0 +1,101 @@ +# page_main.uim - The layout for the main window +# +# (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.06.26 + +[ + ['Window', ':larch', + { 'title': 'larch', 'size': '640_480', + 'icon': 'images/larchicon.png', + 'closesignal': '$$$uiclose$$$', + 'layout': + ['VBOX', + ['HBOX', + ':image', + ['VBOX', + ['HBOX', ':header', '*'], + ['HBOX', ':showlog', ':docs', '*', ':quit'], + ] + ], + ':tabs' + ] + + } + ], + + # - Header + ['Label', ':image', + { 'image': 'images/larch80.png' + } + ], + ['Label', ':header', + { 'markup': ['h1', ['color', '#c55500', ['em', 'larch '], + _("Live Arch Linux Construction Kit")]] + } + ], + ['Button', ':showlog', + { 'text': _("View Log"), + 'tt': _("This button switches to the log viewer"), + }, + 'clicked' + ], + ['Button', ':docs', + { 'text': _("Help"), + 'tt': _("This button switches to the documentation viewer"), + }, + 'clicked' + ], + ['Button', ':quit', + { 'text': _("Quit"), + 'tt': _("Stop the current action and quit the program"), + 'clicked': '$$$uiquit$$$' + }, + ], + +#TODO + # - Main widget + ['Stack', ':tabs', + { 'pages': [':notebook', 'progress:page', 'log:page', + 'doc:page', 'edit:page'] + } + ], + + # - - The main page of the Stack + ['Notebook', ':notebook', + { 'tabs': [ + [':page_settings', _("Project Settings")], + [':page_installation', _("Installation")], + [':page_larchify', _("Larchify")], + [':page_image', _("Medium Profile")], + [':page_medium', _("Make Medium")], + ] + }, + 'changed' + ], + + ['DATA', 'main_page_data', + { 'messages': + { 'authfail': _("Authentication failure"), + 'getpw': _("Enter the password to run as administrator:"), + } + }, + ], +] diff --git a/build_tools/larch7/larch0/gui/layouts/page_medium.uim b/build_tools/larch7/larch0/gui/layouts/page_medium.uim new file mode 100644 index 0000000..713a3c0 --- /dev/null +++ b/build_tools/larch7/larch0/gui/layouts/page_medium.uim @@ -0,0 +1,271 @@ +# page_medium.uim - The layout for the medium building 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.12 + +[ + ['Page', ':page_medium', + { 'layout': + ['VBOX', + ['HBOX', ':source', 'VLINE', ':bootloader'], + 'HLINE', + ['HBOX', ':destination','VLINE', ':detection'], + ['HBOX', '*', ':vlabell', ':vlabele', + ':vlabelb'], + 'HLINE', + ['HBOX', ':chroot', 'VLINE,10', + ':bootcd', '*', ':make_medium'] + ] + } + ], +# Select source: +# larch installation (default, but may be invalid), iso, cd-drive, partition + ['Frame', ':source', + { 'text': _("Select larch source"), + 'layout': + ['VBOX', + ['HBOX', ':source_larch', ':source_dev', + ':source_iso', ':source_path'], + ['HBOX', ':source_show', ':source_select'] + ] + } + ], + ['RadioButton', ':source_larch', + { 'text': _("larchified system"), + 'tt': _("Use the system prepared within the larch build" + " directory") + }, + 'toggled' + ], + ['RadioButton', ':source_dev', + { 'text': _("Device"), + 'tt': _("Use a system on a mountable device") + }, + 'toggled' + ], + ['RadioButton', ':source_iso', + { 'text': _("'iso' file"), + 'tt': _("Use a system on an 'iso' file") + }, + 'toggled' + ], + ['RadioButton', ':source_path', + { 'text': _("Path"), + 'tt': _("Use a directory within the filesystem") + }, + 'toggled' + ], + ['LineEdit', ':source_show', + { 'ro': True, + 'tt': _("The location from where the larch system will" + " be fetched") + } + ], + ['Button', ':source_select', + { 'text': _("Choose"), + 'tt': _("Select the source location") + }, + 'clicked' + ], + +#++++ + ['OptionalFrame', ':destination', + { 'text': _("Write to partition"), + 'tt': _("Don't create an 'iso' (CD/DVD), write the larch" + " system to a partition (e.g. USB-stick)"), + 'layout': + ['VBOX', + ['HBOX', ':lm2', ':larchpart', ':selectpart'], + ['GRID', ['+', ':noformat', ':nombr'], + ['+', ':nolarchboot', ':dosave']] + ] + }, + 'toggled' + ], + ['Label', ':lm2', + { 'text': _("Partition:") + } + ], + ['LineEdit', ':larchpart', + { 'ro': True, + 'tt': _("The partition to which the larch system is to" + " be installed") + } + ], + ['Button', ':selectpart', + { 'text': _("Choose"), + 'tt': _("Select the partition to receive the larch system") + }, + 'clicked' + ], + ['CheckBox', ':noformat', + { 'text': _("Don't format"), + 'tt': _("Copy the data to the partition without formatting" + " first\n(not the normal procedure, NOT RECOMMENDED!)") + }, +# 'toggled' + ], + ['CheckBox', ':nombr', + { 'text': _("Don't install the bootloader"), + 'tt': _("The bootloader will not be installed, leaving" + " the mbr untouched\n" + "(you'll need to provide some other means of booting)") + }, +# 'toggled' + ], + ['CheckBox', ':nolarchboot', + { 'text': _("Not bootable via search"), + 'tt': _("Don't create the file 'larch/larchboot':\n" + " the medium will only be bootable by uuid, label" + " or partition name") + }, + 'toggled' + ], + ['CheckBox', ':dosave', + { 'text': _("Enable session-saving"), + 'tt': _("Can override profile's 'larch/nosave' file," + " to make session-saving possible in that case too") + }, +# 'toggled' + ], +#---- + +#++++++++ + ['Frame', ':detection', + { 'text': _("Medium Detection"), + 'tt': _("Choose how the boot scripts determine where to" + " look for the larch system (ONLY ON PARTITIONED MEDIA)"), + 'layout': + ['VBOX', ':device', ':uuid', ':label', ':search'] + } + ], + ['RadioButton', ':uuid', + { 'text': _("UUID"), + 'tt': _("Use the partition's UUID to find it") + }, +# 'toggled' + ], + ['RadioButton', ':label', + { 'text': _("LABEL"), + 'tt': _("Use the partition's label to find it") + }, +# 'toggled' + ], + ['RadioButton', ':device', + { 'text': _("Partition"), + 'tt': _("Use the partition's name (/dev/sdb1, etc.) to find it") + }, +# 'toggled' + ], + ['RadioButton', ':search', + { 'text': _("Search (for larchboot)"), + 'tt': _("Test all CD/DVD devices and partitions until" + " the file 'larch/larchboot' is found") + }, + 'toggled' + ], +#-------- + +#+ + # Defaults to that of the source + ['Label', ':vlabell', + { 'text': _("Volume Label:") + } + ], + ['LineEdit', ':vlabele', + { 'ro': True, + 'tt': _("The length may not exceed 16 bytes," + " 11 for vfat(syslinux)") + } + ], + ['Button', ':vlabelb', + { 'text': _("Change"), + 'tt': _("Enter a new label for the volume, empty to use default") + }, + 'clicked' + ], +#- + +#++++ + ['Frame', ':bootloader', + { 'text': _("Bootloader"), + 'tt': _("You can choose between GRUB and" + " syslinux/isolinux as bootloader"), + 'layout': + ['VBOX', ':grub', ':syslinux'] + } + ], + ['RadioButton', ':grub', + { 'text': "GRUB", + 'tt': _("Use GRUB as bootloader") + }, + 'toggled' + ], + ['RadioButton', ':syslinux', + { 'text': "syslinux/isolinux", + 'tt': _("Use syslinux (partition) or isolinux (CD/DVD)" + " as bootloader") + }, + 'toggled' + ], +#---- + + ['CheckBox', ':chroot', + { 'text': _("Use chroot"), + 'tt': _("Use the larch installation for the build process\n" + " - the default should be alright in most cases") + }, +# 'toggled' + ], + + ['Button', ':bootcd', + { 'text': _("Create boot iso"), + 'tt': _("Create a small boot iso for this device (for" + " machines that can't boot from USB)") + }, + 'clicked' + ], + ['Button', ':make_medium', + { 'text': _("Write the larch medium"), + 'tt': _("The larch image will be written to the 'iso' file" + " or to the partition, as selected") + }, + 'clicked' + ], + + ['DATA', 'medium_page_data', + { 'messages': + { 'medium_src': _("Select source medium folder"), + 'iso_src': _("Select source iso file"), + 'iso_type': _("larch iso images"), + 'parts_t': _("Select unmounted partition"), + 'parts_src': _("larch system source:"), + 'parts_dst': _("Device to receive larch system.\n" + "WARNING: Be very careful in choosing here,\n" + "if you choose the wrong one you might\n" + "seriously damage your system!"), + 'msg_med': _("Invalid larch medium folder: %s"), + 'prompt_label': _("Volume label (clear to use default):"), + 'isopath': _("Save 'iso' to ..."), + } + }, + ], +] diff --git a/build_tools/larch7/larch0/gui/layouts/page_mediumprofile.uim b/build_tools/larch7/larch0/gui/layouts/page_mediumprofile.uim new file mode 100644 index 0000000..f50a20a --- /dev/null +++ b/build_tools/larch7/larch0/gui/layouts/page_mediumprofile.uim @@ -0,0 +1,99 @@ +# page_mediumprofile.uim - The layout for the medium profile 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.08 + +[ + ['Page', ':page_image', + { 'layout': + ['VBOX', + ':bootlines', + ':grubtemplate', + ':syslinuxtemplate', + ':cdroot', + '*', + ['HBOX', ':nosessionsave', '*', +# ':plabell', ':plabele', ':plabelb' + ] + ] + } + ], + ['Button', ':bootlines', + { 'text': _("Edit boot entries"), + 'tt': _("Edit the file determining the boot entries") + }, + 'clicked' + ], + ['Button', ':grubtemplate', + { 'text': _("Edit grub template"), + 'tt': _("Edit grub's configuration file," + " but not the larch boot entries") + }, + 'clicked' + ], + ['Button', ':syslinuxtemplate', + { 'text': _("Edit syslinux/isolinux template"), + 'tt': _("Edit the syslinux/isolinux configuration file," + " but not the larch boot entries") + }, + 'clicked' + ], + ['Button', ':cdroot', + { 'text': _("Edit cd-root (open in file browser)"), + 'tt': _("Open a file browser on the profile's 'cd-root'" + " folder") + }, + 'clicked' + ], +# ['Label', ':plabell', +# { 'text': _("Volume Label:") +# } +# ], +# ['LineEdit', ':plabele', +# { 'ro': True, +# 'tt': _("The length may not exceed 16 bytes," +# " 11 for vfat(syslinux)") +# } +# ], +# ['Button', ':plabelb', +# { 'text': _("Change"), +# 'tt': _("Enter a new label for the volume") +# }, +# 'clicked' +# ], + ['CheckBox', ':nosessionsave', + { 'text': _("Disable session saving"), + 'tt': _("If checked, the medium will have the file" + " 'larch/nosave',\n" + "which only has an effect on writable media.") + }, + 'toggled' + ], + + +#? +# ['DATA', 'mediumprofile_page_data', +# { 'messages': +# { 'blah': _("Blah blah"), +# } +# }, +# ], +] diff --git a/build_tools/larch7/larch0/gui/layouts/page_project.uim b/build_tools/larch7/larch0/gui/layouts/page_project.uim new file mode 100644 index 0000000..938c646 --- /dev/null +++ b/build_tools/larch7/larch0/gui/layouts/page_project.uim @@ -0,0 +1,181 @@ +# page_project.uim - The layout 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.06.27 + +[ + ['Page', ':page_settings', + { 'layout': + ['VBOX', + ':settings_profile', + '*', + ':options_advanced', + '*' + ] + } + ], + # - - - - The profile selection frame + ['Frame', ':settings_profile', + { 'text': _("Profile"), + 'layout': + ['VBOX', + ['HBOX', ':choose_profile', ':choose_profile_combo', + '*', ':profile_browse'], + ['HBOX', ':profile_rename', ':profile_delete', + ':profile_save'] + ] + } + ], + ['Label', ':choose_profile', + { 'text': _("Select:"), + 'align': 'right' + } + ], + ['ComboBox', ':choose_profile_combo', + { 'width': 200, + 'tt': _("Choose a profile from those already in your" + " larch working folder") + }, + 'changed' + ], + ['Button', ':profile_browse', + { 'text': _("Browse for Profile"), + 'tt': _("Fetch a profile from the file-system") + }, + 'clicked' + ], + ['Button', ':profile_rename', + { 'text': _("Rename"), + 'tt': _("Rename the current profile") + }, + 'clicked' + ], + ['Button', ':profile_delete', + { 'text': _("Delete"), + 'tt': _("Delete an unused profile") + }, + 'clicked' + ], + ['Button', ':profile_save', + { 'text': _("Copy to ..."), + 'tt': _("Copy the current profile to somehere else") + }, + 'clicked' + ], + + # - - - - Advanced project options + ['OptionalFrame', ':options_advanced', + { 'text': _("Advanced Project Options"), + 'layout': + ['HBOX', + #['HBOX', '*', ':lplat', ':platform'], + ['GRID', + ['+', ':choose_project', ':choose_project_combo'], + ['+', ':new_project', ':project_delete'] + ], + 'VLINE,3', + ':installation_path' + ] + } + ], +# Pending better support in Arch/pacman +# ['Label', '::lplat', +# { 'text': _("Platform (processor architecture):") +# } +# ], +# ['ComboBox', ':platform', +# { 'tt': _("Which processor architecture?") +# }, +# 'changed' +# ], + ['Label', ':choose_project', + { 'text': _("Choose Existing Project:") + } + ], + ['ComboBox', ':choose_project_combo', + { 'tt': _("Choose a project from those already defined"), + 'width': 120 + }, + 'changed' + ], + ['Button', ':new_project', + { 'text': _("New Project"), + 'tt': _("Create a new project") + }, + 'clicked' + ], + ['Button', ':project_delete', + { 'text': _("Delete"), + 'tt': _("Delete a project") + }, + 'clicked' + ], + ['Frame', ':installation_path', + { 'text': _("Installation Path"), + 'layout': + ['HBOX', ':installation_path_show', + ':installation_path_change'] + } + ], + ['LineEdit', ':installation_path_show', + { 'ro': True, + 'tt': _("The root directory of the Arch installation" + " to larchify") + } + ], + ['Button', ':installation_path_change', + { 'text': _("Change"), + 'tt': _("Change the root directory of the Arch installation") + }, + 'clicked' + ], + + ['DATA', 'project_page_data', + { 'messages': + { 'file_ps': _("Select profile source folder"), + 'prompt_pr': _("Destination profile exists - replace it?"), + 'prompt_pn': _("Enter new name for current profile:"), + 'prompt_pe': _("Profile '%s' exists already"), + 'msg_pu': _("Can't rename the profile," + " it is in use by other projects"), + 'file_sp': _("Save profile folder"), + 'prompt_dr': _("Destination exists - replace it?"), + 'prompt_dp': _("Select the profile for deletion"), + 'delprof': _("Remove Profile"), + 'msg_npf': _("There are no profiles which can" + " be deleted - all are in use"), + 'msg_dpff': _("Couldn't delete profile '%s' -" + " check permissions"), + 'prompt_ip': _("An empty path here will reset to the default.\n" + " WARNING: Double check your path -\n" + " If you make a mistake here it could destroy your system!" + "\n\nEnter new installation path:"), + 'prompt_np': _("Enter name for new project:"), + 'msg_pe': _("Project '%s' already exists"), + 'prompt_pd': _("Select the project for deletion"), + 'delproj': _("Remove Project"), + 'msg_np': _("There are no projects which can be deleted"), + 'msg_npd': _("'%s' is not a profile folder"), + 'msg_piu': _("The path '%s' is already in use, not saving"), + } + }, + ], +] diff --git a/build_tools/larch7/larch0/gui/layouts/progress.uim b/build_tools/larch7/larch0/gui/layouts/progress.uim new file mode 100644 index 0000000..62ae9e2 --- /dev/null +++ b/build_tools/larch7/larch0/gui/layouts/progress.uim @@ -0,0 +1,64 @@ +# progress.uim - The layout for the progress widget +# +# (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.05.22 + +[ + ['Frame', 'progress:page', + { 'layout': + ['VBOX', + 'progress:header', + ['HBOX', + 'progress:text', + ['VBOX', 'progress:cancel', '*', 'progress:done'] + ], + 'progress:progress' + ] + } + ], + ['Label', 'progress:header', + { 'markup': ['', ['h2', _("Processing ...")], + ['p', _("Here you can follow the detailed, low-level" + " progress of the commands.")]] + } + ], + ['TextEdit', 'progress:text', + { 'ro': True + } + ], + ['LineEdit', 'progress:progress', + { 'ro': True, + 'tt': _("An indication of the progress of the current" + " operation, if possible") + } + ], + ['Button', 'progress:cancel', + { 'text': _("Cancel"), + 'tt': _("Stop the current action"), + 'clicked': '$$$cancel$$$' + } + ], + ['Button', 'progress:done', + { 'text': _("Done"), + 'clicked': '' + } + ], +] diff --git a/build_tools/larch7/larch0/gui/project.py b/build_tools/larch7/larch0/gui/project.py new file mode 100644 index 0000000..f01e578 --- /dev/null +++ b/build_tools/larch7/larch0/gui/project.py @@ -0,0 +1,428 @@ +# project.py - Project management +# +# (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 + +from config import * +import os, shutil, pwd +from glob import glob +from subprocess import call +from userinfo import Userinfo + +CONFIG_DIR = '.config/larch' # within the user's home directory +APP_CONF = 'app.conf' # within larch config directory +PROJECT_CONF = 'project.conf' # within project directory +PROJECT0 = 'larch-0' # default project +PROFILE0 = 'default' # default profile + +# Some default values for the project config file +DEFAULTS = { 'installation_dir' : '', + 'pacman_cache' : '/var/cache/pacman/pkg', + 'profile' : PROFILE0, + 'profile_browse_dir': '', # => use default + 'installrepo' : '', + 'medium_iso' : 'Yes', # 'Yes' | '' + 'medium_btldr' : 'syslinux', # 'grub' | 'syslinux' + 'medium_search' : 'search', # 'search' | 'uuid' | 'label' | 'device' + 'medium_label' : '', # => fetch default + 'isosavedir' : '', + 'isofile' : '', + 'bootisofile' : '', + 'bootisolabel' : '', + } + + +# Default values for the application config file +APP_DEFAULTS = { + 'project' : PROJECT0, + 'filebrowser' : 'xdg-open $', + } + + + +class ProjectManager: + def __init__(self): + add_exports( ( + ('getitem', self.getitem), + ('getbool', self.getbool), + ('setitem', self.setitem), + ('setbool', self.setbool), + ('get_projects', self.list_projects), + ('get_profiles', self.list_profiles), + ('get_installation_dir', self.get_ipath), + ('set_installation_dir', self.set_ipath), + ('testmedium', self.testmedium), + ('set_project', self.set_projectname), + ('get_project', self.get_projectname), + ('delete_project', self.delete_project), + ('delete_profile', self.delete_profile), + ('list_free_projects', self.list_free_projects), + ('list_free_profiles', self.list_free_profiles), + ('get_new_profile', self.get_new_profile), + ('rename_profile', self.rename_profile), + ('can_rename_profile', self.can_rename_profile), + ('save_profile', self.save_profile), + ('get_profile', self.get_profile), + ('set_profile', self.set_profile), + ('get_profile_bookmarks', self.get_profile_bookmarks), + ('get_mediumlabel', self.get_mediumlabel), + ('set_mediumlabel', self.set_mediumlabel), + ('getisosavedir', self.getisosavedir), + ('getisofile', self.getisofile), + ('getbootisofile', self.getbootisofile), + ('getbootisolabel', self.getbootisolabel), + ('newUserinfo', self.newUserinfo), + ('allusers', self.allusers), + ('getuserinfo', self.getuserinfo), + ('newuser', self.newuser), + ('userset', self.userset), + ('deluser', self.deluser), + ('listskels', self.listskels), + ('saveusers', self.saveusers)) + ) + + + def init(self): + self.projects_base = os.path.join(os.environ['HOME'], CONFIG_DIR) + self.profiles_dir = os.path.join(self.projects_base, 'myprofiles') + # Ensure the presence of the larch default project folder + dpf = '%s/p_%s' % (self.projects_base, PROJECT0) + if not os.path.isdir(dpf): + os.makedirs(dpf) + # Ensure the presence of the profiles folder and the 'default' profile + if not os.path.isdir(self.profiles_dir): + os.mkdir(self.profiles_dir) + self.default_profile_dir = os.path.join(self.profiles_dir, PROFILE0) + if not os.path.isdir(self.default_profile_dir): + call(['cp', '-a', base_dir + '/profiles/'+ PROFILE0, + self.profiles_dir]) + + # The application configs + self.aconfig_file = os.path.join(self.projects_base, APP_CONF) + self.aconfig = self.getconfig(self.aconfig_file) + + # The project-specific configs + self.set_projectname(self.appget('project')) + + + def get_projectname(self): + return (True, self.project_name) + + def set_projectname(self, name): + self.project_dir = os.path.join(self.projects_base, 'p_' + name) + plist = self.list_projects()[1] + if name not in plist: + os.mkdir(self.project_dir) + + self.pconfig_file = os.path.join(self.project_dir, PROJECT_CONF) + self.pconfig = self.getconfig(self.pconfig_file) + self.profile_name = self.get('profile') + + self.profile_path = os.path.join(self.profiles_dir, self.profile_name) + self.appset('project', name) + self.project_name = name + return (True, None) + + def delete_project(self, name): + # This should probably be run as root, in case the build directory + # is inside it ... cross that bridge when we come to it! + r = call(['rm', '-r', '--interactive=never', + os.path.join(self.projects_base, 'p_' + name)]) + return (True, r == 0) + + + def delete_profile(self, name): + r = call(['rm', '-r', '--interactive=never', + os.path.join(self.profiles_dir, name)]) + return (True, r == 0) + + + def get_profile(self): + return (True, self.profile_name) + + def set_profile(self, name): + self.set('profile', name) + self.profile_name = name + self.profile_path = os.path.join(self.profiles_dir, self.profile_name) + return (True, None) + + + def rename_profile(self, name): + os.rename(self.profile_path, os.path.join(self.profiles_dir, name)) + self.set_profile(name) + return (True, None) + + + def get_new_profile(self, src): + if not os.path.isfile(src + '/addedpacks'): + return (True, False) + pname = os.path.basename(src) + dst = os.path.join(self.profiles_dir, pname) + call(['rm', '-rf', dst]) + shutil.copytree(src, dst) + self.set_profile(pname) + return (True, True) + + + def get_profile_bookmarks(self): + return (True, ((self.projects_base + '/myprofiles', + _("Working Profiles")), + (base_dir + '/profiles', _("Examples")), + ('/', _("File-system")) + )) + + +# What about not allowing changes to the default profile? +# That would mean also no renaming? +# One would have to copy a profile into the project before going +# any further ... +# Is it right to share profiles between projects? (Probably) + +#+++++++++++++++++++++++++++++++++++++++++++++++ +### A very simple configuration file handler + def getconfig(self, filepath): + cfg = {} + if os.path.isfile(filepath): + fh = open(filepath) + for line in fh: + ls = line.split('=', 1) + if len(ls) > 1: + cfg[ls[0].strip()] = ls[1].strip() + return cfg + + + def saveconfig(self, filepath, config): + fh = open(filepath, 'w') + ci = config.items() + ci.sort() + for kv in ci: + fh.write('%s = %s\n' % kv) + fh.close() +### +#----------------------------------------------- + + def list_projects(self): + projects = [p[2:] for p in os.listdir(self.projects_base) + if p.startswith('p_')] + projects.sort() + return (True, projects) + + + def list_free_projects(self): + """This returns a list of projects which are free for (e.g.) deletion. + """ + plist = self.list_projects()[1] + plist.remove(PROJECT0) # this one is not 'free' + if self.project_name in plist: + plist.remove(self.project_name) + return (True, plist) + + + def list_profiles(self): + profiles = [d for d in os.listdir(self.profiles_dir) if + os.path.isfile(os.path.join(self.profiles_dir, d, 'addedpacks'))] + profiles.sort() + return (True, profiles) + + + def list_free_profiles(self): + """This returns a list of profiles which are not in use by any project. + """ + plist = self.list_profiles()[1] + plist.remove(PROFILE0) # this one is not 'free' + for project in self.list_projects()[1]: + cfg = self.getconfig(os.path.join(self.projects_base, + 'p_' + project, PROJECT_CONF)) + p = cfg.get('profile') + if p in plist: + plist.remove(p) + return (True, plist) + + + def can_rename_profile(self): + if self.profile_name == PROFILE0: + return (True, False) + for project in self.list_projects()[1]: + if project != self.project_name: + cfg = self.getconfig(os.path.join(self.projects_base, + 'p_' + project, PROJECT_CONF)) + if self.profile_name == cfg.get('profile'): + return (True, False) + return (True, True) + + + def save_profile(self, path, force): + #path = os.path.join(path, self.profile_name) + if os.path.exists(path): + if force: + call(['rm', '-rf', path]) + elif os.path.isfile(os.path.join(path, addedpacks)): + # This is an existing profile + return (True, False) + else: + # This location is otherwise in use + return (True, None) + shutil.copytree(self.profile_path, path) + return (True, True) + + + def appget(self, item): + """Read an entry in the application configuration. + """ + v = self.aconfig.get(item) + if v: + return v + elif APP_DEFAULTS.has_key(item): + return APP_DEFAULTS[item] + debug("Unknown application configuration option: %s" % item) + assert False + + def appset(self, item, value): + """Set an entry in the application configuration. + """ + self.aconfig[item] = value.strip() + self.saveconfig(self.aconfig_file, self.aconfig) + + + def getitem(self, item): + return (True, self.get(item)) + + def getbool(self, item): + return (True, bool(self.get(item))) + + def setitem(self, item, value): + self.set(item, value) + return (True, None) + + def setbool(self, item, on): + self.set(item, 'Yes' if on else '') + return (True, None) + + + def get(self, item): + """Read an entry in the project configuration. + """ + v = self.pconfig.get(item) + if v: + return v + elif DEFAULTS.has_key(item): + return DEFAULTS[item] + debug("Unknown configuration option: %s" % item) + assert False + + def set(self, item, value): + """Set an entry in the project configuration. + """ + self.pconfig[item] = value.strip() + self.saveconfig(self.pconfig_file, self.pconfig) +# return True + + + def get_ipath(self): + ip = self.get('installation_dir') + if not ip: + ip = self.set_ipath('')[1] + return (True, ip) + + def set_ipath(self, path): + path = path.strip() + if path: + path = '/' + path.strip('/') + else: + path = os.environ['HOME'] + '/larch_build' + self.set('installation_dir', path) + return (True, path) + + + def testmedium(self): + ipath = self.get_ipath()[1] + path = ipath + CHROOT_DIR_MEDIUM + bl = [] + nosave = False + ok = os.path.isfile(path + '/larch/system.sqf') + if ok: + if os.path.isfile(ipath + GRUBDIR + '/stage2_eltorito'): + bl.append('grub') + if os.path.isfile(ipath + SYSLINUXDIR + '/isolinux.bin'): + bl.append('syslinux') + if os.path.isfile(self.profile_path + '/nosave'): + nosave = True + return (True, (path, ok, bl, nosave)) + + + def newUserinfo(self): + self.userInfo = Userinfo(self.profile_path) + return (True, None) + + def allusers(self): + return (True, self.userInfo.allusers()) + + def getuserinfo(self, user, fields): + return (True, self.userInfo.userinfo(user, fields)) + + def newuser(self, user): + return (True, self.userInfo.newuser(user)) + + def saveusers(self): + return (True, self.userInfo.saveusers()) + + def userset(self, uname, field, text): + self.userInfo.userset(uname, field, text) + return (True, None) + + def deluser(self, user): + return (True, self.userInfo.deluser(user)) + + def listskels(self): + return (True, glob(self.profile_path + '/skel_*')) + + + def get_mediumlabel(self): + l = self.get('medium_label') + return (True, l if l else LABEL) + + def set_mediumlabel(self, l): + if len(l) > 16: + l = l[:16] + self.set('medium_label', l) + return self.get_mediumlabel() + + + def getisosavedir(self): + d = self.get('isosavedir') + return (True, d if d else os.environ['HOME']) + + def getisofile(self): + f = self.get('isofile') + return (True, f if f else ISOFILE) + + def getbootisofile(self): + f = self.get('bootisofile') + return (True, f if f else BOOTISO) + + def getbootisolabel(self): + l = self.get('bootisolabel') + return (True, l if l else ISOLABEL) + + + +import __builtin__ +__builtin__.project_manager = ProjectManager() |