#!/usr/bin/env python2 # -*- coding: UTF-8 -*- # # suim.py # # (c) Copyright 2010 Michael Towers (larch42 at googlemail dot com) # # This file is part of the larch project. # # larch is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # larch is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with larch; if not, write to the Free Software Foundation, Inc., # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # #---------------------------------------------------------------------------- # 2010.10.24 #++++++++++++++++++++++++++++++++++++++++++++++++++++ #TODO # Add more widgets # Add more attribute handling # Add more signal handling # Fetching of image and icon files via a sort of url-like mechanism, I # suppose initially using the 'base:' prefix might be ok. # Then the cwd of the gui script would be irrelevant. #New file dialog for accessing the 'server' end. # I suspect the DialogButtonBox stuff needs changing to cope with # translations. #---------------------------------------------------- """SUIM - Simple User Interface Module The aim is to provide a means of creating graphical user interfaces of moderate complexity while abstracting the interface to the actual underlying toolkit in such a way that (at least potentially) an alternative toolkit could be used. [At present this aspect is rather theoretical since only a pyqt based version has been written.] The gui layout is specified as a python data structure, using widget types, parameter and signal names independent of the underlying toolkit. All widgets are accessible by their tag, which must be specified. A widget is defined by a call to the 'widget' method of the GuiApp instance. The first argument is the widget type, the second is the widget tag, the remaining ones must be named, they form the parameters to the constructor. If the widget is a 'container' (i.e. if it contains other widgets), it will need a 'layout' parameter defining the layout of its contents. There is also a 'widgetlist' method which accepts a list of widget definitions, each definition being itself a list. The first entry in a definition is the widget type, the second is the widget tag, the third is a dictionary containing all the parameters. For convenience (I'm not sure if I will keep this, though) any entries after the dictionary will be treated as signal names. These are just added to the parameter dictionary with value '' (enabling the signal with its default tag). Signals have signatures/keys comprising the tag of the emitting widget and the signal name (separated by '*'), and this will by default also be the tag by which the signal is known for connection purposes. But this can be overridden, for example to allow several widgets to emit the same signal. In the latter case the widget tag can (optionally) be passed as the first argument to the signal handler. Passing signal names as parameters to a widget constructor enables these signals. They can later be disabled, if desired. Connect and disconnect methods are available, to associate (or dissociate) handler functions with (/from) signals. """ import os, sys, traceback, threading from PyQt4 import QtGui, QtCore, QtWebKit from collections import deque def debug(text): sys.stderr.write("GUI: %s\n" % text) sys.stderr.flush() # Either I need to wrap all text input with this or I need to ensure that # I get unicode from outside ... import locale encoding = locale.getdefaultlocale()[1] def convert(text): """Try to handle encoding. """ if isinstance(text, str): return text.decode(encoding) if encoding else text.decode() else: return text # Widget Base Classes - essentially used as 'Mixins' >>>>>>>>>>>>>>>> class WBase: def x__tt(self, text): """Set tooltip. """ self.setToolTip(text) #qt def x__text(self, text=""): """Set widget text. """ self.setText(convert(text)) #qt def x__enable(self, on): """Enable/Disable widget. on should be True to enable the widget (display it in its normal, active state), False to disable it (which will normally be paler and non-interactive). """ self.setEnabled(on) #qt def x__focus(self): self.setFocus() #qt def x__width(self, w): """Set the minimum width for the widget. """ self.setMinimumWidth(w) #qt def x__typewriter(self, on): """Use a typewriter (fixed spacing) font. """ if on: f = QtGui.QFont(self.font()) #qt f.setFamily("Courier") #qt self.setFont(f) #qt def x__busycursor(self, on): """Set/clear the busy-cursor for this widget. """ if on: self.setCursor(QtCore.Qt.BusyCursor) #qt else: self.unsetCursor() #qt class BBase: """Button mixin. """ def x__icon(self, icon): self.setIcon(self.style().standardIcon(icondict[icon])) #qt #qt icondict = { "left" : QtGui.QStyle.SP_ArrowLeft, "right" : QtGui.QStyle.SP_ArrowRight, "down" : QtGui.QStyle.SP_ArrowDown, "up" : QtGui.QStyle.SP_ArrowUp, "reload" : QtGui.QStyle.SP_BrowserReload, } class Container: """This just adds layout management for widgets which contain other widgets. """ def x__layout(self, layout, immediate=False): """A layout specifies and organizes the contents of a widget. Note that the layouting is not immediately performed by default as it is unlikely that all the contained widgets have been defined yet. """ self._layout = layout if immediate: self.x__pack() def x__pack(self): """A layout call specifies and organizes the contents of a widget. The layout can be a layout manager list, or a single widget name (or an empty string, which will cause a warning to be issued, but may be useful during development). There are three sorts of thing which can appear in layout manager lists (apart from the layout type at the head of the list and an optional attribute dict as second item). There can be named widgets, there can be further layout managers (specified as lists, nested as deeply as you like) and there can be layout widgets, like spacers and separators. A layout widget can have optional arguments, which are separated by commas, e.g. 'VLINE,3' passes the argument '3' to the VLINE constructor. """ # getattr avoids having to have an __init__() for Container. if getattr(self, '_layout', None): if self._layout != '$': self.setLayout(self.getlayout(self._layout)) self._layout = '$' else: debug("No layout set on '%s'" % self.w_name) def getlayout(self, item): if isinstance(item, list): try: # Create a layout manager instance layoutmanager = layout_table[item[0]]() assert isinstance(layoutmanager, Layout) except: gui_error("Unknown layout type: %s" % item[0]) if (len(item) > 1) and isinstance(item[1], dict): dictarg = item[1] ilist = item[2:] else: dictarg = {} ilist = item[1:] # Build up the list of objects to lay out # If the layout manager is a GRID, accept only grid rows ('+') if isinstance(layoutmanager, GRID): args = [] rowlen = None for i in ilist: if isinstance(i, list) and (i[0] == '+'): args.append(self.getlayoutlist(i[1:], grid=True)) if rowlen == None: rowlen = len(i) elif len(i) != rowlen: gui_error("Grid (%s) row lengths unequal" % self.w_name) else: gui_error("Grid (%s) layouts must consist of grid" " rows ('+')" % self.w_name) else: # Otherwise the elements of the argument list can be: # A sub-layout # A widget # A SPACE args = self.getlayoutlist(ilist) layoutmanager.do_layout(args) # Attributes for key, val in dictarg: handler = "x__" + key if hasattr(layoutmanager, handler): getattr(layoutmanager, handler)(val) return layoutmanager else: # It must be a widget, which will need to be put in a box (qt) return self.getlayout(['VBOX', item]) def getlayoutlist(self, items, grid=False): objects = [] for i in items: if isinstance(i, list): obj = self.getlayout(i) else: parts = i.split(',') i = parts[0] args = parts[1:] try: obj = layout_table[i](*args) if not (isinstance(obj, SPACE) # or a separator line or isinstance(obj, QtGui.QWidget)): #qt assert (grid and isinstance(obj, Span)) except: obj = guiapp.getwidget(i) if obj != None: if isinstance(obj, Container): obj.x__pack() else: gui_error("Bad item in layout of '%s': '%s'" % (self.w_name, i)) objects.append(obj) return objects class XContainer(Container): """This is a mixin class for containers which can contain more than one layout. """ def x__layout(self, layout): gui_error("An extended container (%s) has no 'layout' method" % self.w_name) class TopLevel(Container): def x__show(self): self.set_visible() def set_visible(self, on=True): self.setVisible(on) #qt def x__size(self, w_h): w, h = [int(i) for i in w_h.split("_")] self.resize(w, h) #qt def x__icon(self, iconpath): guiapp.setWindowIcon(QtGui.QIcon(iconpath)) #qt def x__title(self, text): if text == None: text = guiapp.appname self.setWindowTitle(text) #qt def x__getSize(self): s = self.size() #qt return "%d_%d" % (s.width(), s.height()) #qt def x__getScreenSize(self): dw = guiapp.desktop() #qt geom = dw.screenGeometry(self) #qt return "%d_%d" % (geom.width(), geom.height()) #qt #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< class Window(QtGui.QWidget, TopLevel): #qt """This is needed to trap window closing events. """ def __init__(self): QtGui.QWidget.__init__(self) #qt self.closesignal = "" def closeEvent(self, event): #qt if self.closesignal: guiapp.sendsignal(self.closesignal) event.ignore() #qt return QtGui.QWidget.closeEvent(self, event) #qt def x__closesignal(self, text): self.closesignal = text class Dialog(QtGui.QDialog, TopLevel): def __init__(self): QtGui.QDialog.__init__(self) #qt def x__showmodal(self): return self.exec_() == QtGui.QDialog.Accepted #qt class DialogButtons(QtGui.QDialogButtonBox): #qt def __init__(self): return def x__buttons(self, args): """This keyword argument MUST be present. """ buttons = 0 for a in args: try: b = getattr(QtGui.QDialogButtonBox, a) #qt assert isinstance(b, int) #qt buttons |= b #qt except: gui_warning("Unknown Dialog button: %s" % a) QtGui.QDialogButtonBox.__init__(self, buttons) #qt def x__dialog(self, dname): """This must be set or else the dialog buttons won't do anything. """ self._dialog = guiapp.getwidget(dname) self.connect(self, QtCore.SIGNAL("clicked(QAbstractButton *)"), #qt self._clicked) #qt def _clicked(self, button): #qt if self.buttonRole(button) == self.AcceptRole: #qt self._dialog.accept() #qt else: self._dialog.reject() #qt def textLineDialog(label="???:", title=None, text="", pw=False): if title == None: title = guiapp.appname echo = QtGui.QLineEdit.Password if pw else QtGui.QLineEdit.Normal #qt res, ok = QtGui.QInputDialog.getText(None, title, label, echo, text) #qt return (ok, unicode(res)) def listDialog(label="???", title=None, items=[], current=0): if title == None: title = guiapp.appname res, ok = QtGui.QInputDialog.getItem(None, title, label, items, current) return (ok, unicode(res)) def confirmDialog(message, title=None): if title == None: title = guiapp.appname return (QtGui.QMessageBox.question(None, title, convert(message), QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) == QtGui.QMessageBox.Ok) def infoDialog(message, title=None): if title == None: title = guiapp.appname QtGui.QMessageBox.information(None, title, message) #+++++++++++++++++++++++++++ # Error handling def gui_error(message, title=None): if title == None: title = "!!! %s !!!" % guiapp.appname QtGui.QMessageBox.critical(None, title, message) guiapp.quit() def gui_warning(message, title=None): if title == None: title = guiapp.appname QtGui.QMessageBox.warning(None, title, message) def onexcept(text): debug(traceback.format_exc()) gui_error(text, "Exception") #--------------------------- fileDialogDir = '' # In tests with the gtk dialog the options were not respected, # with ro you could still create new directories # and all files were shown when only-directories was specified. # So I am using the non-native dialogs. def fileDialog_getdir(caption = None, ro = True, startdir=None): global fileDialogDir if caption == None: caption = '' if startdir == None: startdir = fileDialogDir options = QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog if ro: options |= QtGui.QFileDialog.ReadOnly d = QtGui.QFileDialog.getExistingDirectory(None, caption, startdir, options) if d: d = unicode(d) fileDialogDir = d return d def fileDialog_open(caption = None, startdir=None, filter=None): global fileDialogDir if caption == None: caption = '' if startdir == None: startdir = fileDialogDir if filter: filter = '%s (%s)' % (filter[0], ' '.join(filter[1:])) #qt else: filter = '' d = QtGui.QFileDialog.getOpenFileName(None, caption, startdir, filter, #qt options=QtGui.QFileDialog.DontUseNativeDialog | QtGui.QFileDialog.ReadOnly) #qt if d: d = unicode(d) fileDialogDir = os.path.dirname(d) return d def fileDialog_save(caption = None, startdir=None, filter=None): global fileDialogDir if caption == None: caption = '' if startdir == None: startdir = fileDialogDir if filter: filter = '%s (%s)' % (filter[0], ' '.join(filter[1:])) #qt else: filter = '' d = QtGui.QFileDialog.getSaveFileName(None, caption, startdir, filter, #qt options=QtGui.QFileDialog.DontUseNativeDialog) #qt if d: d = unicode(d) fileDialogDir = os.path.dirname(d) return d class Stack(QtGui.QStackedWidget, XContainer): #qt def __init__(self): QtGui.QStackedWidget.__init__(self) #qt self.x_twidgets = [] def x__pages(self, pages): self.x_twidgets = pages def x__pack(self): for name in self.x_twidgets: w = guiapp.getwidget(name) w.x__pack() self.addWidget(w) #qt def x__set(self, index=0): self.setCurrentIndex(index) #qt class Notebook(QtGui.QTabWidget, XContainer): #qt def __init__(self): QtGui.QTabWidget.__init__(self) #qt self.x_tabs = [] self.x_twidgets = [] def x__changed(self, name=''): guiapp.signal(self, 'changed', name, 'currentChanged(int)') #qt def x__tabs(self, tabs): self.x_twidgets = tabs def x__pack(self): for name, title in self.x_twidgets: w = guiapp.getwidget(name) w.x__pack() self.addTab(w, title) #qt self.x_tabs.append([name, w]) def x__set(self, index=0): self.setCurrentIndex(index) #qt def x__enableTab(self, index, on): self.setTabEnabled(index, on) #qt class Page(QtGui.QWidget, Container): #qt def __init__(self): QtGui.QWidget.__init__(self) #qt def x__enable(self, on): """Enable/Disable widget. on should be True to enable the widget (display it in its normal, active state), False to disable it (which will normally be paler and non-interactive). """ self.setEnabled(on) #qt class Frame(QtGui.QGroupBox, WBase, Container): #qt def __init__(self): QtGui.QGroupBox.__init__(self) #qt self._text = None def x__text(self, text): self._text = text self.setTitle(text) #qt # A hack to improve spacing def setLayout(self, layout): topgap = 10 if self._text else 0 layout.setContentsMargins(0, topgap, 0, 0) #qt QtGui.QGroupBox.setLayout(self, layout) class OptionalFrame(Frame): #qt def __init__(self): #qt Frame.__init__(self) #qt self.setCheckable(True) #qt self.setChecked(False) #qt def x__toggled(self, name=''): guiapp.signal(self, 'toggled', name, 'toggled(bool)') #qt def x__opton(self, on): self.setChecked(on) #qt #TODO: Is this still needed? (I think it's a qt bug) def x__enable_hack(self): #qt if not self.isChecked(): #qt self.setChecked(True) #qt self.setChecked(False) #qt def x__active(self): return self.isChecked() #qt def read_markup(markup): def read_markup0(mlist): text = '' for i in mlist: text += read_markup(i) if isinstance(i, list) else i return text tag = markup[0] if tag == '': return read_markup0(markup[1:]) elif tag in ('h1', 'h2', 'h3', 'h4', 'p', 'em', 'strong'): return '<%s>%s' % (tag, read_markup0(markup[1:]), tag) elif tag == 'color': return '%s' % (markup[1], read_markup0(markup[2:])) return "Markup parse error" class Label(QtGui.QLabel, WBase): #qt def __init__(self): QtGui.QLabel.__init__(self) #qt def x__markup(self, markup): self.setText(read_markup(markup)) #qt def x__image(self, path): self.setPixmap(QtGui.QPixmap(path)) #qt def x__align(self, pos): if pos == "center": a = QtCore.Qt.AlignCenter #qt else: a = QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter #qt self.setAlignment(a) #qt class Button(QtGui.QPushButton, WBase, BBase): #qt def __init__(self): QtGui.QPushButton.__init__(self) #qt def x__clicked(self, name=''): guiapp.signal(self, 'clicked', name, 'clicked()') #qt class ToggleButton(QtGui.QPushButton, WBase, BBase): #qt def __init__(self): QtGui.QPushButton.__init__(self) #qt self.setCheckable(True) #qt def x__toggled(self, name=''): guiapp.signal(self, 'toggled', name, 'toggled(bool)') #qt def x__set(self, on): self.setChecked(on) #qt class CheckBox(QtGui.QCheckBox, WBase): #qt def __init__(self): QtGui.QCheckBox.__init__(self) #qt def x__toggled(self, name=''): # A bit of work is needed to get True/False state #qt # instead of 0/1/2 #qt guiapp.signal(self, 'toggled', name, 'toggled(bool)', self.s_toggled) #qt def s_toggled(self, state): #qt """Convert the argument to True/False. """ #qt return (state != QtCore.Qt.Unchecked,) #qt def x__set(self, on): self.setCheckState(2 if on else 0) #qt def x__active(self): return self.checkState() != QtCore.Qt.Unchecked #qt class RadioButton(QtGui.QRadioButton, WBase): #qt def __init__(self): QtGui.QPushButton.__init__(self) #qt def x__toggled(self, name=''): guiapp.signal(self, 'toggled', name, 'toggled(bool)') #qt def x__set(self, on): self.setChecked(on) #qt def x__active(self): return self.isChecked() #qt class ComboBox(QtGui.QComboBox, WBase): #qt def __init__(self): QtGui.QComboBox.__init__(self) #qt def x__changed(self, name=''): guiapp.signal(self, 'changed', name, 'currentIndexChanged(int)') #qt def x__changedstr(self, name=''): guiapp.signal(self, 'changedstr', name, 'currentIndexChanged(const QString &)') #qt def x__set(self, items, index=0): self.blockSignals(True) self.clear() #qt if items: self.addItems(items) #qt self.setCurrentIndex(index) #qt self.blockSignals(False) def x__index(self): return self.currentIndex() #qt def x__setindex(self, index): return self.setCurrentIndex(index) #qt class ListChoice(QtGui.QListWidget, WBase): #qt def __init__(self): QtGui.QListWidget.__init__(self) #qt def x__changed(self, name=''): guiapp.signal(self, 'changed', name, 'currentRowChanged(int)') #qt def x__set(self, items, index=0): self.blockSignals(True) self.clear() #qt if items: self.addItems(items) #qt self.setCurrentRow(index) #qt self.blockSignals(False) class List(QtGui.QTreeWidget, WBase): #qt # Only using top-level items of the tree def __init__(self): QtGui.QTreeWidget.__init__(self) #qt self.mode = "" self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) #qt self.setRootIsDecorated(False) #qt self._hcompact = False # used for scheduling header-compaction def x__select(self, name=''): guiapp.signal(self, 'select', name, 'itemSelectionChanged()', self.s_select) #qt def x__clicked(self, name=''): guiapp.signal(self, 'clicked', name, 'itemClicked(QTreeWidgetItem *,int)', self.s_clicked) #qt def s_select(self): # Signal a selection change, passing the new selection list (indexes) s = [self.indexOfTopLevelItem(i) for i in self.selectedItems()] #qt if self.mode == "Single": return s else: return (s,) def s_clicked(self, item, col): #qt """This is intended for activating a user-defined editing function. Tests showed that this is called after the selection is changed, so if using this signal, use it only in 'Single' selection mode and use this, not 'select' to record selection changes. Clicking on the selected row should start editing the cell, otherwise just change the selection. """ ix = self.indexOfTopLevelItem(item) #qt return (ix, col) def x__selectionmode(self, sm): self.mode = sm if sm == "None": self.setSelectionMode(QtGui.QAbstractItemView.NoSelection) #qt elif sm == "Single": self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) #qt else: self.mode = "" self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) #qt def x__headers(self, headers): #qt self.setHeaderLabels(headers) #qt if self._hcompact: self._compact() def x__set(self, items, index=0): #qt # Note that each item must be a tuple/list containing # entries for each column. self.clear() #qt c = 0 for i in items: item = QtGui.QTreeWidgetItem(self, i) #qt self.addTopLevelItem(item) #qt if c == index: self.setCurrentItem(item) c += 1 if self._hcompact: self._compact() def x__compact(self, on=True): self._hcompact = on if on: self._compact() def _compact(self): for i in range(self.columnCount()): #qt self.resizeColumnToContents(i) #qt class LineEdit(QtGui.QLineEdit, WBase): #qt def __init__(self): QtGui.QLineEdit.__init__(self) #qt def x__enter(self, name=''): guiapp.signal(self, 'enter', name, 'returnPressed()') #qt def x__changed(self, name=''): guiapp.signal(self, 'changed', name, 'textEdited(const QString &)') #qt def x__get(self): return unicode(self.text()) #qt def x__ro(self, ro): self.setReadOnly(ro) #qt def x__pw(self, star): self.setEchoMode(QtGui.QLineEdit.Password if star == "+" #qt else QtGui.QLineEdit.NoEcho if star == "-" #qt else QtGui.QLineEdit.Normal) #qt class CheckList(QtGui.QWidget, WBase): #qt def __init__(self): QtGui.QWidget.__init__(self) #qt self.box = QtGui.QVBoxLayout(self) #qt self.title = None if text: #qt l.addWidget(QtGui.QLabel(text)) #qt self.widget = QtGui.QListWidget() #qt l.addWidget(self.widget) #qt def x__title(self, text): if self.title: self.title.setText(text) #qt else: self.title = QtGui.QLabel(text) #qt self.box.insertWidget(0, self.title) #qt def x__checked(self, index): return (self.widget.item(index).checkState() == #qt QtCore.Qt.Checked) #qt def x__set(self, items): self.widget.blockSignals(True) #qt self.widget.clear() #qt if items: for s, c in items: wi = QtGui.QListWidgetItem(s, self.widget) #qt wi.setCheckState(QtCore.Qt.Checked if c #qt else QtCore.Qt.Unchecked) #qt self.blockSignals(False) #qt class TextEdit(QtGui.QTextEdit, WBase): #qt def __init__(self): QtGui.QTextEdit.__init__(self) #qt def x__ro(self, ro): self.setReadOnly(ro) #qt def x__append_and_scroll(self, text): self.append(text) #qt self.ensureCursorVisible() #qt def x__get(self): return unicode(self.toPlainText()) #qt def x__undo(self): QtGui.QTextEdit.undo(self) #qt def x__redo(self): QtGui.QTextEdit.redo(self) #qt def x__copy(self): QtGui.QTextEdit.copy(self) #qt def x__cut(self): QtGui.QTextEdit.cut(self) #qt def x__paste(self): QtGui.QTextEdit.paste(self) #qt class HtmlView(QtWebKit.QWebView, WBase): #qt def __init__(self): QtWebKit.QWebView.__init__(self) #qt def x__html(self, content): self.setHtml(content) #qt def x__setUrl(self, url): self.load(QtCore.QUrl(url)) #qt def x__prev(self): self.back() #qt def x__next(self): self.forward() #qt class SpinBox(QtGui.QDoubleSpinBox, WBase): #qt def __init__(self): QtGui.QDoubleSpinBox.__init__(self) #qt self.step = None def x__changed(self, name=''): guiapp.signal(self, 'changed', name, 'valueChanged(double)') #qt def x__min(self, min): self.setMinimum(min) def x__max(self, max): self.setMaximum(max) def x__decimals(self, dec): self.setDecimals(dec) if not self.step: self.setSingleStep(10**(-dec)) def x__step(self, step): self.setSingleStep(step) def x__value(self, val): self.setValue(val) class ProgressBar(QtGui.QProgressBar, WBase): #qt def __init__(self): QtGui.QProgressBar.__init__(self) #qt def x__set(self, value): self.setValue(value) #qt def x__max(self, max): self.setMaximum(max) #qt # Layout classes class Layout: """A mixin base class for all layout widgets. """ pass boxmargin=3 class _BOX(Layout): def do_layout(self, items): self.setContentsMargins(boxmargin, boxmargin, boxmargin, boxmargin) #qt for wl in items: if isinstance(wl, QtGui.QWidget): #qt self.addWidget(wl) #qt elif isinstance(wl, SPACE): #qt if wl.size: #qt self.addSpacing(wl.size) #qt self.addStretch() #qt elif isinstance(wl, Layout): #qt self.addLayout(wl) #qt else: #qt gui_error("Invalid Box entry: %s" % repr(wl)) class VBOX(QtGui.QVBoxLayout, _BOX): #qt def __init__(self): QtGui.QVBoxLayout.__init__(self) #qt class HBOX(QtGui.QHBoxLayout, _BOX): #qt def __init__(self): QtGui.QHBoxLayout.__init__(self) #qt class GRID(QtGui.QGridLayout, Layout): #qt def __init__(self): QtGui.QGridLayout.__init__(self) #qt def do_layout(self, rows): y = -1 for row in rows: y += 1 x = -1 for wl in row: x += 1 if isinstance(wl, Span): continue # Determine the row and column spans x1 = x + 1 while (x1 < len(row)) and isinstance(row[x1], CSPAN): x1 += 1 y1 = y + 1 while (y1 < len(rows)) and isinstance(rows[y1][x], RSPAN): y1 += 1 if isinstance(wl, QtGui.QWidget): #qt self.addWidget(wl, y, x, y1-y, x1-x) #qt elif isinstance(wl, Layout): self.addLayout(wl, y, x, y1-y, x1-x) #qt elif isinstance(wl, SPACE): self.addItem(QtGui.QSpacerItem(wl.size, wl.height), y, x, y1-y, x1-x) #qt else: gui_error("Invalid entry in Grid layout: %s" % repr(wl)) class SPACE: """Can be used in boxes and grids. In boxes only size is of interest, and it also means vertical size in the case of a vbox. In grids size is the width. """ def __init__(self, size_width='0', height='0'): #qt self.size = int(size_width) #qt self.height = int(height) #qt class Span: """Class to group special grid layout objects together - it doesn't actually do anything itself, but is used for checking object types. """ pass class CSPAN(Span): """Column-span layout item. It doesn't do anything itself, but it is used by the Grid layout constructor. """ pass class RSPAN(Span): """Row-span layout item. It doesn't do anything itself, but it is used by the Grid layout constructor. """ pass class HLINE(QtGui.QFrame): #qt def __init__(self, pad=None): # pass the pad argument thus: 'HLINE,3' QtGui.QFrame.__init__(self) #qt self.setFrameShape(QtGui.QFrame.HLine) #qt if pad: self.setFixedHeight(1 + 2*int(pad)) #qt class VLINE(QtGui.QFrame): #qt def __init__(self, pad=None): # pass the pad argument thus: 'VLINE,3' QtGui.QFrame.__init__(self) #qt self.setFrameShape(QtGui.QFrame.VLine) #qt if pad: self.setFixedWidth(1 + 2*int(pad)) #qt class DATA: """This is not really a widget, it just holds a dictionary of potentially internationalized messages. """ def x__messages(self, mdict): self.messages = mdict def x__get(self, key): return self.messages.get(key) class Suim(QtGui.QApplication): """This class represents an application gui, possibly with more than one top level window. """ timers = [] # timer objects def __init__(self, appname='suim', busywidgets = []): global guiapp, T_ guiapp = self QtGui.QApplication.__init__(self, []) #qt self.appname = appname self.eno = QtCore.QEvent.registerEventType() #qt # This overcomplicated looking bit should deal with translating the built in dialogs _translator = QtCore.QTranslator(self) if (_translator.load('qt_'+ QtCore.QLocale.system().name(), QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath))): self.installTranslator(_translator) # list of widgets to disable while 'busy' self.busywidgets = busywidgets self.busystate = False self.busy_lock = threading.Lock() self.setQuitOnLastWindowClosed(False) #qt self.widgets = {} # all widgets, key = widget tag self.signal_dict = {} # signal connections, key = signature # callback list for event loop: (callback, arglist) pairs: self.idle_calls = deque() def event(self, e): if e.type() == self.eno: # Process item from list cb, a = self.idle_calls.popleft() cb(*a) return True else: return QtGui.QApplication.event(self, e) #qt def run(self): self.exec_() #qt def quit(self, cc=0): # QCoreApplication provides the static function 'exit' self.exit(cc) #qt def addwidget(self, fullname, wo): if self.widgets.has_key(fullname): gui_error("Attempted to define widget '%s' twice." % fullname) self.widgets[fullname] = wo def getwidget(self, w): widget = self.widgets.get(w) if widget == None: gui_warning("Unknown widget: %s" % w) return widget def show(self, windowname): self.getwidget(windowname).setVisible() def command(self, cmdtext, *args): cmd = specials_table.get(cmdtext) if not cmd: w, m = cmdtext.split(".") wo = self.getwidget(w) cmd = getattr(wo, 'x__' + m) return cmd(*args) def widget(self, wtype, wname, args): wobj = widget_table[wtype]() wobj.w_name = wname # Attributes for key, val in args.iteritems(): handler = "x__" + key if hasattr(wobj, handler): getattr(wobj, handler)(val) # Unrecognized attributes are ignored ... self.addwidget(wname, wobj) def widgetlist(self, wlist): for w in wlist: # Add simple signals for s in w[3:]: w[2][s] = '' self.widget(w[0], w[1], w[2]) def signal(self, source, signal, name=None, xsignal=None, convert=None): """Enable or disable a signal. Signal.signals is a dictionary of enabled signals. The key is constructed from the widget name and the formal signal name. The name of the signal which actually gets generated will be the same as the key unless the 'name' parameter is set. See the 'Signal' class for further details. If 'name' is None (not ''!), the signal will be disabled. """ widsig = source.w_name + '*' + signal if name == None: s = Signal.signals.get(widsig) if not s: gui_error("Can't disable signal '%s' - it's not enabled" % widsig) s.disconnect() # Probably not necessary in qt del(Signal.signals[widsig]) else: if Signal.signals.has_key(widsig): gui_error("Signal already connected: %s" % widsig) Signal.signals[widsig] = Signal(source, signal, name, xsignal, convert) def connect(self, signal, function): if self.signal_dict.has_key(signal): self.signal_dict[signal].append(function) else: self.signal_dict[signal] = [function] def connectlist(self, *slotlist): for s in slotlist: self.connect(*s) def disconnect(self, signal, function): try: l = self.signal_dict[signal] l.remove(function) except: gui_error("Slot disconnection for signal '%s' failed" % signal) def sendsignal(self, name, *args): # When there are no slots a debug message is output. slots = self.signal_dict.get(name) if slots: try: for slot in slots: slot(*args) except: gui_error("Signal handling error:\n %s" % traceback.format_exc()) else: debug("Unhandled signal: %s %s" % (name, repr(args))) def idle_add(self, callback, *args): self.idle_calls.append((callback, args)) e = QtCore.QEvent(self.eno) #qt self.postEvent(self, e) #qt def timer(self, callback, period): """Start a timer which calls the callback function on timeout. Only if the callback returns True will the timer be retriggered. """ Suim.timers.append(Timer(callback, period)) def busy(self, on, busycursor=True): """This activates (or deactivates, for on=False) a 'busy' mechanism, which can be one or both of the following: Make the application's cursor change to the 'busy cursor'. Disable a group of widgets. There is a lock to prevent the busy state from being set when it is already active, or unset it when already unset. """ self.busy_lock.acquire() if on: if self.busystate: self.busy_lock.release() return self.busycursor = busycursor if busycursor: self.setOverrideCursor(QtCore.Qt.BusyCursor) #qt else: if not self.busystate: self.busy_lock.release() return if self.busycursor: self.restoreOverrideCursor() #qt self.busystate = on self.busy_lock.release() for wn in self.busywidgets: w = self.getwidget(wn) if w: w.setEnabled(not on) #qt else: debug("*ERROR* No widget '%s'" % wn) class Timer(QtCore.QTimer): #qt def __init__(self, timers, callback, period): QtCore.QTimer.__init__(self) #qt self.x_callback = callback self.connect(self, QtCore.SIGNAL("timeout()"), #qt self.x_timeout) self.start(int(period * 1000)) #qt def x_timeout(self): if not self.x_callback(): self.stop() #qt Suim.timers.remove(self) class Signal: """Each instance represents a single connection. """ signals = {} # Enabled signals def __init__(self, source, signal, name, xsignal, convert): """'source' is the widget object which initiates the signal. 'signal' is the signal name. If 'name' is given (not empty), the signal will get this as its name, and this name may be used for more than one connection. Otherwise the name is built from the name of the source widget and the signal type as 'source*signal' and this is unique. If 'name' begins with '+' an additional argument, the source widget name, will be inserted at the head of the argument list. 'xsignal' is a toolkit specific signal descriptor. 'convert' is an optional function (default None) - toolkit specific - to perform signal argument conversions. """ self.widsig = '%s*%s' % (source.w_name, signal) #+ For disconnect? self.xsignal = xsignal #- self.convert = convert # Argument conversion function self.tag = name if name else self.widsig self.wname = source.w_name if self.tag[0] == '+' else None if not source.connect(source, QtCore.SIGNAL(xsignal), #qt self.signal): gui_error("Couldn't enable signal '%s'" % self.widsig) def signal(self, *args): if self.convert: args = self.convert(*args) if self.wname: guiapp.sendsignal(self.tag, self.wname, *args) else: guiapp.sendsignal(self.tag, *args) def disconnect(self): w = guiapp.getwidget(self.widsig.split('*')[0]) w.disconnect(w, QtCore.SIGNAL(self.xsignal), self.signal) #qt #+++++++++++++++++++++++++++ # Catch all unhandled errors. def errorTrap(type, value, tb): etext = "".join(traceback.format_exception(type, value, tb)) debug(etext) gui_error(etext, "This error could not be handled.") sys.excepthook = errorTrap #--------------------------- widget_table = { "DATA": DATA, "Window": Window, "Dialog": Dialog, "DialogButtons": DialogButtons, "Notebook": Notebook, "Stack": Stack, "Page": Page, "Frame": Frame, "Button": Button, "ToggleButton": ToggleButton, "RadioButton": RadioButton, "CheckBox": CheckBox, "Label": Label, "CheckList": CheckList, "List": List, "OptionalFrame": OptionalFrame, "ComboBox": ComboBox, "ListChoice": ListChoice, "LineEdit": LineEdit, "TextEdit": TextEdit, "HtmlView": HtmlView, "SpinBox": SpinBox, "ProgressBar": ProgressBar, } specials_table = { "textLineDialog": textLineDialog, "infoDialog": infoDialog, "confirmDialog": confirmDialog, "errorDialog": gui_error, "warningDialog": gui_warning, "fileDialog_getdir": fileDialog_getdir, "fileDialog_open": fileDialog_open, "fileDialog_save": fileDialog_save, "listDialog": listDialog, } layout_table = { "VBOX": VBOX, "HBOX": HBOX, "GRID": GRID, # "+": GRIDROW, "-": CSPAN, "|": RSPAN, "*": SPACE, "VLINE": VLINE, "HLINE": HLINE, }