Attached is a first cut at a patch for adding bitmap image support. Actually there's three things; a patch a new file with the units changes (measurement.py) that lives in utils and bitmap.py that lives in widgets. There are a few known issues - the occasional rounding error, some slightly hackish approaches to problems, a few existing bugs that I haven't fixed (distances need to know if they're x or y so measurements as fractions of the canvas size aren't broken) but I think it's at the stage where it's worth other people commenting on and using it.

In the slightly longer term it might be worth adding images as a particular subset of 2D datasets, especially when we have cool functions for manipulating 2D data... :) But that's a bit non-trivial to do at the moment (what's the x,y scale on an image, for example).

I may have messed something up in the patch because I managed to break my CVS so I merged the changes with the trunk manually :( So if something really obvious is broken that's most likely why...

--
"As soon as people come up with a measurable substitute for whatever it is they care about they start treating it as more important than the real thing"
-Boris Zbarsky
? bitmap.diff
? veusz.e3t
? dialogs/__init__.pyc
? dialogs/aboutdialog.pyc
? dialogs/dataeditdialog.pyc
? dialogs/exceptiondialog.pyc
? dialogs/importdialog.pyc
? dialogs/importfits.pyc
? dialogs/reloaddata.pyc
? document/__init__.pyc
? document/commandinterface.pyc
? document/commandinterpreter.pyc
? document/doc.pyc
? document/simpleread.pyc
? setting/__init__.pyc
? setting/collections.pyc
? setting/controls.pyc
? setting/setting.pyc
? setting/settingdb.pyc
? setting/settings.pyc
? utils/__init__.pyc
? utils/fitlm.pyc
? utils/measurement.py
? utils/measurement.pyc
? utils/points.pyc
? utils/pref.pyc
? utils/preftypes.pyc
? utils/textrender.pyc
? utils/utilfuncs.pyc
? utils/version.pyc
? widgets/__init__.pyc
? widgets/axis.pyc
? widgets/axisticks.pyc
? widgets/bitmap.py
? widgets/bitmap.pyc
? widgets/containers.pyc
? widgets/fit.pyc
? widgets/graph.pyc
? widgets/image.pyc
? widgets/key.pyc
? widgets/page.pyc
? widgets/plotters.pyc
? widgets/root.pyc
? widgets/widget.pyc
? widgets/widgetfactory.pyc
? windows/__init__.pyc
? windows/action.pyc
? windows/consolewindow.pyc
? windows/mainwindow.pyc
? windows/plotwindow.pyc
? windows/treeeditwindow.pyc
Index: setting/controls.py
===================================================================
RCS file: /cvs/veusz/veusz/setting/controls.py,v
retrieving revision 1.19
diff -u -r1.19 controls.py
--- setting/controls.py	30 Jun 2005 20:40:09 -0000	1.19
+++ setting/controls.py	22 Jul 2005 22:32:11 -0000
@@ -26,6 +26,7 @@
 
 import qt
 import setting
+from utils import measurement
 
 class SettingEdit(qt.QLineEdit):
     """Main control for editing settings which are text."""
@@ -69,7 +70,7 @@
         except setting.InvalidType:
             self.setPaletteBackgroundColor(qt.QColor('red'))
 
-    def onModified(self, mod):
+    def onModified(self, setting, mod):
         """called when the setting is changed remotely"""
         self.setText( self.setting.toText() )
 
@@ -260,7 +261,7 @@
         except setting.InvalidType:
             self.edit.setPaletteBackgroundColor(qt.QColor('red'))
 
-    def onModified(self, mod):
+    def onModified(self, setting, mod):
         """called when the setting is changed remotely"""
         self.edit.setText( self.setting.toText() )
         
@@ -290,7 +291,7 @@
         """Emitted when checkbox toggled."""
         self.setting.set(state)
         
-    def onModified(self, mod):
+    def onModified(self, setting, mod):
         """called when the setting is changed remotely"""
         self.setChecked( self.setting.get() )
 
@@ -345,7 +346,7 @@
         except setting.InvalidType:
             self.setPaletteBackgroundColor(qt.QColor('red'))
 
-    def onModified(self, mod):
+    def onModified(self, setting, mod):
         """called when the setting is changed remotely"""
         self.setCurrentText( self.setting.toText() )
 
@@ -390,7 +391,7 @@
         except setting.InvalidType:
             self.setPaletteBackgroundColor(qt.QColor('red'))
 
-    def onModified(self, mod):
+    def onModified(self, setting, mod):
         """called when the setting is changed remotely"""
         self.setText( self.setting.toText() )
 
@@ -421,11 +422,16 @@
         # get rid of non-numeric things from the string
         num = self.stripnumre.sub('', text)
 
-        # here are a list of possible different units the user can choose
-        # between. should this be in utils?
-        newitems = [ num+'pt', num+'cm', num+'mm',
-                     num+'in', num+'%', '1/'+num ]
+        mainwindow = self.topLevelWidget()
 
+        #Create a list of the current distance in all possible units
+        newitems = []
+        for unit in measurement.measurementClasses.keys():
+            try:
+                newitems.append(self.setting.convert(mainwindow.painter,unit))
+            except measurement.InvalidType:
+                pass
+            
         # if we're already in this list, we position the current selection
         # to the correct item (up and down keys work properly then)
         # spaces are removed to make sure we get sensible matches
@@ -502,4 +508,106 @@
     def slotModified(self, modified):
         """Update the list of datasets if the document is modified."""
         self.populateEntries()
-        
+
+class FilenameSetting(qt.QHBox):
+    """Setting that accepts a filename
+    TODO: Factor out image-specific code
+    make text box a bit more intelligent (strip leading + trailing whitespace)
+    make file not found turn the control invalid"""
+    def __init__(self, setting, parent):
+        qt.QHBox.__init__(self, parent)
+
+        self.setting = setting
+        self.edit = qt.QLineEdit(self)
+        b = self.button = qt.QPushButton('Browse...', self)
+
+        self.bgcolour = self.paletteBackgroundColor()
+
+        # set the text of the widget to the 
+        self.edit.setText( setting.toText() )
+
+        self.connect(self.edit, qt.SIGNAL('returnPressed()'),
+                     self.validateAndSet)
+        self.connect(self.edit, qt.SIGNAL('lostFocus()'),
+                     self.validateAndSet)
+        self.connect(b, qt.SIGNAL('clicked()'),
+                     self.browseFile)
+
+        self.setting.setOnModified(self.onModified)
+
+        if setting.readonly:
+            self.edit.setReadOnly(True)
+
+    def done(self):
+        """Delete modification notification."""
+        self.setting.removeOnModified(self.onModified)
+
+    def focusOutEvent(self, *args):
+        """Allows us to check the contents of the widget."""
+        qt.QTextEdit.focusOutEvent(self.edit, *args)
+
+        text = unicode(self.text())
+        try:
+            val = self.setting.fromText(text)
+            self.edit.setPaletteBackgroundColor(self.bgcolour)
+
+            # value has changed
+            if self.setting.get() != val:
+                self.setting.set(val)
+
+        except setting.InvalidType:
+            self.edit.setPaletteBackgroundColor(qt.QColor('red'))
+
+    def onModified(self, setting, mod):
+        """called when the setting is changed remotely"""
+        self.edit.setText( self.setting.toText() )
+
+    def browseFile(self):
+        mainwindow = self.topLevelWidget()
+        if mainwindow.bitmapDirName:
+            dirname = mainwindow.bitmapDirName
+        else:
+            dirname = mainwindow.dirname
+
+            fd = qt.QFileDialog(self, 'open dialog', True)
+            fd.setDir( dirname )
+            fd.setMode( qt.QFileDialog.ExistingFile )
+            fd.setFilters ( "Portable Network Graphics (*.png);;"
+                            "Graphics Interchange Format (*.gif);;"
+                            "Microsoft Bitmap (*.bmp);;"
+                            "JPEG  (*.jpeg *.jpg);;"
+                            "MNG (*.mng);;"
+                            "PBM (*.pbm);;"
+                            "PGM (*.pgm);;"
+                            "PPM (*.ppm);;"
+                            "XBM (*.xbm);;"
+                            "XPM (*.xpm)" )
+            fd.setCaption('Select Image')
+            # if the user chooses a file
+            if fd.exec_loop() == qt.QDialog.Accepted:
+                mainwindow.bitmapDirName = fd.dir()
+                self.edit.setText(unicode(fd.selectedFile()))
+                self.validateAndSet()
+    def done(self):
+        """Delete modification notification."""
+        self.setting.removeOnModified(self.onModified)
+
+    def validateAndSet(self):
+        """Check the text is a valid setting and update it."""
+
+        text = unicode(self.edit.text())
+        try:
+            val = self.setting.fromText(text)
+            self.setPaletteBackgroundColor(self.bgcolour)
+
+            # value has changed
+            if self.setting.get() != val:
+                self.setting.set(val)
+
+        except setting.InvalidType:
+            self.setPaletteBackgroundColor(qt.QColor('red'))
+
+    def onModified(self, setting, mod):
+        """called when the setting is changed remotely"""
+        self.edit.setText( self.setting.toText() )        
+
Index: setting/setting.py
===================================================================
RCS file: /cvs/veusz/veusz/setting/setting.py,v
retrieving revision 1.20
diff -u -r1.20 setting.py
--- setting/setting.py	30 Jun 2005 20:40:09 -0000	1.20
+++ setting/setting.py	22 Jul 2005 22:32:12 -0000
@@ -33,6 +33,7 @@
 import qt
 
 import utils
+from utils import measurement
 import controls
 import widgets
 from settingdb import settingdb
@@ -51,7 +52,7 @@
         self.parent = None
         self.name = name
         self.descr = descr
-        self.default = val
+        self.default = self.convertFrom(val)
         self.onmodified = []
         self.set(val)
 
@@ -159,7 +160,7 @@
         """Save the stored setting."""
         self.val = self.convertTo( val )
         for i in self.onmodified:
-            i(True)
+            i(self, True)
 
     def setSilent(self, val):
         """Set the setting, without propagating modified flags.
@@ -352,91 +353,51 @@
     def makeControl(self, *args):
         return controls.SettingChoice(self, True, ['Auto'], *args)
 
-
-# these are functions used by the distance setting below.
-# they don't work as class methods
-
-def _calcPixPerPt(painter):
-    """Calculate the numbers of pixels per point for the painter.
-
-    This is stored in the variable veusz_pixperpt."""
-
-    dm = qt.QPaintDeviceMetrics(painter.device())
-    painter.veusz_pixperpt = dm.logicalDpiY() / 72.
-
-def _distPhys(match, painter, mult):
-    """Convert a physical unit measure in multiples of points."""
-
-    if not hasattr(painter, 'veusz_pixperpt'):
-        _calcPixPerPt(painter)
-
-    return int( math.ceil(painter.veusz_pixperpt * mult *
-                          float(match.group(1)) * painter.veusz_scaling ) )
-
-def _distPerc(match, painter, maxsize):
-    """Convert from a percentage of maxsize."""
-
-    return int( math.ceil(maxsize * 0.01 * float(match.group(1))) )
-
-def _distFrac(match, painter, maxsize):
-    """Convert from a fraction a/b of maxsize."""
-
-    return int( math.ceil(maxsize * float(match.group(1)) /
-                          float(match.group(2))) )
-
-def _distRatio(match, painter, maxsize):
-    """Convert from a simple 0.xx ratio of maxsize."""
-
-    # if it's greater than 1 then assume it's a point measurement
-    if float(match.group(1)) > 1.:
-        return _distPhys(match, painter, 1)
-
-    return int( math.ceil(maxsize * float(match.group(1))) )
-
-# mappings from regular expressions to function to convert distance
-# the recipient function takes regexp match,
-# painter and maximum size of frac
-_distregexp = [ ( re.compile('^([0-9\.]+) *%$'),
-                  _distPerc ),
-                ( re.compile('^([0-9\.]+) */ *([0-9\.]+)$'),
-                  _distFrac ),
-                ( re.compile('^([0-9\.]+) *pt$'),
-                  lambda match, painter, t:
-                  _distPhys(match, painter, 1.) ),
-                ( re.compile('^([0-9\.]+) *cm$'),
-                  lambda match, painter, t:
-                  _distPhys(match, painter, 28.452756) ),
-                ( re.compile('^([0-9\.]+) *mm$'),
-                  lambda match, painter, t:
-                  _distPhys(match, painter, 2.8452756) ),
-                ( re.compile('^([0-9\.]+) *(inch|in|")$'),
-                  lambda match, painter, t:
-                  _distPhys(match, painter, 72.27) ),
-                ( re.compile('^([0-9\.]+)$'),
-                  _distRatio )
-                ]
-
 class Distance(Setting):
     """A veusz distance measure, e.g. 1pt or 3%."""
-
     def isDist(self, dist):
         """Is the text a valid distance measure?"""
-        
-        dist = dist.strip()
-        for reg, fn in _distregexp:
-            if reg.match(dist):
-                return True
-            
-        return False
+
+        matchFound = False
+        for measure in measurement.measurementClasses.values():
+            if measure.matches(dist):
+                matchFound=True
+                break
+        return matchFound
 
     def convertTo(self, val):
         if self.isDist(val):
-            return val
+            for measure in measurement.measurementClasses.values():
+                if measure.matches(val):
+                    measureVal = measure()
+                    measureVal.fromStr(val)
+                    return measureVal
+            raise InvalidType
         else:
             raise InvalidType
 
+    def convertFrom(self, val):
+        return str(val)
+
+    def getUnit(self):
+        """Return the unit system in which the value is measured"""
+        return self.val.name
+
+    def setUnit(self, painter, name):
+        """Alter the unit system in which the value is measured"""
+        painterValue = self.val.toPainter(painter)
+        newMeasurement = measurement.measurementClasses[name]()
+        newMeasurement.fromPainter(painter, painterValue)
+        self.set(str(newMeasurement))
+
+    def toPainter(self, painter):
+        return self.val.toPainter(painter)
+
+    def fromPainter(self, painter):
+        return self.val.fromPainter(painter)
+
     def toText(self):
-        return self.val
+        return str(self.val)
 
     def fromText(self, text):
         if self.isDist(text):
@@ -447,46 +408,31 @@
     def makeControl(self, *args):
         return controls.SettingDistance(self, *args)
 
-    def convert(self, painter):
-        '''Convert a distance to plotter units.
+    def convert(self, painter, units=None, withSuffix=True):
+        '''Convert a distance to plotter units and return as a string.
 
-        dist: eg 0.1 (fraction), 10% (percentage), 1/10 (fraction),
-                 10pt, 1cm, 20mm, 1inch, 1in, 1" (size)
-        maxsize: size fractions are relative to
-        painter: painter to get metrics to convert physical sizes
+        painter - the painter with which the conversion is performed
+        units - the type of units to return
+        withSuffix - append the suffix to the units (if one exists) e.g. cm
         '''
-
-        # we set a scaling variable in the painter if it's not set
-        if 'veusz_scaling' not in painter.__dict__:
-            painter.veusz_scaling = 1.
-
-        # work out maximum size
-        try:
-            maxsize = max( *painter.veusz_page_size )
-        except AttributeError:
-            w = painter.window()
-            maxsize = max(w.width(), w.height())
-
-        dist = self.val.strip()
-
-        # compare string against each regexp
-        for reg, fn in _distregexp:
-            m = reg.match(dist)
-
-            # if there's a match, then call the appropriate conversion fn
-            if m:
-                return fn(m, painter, maxsize)
-
-        # none of the regexps match
-        raise ValueError( "Cannot convert distance in form '%s'" %
-                          dist )
+        #TODO: Make conversions not produce loads of dp (maybe make set round to 1px?)
+        if not units:
+            return self.val.toPainter(painter)
+        else:
+            painterValue = self.val.toPainter(painter, False)
+            measure = measurement.measurementClasses[units]()
+            measure.fromPainter(painter, painterValue)
+            rv = str(measure)
+            if (not withSuffix) and measure.suffix:
+                rv = rv[:rv.index(measure.suffix)]
+            return rv
 
     def convertPts(self, painter):
-        """Get the distance in points."""
-        if not hasattr(painter, 'veusz_pixperpt'):
-            _calcPixPerPt(painter)
+        return float(self.convert(painter, "pt", False))
+
+    def getUnit(self):
+        return self.val.name
 
-        return self.convert(painter) / painter.veusz_pixperpt
         
 class Choice(Setting):
     """One out of a list of strings."""
@@ -691,3 +637,7 @@
         return controls.DatasetChoose(self, self.document, self.dimensions,
                                       *args)
     
+class Filename(Str):
+    def makeControl(self, *args):
+        """Allow user to choose between the datasets."""
+        return controls.FilenameSetting(self, *args)
Index: setting/settings.py
===================================================================
RCS file: /cvs/veusz/veusz/setting/settings.py,v
retrieving revision 1.9
diff -u -r1.9 settings.py
--- setting/settings.py	28 Jun 2005 20:43:58 -0000	1.9
+++ setting/settings.py	22 Jul 2005 22:32:12 -0000
@@ -76,7 +76,7 @@
         if readonly:
             setting.readonly = True
 
-    def setModified(self, modified = True):
+    def setModified(self, setting=None, modified = True):
         """Set the modification flag."""
         self.modified = modified
         self.changeset += 1
Index: widgets/__init__.py
===================================================================
RCS file: /cvs/veusz/veusz/widgets/__init__.py,v
retrieving revision 1.7
diff -u -r1.7 __init__.py
--- widgets/__init__.py	26 Jun 2005 14:06:29 -0000	1.7
+++ widgets/__init__.py	22 Jul 2005 22:32:12 -0000
@@ -22,6 +22,7 @@
 
 from widget import *
 from axis import *
+from bitmap import *
 from graph import *
 from containers import *
 from plotters import *
Index: windows/mainwindow.py
===================================================================
RCS file: /cvs/veusz/veusz/windows/mainwindow.py,v
retrieving revision 1.42
diff -u -r1.42 mainwindow.py
--- windows/mainwindow.py	12 Jul 2005 10:06:56 -0000	1.42
+++ windows/mainwindow.py	22 Jul 2005 22:32:13 -0000
@@ -30,6 +30,7 @@
 import treeeditwindow
 import document
 import utils
+from utils import measurement
 import setting
 
 import dialogs.aboutdialog
@@ -62,6 +63,17 @@
         self.plot = plotwindow.PlotWindow(self.document, self)
         self.plotzoom = 0
 
+        #Create a painter object that is accessible everywhere.
+        #This should be used for unit conversions only, not for actual painting
+        #XXX - Do we even need a real painter here?
+        self.painter = qt.QPainter(self.plot)
+        self.painter.veusz_scaling = self.plot.zoomfactor
+        if not measurement.windowPxPerPt:
+            measurement.calcPixPerPt(self.painter)
+            measurement.windowPxPerPt  = self.painter.veusz_pixperpt
+            self.painter.veusz_page_size = self.document.basewidget.getSize(self.painter)
+        self.painter.end()
+
         # likewise with the tree-editing window
         self.treeedit = treeeditwindow.TreeEditWindow(self.document, self)
         self.moveDockWindow( self.treeedit, qt.Qt.DockLeft, True, 1 )
@@ -84,6 +96,9 @@
         self.statusBar().addWidget(self.pagelabel)
 
         self.dirname = ''
+        #Dir to use for loading bitmaps
+        self.bitmapDirName = ''
+        #Dir for file export
         self.exportDir = ''
         
         self.connect( self.plot, qt.PYSIGNAL("sigUpdatePage"),
Index: windows/plotwindow.py
===================================================================
RCS file: /cvs/veusz/veusz/windows/plotwindow.py,v
retrieving revision 1.25
diff -u -r1.25 plotwindow.py
--- windows/plotwindow.py	12 Jun 2005 17:12:45 -0000	1.25
+++ windows/plotwindow.py	22 Jul 2005 22:32:13 -0000
@@ -36,12 +36,14 @@
 class PlotWindow( qt.QScrollView ):
     """Class to show the plot(s) in a scrollable window."""
     
-    def __init__(self, document, *args):
+    def __init__(self, document, mainwindow, *args):
         """Initialise the window."""
 
-        qt.QScrollView.__init__(self, *args)
+        qt.QScrollView.__init__(self, mainwindow, *args)
         self.viewport().setBackgroundMode( qt.Qt.NoBackground )
 
+        self.mainwindow = mainwindow
+        
         # set up so if document is modified we are notified
         self.document = document
         self.docchangeset = -100
@@ -77,6 +79,8 @@
         painter = qt.QPainter( self )
         painter.veusz_scaling = self.zoomfactor
         size = self.document.basewidget.getSize(painter)
+        if hasattr(self.mainwindow, 'painter'):
+            self.mainwindow.painter.veusz_page_size = size
         painter.end()
 
         # make new buffer and resize widget

Attachment: measurement.py
Description: application/python

Attachment: bitmap.py
Description: application/python

Répondre à