diff -Nru veusz-1.10/ChangeLog veusz-1.14/ChangeLog --- veusz-1.10/ChangeLog 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/ChangeLog 2011-11-22 20:23:31.000000000 +0000 @@ -1,240 +1,167 @@ -Changes from 1.9 to 1.10 are shown below. -Please see the README file for an easier to read version of this ChangeLog. +Changes in 1.14: + * Added interactive tutorial + * Points in graphs can be colored depending on another dataset and + the scale shown in a colorbar widget + * Improved CSV import + - better data type detection + - locale-specific numeric and date formats + - single/multiple/none header modes + - option to skip lines at top of file + - better handling of missing values + * Data can be imported from clipboard + * Substantially reduced size of output SVG files + * In standard data import, descriptor can be left blank to generate + dataset names colX + * Axis plotting range can be interactively manipulated + * If axis is in date-time format, show and allow the min and max + values to be in date-time format + * ImageFile widget can have image data embedded in document file + * Fit widget can update the fit parameters and fit quality to a + label widget + * Allow editing of 2D datasets in data edit dialog + * Add copy and paste dataset command to dataset browser context menu + +Minor and API changes: + * Examples added to help menu + * Picker shows date values as dates + * Allow descriptor statement in standard data files after a comment + character, e.g. "#descriptor x y" + * Added some further color maps + * Draw key symbols for vector field widget + * Import plugin changes + - Register classes rather than instances (backward compatibility + is retained) + - Plugins can return constants and functions (see Constant and + Function types) + - Add DatasetDateTime for returning date-time datasets + * Custom definitions + - Add RemoveCustom API to remove custom definitions + - AddCustom API can specify order where custom definition is added + * C++ code to speed up plotting points of different sizes / colors + * Expand files by default in data navigator window + * Select created datasets in data edit dialog + * Tooltip wrapping used in data navigator window + * Grid lines are dropped if they overlap with edge of graph + +Bug fixes + * Fix initial extension in export dialog + * Fix crash on hiding pages + * Fixed validation for numeric values + * Position of grid lines in perpendicular direction for non default + positions + * Catch errors in example import plugin + * Fix crash for non existent key symbols + * Fix crash when mismatch of dataset sizes when combining 1D datasets + to make 2D dataset + +Changes in 1.13: + * Graphs are rendered in separate threads for speed and a responsive + user interface + * A changed Graph is rendered immediately on document modification, + improving latency + * A new ternary plot widget is included + * Size of pages can be modified individually in a document + * Binary data import added + * NPY/NPZ numpy data import added + * Axis and tick labels on axes can be rotated at 45 deg intervals + * Labels can be plotted next to points on non-orthogonal plots + * Add an option for DPI of output EPS and PDF files + +Minor improvements: + * Import dialog detects filename extension to show correct tab + * Polygon fill mode for non orthogonal plotting + * --plugin command line option added, for loading and testing plugins + * Plugin for swapping two colors in a plot + * Dataset navigator is moved to right of window by default + * Mac OS X binary release updated to Python 2.7.2 + * Import plugins can say which file extensions they support + * Import plugins can be "promoted" to their own tab on the import dialog + * ForceUpdate command added to embedding API, to force an update of + the displayed plot (useful if SetUpdateInterval is set to 0) + * X or Y dataset can be left blank in plotter to plot by row number + +Bugs fixed: + * Images plotted when axes are inverted are inverted too + * Fixed crash when selecting datasets for plotting in the popup menu + * Picker crashes with a constant function + * 2D dataset creation using expressions fixed + * CSV reader treated dataset names ending in + or - incorrectly + * unique1d function no longer available in numpy + +Changes in 1.12: + * Multiple widgets can now be selected for editing properties + * Add Edit->Select menu and context menu for above + * Added context menu on dataset browser for filenames to reload, + delete or unlink all associated datasets + * New tree-like dataset browsing widget is shown in data edit dialog + * Importing 1D fits images is now supported + * Date / time data has its own dataset type + * The data edit dialog box can create or edit date/time data in + human-readable form + +Minor improvements: + * Add LaTeX commands \cdot, \nabla, \overline plus some arrows + * Inform user in exception dialog if a new version is available + * Add linevertbar and linehorzbar error bar styles + +Bug fixes: + * Fix crash on filling filled error regions if no error bars + * Remove grouping separator to numbers in locale as it creates + ambiguous lists of numbers + * Undo works properly for boolean and integer settings + * Prevent widgets getting the same names when dragging and dropping + * Hidden plot widgets are ignored when calculating axis ranges + * Combo boxes are now case sensitive when displaying matches with + previous text + * Fix errors if plotting DatasetRange or Dataset1DPlugin datasets + against data with nan values + * Fix division by zero in dataset preview + * Do not leave settings pointing to deleted widgets after an undo + * Fix errors when using super/subscripts of super/subscripts + * Fix crash when giving positions of bar plot and labels + * Do not allow dataset names to be invalid after remaining + * Several EMF format bug fixes, including not showing hidden lines + and not connecting points making curves + * Stop crash when contouring zero-sized datasets + +Changes in 1.11: + * New data point picker for finding coordinates of points on plot + (contributed by B.K. Stuhl) + * New data navigator window for filtering, sorting and examining + dataset statistics + * ".." button next to dataset settings pops up data navigator for + choosing datasets + * Data fitting can now use PyMinuit, giving error estimates + (B.K. Stuhl) + * Console history now uses currently entered characters to select + lines from history (B.K. Stuhl) + * New self test script, comparing graph output with expected output + * Put superscripts and subscripts above each other when + formatting (B.K. Stuhl) + * Key entries can have multiple lines (using \\) (B.K. Stuhl) + * Option to treat blanks as data items in CSV files + * Locale support added for number formatting + - Can use current locale or US/English in documents + - Can use US/English or current local in user interface + * Contours avoid missing (nan) values + + * Linux binaries are now created on a more modern system + * Windows binaries now use MSVC for compilation + +Bug fixes: + * CSV import with blank columns fixed + * Embedding module now working when using binary + * Remember current directory with unicode characters + * Extension module now compiles under MSVC in Windows + * Output is always appended to console (B.K. Stuhl) + * \r characters sometimes break data import in Windows + * If using --export option, add directory of script to import path + +Minor bug fixes: + * Zero sized dataset contour plot fix + * Fix problem on context menu for axis match setting + * Small values on log axis fix + * Disable data edit dialog context menu when no datasets + * Loading files with unicode filenames on command line + * Do not allow non finite float settings ------------------------------------------------------------------------- -r1471 | jeremysanders | 2010-12-10 22:18:24 +0000 (Fri, 10 Dec 2010) | 1 line - -Add option to ignore N lines after headers in CSV files ------------------------------------------------------------------------- -r1470 | jeremysanders | 2010-12-09 09:39:12 +0000 (Thu, 09 Dec 2010) | 1 line - -Avoid error in axis range if no data given ------------------------------------------------------------------------- -r1469 | jeremysanders | 2010-12-08 22:15:00 +0000 (Wed, 08 Dec 2010) | 1 line - -Provide manual mode for box plots ------------------------------------------------------------------------- -r1468 | jeremysanders | 2010-12-02 19:56:44 +0000 (Thu, 02 Dec 2010) | 1 line - -Documentation updates ------------------------------------------------------------------------- -r1467 | jeremysanders | 2010-12-02 19:29:44 +0000 (Thu, 02 Dec 2010) | 1 line - -Increase line width in box plot example ------------------------------------------------------------------------- -r1466 | jeremysanders | 2010-12-02 13:58:20 +0000 (Thu, 02 Dec 2010) | 1 line - -Make compression larger for PNG export ------------------------------------------------------------------------- -r1465 | jeremysanders | 2010-12-01 17:20:48 +0000 (Wed, 01 Dec 2010) | 1 line - -Add man page generation to generate_manual script ------------------------------------------------------------------------- -r1464 | jeremysanders | 2010-12-01 17:17:37 +0000 (Wed, 01 Dec 2010) | 1 line - -Move pod manual to Documents ------------------------------------------------------------------------- -r1463 | jeremysanders | 2010-12-01 17:06:14 +0000 (Wed, 01 Dec 2010) | 1 line - -Use separate reading thread for veusz_listen to make it compatible with Windows ------------------------------------------------------------------------- -r1462 | jeremysanders | 2010-11-30 09:36:37 +0000 (Tue, 30 Nov 2010) | 1 line - -Update boxplot and change version to beta ------------------------------------------------------------------------- -r1461 | jeremysanders | 2010-11-29 21:44:43 +0000 (Mon, 29 Nov 2010) | 1 line - -Add box plot example ------------------------------------------------------------------------- -r1460 | jeremysanders | 2010-11-29 21:32:08 +0000 (Mon, 29 Nov 2010) | 1 line - -Add man pages for veusz and veusz_listen. Add --quiet option for executing commands without opening a window. ------------------------------------------------------------------------- -r1459 | jeremysanders | 2010-11-29 20:45:15 +0000 (Mon, 29 Nov 2010) | 1 line - -Rework main program. Add --listen option to act as veusz_listen. Add --export option to export document to graphics files and exit ------------------------------------------------------------------------- -r1458 | jeremysanders | 2010-11-28 12:34:37 +0000 (Sun, 28 Nov 2010) | 1 line - -Implement 2d dataset creation based on expressions of other 2D datasets ------------------------------------------------------------------------- -r1457 | jeremysanders | 2010-11-28 11:25:22 +0000 (Sun, 28 Nov 2010) | 1 line - -Check if files have changed before reloading data ------------------------------------------------------------------------- -r1456 | jeremysanders | 2010-11-26 22:43:04 +0000 (Fri, 26 Nov 2010) | 1 line - -Increase reload max time and make dialog close by default ------------------------------------------------------------------------- -r1455 | jeremysanders | 2010-11-26 22:35:03 +0000 (Fri, 26 Nov 2010) | 1 line - -Make reload data dialog box non modal. Add option to reload at intervals or manually if button pressed. ------------------------------------------------------------------------- -r1454 | jeremysanders | 2010-11-26 20:01:51 +0000 (Fri, 26 Nov 2010) | 1 line - -Update README and bump version number to 1.10 ------------------------------------------------------------------------- -r1453 | jeremysanders | 2010-11-25 22:19:02 +0000 (Thu, 25 Nov 2010) | 1 line - -Add polar example ------------------------------------------------------------------------- -r1452 | jeremysanders | 2010-11-25 21:58:24 +0000 (Thu, 25 Nov 2010) | 1 line - -If ISO date/time conversion fails, try local date time conversion ------------------------------------------------------------------------- -r1451 | jeremysanders | 2010-11-25 21:25:41 +0000 (Thu, 25 Nov 2010) | 1 line - -Add margin size setting to key widget ------------------------------------------------------------------------- -r1450 | jeremysanders | 2010-11-24 15:56:05 +0000 (Wed, 24 Nov 2010) | 1 line - -Fix fitting after changes to function widget ------------------------------------------------------------------------- -r1449 | jeremysanders | 2010-11-22 09:26:58 +0000 (Mon, 22 Nov 2010) | 10 lines - -Fixed error report Tue, 28 Sep 2010 16:16:45 +0000: zero division in function widget from number of steps -Fixed error report Fri, 15 Oct 2010 12:05:12 +0000: force parts of CSV datasets o have same length by adding NaNs if they are too short -Fixed error report Mon, 25 Oct 2010 08:27:04 +0000: zero division - if DPI is 0, fix to 72 -Fixed error report Fri, 05 Nov 2010 23:36:38 +0000: catch exceptions from conversion of expression evaluation result to numpy array for function plotting -Fixed error report Mon, 08 Nov 2010 22:28:28 +0000: allow saving of histogram datasets where one of the output datasets is not used -Fixed error report Thu, 11 Nov 2010 23:55:59 +0000: somehow veusz data on clipboard is invalid: catch invalid integers in paste: FIXME How does this happen? -Fixed error report Sat, 13 Nov 2010 03:58:11 +0000: catch invalid data ranges for datasets in bar axis ranging - -Added endsize setting to error bar line which scales the ends of error bars by this factor - ------------------------------------------------------------------------- -r1448 | jeremysanders | 2010-11-13 20:07:34 +0000 (Sat, 13 Nov 2010) | 1 line - -Use separate instance of plugin when applying in plugin dialog, to avoid shared plugin confusion ------------------------------------------------------------------------- -r1447 | jeremysanders | 2010-11-11 20:35:25 +0000 (Thu, 11 Nov 2010) | 1 line - -Merge nonorthogonal branch 1429:1446 to trunk ------------------------------------------------------------------------- -r1433 | jeremysanders | 2010-11-04 19:12:00 +0000 (Thu, 04 Nov 2010) | 1 line - -Fix bug when editing dataset plugins ------------------------------------------------------------------------- -r1428 | jeremysanders | 2010-10-24 12:00:37 +0100 (Sun, 24 Oct 2010) | 1 line - -Fix error handling for linked datasets which reload using reloadViaOperation. Automatic bug report Tue, 12 Oct 2010 13:39:41 +0000. ------------------------------------------------------------------------- -r1427 | jeremysanders | 2010-10-23 22:03:33 +0100 (Sat, 23 Oct 2010) | 1 line - -Rewrite dataset list model in data edit dialog to be more robust. Add button to create new datasets. ------------------------------------------------------------------------- -r1426 | jeremysanders | 2010-10-23 17:52:11 +0100 (Sat, 23 Oct 2010) | 1 line - -Add blank row at end of dataset in edit dialog to insert new data ------------------------------------------------------------------------- -r1425 | jeremysanders | 2010-10-23 17:17:28 +0100 (Sat, 23 Oct 2010) | 1 line - -Implement label axes for boxplots ------------------------------------------------------------------------- -r1423 | jeremysanders | 2010-10-19 20:30:44 +0100 (Tue, 19 Oct 2010) | 1 line - -Start of box plot implementation ------------------------------------------------------------------------- -r1422 | jeremysanders | 2010-09-28 10:15:27 +0100 (Tue, 28 Sep 2010) | 1 line - -make read terr in qdp import plugin get error bars round the right way ------------------------------------------------------------------------- -r1421 | jeremysanders | 2010-09-27 18:03:07 +0100 (Mon, 27 Sep 2010) | 1 line - -Add initial QDP import plugin ------------------------------------------------------------------------- -r1420 | jeremysanders | 2010-09-27 18:02:45 +0100 (Mon, 27 Sep 2010) | 1 line - -Make multiple datasets imported by plugins use same linking, if appropriate. Fix description of operation using dataset plugins. ------------------------------------------------------------------------- -r1419 | jeremysanders | 2010-09-26 16:43:42 +0100 (Sun, 26 Sep 2010) | 1 line - -Remove extra copy of posn variable ------------------------------------------------------------------------- -r1418 | jeremysanders | 2010-09-26 16:40:57 +0100 (Sun, 26 Sep 2010) | 1 line - -Fix bug report Sun, 26 Sep 2010 06:35:58 -0600. Cleanup function plotting evaluation. Fix setting min and max values for plotting functions of y. ------------------------------------------------------------------------- -r1417 | jeremysanders | 2010-09-26 15:44:55 +0100 (Sun, 26 Sep 2010) | 1 line - -Fix error report Sun, 26 Sep 2010 06:27:36 -0600 ------------------------------------------------------------------------- -r1416 | jeremysanders | 2010-09-25 10:14:16 +0100 (Sat, 25 Sep 2010) | 1 line - -Fix problem report Sun, 2 May 2010 09:42:41 -0400 ------------------------------------------------------------------------- -r1415 | jeremysanders | 2010-09-25 10:05:25 +0100 (Sat, 25 Sep 2010) | 1 line - -Fix error report Tue, 21 Sep 2010 06:25:43 -0600 ------------------------------------------------------------------------- -r1414 | jeremysanders | 2010-09-25 09:56:43 +0100 (Sat, 25 Sep 2010) | 1 line - -Fix error report Thu, 16 Sep 2010 14:20:26 -0600 ------------------------------------------------------------------------- -r1413 | jeremysanders | 2010-09-25 09:48:02 +0100 (Sat, 25 Sep 2010) | 1 line - -Fix error report Sun, 12 Sep 2010 23:56:57 -0600 ------------------------------------------------------------------------- -r1412 | jeremysanders | 2010-09-25 09:42:01 +0100 (Sat, 25 Sep 2010) | 1 line - -Fix error report Sat, 11 Sep 2010 06:06:13 -0600 ------------------------------------------------------------------------- -r1411 | jeremysanders | 2010-09-25 09:38:27 +0100 (Sat, 25 Sep 2010) | 1 line - -Fix bounding box appearance when resizing rectangles, ellipses and image files ------------------------------------------------------------------------- -r1410 | jeremysanders | 2010-09-20 22:41:18 +0100 (Mon, 20 Sep 2010) | 1 line - -Make X and Y ranges of 1D->2D datasets correct ------------------------------------------------------------------------- -r1409 | jeremysanders | 2010-09-20 20:07:36 +0100 (Mon, 20 Sep 2010) | 1 line - -CSV reader will now assume a dataset is a text dataset if it cannot convert the first item of a column/row to a number ------------------------------------------------------------------------- -r1408 | jeremysanders | 2010-09-16 20:33:07 +0100 (Thu, 16 Sep 2010) | 1 line - -Add colors sequence plugin ------------------------------------------------------------------------- -r1407 | jeremysanders | 2010-09-12 15:38:24 +0100 (Sun, 12 Sep 2010) | 1 line - -Drop histo test as there is a subtle clipping issue ------------------------------------------------------------------------- -r1406 | jeremysanders | 2010-09-12 15:35:03 +0100 (Sun, 12 Sep 2010) | 1 line - -Solve -0 issues in self tests ------------------------------------------------------------------------- -r1405 | jeremysanders | 2010-09-12 15:26:35 +0100 (Sun, 12 Sep 2010) | 1 line - -Use bmp file format for self test - hopefully should be same between platforms ------------------------------------------------------------------------- -r1404 | jeremysanders | 2010-09-12 15:12:15 +0100 (Sun, 12 Sep 2010) | 1 line - -More attempts to get same output for tests between windows/unix ------------------------------------------------------------------------- -r1403 | jeremysanders | 2010-09-12 14:44:35 +0100 (Sun, 12 Sep 2010) | 1 line - -Round svg coordinates and sizes to 2dp to get consistent behaviour on different platforms ------------------------------------------------------------------------- -r1402 | jeremysanders | 2010-09-11 11:19:33 +0100 (Sat, 11 Sep 2010) | 1 line - -More fixes for self testing. Return exit code if tests failed ------------------------------------------------------------------------- -r1401 | jeremysanders | 2010-09-11 10:28:00 +0100 (Sat, 11 Sep 2010) | 1 line - -Fix other usages of metrics to get the same self test output ------------------------------------------------------------------------- -r1400 | jeremysanders | 2010-09-10 22:08:10 +0100 (Fri, 10 Sep 2010) | 1 line - -Add start of self test routines ------------------------------------------------------------------------- -r1399 | jeremysanders | 2010-09-08 22:26:12 +0100 (Wed, 08 Sep 2010) | 1 line - -Cleanups in text renderer ------------------------------------------------------------------------- -r1391 | jeremysanders | 2010-08-31 22:22:32 +0100 (Tue, 31 Aug 2010) | 1 line - -Fix propagation of settings to specific widgets ------------------------------------------------------------------------- -r1389 | jeremysanders | 2010-08-30 17:13:49 +0100 (Mon, 30 Aug 2010) | 1 line - -Fix if both min and max in fit are set diff -Nru veusz-1.10/debian/changelog veusz-1.14/debian/changelog --- veusz-1.10/debian/changelog 2011-12-31 02:15:19.000000000 +0000 +++ veusz-1.14/debian/changelog 2012-02-15 22:19:12.000000000 +0000 @@ -1,3 +1,23 @@ +veusz (1.14-1ubuntu1) precise; urgency=low + + * disable testsuite on armel/armhf + fails due to too strict float comparisons + + -- Julian Taylor Wed, 15 Feb 2012 19:45:22 +0100 + +veusz (1.14-1) unstable; urgency=low + + * Update to upstream Veusz 1.14 (Closes: #648957) + * Enable test suite in build, adding Build-Depends of xauth, xfonts-base + and xvfb + * Properly attribute copyright of pyqtdistutils.py file to Develer Srl + * Add Break statement in control for veusz-helpers rather than use a + Depends statement + * Bumped Standards-Version to 3.9.2 + * Enabled dpkg-buildflags for build + + -- Jeremy Sanders Sun, 04 Dec 2011 19:02:17 +0200 + veusz (1.10-2build2) precise; urgency=low * Rebuild to drop python2.6 dependencies. @@ -10,13 +30,6 @@ -- Stefano Rivera Wed, 28 Sep 2011 20:07:20 +0200 -veusz (1.10-2) unstable; urgency=low - - * Team upload. - * Rebuild to add Python 2.7 support - - -- Piotr Ożarowski Sun, 08 May 2011 16:45:40 +0200 - veusz (1.10-1) unstable; urgency=low * Initial release (Closes: #447524) diff -Nru veusz-1.10/debian/control veusz-1.14/debian/control --- veusz-1.10/debian/control 2011-03-29 22:29:33.000000000 +0000 +++ veusz-1.14/debian/control 2012-02-15 18:44:15.000000000 +0000 @@ -16,8 +16,11 @@ python-qt4-dbg, python-qt4-dev, python-sip-dbg, - python-sip-dev -Standards-Version: 3.9.1 + python-sip-dev, + xauth, + xfonts-base, + xvfb +Standards-Version: 3.9.2 X-Python-Version: >= 2.4 Homepage: http://home.gna.org/veusz/ Vcs-Browser: http://svn.debian.org/viewsvn/python-apps/packages/veusz/trunk/ @@ -27,12 +30,10 @@ Architecture: all Depends: python-numpy, python-qt4, - veusz-helpers (<< ${source:Version}.1~), veusz-helpers (>= ${source:Version}), ${misc:Depends}, ${python:Depends} Suggests: python-pyfits -Breaks: ${python:Breaks} Description: 2D scientific plotting application with graphical interface Veusz is a 2D scientific plotting and graphing package, designed to produce publication-ready Postscript or PDF output. Veusz provides a GUI, @@ -47,7 +48,7 @@ ${misc:Depends}, ${python:Depends}, ${shlibs:Depends} -Breaks: ${python:Breaks} +Breaks: veusz (<< ${source:Version}) Suggests: veusz (= ${source:Version}) Description: Architecture-specific helper module for Veusz This package contains the architecture specific files for Veusz, a scientific @@ -62,7 +63,6 @@ ${misc:Depends}, ${python:Depends}, ${shlibs:Depends} -Breaks: ${python:Breaks} Recommends: python-dbg, python-numpy-dbg, python-qt4-dbg Description: Architecture-specific helper module for Veusz (debug extension) This package contains the architecture specific files for Veusz compiled to diff -Nru veusz-1.10/debian/copyright veusz-1.14/debian/copyright --- veusz-1.10/debian/copyright 2011-03-29 22:29:33.000000000 +0000 +++ veusz-1.14/debian/copyright 2012-02-15 18:44:15.000000000 +0000 @@ -1,10 +1,14 @@ -Format: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=173 +Format: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=174 Upstream-Name: veusz Upstream-Contact: http://home.gna.org/veusz/ Source: http://download.gna.org/veusz/ Files: * -Copyright: 2003-2010 Jeremy Sanders +Copyright: 2003-2011 Jeremy Sanders +License: GPL-2+ + +Files: widgets/pickable.py +Copyright: 2011 Benjamin K. Stuhl License: GPL-2+ Files: helpers/src/beziers.cpp helpers/src/beziers.h @@ -14,21 +18,6 @@ 2003,2004 Monash University License: GPL-2+ -License: GPL-2+ - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - . - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - . - On Debian systems, the full text of the GNU General Public - License version 2 can be found in the file - `/usr/share/common-licenses/GPL-2'. - Files: helpers/src/_nc_cntr.c Copyright: (c) 2002-2005 John D. Hunter License: @@ -212,3 +201,23 @@ Files: windows/icons/kde-*-veuszedit.svg Copyright: 2003-2010 Jeremy Sanders License: GPL-2+ + +Files: pyqtdistutils.py +Copyright: Develer Srl +Comment: Licensing given in http://old.nabble.com/Re%3A-sipdistutils.py-p31453589.html +License: GPL-2+ + +License: GPL-2+ + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + On Debian systems, the full text of the GNU General Public + License version 2 can be found in the file + `/usr/share/common-licenses/GPL-2'. diff -Nru veusz-1.10/debian/patches/datafiles-usr-share.patch veusz-1.14/debian/patches/datafiles-usr-share.patch --- veusz-1.10/debian/patches/datafiles-usr-share.patch 2011-03-29 22:29:33.000000000 +0000 +++ veusz-1.14/debian/patches/datafiles-usr-share.patch 2012-02-15 18:44:15.000000000 +0000 @@ -6,12 +6,10 @@ distutils and compatibility with other operating systems. Author: Jeremy Sanders Forwarded: not-needed -Last-Update: 2011-03-24 -Index: veusz-1.10/setup.py -=================================================================== ---- veusz-1.10.orig/setup.py 2011-01-13 22:33:45.766837973 +0000 -+++ veusz-1.10/setup.py 2011-01-13 22:28:58.000000000 +0000 -@@ -52,7 +52,7 @@ +Last-Update: 2011-11-25 +--- veusz-1.14.orig/setup.py ++++ veusz-1.14/setup.py +@@ -50,7 +50,7 @@ def run(self): # need to change self.install_dir to the library dir install_cmd = self.get_finalized_command('install') @@ -20,16 +18,14 @@ return install_data.run(self) descr = '''Veusz is a scientific plotting package, designed to create -Index: veusz-1.10/utils/utilfuncs.py -=================================================================== ---- veusz-1.10.orig/utils/utilfuncs.py 2011-01-13 22:35:43.818837974 +0000 -+++ veusz-1.10/utils/utilfuncs.py 2011-01-13 22:35:59.458840542 +0000 -@@ -48,7 +48,7 @@ +--- veusz-1.14.orig/utils/utilfuncs.py ++++ veusz-1.14/utils/utilfuncs.py +@@ -50,7 +50,7 @@ # standard installation return os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -veuszDirectory = _getVeuszDirectory() +veuszDirectory = '/usr/share/veusz' + exampleDirectory = os.path.join(veuszDirectory, 'examples') id_re = re.compile('^[A-Za-z_][A-Za-z0-9_]*$') - def validPythonIdentifier(name): diff -Nru veusz-1.10/debian/patches/examples-location.patch veusz-1.14/debian/patches/examples-location.patch --- veusz-1.10/debian/patches/examples-location.patch 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/debian/patches/examples-location.patch 2012-02-15 18:44:15.000000000 +0000 @@ -0,0 +1,28 @@ +Description: Do not install examples in /usr/share/veusz/examples + We install the examples to /usr/share/doc/veusz/examples, so do not install + into /usr/share/veusz. Update the location of the examples in the Veusz + code so that Veusz could find it to show in the menus. +Author: Jeremy Sanders +Forwarded: not-needed +Last-Update: 2011-11-25 +--- veusz-1.14.orig/setup.py ++++ veusz-1.14/setup.py +@@ -128,7 +128,6 @@ + data_files = [ ('veusz', ['VERSION']), + findData('dialogs', ('ui',)), + findData('windows/icons', ('png', 'svg')), +- findData('examples', ('vsz', 'py', 'csv', 'dat')), + ], + packages = [ 'veusz', + 'veusz.dialogs', +--- veusz-1.14.orig/utils/utilfuncs.py ++++ veusz-1.14/utils/utilfuncs.py +@@ -51,7 +51,7 @@ + return os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + + veuszDirectory = '/usr/share/veusz' +-exampleDirectory = os.path.join(veuszDirectory, 'examples') ++exampleDirectory = '/usr/share/doc/veusz/examples' + + id_re = re.compile('^[A-Za-z_][A-Za-z0-9_]*$') + def validPythonIdentifier(name): diff -Nru veusz-1.10/debian/patches/license-file-location.patch veusz-1.14/debian/patches/license-file-location.patch --- veusz-1.10/debian/patches/license-file-location.patch 2011-03-29 22:29:33.000000000 +0000 +++ veusz-1.14/debian/patches/license-file-location.patch 2012-02-15 18:44:15.000000000 +0000 @@ -1,14 +1,12 @@ -Description: Use standard location of license file to fill in text in about - dialog box. - . - Veusz ships its own COPYING file. Debian does not want extra license files - installed. +Description: Use standard location of license file for about dialog. + We modify the about dialog box to point to the standard COPYING file as + Debian does not want extra license files installed. Author: Jeremy Sanders Forwarded: not-needed -Last-Update: 2011-03-24 ---- veusz-1.9.orig/dialogs/aboutdialog.py -+++ veusz-1.9/dialogs/aboutdialog.py -@@ -59,7 +59,7 @@ +Last-Update: 2011-11-25 +--- veusz-1.14.orig/dialogs/aboutdialog.py ++++ veusz-1.14/dialogs/aboutdialog.py +@@ -57,7 +57,7 @@ VeuszDialog.__init__(self, parent, 'license.ui') try: diff -Nru veusz-1.10/debian/patches/self-test-data-location.patch veusz-1.14/debian/patches/self-test-data-location.patch --- veusz-1.10/debian/patches/self-test-data-location.patch 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/debian/patches/self-test-data-location.patch 2012-02-15 18:44:15.000000000 +0000 @@ -0,0 +1,21 @@ +Description: Monkey patch location of Veusz data files while running self-test. + We monkey patch Veusz to make it look for its data file locations in the + build directory. This is required because we change the data file locations in + datafiles-usr-share.patch to be in /usr/share. +Author: Jeremy Sanders +Forwarded: not-needed +Last-Update: 2011-11-25 +--- veusz-1.14.orig/tests/runselftest.py ++++ veusz-1.14/tests/runselftest.py +@@ -41,6 +41,11 @@ + import os.path + import sys + ++import veusz.utils ++datadir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') ++veusz.utils.veuszDirectory = veusz.utils.utilfuncs.veuszDirectory = datadir ++veusz.utils.action.imagedir = os.path.join(datadir, 'windows', 'icons') ++ + import veusz.qtall as qt4 + import veusz.utils.textrender + import veusz.document as document diff -Nru veusz-1.10/debian/patches/series veusz-1.14/debian/patches/series --- veusz-1.10/debian/patches/series 2011-02-04 09:21:07.000000000 +0000 +++ veusz-1.14/debian/patches/series 2012-02-15 18:44:15.000000000 +0000 @@ -1,2 +1,4 @@ license-file-location.patch datafiles-usr-share.patch +self-test-data-location.patch +examples-location.patch diff -Nru veusz-1.10/debian/rules veusz-1.14/debian/rules --- veusz-1.10/debian/rules 2011-02-15 13:31:41.000000000 +0000 +++ veusz-1.14/debian/rules 2012-02-15 18:50:09.000000000 +0000 @@ -1,5 +1,17 @@ #!/usr/bin/make -f +export CFLAGS=$(shell dpkg-buildflags --get CFLAGS) +export CPPFLAGS=$(shell dpkg-buildflags --get CPPFLAGS) +export LDFLAGS=$(shell dpkg-buildflags --get LDFLAGS) +DEBIAN_ARCH:=$(shell dpkg-architecture -qDEB_HOST_ARCH) + +# Get the supported Python versions +PYVERS = $(shell pyversions -r -v) + +# Callable functions to determine the correct PYTHONPATH +pythonpath = $$(ls -d $(CURDIR)/build/lib.*-$(1)) +pythonpath_dbg = $$(ls -d $(CURDIR)/build/lib.*-$(1)-pydebug 2>/dev/null || ls -d $(CURDIR)/lib.*$(1)-pydebug) + %: dh $@ --with python2 @@ -11,6 +23,22 @@ # -- --force works around bug #589759 dh_auto_build -- --force +override_dh_auto_test: +ifeq (,$(findstring nocheck,$(DEB_BUILD_OPTIONS))) +# tests fail on arm due to different float results see debian bug 654604 +ifeq (,$(filter $(DEBIAN_ARCH), armel armhf)) + set -e -x;\ + for py in $(PYVERS); do \ + PYTHONPATH=$(call pythonpath,$$py) xvfb-run -a \ + --server-args "-screen 0 640x480x24" \ + python$$py tests/runselftest.py ;\ + PYTHONPATH=$(call pythonpath_dbg,$$py) xvfb-run -a \ + --server-args "-screen 0 640x480x24" \ + python$$py-dbg tests/runselftest.py ;\ + done +endif +endif + override_dh_strip: dh_strip --dbg-package=veusz-helpers-dbg diff -Nru veusz-1.10/dialogs/aboutdialog.py veusz-1.14/dialogs/aboutdialog.py --- veusz-1.10/dialogs/aboutdialog.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/aboutdialog.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,8 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: aboutdialog.py 1358 2010-08-14 16:40:46Z jeremysanders $ - """About dialog module.""" import os.path diff -Nru veusz-1.10/dialogs/about.ui veusz-1.14/dialogs/about.ui --- veusz-1.10/dialogs/about.ui 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/about.ui 2011-11-22 20:23:31.000000000 +0000 @@ -54,19 +54,20 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Arial'; font-size:9pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600; color:#800080;">Veusz %(version)s</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" color:#000000;">Copyright © 2003-2010 Jeremy Sanders </span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a href="http://home.gna.org/veusz/"><span style=" text-decoration: underline; color:#539fa3;">http://home.gna.org/veusz/</span></a></p> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Authors:</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Jeremy Sanders &lt;jeremy@jeremysanders.net&gt;</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">James Graham &lt;jg307@cam.ac.uk&gt;</p> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Thanks to:</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Bryan Harris</p> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Veusz comes with ABSOLUTELY NO WARRANTY. Veusz is Free Software and you are entitled to distribute it under the terms of the GNU Public License (GPL). See the file COPYING for details, or click &quot;Show license&quot;.</p></body></html> +</style></head><body style=" font-family:'Arial'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:9pt; font-weight:600; color:#800080;">Veusz %(version)s</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:9pt; color:#000000;">Copyright © 2003-2011 Jeremy Sanders and contributors</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a href="http://home.gna.org/veusz/"><span style=" font-size:9pt; text-decoration: underline; color:#539fa3;">http://home.gna.org/veusz/</span></a></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:9pt;"></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:9pt;">Authors:</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:9pt;">Jeremy Sanders &lt;jeremy@jeremysanders.net&gt;</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:9pt;">James Graham &lt;jg307@cam.ac.uk&gt;</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:9pt;"></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:9pt;">Thanks to:</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:9pt;">Benjamin K. Stuhl</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:9pt;">Bryan Harris</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:9pt;"></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:9pt;">Veusz comes with ABSOLUTELY NO WARRANTY. Veusz is Free Software and you are entitled to distribute it under the terms of the GNU Public License (GPL). See the file COPYING for details, or click &quot;Show license&quot;.</span></p></body></html> Qt::AlignCenter diff -Nru veusz-1.10/dialogs/capturedialog.py veusz-1.14/dialogs/capturedialog.py --- veusz-1.10/dialogs/capturedialog.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/capturedialog.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: capturedialog.py 1358 2010-08-14 16:40:46Z jeremysanders $ - """Veusz data capture dialog.""" import veusz.qtall as qt4 diff -Nru veusz-1.10/dialogs/custom.py veusz-1.14/dialogs/custom.py --- veusz-1.10/dialogs/custom.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/custom.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: custom.py 1358 2010-08-14 16:40:46Z jeremysanders $ - import veusz.qtall as qt4 import veusz.utils as utils import veusz.document as document diff -Nru veusz-1.10/dialogs/datacreate2d.py veusz-1.14/dialogs/datacreate2d.py --- veusz-1.10/dialogs/datacreate2d.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/datacreate2d.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: datacreate2d.py 1458 2010-11-28 12:34:37Z jeremysanders $ - """Dataset creation dialog for 2d data.""" import veusz.qtall as qt4 diff -Nru veusz-1.10/dialogs/datacreate.py veusz-1.14/dialogs/datacreate.py --- veusz-1.10/dialogs/datacreate.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/datacreate.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: datacreate.py 1358 2010-08-14 16:40:46Z jeremysanders $ - """Dataset creation dialog.""" import veusz.qtall as qt4 diff -Nru veusz-1.10/dialogs/dataeditdialog.py veusz-1.14/dialogs/dataeditdialog.py --- veusz-1.10/dialogs/dataeditdialog.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/dataeditdialog.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: dataeditdialog.py 1427 2010-10-23 21:03:33Z jeremysanders $ - """Module for implementing dialog box for viewing/editing data.""" import bisect @@ -28,6 +26,7 @@ import veusz.document as document import veusz.utils as utils +from veusz.qtwidgets.datasetbrowser import DatasetBrowser from veuszdialog import VeuszDialog # register function to dataset class to edit dataset @@ -75,13 +74,11 @@ if index.row() == len(data): return qt4.QVariant() + # convert data to QVariant d = data[index.row()] - if isinstance(d, basestring): - return qt4.QVariant(d) - else: - # value needs converting to float as QVariant doesn't - # support numpy numeric types - return qt4.QVariant(float(d)) + return ds.uiDataItemToQVariant(d) + + # empty entry return qt4.QVariant() def headerData(self, section, orientation, role): @@ -147,7 +144,7 @@ # update if conversion okay try: - val = ds.convertToDataItem( value.toString() ) + val = ds.uiConvertToDataItem( value.toString() ) except ValueError: return False @@ -166,6 +163,8 @@ self.document = document self.dsname = datasetname + self.connect(document, qt4.SIGNAL('sigModified'), + self.slotDocumentModified) def rowCount(self, parent): ds = self.document.data[self.dsname].data @@ -210,65 +209,39 @@ return qt4.QVariant( '%g' % val ) return qt4.QVariant() + + def flags(self, index): + """Update flags to say that items are editable.""" + if not index.isValid(): + return qt4.Qt.ItemIsEnabled + else: + return qt4.QAbstractTableModel.flags(self, index) | qt4.Qt.ItemIsEditable -class DatasetListModel(qt4.QStringListModel): - def __init__(self, parent, document): - dsnames = document.data.keys() - dsnames.sort() - qt4.QStringListModel.__init__(self, dsnames, parent) - self.connect(document, qt4.SIGNAL('sigModified'), - self.slotDocumentModified) - self.document = document + def slotDocumentModified(self): + """Called when document modified.""" + self.emit( qt4.SIGNAL('layoutChanged()') ) - def datasetName(self, index): - """Get name at index.""" - return unicode(self.stringList()[index.row()]) - - @property - def datasets(self): - """Get list of datasets.""" - return [unicode(x) for x in self.stringList()] + def setData(self, index, value, role): + """Called to set the data.""" - def slotDocumentModified(self): - """Update list when document modified.""" - dslist = self.datasets - old = set(dslist) - new = set(self.document.data.keys()) - - # dslist used to keep track of changes - # add new entries in appropriate (sorted) place - for a in new-old: - i = bisect.bisect_left(dslist, a) - dslist.insert(i, a) - self.insertRows(i, 1) - qt4.QStringListModel.setData( - self, self.index(i, 0), qt4.QVariant(a) ) - - # remove entries no longer there - for d in old-new: - i = dslist.index(d) - del dslist[i] - self.removeRows(i, 1) - - def setData(self, index, value, role=qt4.Qt.EditRole): - """Called to rename a dataset.""" - - if index.isValid() and role == qt4.Qt.EditRole: - name = self.datasetName(index) - newname = unicode( value.toString() ) + if not index.isValid() or role != qt4.Qt.EditRole: + return False - if not utils.validateDatasetName(newname): - return False + ds = self.document.data[self.dsname] + row = ds.data.shape[0]-index.row()-1 + col = index.column() - self.document.applyOperation( - document.OperationDatasetRename(name, newname)) - self.emit(qt4.SIGNAL( - 'dataChanged(const QModelIndex &, const QModelIndex &'), - index, index) - return True + # update if conversion okay + try: + val = ds.uiConvertToDataItem( value.toString() ) + except ValueError: + return False + + op = document.OperationDatasetSetVal2D( + self.dsname, row, col, val) + self.document.applyOperation(op) + return True - return False - class DataEditDialog(VeuszDialog): """Dialog for editing and rearranging data sets.""" @@ -277,13 +250,8 @@ self.document = document # set up dataset list - self.dslistmodel = DatasetListModel(self, document) - - #self.modelproxy = qt4.QSortFilterProxyModel(self) - #self.modelproxy.setSourceModel(self.dslistmodel) - #self.modelproxy.setDynamicSortFilter(True) - - self.datasetlistview.setModel(self.dslistmodel) + self.dsbrowser = DatasetBrowser(document, parent, parent) + self.splitter.insertWidget(0, self.dsbrowser) # actions for data table for text, slot in ( @@ -297,8 +265,8 @@ self.datatableview.setContextMenuPolicy( qt4.Qt.ActionsContextMenu ) # layout edit dialog improvement - self.splitter.setStretchFactor(0, 1) - self.splitter.setStretchFactor(1, 3) + self.splitter.setStretchFactor(0, 3) + self.splitter.setStretchFactor(1, 4) # don't want text to look editable or special self.linkedlabel.setFrameShape(qt4.QFrame.NoFrame) @@ -308,16 +276,19 @@ self.connect(document, qt4.SIGNAL('sigModified'), self.slotDocumentModified) - # receive change in selection - self.connect(self.datasetlistview.selectionModel(), - qt4.SIGNAL('selectionChanged(const QItemSelection &, const QItemSelection &)'), - self.slotDatasetSelected) + # select first item, if any or initialise if none + if len(self.document.data) > 0: + self.selectDataset( sorted(self.document.data.keys())[0] ) + + #if self.dslistmodel.rowCount() > 0: + # self.datasetlistview.selectionModel().select( + # self.dslistmodel.createIndex(0, 0), + # qt4.QItemSelectionModel.Select) + #else: + # self.slotDatasetSelected(None, None) - # select first item (phew) - if self.dslistmodel.rowCount() > 0: - self.datasetlistview.selectionModel().select( - self.dslistmodel.createIndex(0, 0), - qt4.QItemSelectionModel.Select) + self.connect(self.dsbrowser.navtree, qt4.SIGNAL("selecteditem"), + self.slotDatasetSelected) # connect buttons for btn, slot in ( (self.deletebutton, self.slotDatasetDelete), @@ -332,49 +303,52 @@ # menu for new button self.newmenu = qt4.QMenu() for text, slot in ( ('Numerical dataset', self.slotNewNumericalDataset), - ('Text dataset', self.slotNewTextDataset) ): + ('Text dataset', self.slotNewTextDataset), + ('Date/time dataset', self.slotNewDateDataset) ): a = self.newmenu.addAction(text) - self.connect(a, qt4.SIGNAL('activated()'), slot) + self.connect(a, qt4.SIGNAL('triggered()'), slot) self.newbutton.setMenu(self.newmenu) - def slotDatasetSelected(self, current, deselected): + def slotDatasetSelected(self, name): """Called when a new dataset is selected.""" # FIXME: Make readonly models readonly!! - indexes = current.indexes() - if len(indexes) == 0: - self.datatableview.setModel(None) - return - - name = self.dslistmodel.datasetName(indexes[0]) - ds = self.document.data[name] - - if ds.dimensions == 1: - model = DatasetTableModel1D(self, self.document, name) - elif ds.dimensions == 2: - model = DatasetTableModel2D(self, self.document, name) + model = None + if name: + # get selected dataset + ds = self.document.data[name] + + # make model for dataset + if ds.dimensions == 1: + model = DatasetTableModel1D(self, self.document, name) + elif ds.dimensions == 2: + model = DatasetTableModel2D(self, self.document, name) + + # disable context menu if no menu + for a in self.datatableview.actions(): + a.setEnabled(model is not None) - self.datatableview.setModel(model) - + self.datatableview.setModel(model) self.setUnlinkState() def setUnlinkState(self): """Enable the unlink button correctly.""" # get dataset dsname = self.getSelectedDataset() - try: ds = self.document.data[dsname] + unlink = ds.canUnlink() + linkinfo = ds.linkedInformation() except KeyError: - return - - # linked dataset - unlink = ds.canUnlink() - readonly = not unlink + ds = None + unlink = False + linkinfo = "" self.editbutton.setVisible(type(ds) in recreate_register) self.unlinkbutton.setEnabled(unlink) - self.linkedlabel.setText( ds.linkedInformation() ) + self.linkedlabel.setText(linkinfo) + self.deletebutton.setEnabled(ds is not None) + self.duplicatebutton.setEnabled(ds is not None) def slotDocumentModified(self): """Set unlink status when document modified.""" @@ -382,34 +356,27 @@ def getSelectedDataset(self): """Return the selected dataset.""" - selitems = self.datasetlistview.selectionModel().selection().indexes() - if len(selitems) != 0: - return self.dslistmodel.datasetName(selitems[0]) - else: - return None - - def slotDatasetDelete(self): - """Delete selected dataset.""" + return self.dsbrowser.navtree.getSelectedDataset() - datasetname = self.getSelectedDataset() - if datasetname is not None: - row = self.datasetlistview.selectionModel( - ).selection().indexes()[0].row() + def selectDataset(self, dsname): + """Select dataset with name given.""" + self.dsbrowser.navtree.selectDataset(dsname) + self.slotDatasetSelected(dsname) - self.document.applyOperation( - document.OperationDatasetDelete(datasetname)) + def slotDatasetDelete(self): + """Delete selected dataset.""" + self.document.applyOperation( + document.OperationDatasetDelete(self.getSelectedDataset()) ) def slotDatasetUnlink(self): """Allow user to remove link to file or other datasets.""" - datasetname = self.getSelectedDataset() - if datasetname is not None: - d = self.document.data[datasetname] - if d.linked is not None: - op = document.OperationDatasetUnlinkFile(datasetname) - else: - op = document.OperationDatasetUnlinkRelation(datasetname) - self.document.applyOperation(op) + d = self.document.data[datasetname] + if d.linked is not None: + op = document.OperationDatasetUnlinkFile(datasetname) + else: + op = document.OperationDatasetUnlinkRelation(datasetname) + self.document.applyOperation(op) def slotDatasetDuplicate(self): """Duplicate selected dataset.""" @@ -488,9 +455,13 @@ self.newDataset( document.Dataset(data=[0.]) ) def slotNewTextDataset(self): - """Add new value dataset.""" + """Add new text dataset.""" self.newDataset( document.DatasetText(data=['']) ) + def slotNewDateDataset(self): + """Add new date dataset.""" + self.newDataset( document.DatasetDateTime(data=[]) ) + def newDataset(self, ds): """Add new dataset to document.""" # get a name for dataset @@ -504,3 +475,5 @@ # add new dataset self.document.applyOperation( document.OperationDatasetSet(name, ds)) + + self.dsbrowser.selectDataset(name) diff -Nru veusz-1.10/dialogs/dataedit.ui veusz-1.14/dialogs/dataedit.ui --- veusz-1.10/dialogs/dataedit.ui 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/dataedit.ui 2011-11-22 20:23:31.000000000 +0000 @@ -7,8 +7,8 @@ 0 0 - 625 - 426 + 749 + 439 @@ -29,7 +29,6 @@ false - diff -Nru veusz-1.10/dialogs/errorloading.py veusz-1.14/dialogs/errorloading.py --- veusz-1.10/dialogs/errorloading.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/errorloading.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: errorloading.py 1358 2010-08-14 16:40:46Z jeremysanders $ - """Dialog to show if there is an error loading.""" import veusz.qtall as qt4 diff -Nru veusz-1.10/dialogs/exceptiondialog.py veusz-1.14/dialogs/exceptiondialog.py --- veusz-1.10/dialogs/exceptiondialog.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/exceptiondialog.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: exceptiondialog.py 1366 2010-08-20 18:24:33Z jeremysanders $ - '''Dialog to pop up if an exception occurs in Veusz. This allows the user to send a bug report in via email.''' @@ -27,6 +25,7 @@ import traceback import urllib2 import sip +import re import numpy @@ -105,6 +104,8 @@ "connected?") return + qt4.QMessageBox.information(self, "Submitted", + "Thank you for submitting an error report") VeuszDialog.accept(self) def _raiseIgnoreException(): @@ -132,6 +133,31 @@ self.connect(self.ignoreSessionButton, qt4.SIGNAL('clicked()'), self.ignoreSessionSlot) + self.checkVeuszVersion() + + def checkVeuszVersion(self): + """See whether there is a later version of veusz and inform the + user.""" + try: + p = urllib2.urlopen('http://download.gna.org/veusz/').read() + versions = re.findall('veusz-([0-9.]+).tar.gz', p) + except urllib2.URLError: + msg = 'Could not check the latest Veusz version' + else: + vsort = sorted([[int(i) for i in v.split('.')] for v in versions]) + latest = '.'.join([str(x) for x in vsort[-1]]) + + current = [int(i) for i in utils.version().split('.')] + if current == vsort[-1]: + msg = 'You are running the latest released Veusz version' + elif current > vsort[-1]: + msg = 'You are running an unreleased Veusz version' + else: + msg = ('Your current version of Veusz is old. ' + 'Veusz %s is available.' % latest) + + self.veuszversionlabel.setText(msg) + def accept(self): """Accept by opening send dialog.""" d = ExceptionSendDialog(self.backtrace, self) diff -Nru veusz-1.10/dialogs/exceptionlist.ui veusz-1.14/dialogs/exceptionlist.ui --- veusz-1.10/dialogs/exceptionlist.ui 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/exceptionlist.ui 2011-11-22 20:23:31.000000000 +0000 @@ -34,6 +34,16 @@ + + + TextLabel + + + true + + + + Details diff -Nru veusz-1.10/dialogs/exceptionsend.ui veusz-1.14/dialogs/exceptionsend.ui --- veusz-1.10/dialogs/exceptionsend.ui 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/exceptionsend.ui 2011-11-22 20:23:31.000000000 +0000 @@ -13,17 +13,18 @@ Send problem - Veusz - - - 6 - - - 9 - + - Optionally provide email for feedback on bug: + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Arial'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Email address</span> (optional). If provided you can be notified about the bug status or to get further details.</p></body></html> + + + true @@ -33,7 +34,14 @@ - Optionally say what you were doing when the problem occured: + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Arial'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">What you were doing when the problem occured</span> (optional). This is very helpful for trying to reproduce the bug.</p></body></html> + + + true diff -Nru veusz-1.10/dialogs/histodata.py veusz-1.14/dialogs/histodata.py --- veusz-1.10/dialogs/histodata.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/histodata.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: histodata.py 1371 2010-08-21 18:30:13Z jeremysanders $ - import veusz.qtall as qt4 import veusz.setting as setting import veusz.utils as utils diff -Nru veusz-1.10/dialogs/historycheck.py veusz-1.14/dialogs/historycheck.py --- veusz-1.10/dialogs/historycheck.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/historycheck.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,65 +0,0 @@ -# Copyright (C) 2009 Jeremy S. Sanders -# Email: Jeremy Sanders -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -############################################################################## - -# $Id: historycheck.py 1101 2009-11-08 17:10:07Z jeremysanders $ - -import veusz.qtall as qt4 -import veusz.setting as setting - -class HistoryCheck(qt4.QCheckBox): - """Checkbox remembers its setting between calls - """ - - def __init__(self, *args): - qt4.QCheckBox.__init__(self, *args) - self.default = False - - def getSettingName(self): - """Get name for saving in settings.""" - # get dialog for widget - dialog = self.parent() - while not isinstance(dialog, qt4.QDialog): - dialog = dialog.parent() - - # combine dialog and object names to make setting - return '%s_%s_HistoryCheck' % ( dialog.objectName(), - self.objectName() ) - - def loadHistory(self): - """Load contents of HistoryCheck from settings.""" - checked = setting.settingdb.get(self.getSettingName(), self.default) - # this is to ensure toggled() signals get sent - self.setChecked(not checked) - self.setChecked(checked) - - def saveHistory(self): - """Save contents of HistoryCheck to settings.""" - setting.settingdb[self.getSettingName()] = self.isChecked() - - def showEvent(self, event): - """Show HistoryCheck and load history.""" - qt4.QCheckBox.showEvent(self, event) - # we do this now rather than in __init__ because the widget - # has no name set at __init__ - self.loadHistory() - - def hideEvent(self, event): - """Save history as widget is hidden.""" - qt4.QCheckBox.hideEvent(self, event) - self.saveHistory() - diff -Nru veusz-1.10/dialogs/historycombo.py veusz-1.14/dialogs/historycombo.py --- veusz-1.10/dialogs/historycombo.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/historycombo.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,136 +0,0 @@ -# Copyright (C) 2009 Jeremy S. Sanders -# Email: Jeremy Sanders -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -############################################################################## - -# $Id: historycombo.py 1228 2010-05-15 22:03:15Z jeremysanders $ - -"""A combobox which remembers its history. - -The history is stored in the Veusz settings database. -""" - -import veusz.qtall as qt4 -import veusz.setting as setting - -class HistoryCombo(qt4.QComboBox): - """This combobox records what items have been entered into it so the - user can choose them again. - - Duplicates and blanks are ignored. - """ - - def __init__(self, *args): - qt4.QComboBox.__init__(self, *args) - - # sane defaults - self.setEditable(True) - self.setAutoCompletion(True) - self.setMaxCount(50) - self.setInsertPolicy(qt4.QComboBox.InsertAtTop) - self.setDuplicatesEnabled(False) - self.setSizePolicy( qt4.QSizePolicy(qt4.QSizePolicy.MinimumExpanding, - qt4.QSizePolicy.Fixed) ) - - # stops combobox readjusting in size to fit contents - self.setSizeAdjustPolicy( - qt4.QComboBox.AdjustToMinimumContentsLengthWithIcon) - - self.default = [] - self.hasshown = False - - def text(self): - """Get text in combobox - - this gives it the same interface as QLineEdit.""" - return self.currentText() - - def setText(self, text): - """Set text in combobox - - gives same interface as QLineEdit.""" - self.lineEdit().setText(text) - - def hasAcceptableInput(self): - """Input valid? - - gives same interface as QLineEdit.""" - return self.lineEdit().hasAcceptableInput() - - def replaceAndAddHistory(self, item): - """Replace the text and place item at top of history.""" - - self.lineEdit().setText(item) - index = self.findText(item) # lookup for existing item (if any) - if index != -1: - # remove any old items matching this - self.removeItem(index) - - # put new item in - self.insertItem(0, item) - # set selected item in drop down list match current item - self.setCurrentIndex(0) - - def getSettingName(self): - """Get name for saving in settings.""" - - # get dialog for widget - dialog = self.parent() - while not isinstance(dialog, qt4.QDialog): - dialog = dialog.parent() - - # combine dialog and object names to make setting - return '%s_%s_HistoryCombo' % ( dialog.objectName(), - self.objectName() ) - - def loadHistory(self): - """Load contents of history combo from settings.""" - self.clear() - history = setting.settingdb.get(self.getSettingName(), self.default) - self.insertItems(0, history) - - self.hasshown = True - - def saveHistory(self): - """Save contents of history combo to settings.""" - - # only save history if it has been loaded - if not self.hasshown: - return - - # collect current items - history = [ unicode(self.itemText(i)) for i in xrange(self.count()) ] - history.insert(0, unicode(self.currentText())) - - # remove dups - histout = [] - histset = set() - for item in history: - if item not in histset: - histout.append(item) - histset.add(item) - - # save the history - setting.settingdb[self.getSettingName()] = histout - - def showEvent(self, event): - """Show HistoryCombo and load history.""" - qt4.QComboBox.showEvent(self, event) - # we do this now rather than in __init__ because the widget - # has no name set at __init__ - self.loadHistory() - - def hideEvent(self, event): - """Save history as widget is hidden.""" - qt4.QComboBox.hideEvent(self, event) - self.saveHistory() diff -Nru veusz-1.10/dialogs/historygroupbox.py veusz-1.14/dialogs/historygroupbox.py --- veusz-1.10/dialogs/historygroupbox.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/historygroupbox.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,78 +0,0 @@ -# Copyright (C) 2010 Jeremy S. Sanders -# Email: Jeremy Sanders -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -############################################################################## - -# $Id: historygroupbox.py 1221 2010-05-09 16:49:49Z jeremysanders $ - -import veusz.qtall as qt4 -import veusz.setting as setting - -class HistoryGroupBox(qt4.QGroupBox): - """Group box remembers settings of radio buttons inside it. - - emits radioClicked(radiowidget) when clicked - """ - - def getSettingName(self): - """Get name for saving in settings.""" - # get dialog for widget - dialog = self.parent() - while not isinstance(dialog, qt4.QDialog): - dialog = dialog.parent() - - # combine dialog and object names to make setting - return '%s_%s_HistoryGroup' % ( dialog.objectName(), - self.objectName() ) - - def loadHistory(self): - """Load from settings.""" - # connect up radio buttons to emit clicked signal - for w in self.children(): - if isinstance(w, qt4.QRadioButton): - def doemit(w=w): - self.emit(qt4.SIGNAL("radioClicked"), w) - self.connect( w, qt4.SIGNAL('clicked()'), doemit) - - # set item to be checked - checked = setting.settingdb.get(self.getSettingName(), "") - for w in self.children(): - if isinstance(w, qt4.QRadioButton) and ( - w.objectName() == checked or checked == ""): - w.click() - return - - def getRadioChecked(self): - """Get name of radio button checked.""" - for w in self.children(): - if isinstance(w, qt4.QRadioButton) and w.isChecked(): - return w - return None - - def saveHistory(self): - """Save to settings.""" - name = unicode(self.getRadioChecked().objectName()) - setting.settingdb[self.getSettingName()] = name - - def showEvent(self, event): - """Show and load history.""" - qt4.QGroupBox.showEvent(self, event) - self.loadHistory() - - def hideEvent(self, event): - """Save history as widget is hidden.""" - qt4.QGroupBox.hideEvent(self, event) - self.saveHistory() diff -Nru veusz-1.10/dialogs/historyspinbox.py veusz-1.14/dialogs/historyspinbox.py --- veusz-1.10/dialogs/historyspinbox.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/historyspinbox.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,59 +0,0 @@ -# Copyright (C) 2010 Jeremy S. Sanders -# Email: Jeremy Sanders -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -############################################################################## - -# $Id: historyspinbox.py 1471 2010-12-10 22:18:24Z jeremysanders $ - -import veusz.qtall as qt4 -import veusz.setting as setting - -class HistorySpinBox(qt4.QSpinBox): - """A SpinBox which remembers its setting between calls.""" - - def __init__(self, *args): - qt4.QSpinBox.__init__(self, *args) - self.default = 0 - - def getSettingName(self): - """Get name for saving in settings.""" - # get dialog for widget - dialog = self.parent() - while not isinstance(dialog, qt4.QDialog): - dialog = dialog.parent() - - # combine dialog and object names to make setting - return "%s_%s_HistorySpinBox" % ( dialog.objectName(), - self.objectName() ) - - def loadHistory(self): - """Load contents of HistorySpinBox from settings.""" - num = setting.settingdb.get(self.getSettingName(), self.default) - self.setValue(num) - - def saveHistory(self): - """Save contents of HistorySpinBox to settings.""" - setting.settingdb[self.getSettingName()] = self.value() - - def showEvent(self, event): - """Show HistorySpinBox and load history.""" - qt4.QSpinBox.showEvent(self, event) - self.loadHistory() - - def hideEvent(self, event): - """Save history as widget is hidden.""" - qt4.QSpinBox.hideEvent(self, event) - self.saveHistory() diff -Nru veusz-1.10/dialogs/historyvaluecombo.py veusz-1.14/dialogs/historyvaluecombo.py --- veusz-1.10/dialogs/historyvaluecombo.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/historyvaluecombo.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,92 +0,0 @@ -# Copyright (C) 2009 Jeremy S. Sanders -# Email: Jeremy Sanders -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -############################################################################## - -# $Id: historyvaluecombo.py 1056 2009-09-05 16:51:59Z jeremysanders $ - -"""A combobox which remembers previous setting -""" - -import veusz.qtall as qt4 -import veusz.setting as setting - -class HistoryValueCombo(qt4.QComboBox): - """This combobox records what value was previously saved - """ - - def __init__(self, *args): - qt4.QComboBox.__init__(self, *args) - self.defaultlist = [] - self.defaultval = None - self.hasshown = False - - def getSettingName(self): - """Get name for saving in settings.""" - - # get dialog for widget - dialog = self.parent() - while not isinstance(dialog, qt4.QDialog): - dialog = dialog.parent() - - # combine dialog and object names to make setting - return '%s_%s_HistoryValueCombo' % ( dialog.objectName(), - self.objectName() ) - - def saveHistory(self): - """Save contents of history combo to settings.""" - - # only save history if it has been loaded - if not self.hasshown: - return - - # collect current items - history = [ unicode(self.itemText(i)) for i in xrange(self.count()) ] - history.insert(0, unicode(self.currentText())) - - # remove dups - histout = [] - histset = set() - for item in history: - if item not in histset: - histout.append(item) - histset.add(item) - - # save the history - setting.settingdb[self.getSettingName()] = histout - - def showEvent(self, event): - """Show HistoryCombo and load history.""" - qt4.QComboBox.showEvent(self, event) - - self.clear() - self.addItems(self.defaultlist) - text = setting.settingdb.get(self.getSettingName(), self.defaultval) - indx = self.findText(text) - if indx < 0: - self.insertItem(0, text) - indx = 0 - self.setCurrentIndex(indx) - self.hasshown = True - - def hideEvent(self, event): - """Save history as widget is hidden.""" - qt4.QComboBox.hideEvent(self, event) - - if self.hasshown: - text = unicode(self.currentText()) - setting.settingdb[self.getSettingName()] = text - diff -Nru veusz-1.10/dialogs/import_csv.ui veusz-1.14/dialogs/import_csv.ui --- veusz-1.10/dialogs/import_csv.ui 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/import_csv.ui 2011-11-22 20:23:31.000000000 +0000 @@ -6,11 +6,11 @@ 0 0 - 380 - 274 + 548 + 491 - + @@ -29,90 +29,193 @@ - - - - - Delimiter - + + + + + Behaviour + + + + + + Header mode + + + + + + + 'Multiple': allow multiple headers in file +'Single': 1st non-blank row is header +'None': no headers, guess data types + + + + + + + &Direction + + + csvdirectioncombo + + + + + + + Are the data arranged in columns or rows? + + + + Columns + + + + + Rows + + + + + + + + Ignore rows at top + + + + + + + Ignore N row at the top of the file. +If data are arranged in rows, ignores columns instead. + + + + + + + Ignore rows after headers + + + + + + + After reading a header, ignore N rows in that column. +If Direction is set to Rows, ignore N columns instead. + + + + + + + Treat blanks as data values + + + + + + + Help on how CSV files should be formatted + + + Help + + + + - - - Delimiter between fields. This is usually a comma. - - - true - - - - - - - Text delimiter - - - - - - - Character to delimit text, usually a quote ("). - - - true - - - - - - - &Direction - - - csvdirectioncombo - - - - - - - Are the data arranged in columns or rows? - - - - Columns - - - - - Rows - - - - - - - - Help on how CSV files should be formatted - - - Help - - - - - - - Ignore lines after headers - - - - - - - After reading a header item, Veusz will ignore the following number of lines in that column - + + + Locale + + + + + + Numerics + + + + + + + Numerical format of numbers in file: + System - what this computer is set to use + English - format 123,456.78 + European - format 123.456,78 + + + + + + + Dates + + + + + + + Format for dates and times in file. +This will be combination of +YYYY, YY, MM, M, DD, D, hh, h, mm, m, ss and s +separated by | + + + true + + + + + + + + + + Delimiters + + + + + + Column + + + + + + + Delimiter between fields. This is usually a comma. + + + true + + + + + + + Text + + + + + + + Character to delimit text, usually a quote ("). + + + true + + + + @@ -130,6 +233,16 @@ QSpinBox
historyspinbox.h
+ + HistoryCheck + QCheckBox +
historycheck.h
+
+ + HistoryValueCombo + QComboBox +
historyvaluecombo.h
+
diff -Nru veusz-1.10/dialogs/importdialog.py veusz-1.14/dialogs/importdialog.py --- veusz-1.10/dialogs/importdialog.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/importdialog.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: importdialog.py 1471 2010-12-10 22:18:24Z jeremysanders $ - """Module for implementing dialog boxes for importing data in Veusz.""" import os.path @@ -69,6 +67,15 @@ """Secondary check (after preview) for enabling import button.""" return True + def isFiletypeSupported(self, ftype): + """Is the filetype supported by this tab?""" + return False + + def useFiletype(self, ftype): + """If the tab can do something with the selected filetype, + update itself.""" + pass + class ImportTabStandard(ImportTab): """Standard import format tab.""" @@ -144,10 +151,14 @@ if len(lines) != 0: lines.append('') - lines += self.dialog.retnDatasetInfo(dsnames) + lines += self.dialog.retnDatasetInfo(dsnames, linked, filename) self.previewedit.setPlainText( '\n'.join(lines) ) + def isFiletypeSupported(self, ftype): + """Is the filetype supported by this tab?""" + return ftype in ('.dat', '.txt') + class ImportTabCSV(ImportTab): """For importing data from CSV files.""" @@ -167,6 +178,13 @@ self.csvdelimitercombo.default = [ ',', '{tab}', '{space}', '|', ':', ';'] self.csvtextdelimitercombo.default = ['"', "'"] + self.csvdatefmtcombo.default = [ + 'YYYY-MM-DD|T|hh:mm:ss', + 'DD/MM/YY| |hh:mm:ss', + 'M/D/YY| |hh:mm:ss' + ] + self.csvnumfmtcombo.defaultlist = ['System', 'English', 'European'] + self.csvheadermodecombo.defaultlist = ['Multiple', '1st row', 'None'] def reset(self): """Reset controls.""" @@ -174,6 +192,12 @@ self.csvtextdelimitercombo.setEditText('"') self.csvdirectioncombo.setCurrentIndex(0) self.csvignorehdrspin.setValue(0) + self.csvignoretopspin.setValue(0) + self.csvblanksdatacheck.setChecked(False) + self.csvnumfmtcombo.setCurrentIndex(0) + self.csvdatefmtcombo.setEditText( + document.ParamsCSV.defaults['dateformat']) + self.csvheadermodecombo.setCurrentIndex(0) def slotHelp(self): """Asked for help.""" @@ -212,7 +236,7 @@ return False try: - reader = utils.UnicodeCSVReader( open(filename), + reader = utils.UnicodeCSVReader( filename, delimiter=delimiter, quotechar=textdelimiter, encoding=encoding ) @@ -256,20 +280,38 @@ except UnicodeEncodeError: return + numericlocale = ( str(qt4.QLocale().name()), + 'en_US', + 'de_DE' )[self.csvnumfmtcombo.currentIndex()] headerignore = self.csvignorehdrspin.value() - op = document.OperationDataImportCSV(filename, readrows=inrows, - prefix=prefix, suffix=suffix, - linked=linked, - delimiter=delimiter, - textdelimiter=textdelimiter, - headerignore=headerignore, - encoding=encoding) - + rowsignore = self.csvignoretopspin.value() + blanksaredata = self.csvblanksdatacheck.isChecked() + dateformat = unicode(self.csvdatefmtcombo.currentText()) + headermode = ('multi', '1st', 'none')[ + self.csvheadermodecombo.currentIndex()] + + # create import parameters and operation objects + params = document.ParamsCSV( + filename, + readrows=inrows, + encoding=encoding, + delimiter=delimiter, + textdelimiter=textdelimiter, + headerignore=headerignore, + rowsignore=rowsignore, + blanksaredata=blanksaredata, + numericlocale=numericlocale, + dateformat=dateformat, + headermode=headermode, + dsprefix=prefix, dssuffix=suffix + ) + op = document.OperationDataImportCSV(params, linked=linked) + # actually import the data dsnames = doc.applyOperation(op) - - # what datasets were imported - lines = self.dialog.retnDatasetInfo(dsnames) + + # update output, showing what datasets were imported + lines = self.dialog.retnDatasetInfo(dsnames, linked, filename) t = self.previewtablecsv t.verticalHeader().hide() @@ -283,6 +325,10 @@ item = qt4.QTableWidgetItem(l) t.setItem(i, 0, item) + def isFiletypeSupported(self, ftype): + """Is the filetype supported by this tab?""" + return ftype in ('.tsv', '.csv') + class ImportTab2D(ImportTab): """Tab for importing from a 2D data file.""" @@ -401,7 +447,7 @@ self.connect( self.fitshdulist, qt4.SIGNAL('itemSelectionChanged()'), self.slotFitsUpdateCombos ) self.connect( self.fitsdatasetname, - qt4.SIGNAL('textChanged(const QString&)'), + qt4.SIGNAL('editTextChanged(const QString&)'), self.dialog.enableDisableImport ) self.connect( self.fitsdatacolumn, qt4.SIGNAL('currentIndexChanged(int)'), @@ -482,7 +528,7 @@ except AttributeError: # this is an image naxis = header['NAXIS'] - if naxis ==2: + if naxis == 1 or naxis == 2: data = ['image'] else: data = ['invalidimage'] @@ -592,11 +638,24 @@ self.fitsimportstatus.setText("Imported dataset '%s'" % name) qt4.QTimer.singleShot(2000, self.fitsimportstatus.clear) + def isFiletypeSupported(self, ftype): + """Is the filetype supported by this tab?""" + return ftype in ('.fit', '.fits') + class ImportTabPlugins(ImportTab): """Tab for importing using a plugin.""" resource = 'import_plugins.ui' + def __init__(self, importdialog, promote=None): + """Initialise dialog. importdialog is the import dialog itself. + + If promote is set to a name of a plugin, it is promoted to its own tab + """ + ImportTab.__init__(self, importdialog) + self.promote = promote + self.plugininstance = None + def loadUi(self): """Load the user interface.""" ImportTab.loadUi(self) @@ -611,13 +670,22 @@ self.fields = [] # load previous plugin - if 'import_plugin' in setting.settingdb: - try: - idx = names.index(setting.settingdb['import_plugin']) - self.pluginType.setCurrentIndex(idx) - except ValueError: - pass + idx = -1 + if self.promote is None: + if 'import_plugin' in setting.settingdb: + try: + idx = names.index(setting.settingdb['import_plugin']) + except ValueError: + pass + else: + # set the correct entry for the plugin + idx = names.index(self.promote) + # then hide the widget so it can't be changed + self.pluginchoicewidget.hide() + if idx >= 0: + self.pluginType.setCurrentIndex(idx) + self.pluginChanged(-1) def getPluginFields(self): @@ -636,12 +704,23 @@ idx = names.index(selname) except ValueError: return None - return plugins.importpluginregistry[idx] + + p = plugins.importpluginregistry[idx] + if isinstance(p, type): + # this is a class, rather than an object + if not isinstance(self.plugininstance, p): + # create new instance, if required + self.plugininstance = p() + return self.plugininstance + else: + # backward compatibility with old API + return p def pluginChanged(self, index): """Update controls based on index.""" plugin = self.getSelectedPlugin() - setting.settingdb['import_plugin'] = plugin.name + if self.promote is None: + setting.settingdb['import_plugin'] = plugin.name # delete old controls layout = self.pluginParams.layout() @@ -669,12 +748,13 @@ """Preview using plugin.""" # check file exists - try: - f = open(filename, 'r') - f.close() - except IOError: - self.pluginPreview.setPlainText('') - return False + if filename != '{clipboard}': + try: + f = open(filename, 'r') + f.close() + except IOError: + self.pluginPreview.setPlainText('') + return False # get the plugin selected plugin = self.getSelectedPlugin() @@ -702,17 +782,49 @@ plugin, filename, linked=linked, encoding=encoding, prefix=prefix, suffix=suffix, **params) try: - results = doc.applyOperation(op) + datasets, customs = doc.applyOperation(op) except plugins.ImportPluginException, ex: self.pluginPreview.setPlainText( unicode(ex) ) return out = ['Imported data for datasets:'] - for ds in results: + for ds in datasets: out.append( doc.data[ds].description(showlinked=False) ) + if customs: + out.append('') + out.append('Set custom definitions:') + out += customs self.pluginPreview.setPlainText('\n'.join(out)) + def isFiletypeSupported(self, ftype): + """Is the filetype supported by this tab?""" + + if self.promote is None: + # look through list of supported plugins to check filetypes + inany = False + for p in plugins.importpluginregistry: + if ftype in p.file_extensions: + inany = True + return inany + else: + # find plugin class and check filetype + for p in plugins.importpluginregistry: + if p.name == self.promote: + return ftype in p.file_extensions + + def useFiletype(self, ftype): + """Select the plugin corresponding to the filetype.""" + + if self.promote is None: + plugin = None + for p in plugins.importpluginregistry: + if ftype in p.file_extensions: + plugin = p.name + idx = self.pluginType.findText(plugin, qt4.Qt.MatchExactly) + self.pluginType.setCurrentIndex(idx) + self.pluginChanged(-1) + class ImportDialog(VeuszDialog): """Dialog box for importing data. See ImportTab classes above which actually do the work of importing @@ -739,6 +851,13 @@ ): w = tabclass(self) self.methodtab.addTab(w, tabname) + + # add promoted plugins + for p in plugins.importpluginregistry: + if p.promote_tab is not None: + w = ImportTabPlugins(self, promote=p.name) + self.methodtab.addTab(w, p.promote_tab) + self.connect( self.methodtab, qt4.SIGNAL('currentChanged(int)'), self.slotUpdatePreview ) @@ -783,6 +902,14 @@ self.encodingcombo.defaultlist = utils.encodings self.encodingcombo.defaultval = 'utf_8' + # load icon for clipboard + self.clipbutton.setIcon( utils.getIcon('kde-clipboard') ) + self.connect(qt4.QApplication.clipboard(), qt4.SIGNAL('dataChanged()'), + self.updateClipPreview) + self.connect( + self.clipbutton, qt4.SIGNAL("clicked()"), self.slotClipButtonClicked) + self.updateClipPreview() + def slotBrowseClicked(self): """Browse for a data file.""" @@ -802,6 +929,29 @@ if fd.exec_() == qt4.QDialog.Accepted: ImportDialog.dirname = fd.directory().absolutePath() self.filenameedit.replaceAndAddHistory( fd.selectedFiles()[0] ) + self.guessImportTab() + + def guessImportTab(self): + """Guess import tab based on filename.""" + filename = unicode( self.filenameedit.text() ) + + fname, ftype = os.path.splitext(filename) + # strip off any gz, bz2 extensions to get real extension + while ftype.lower() in ('gz', 'bz2'): + fname, ftype = os.path.splitext(fname) + ftype = ftype.lower() + + # examine from left to right + # promoted plugins come after plugins + idx = -1 + for i in xrange(self.methodtab.count()): + w = self.methodtab.widget(i) + if w.isFiletypeSupported(ftype): + idx = i + + if idx >= 0: + self.methodtab.setCurrentIndex(idx) + self.methodtab.widget(idx).useFiletype(ftype) def slotUpdatePreview(self, *args): """Update preview window when filename or tab changed.""" @@ -838,15 +988,20 @@ """Do the importing""" filename = unicode( self.filenameedit.text() ) - filename = os.path.abspath(filename) linked = self.linkcheckbox.isChecked() encoding = str(self.encodingcombo.currentText()) + if filename == '{clipboard}': + linked = False + else: + # normalise filename + filename = os.path.abspath(filename) # import according to tab selected importtab = self.methodtab.currentWidget() prefix, suffix = self.getPrefixSuffix(filename) try: qt4.QApplication.setOverrideCursor( qt4.QCursor(qt4.Qt.WaitCursor) ) + self.document.suspendUpdates() importtab.doImport(self.document, filename, linked, encoding, prefix, suffix) qt4.QApplication.restoreOverrideCursor() @@ -856,8 +1011,9 @@ # show exception dialog d = exceptiondialog.ExceptionDialog(sys.exc_info(), self) d.exec_() + self.document.enableUpdates() - def retnDatasetInfo(self, dsnames): + def retnDatasetInfo(self, dsnames, linked, filename): """Return a list of information for the dataset names given.""" lines = ['Imported data for datasets:'] @@ -867,10 +1023,6 @@ # build up description lines.append( ' %s' % ds.description(showlinked=False) ) - linked = self.linkcheckbox.isChecked() - filename = unicode( self.filenameedit.text() ) - filename = os.path.abspath(filename) - # whether the data were linked if linked: lines.append('') @@ -899,3 +1051,14 @@ importtab = self.methodtab.currentWidget() importtab.reset() + + def slotClipButtonClicked(self): + """Clicked clipboard button.""" + self.filenameedit.setText("{clipboard}") + + def updateClipPreview(self): + """Clipboard contents changed, so update preview if showing clipboard.""" + + filename = unicode(self.filenameedit.text()) + if filename == '{clipboard}': + self.slotUpdatePreview() diff -Nru veusz-1.10/dialogs/importhelpcsv.ui veusz-1.14/dialogs/importhelpcsv.ui --- veusz-1.10/dialogs/importhelpcsv.ui 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/importhelpcsv.ui 2011-11-22 20:23:31.000000000 +0000 @@ -6,8 +6,8 @@ 0 0 - 581 - 368 + 457 + 400
@@ -24,17 +24,28 @@ <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Arial'; font-size:9pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Comma Separated Value (CSV) files are often used to export data from applications such as Excel and OpenOffice.</p> +<p style=" margin-top:18px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:11pt; font-weight:600;">CSV (Comma Separated Value)</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">CSV files are often used to export data from applications such as Excel and OpenOffice.</p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Place a dataset name at the top of each column. Multiple datasets can be placed below each other if new names are given. To import error bars, columns with the names &quot;+&quot;, &quot;-&quot; or &quot;+-&quot; should be given in columns immediately to the right of the dataset, for positive, negative or symmetric errors, respectively.</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Headers</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Place a dataset name at the top of each column. To import error bars, columns with the names &quot;+&quot;, &quot;-&quot; or &quot;+-&quot; should be given in columns immediately to the right of the dataset, for positive, negative or symmetric errors, respectively.</p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Veusz can also read data organised in rows rather than columns (choose the rows option under Directions).</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">In the standard Multiple header mode, multiple datasets can be placed below each other if new names are given. If your data only consist of a single header, you can choose the Single header mode, which will prevent Veusz from starting new datasets if it sees text. If your data have no header, choose None and your columns will be named automatically.</p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If you want to read in text use (text) after the name of the dataset in the top column, e.g. &quot;name (text)&quot;. If you do not do this, Veusz assumes columns it cannot convert to numbers are text data. Dates should have &quot; (date)&quot; after the column name. </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Veusz can also read data organised in rows rather than columns (choose the rows option under Directions). Veusz can also ignore the specified number of lines in columns which follow header items.</p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Veusz can also ignore the specified number of lines in columns which follow header items.</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Data types</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Veusz will try to guss the data type (numeric, text or date) depending on what values it sees. You can override this by putting the datatype name in brackets in the column header. If you want to read in text use (text) after the name of the dataset in the top column, e.g. &quot;name (text)&quot;. Date-times can have &quot; (date)&quot; after the column name. Veusz uses ISO dates by default YYYY-MM-DDThh:mm:ss, but this can be changed.</p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If you are using non-ASCII characters in your text you need to encode the text in your file with a Unicode encoding (e.g. UTF-8) and choose the correct encoding in the encoding drop down box. </p></body></html> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Options</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">There are several variants of CSV files used. You may wish to change the delimiter to be a tab (TSV) or space. Your file may also use European or English numerical values e.g. (1,23 or 1.23), so you may wish to override your computer's default format.</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">There are many different date and time formats. You can enter your own format in the box given [a combination of YYYY (or YY), MM (or M), DD (or D), HH, MM and SS].</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The &quot;Treat blanks as data values&quot; option will insert NaN values or empty strings into datasets, if blank data values are encountered.</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If you are using non-ASCII characters in your text you need to encode the text in your file with a Unicode encoding (e.g. UTF-8) and choose the correct encoding in the encoding drop down box. </p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p></body></html> diff -Nru veusz-1.10/dialogs/importhelp.ui veusz-1.14/dialogs/importhelp.ui --- veusz-1.10/dialogs/importhelp.ui 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/importhelp.ui 2011-11-22 20:23:31.000000000 +0000 @@ -24,9 +24,9 @@ <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Arial'; font-size:9pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Veusz assumes that data are stored as columns in a text file separated by tabs or spaces. Names should be entered for the datasets read from each column, separated by spaces or commas (in the dataset names or descriptor box).</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Veusz assumes that data are stored as columns in a text file separated by tabs or spaces. Names should be entered for the datasets read from each column, separated by spaces or commas (in the dataset names or descriptor box). If you leave the descriptor blank, automatic dataset names will be used (prefix + column + suffix, or &quot;colX&quot; if the prefix and suffix are blank).</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:10pt;"></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">If you want to supply errors or uncertainties on the data, these can be given in the columns following the dataset column (one column for symmetric errors or two for asymmetric errors). To tell Veusz that a dataset has errors, add "</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">+-</span><span style=" font-size:10pt;">" or "</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">+,-</span><span style=" font-size:10pt;">" to the dataset name to specify symmetric or asymmetric errors, respectively.</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">If you want to supply errors or uncertainties on the data, these can be given in the columns following the dataset column (one column for symmetric errors or two for asymmetric errors). To tell Veusz that a dataset has errors, add &quot;</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">+-</span><span style=" font-size:10pt;">&quot; or &quot;</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">+,-</span><span style=" font-size:10pt;">&quot; to the dataset name to specify symmetric or asymmetric errors, respectively.</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:10pt;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Commas or spaces separate the dataset name and the error bars. They are interchangable, except multiple commas will skip an input columns. </span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:10pt;"></p> @@ -39,15 +39,15 @@ <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Courier New,courier'; font-size:10pt;">,x,y,-,+</span><span style=" font-size:10pt;"> skip first column, x with no errors, y followed by negative then postive error bars</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:10pt;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt; font-weight:600;">Data types</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">A file can contain different types of data. This type is specified immediately after the dataset name in round brackets, e.g. "x(float)", "labels(text)" or "y(float),+-".</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">A file can contain different types of data. This type is specified immediately after the dataset name in round brackets, e.g. &quot;x(float)&quot;, &quot;labels(text)&quot; or &quot;y(float),+-&quot;.</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:10pt;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Only numerical (use float, number or numeric) and text (using text or string) data are supported. If a text column has spaces it should be surrounded by quotation marks.</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:10pt; font-weight:600;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt; font-weight:600;">Comments</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">If any of the "</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">#</span><span style=" font-size:10pt;">", "</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">!</span><span style=" font-size:10pt;">", "</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">;</span><span style=" font-size:10pt;">" or "</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">%</span><span style=" font-size:10pt;">" characters are found without being inside quotation marks, the rest of a line is ignored. Use these characters to add comments to a file.</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">If any of the &quot;</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">#</span><span style=" font-size:10pt;">&quot;, &quot;</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">!</span><span style=" font-size:10pt;">&quot;, &quot;</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">;</span><span style=" font-size:10pt;">&quot; or &quot;</span><span style=" font-family:'Courier New,courier'; font-size:10pt;">%</span><span style=" font-size:10pt;">&quot; characters are found without being inside quotation marks, the rest of a line is ignored. Use these characters to add comments to a file.</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:10pt; font-weight:600;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt; font-weight:600;">Further notes</span></p> -<ol style="-qt-list-indent: 1;"><li style=" font-size:10pt;" style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Extra tabs or spaces between columns are ignored</li> +<ol style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" font-size:10pt;" style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Extra tabs or spaces between columns are ignored</li> <li style=" font-size:10pt;" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Extra data at the end of a line are ignored</li> <li style=" font-size:10pt;" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The text <span style=" font-family:'Courier New,courier';">nan</span> or <span style=" font-family:'Courier New,courier';">inf</span> translates to the usual numerical values. These values aren't plotted in a plot, giving a break in the line.</li> <li style=" font-size:10pt;" style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You can encode the descriptor describing the data in the file itself with a line <span style=" font-family:'Courier New,courier';">descriptor XXX</span> before the data. Leave the descriptor blank in the import dialog if you do this. Multiple descriptors can be placed in the file to store multiple sets of data. </li></ol> diff -Nru veusz-1.10/dialogs/import_plugins.ui veusz-1.14/dialogs/import_plugins.ui --- veusz-1.10/dialogs/import_plugins.ui 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/import_plugins.ui 2011-11-22 20:23:31.000000000 +0000 @@ -12,18 +12,20 @@ - - - - - Plugin: - - - - - - - + + + + + + Plugin: + + + + + + + + diff -Nru veusz-1.10/dialogs/import.ui veusz-1.14/dialogs/import.ui --- veusz-1.10/dialogs/import.ui 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/import.ui 2011-11-22 20:23:31.000000000 +0000 @@ -46,6 +46,16 @@ + + + + Read in data from clipboard rather than file + + + + + + @@ -101,7 +111,8 @@ - Prefix to prepend to each dataset name imported, or enter $FILENAME to have filename prepended + Prefix to prepend to each dataset name imported, +or enter $FILENAME to have filename prepended true @@ -141,7 +152,8 @@ - Suffix to append to each dataset name imported, or enter $FILENAME to have filename appended + Suffix to append to each dataset name imported, +or enter $FILENAME to have filename appended true @@ -151,7 +163,6 @@ - diff -Nru veusz-1.10/dialogs/__init__.py veusz-1.14/dialogs/__init__.py --- veusz-1.10/dialogs/__init__.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/__init__.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,23 +16,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: __init__.py 1471 2010-12-10 22:18:24Z jeremysanders $ - """Veusz dialogs module.""" -# insert history combo into the list of modules so that it can be found -# by loadUi - yuck -import sys -import historycombo -import historycheck -import historyvaluecombo -import historygroupbox -import historyspinbox -import recentfilesbutton - -sys.modules['historycombo'] = historycombo -sys.modules['historycheck'] = historycheck -sys.modules['historyvaluecombo'] = historyvaluecombo -sys.modules['historygroupbox'] = historygroupbox -sys.modules['historyspinbox'] = historyspinbox -sys.modules['recentfilesbutton'] = recentfilesbutton +# load custom widgets +import veusz.qtwidgets diff -Nru veusz-1.10/dialogs/plugin.py veusz-1.14/dialogs/plugin.py --- veusz-1.10/dialogs/plugin.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/plugin.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: plugin.py 1448 2010-11-13 20:07:34Z jeremysanders $ - """Dialog boxes for tools and dataset plugins.""" import sys @@ -39,7 +37,9 @@ d = PluginDialog(mainwindow, doc, plugin, pluginkls) mainwindow.showDialog(d) else: - fields = {'currentwidget': mainwindow.treeedit.selwidget.path} + fields = {'currentwidget': '/'} + if mainwindow.treeedit.selwidgets: + fields = {'currentwidget': mainwindow.treeedit.selwidgets[0].path} runPlugin(mainwindow, doc, plugin, fields) def wordwrap(text, linelength=80): @@ -94,7 +94,9 @@ cntrl.deleteLater() del self.fieldcntrls[:] - currentwidget = self.mainwindow.treeedit.selwidget.path + currentwidget = '/' + if self.mainwindow.treeedit.selwidgets: + currentwidget = self.mainwindow.treeedit.selwidgets[0].path for row, field in enumerate(self.plugininst.fields): if isinstance(field, list) or isinstance(field, tuple): for c, f in enumerate(field): @@ -125,7 +127,9 @@ """Use the plugin with the inputted data.""" # default field - fields = {'currentwidget': self.mainwindow.treeedit.selwidget.path} + fields = {'currentwidget': '/'} + if self.mainwindow.treeedit.selwidgets: + fields = {'currentwidget': self.mainwindow.treeedit.selwidgets[0].path} # read values from controls for field, cntrls in izip(self.fields, self.fieldcntrls): diff -Nru veusz-1.10/dialogs/preferences.py veusz-1.14/dialogs/preferences.py --- veusz-1.10/dialogs/preferences.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/preferences.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: preferences.py 1358 2010-08-14 16:40:46Z jeremysanders $ - import veusz.qtall as qt4 import veusz.setting as setting import veusz.utils as utils @@ -37,12 +35,19 @@ # view settings self.antialiasCheck.setChecked( setdb['plot_antialias'] ) - self.intervalCombo.addItem('Disabled') - for intv in self.plotwindow.intervals[1:]: - self.intervalCombo.addItem('%gs' % (intv * 0.001)) - index = self.plotwindow.intervals.index( - setdb['plot_updateinterval']) + self.englishCheck.setChecked( setdb['ui_english'] ) + for intv in self.plotwindow.updateintervals: + self.intervalCombo.addItem(intv[1]) + index = [i[0] for i in self.plotwindow.updateintervals].index( + setdb['plot_updatepolicy']) self.intervalCombo.setCurrentIndex(index) + self.threadSpinBox.setValue( setdb['plot_numthreads'] ) + + # disable thread option if not supported + if not qt4.QFontDatabase.supportsThreadedFontRendering(): + self.threadSpinBox.setEnabled(False) + self.threadSpinBox.setToolTip("Disabled because of lack of " + "threaded drawing support") # use cwd for file dialogs self.cwdCheck.setChecked( setdb['dirname_usecwd'] ) @@ -53,8 +58,14 @@ str(setdb['toolbar_size']))) # set export dpi + dpis = ('75', '90', '100', '150', '200', '300') + self.exportDPI.addItems(dpis) + self.exportDPIPDF.addItems(dpis) + self.exportDPI.setValidator( qt4.QIntValidator(10, 10000, self) ) self.exportDPI.setEditText( str(setdb['export_DPI']) ) + self.exportDPIPDF.setValidator( qt4.QIntValidator(10, 10000, self) ) + self.exportDPIPDF.setEditText( str(setdb['export_DPI_PDF']) ) # set export antialias self.exportAntialias.setChecked( setdb['export_antialias']) @@ -179,9 +190,11 @@ # view settings setdb = setting.settingdb - setdb['plot_updateinterval'] = ( - self.plotwindow.intervals[ self.intervalCombo.currentIndex() ] ) + setdb['plot_updatepolicy'] = ( + self.plotwindow.updateintervals[self.intervalCombo.currentIndex()][0] ) setdb['plot_antialias'] = self.antialiasCheck.isChecked() + setdb['ui_english'] = self.englishCheck.isChecked() + setdb['plot_numthreads'] = self.threadSpinBox.value() # use cwd setdb['dirname_usecwd'] = self.cwdCheck.isChecked() @@ -195,10 +208,16 @@ widget.setIconSize( qt4.QSize(iconsize, iconsize) ) # update dpi if possible - try: - setdb['export_DPI'] = int(self.exportDPI.currentText()) - except ValueError: - pass + # FIXME: requires some sort of visual notification of validator + for cntrl, setn in ((self.exportDPI, 'export_DPI'), + (self.exportDPIPDF, 'export_DPI_PDF')): + try: + text = cntrl.currentText() + valid = cntrl.validator().validate(text, 0)[0] + if valid == qt4.QValidator.Acceptable: + setdb[setn] = int(text) + except ValueError: + pass # export settings setdb['export_antialias'] = self.exportAntialias.isChecked() diff -Nru veusz-1.10/dialogs/preferences.ui veusz-1.14/dialogs/preferences.ui --- veusz-1.10/dialogs/preferences.ui 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/preferences.ui 2011-11-22 20:23:31.000000000 +0000 @@ -28,29 +28,46 @@ + + Use antialiasing to smooth jagged edges + Antialiasing - + + + + Please restart Veusz after changing this option + + + Override system locale settings to show Veusz in US/English + + + + Update interval - - + + + + How often Veusz will update the plot if it has changed + + - + Toolbar icon size - + @@ -84,6 +101,24 @@ + + + + Number of drawing threads + + + + + + + Maximum number of parallel threads to use for drawing plots. +Set to 0 to disable threads. + + + 16 + + + @@ -96,7 +131,7 @@ - File dialogs will open in the current working directory of Veusz, rather than the one used by Veusz when it was last run + <html><head><meta name="qrichtext" content="1" /></head><body style=" white-space: pre-wrap; font-family:Sans Serif; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">File dialogs will open in the current working directory of Veusz, rather than the one used by Veusz when it was last run</p></body></html> Open file dialog in current working directory @@ -125,26 +160,6 @@ true - - - 75 - - - - - 100 - - - - - 150 - - - - - 300 - - @@ -158,20 +173,47 @@ + + + PDF/EPS DPI + + + + + + + <html><head><meta name="qrichtext" content="1" /></head><body style=" white-space: pre-wrap; font-family:Sans Serif; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">This value is the number of dots per inch used for writing PDF and EPS files. As these are vector formats, this does not make much difference to the output, but larger values improve the placement of characters and also make hatched fills finer.</p></body></html> + + + true + + + + Bitmap background - + + + + Use alpha channel values of 0 for transparency + + + + + + + Jpeg quality - + <html><head><meta name="qrichtext" content="1" /></head><body style=" white-space: pre-wrap; font-family:Sans Serif; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Choose Jpeg quality setting. Lower values are lower quality with more compression.</p></body></html> @@ -187,14 +229,14 @@ - + Postscript color - + <html><head><meta name="qrichtext" content="1" /></head><body style=" white-space: pre-wrap; font-family:Sans Serif; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Output postscript as full color, or convert to greyscale</p></body></html> @@ -211,16 +253,6 @@ - - - - Use alpha channel values of 0 for transparency - - - - - - diff -Nru veusz-1.10/dialogs/recentfilesbutton.py veusz-1.14/dialogs/recentfilesbutton.py --- veusz-1.10/dialogs/recentfilesbutton.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/recentfilesbutton.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,83 +0,0 @@ -# Copyright (C) 2009 Jeremy S. Sanders -# Email: Jeremy Sanders -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -############################################################################## - -# $Id: recentfilesbutton.py 1093 2009-10-31 11:42:51Z jeremysanders $ - -import os.path - -import veusz.qtall as qt4 -import veusz.setting as setting - -def removeBadRecents(itemlist): - """Remove duplicates from list and bad entries.""" - previous = set() - i = 0 - while i < len(itemlist): - if itemlist[i] in previous: - del itemlist[i] - elif not os.path.exists(itemlist[i]): - del itemlist[i] - else: - previous.add(itemlist[i]) - i += 1 - - # trim list - del itemlist[10:] - -class RecentFilesButton(qt4.QPushButton): - """A button for remembering recent files. - - emits filechosen(filename) if a file is chosen - """ - - def __init__(self, *args): - qt4.QPushButton.__init__(self, *args) - - self.menu = qt4.QMenu() - self.setMenu(self.menu) - self.settingname = None - - def setSetting(self, name): - """Specify settings to use when loading menu. - Should be called before use.""" - self.settingname = name - self.fillMenu() - - def fillMenu(self): - """Add filenames to menu.""" - self.menu.clear() - recent = setting.settingdb.get(self.settingname, []) - removeBadRecents(recent) - setting.settingdb[self.settingname] = recent - - for filename in recent: - if os.path.exists(filename): - act = self.menu.addAction( os.path.basename(filename) ) - def loadRecentFile(filename=filename): - self.emit(qt4.SIGNAL('filechosen'), filename) - self.connect( act, qt4.SIGNAL('triggered()'), - loadRecentFile ) - - def addFile(self, filename): - """Add filename to list of recent files.""" - recent = setting.settingdb.get(self.settingname, []) - recent.insert(0, os.path.abspath(filename)) - setting.settingdb[self.settingname] = recent - self.fillMenu() - - diff -Nru veusz-1.10/dialogs/reloaddata.py veusz-1.14/dialogs/reloaddata.py --- veusz-1.10/dialogs/reloaddata.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/reloaddata.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: reloaddata.py 1457 2010-11-28 11:25:22Z jeremysanders $ - """Dialog for reloading linked data.""" import os @@ -32,11 +30,17 @@ class ReloadData(VeuszDialog): """Dialog for reloading linked datasets.""" - def __init__(self, document, parent): - """Initialise the dialog.""" + def __init__(self, document, parent, filenames=None): + """Initialise the dialog. + + document: veusz document + parent: parent window + filenames: if a set() only reload from these filenames + """ VeuszDialog.__init__(self, parent, 'reloaddata.ui') self.document = document + self.filenames = filenames # update on reloading self.reloadct = 1 @@ -101,9 +105,11 @@ """Reload linked data. Show the user what was done.""" text = '' + self.document.suspendUpdates() try: # try to reload the datasets - datasets, errors = self.document.reloadLinkedDatasets() + datasets, errors = self.document.reloadLinkedDatasets( + self.filenames) # show errors in read data for var, count in errors.items(): @@ -126,9 +132,13 @@ text = 'Error reading file:\n' + unicode(e) except document.DescriptorError: text = 'Could not interpret descriptor. Reload failed.' + except: + self.document.enableUpdates() + raise if text == '': text = 'Nothing to do. No linked datasets.' + self.document.enableUpdates() self.outputedit.setPlainText(text) diff -Nru veusz-1.10/dialogs/safetyimport.py veusz-1.14/dialogs/safetyimport.py --- veusz-1.10/dialogs/safetyimport.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/safetyimport.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: safetyimport.py 1180 2010-02-27 18:03:13Z jeremysanders $ - """Ask user whether to import symbols.""" import veusz.qtall as qt4 diff -Nru veusz-1.10/dialogs/stylesheet.py veusz-1.14/dialogs/stylesheet.py --- veusz-1.10/dialogs/stylesheet.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/stylesheet.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,13 +16,12 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: stylesheet.py 1358 2010-08-14 16:40:46Z jeremysanders $ - import veusz.utils as utils import veusz.qtall as qt4 import veusz.document as document import veusz.setting as setting -from veusz.windows.treeeditwindow import TabbedFormatting, PropertyList +from veusz.windows.treeeditwindow import TabbedFormatting, PropertyList, \ + SettingsProxySingle from veuszdialog import VeuszDialog class StylesheetDialog(VeuszDialog): @@ -92,12 +91,13 @@ settings = current.VZsettings # update formatting properties - self.tabformat = TabbedFormatting(self.document, settings) + setnsproxy = SettingsProxySingle(self.document, settings) + self.tabformat = TabbedFormatting(self.document, setnsproxy) self.formattingGroup.layout().addWidget(self.tabformat) # update properties - self.properties = PropertyList(self.document, showsubsettings=False) - self.properties.updateProperties(settings, showformatting=False) + self.properties = PropertyList(self.document, showformatsettings=False) + self.properties.updateProperties(setnsproxy, showformatting=False) self.propertiesScrollArea.setWidget(self.properties) def slotSaveStyleSheet(self): diff -Nru veusz-1.10/dialogs/veuszdialog.py veusz-1.14/dialogs/veuszdialog.py --- veusz-1.10/dialogs/veuszdialog.py 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/dialogs/veuszdialog.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: veuszdialog.py 1358 2010-08-14 16:40:46Z jeremysanders $ - """Define a base dialog class cleans up self after being hidden.""" import os.path diff -Nru veusz-1.10/document/capture.py veusz-1.14/document/capture.py --- veusz-1.10/document/capture.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/capture.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: capture.py 1101 2009-11-08 17:10:07Z jeremysanders $ - import select import subprocess import os diff -Nru veusz-1.10/document/commandinterface.py veusz-1.14/document/commandinterface.py --- veusz-1.10/document/commandinterface.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/commandinterface.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,14 +19,13 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: commandinterface.py 1471 2010-12-10 22:18:24Z jeremysanders $ - """ Module supplies the command interface used in the program, and for external programs. """ import os.path +import traceback import veusz.qtall as qt4 import veusz.setting as setting @@ -37,6 +36,8 @@ import operations import dataset_histo import mime +import export +import readcsv class CommandInterface(qt4.QObject): """Class provides command interface.""" @@ -66,6 +67,7 @@ 'NodeType', 'ReloadData', 'Remove', + 'RemoveCustom', 'Rename', 'ResolveReference', 'Set', @@ -128,12 +130,16 @@ self.verbose = v def Add(self, widgettype, *args, **args_opt): - """Add a graph to the graph with the type given. + """Add a widget to the widget with the type given. optional argument: widget: widget path to place widget in The optional arguments are sent to construct the widget. + + If autoadd is True (the default), then any sub widgets + associated with the widgets are added automatically (e.g. axes + for graph widgets). """ at = self.currentwidget @@ -145,19 +151,58 @@ w = self.document.applyOperation(op) if self.verbose: - print "Added a graph of type '%s' (%s)" % (type, w.userdescription) + print "Added a widget of type '%s' (%s)" % (type, w.userdescription) return w.name - def AddCustom(self, name, ctype, val): + def AddCustom(self, ctype, name, val, mode='appendalways'): """Add a custom definition for evaluation of expressions. - - name is name of constant, or function(params) - ctype is constant or function - val is definition.""" + This can define a constant (can be in terms of other + constants), a function of 1 or more variables, or a function + imported from an external python module. + + ctype is "constant", "function" or "import". + + name is name of constant, or "function(x, y, ...)" or module + name. + + val is definition for constant or function (both are + _strings_), or is a list of symbols for a module (comma + separated items in a string). + + if mode is 'appendalways', the custom value is appended to the + end of the list even if there is one with the same name. If + mode is 'replace', it replaces any existing definition in the + same place in the list or is appended otherwise. If mode is + 'append', then an existing definition is deleted, and the new + one appended to the end. + """ + + if not isinstance(val, basestring): + raise RuntimeError, 'Value should be string' + if mode not in ('appendalways', 'append', 'replace'): + raise RuntimeError, 'Invalid mode' + if ctype not in ('constant', 'import', 'function'): + raise RuntimeError, 'Invalid type' vals = list( self.document.customs ) - vals.append( [name, ctype, val] ) + item = [ctype, name, val] + if mode == 'appendalways': + vals.append(item) + else: + # find any existing item + for i, (t, n, v) in enumerate(vals): + if n == name: + if mode == 'append': + del vals[i] + vals.append(item) + else: # replace + vals[i] = item + break + else: + # no existing item, so append + vals.append(item) + op = operations.OperationSetCustom(vals) self.document.applyOperation(op) @@ -240,17 +285,32 @@ if self.verbose: print "Removed widget '%s'" % name + def RemoveCustom(self, name): + """Removes a custom-defined constant, function or import.""" + vals = list( self.document.customs ) + for i, (t, n, v) in enumerate(vals): + if n == name: + del vals[i] + break + else: + raise ValueError, 'Custom variable not defined' + op = operations.OperationSetCustom(vals) + self.document.applyOperation(op) + def To(self, where): - """Change to a graph within the current graph.""" + """Change to a widget within the current widget. + + where is a path to the widget relative to the current widget + """ self.currentwidget = self.document.resolve(self.currentwidget, where) if self.verbose: - print "Changed to graph '%s'" % self.currentwidget.path + print "Changed to widget '%s'" % self.currentwidget.path def List(self, where='.'): - """List the contents of a graph.""" + """List the contents of a widget, by default the current widget.""" widget = self.document.resolve(self.currentwidget, where) children = widget.childnames @@ -630,27 +690,39 @@ return (dsnames, errors) - def ImportFileCSV(self, filename, readrows=False, prefix=None, + def ImportFileCSV(self, filename, + readrows=False, delimiter=',', textdelimiter='"', encoding='utf_8', - headerignore=0, - dsprefix='', dssuffix='', + headerignore=0, rowsignore=0, + blanksaredata=False, + numericlocale='en_US', + dateformat='YYYY-MM-DD|T|hh:mm:ss', + headermode='multi', + dsprefix='', dssuffix='', prefix=None, linked=False): """Read data from a comma separated file (CSV). Data are read from filename - If readrows is True, then data are read across rather than down + readrows: if true, data are read across rather than down + delimiter: character for delimiting data (usually ',') + textdelimiter: character surrounding text (usually '"') + encoding: encoding used in file + headerignore: number of lines to ignore after header text + rowsignore: number of rows to ignore at top of file + blanksaredata: treats blank lines in csv files as blank data values + numericlocale: format to use for reading numbers + dateformat: format for interpreting dates + headermode: 'multi': multiple headers allowed in file + '1st': first text found are headers + 'none': no headers, guess data and use default names + Dataset names are prepended and appended, by dsprefix and dssuffix, respectively (prefix is backware compatibility only, it adds an underscore relative to dsprefix) - delimiter is the character for delimiting data (usually ',') - textdelimiter is the character surrounding text (usually '"') - encoding is the encoding used in the file - headerignore is number of lines to ignore after header text - If linked is True the data are linked with the file.""" # backward compatibility @@ -660,13 +732,17 @@ # lookup filename realfilename = self.findFileOnImportPath(filename) - op = operations.OperationDataImportCSV( + params = readcsv.ParamsCSV( realfilename, readrows=readrows, delimiter=delimiter, textdelimiter=textdelimiter, encoding=encoding, - headerignore=headerignore, - prefix=dsprefix, suffix=dssuffix, - linked=linked) + headerignore=headerignore, rowsignore=rowsignore, + blanksaredata=blanksaredata, + numericlocale=numericlocale, dateformat=dateformat, + headermode=headermode, + dsprefix=dsprefix, dssuffix=dssuffix, + ) + op = operations.OperationDataImportCSV(params, linked=linked) dsnames = self.document.applyOperation(op) if self.verbose: @@ -717,8 +793,10 @@ **args) try: self.document.applyOperation(op) - except Exception, ex: - self.document.log("Error in plugin %s: %s" % (plugin, unicode(ex))) + except: + self.document.log("Error in plugin %s" % plugin) + exc = ''.join(traceback.format_exc()) + self.document.log(exc) def ReloadData(self): """Reload any linked datasets. @@ -749,7 +827,8 @@ range(self.document.getNumberPages()) ) def Export(self, filename, color=True, page=0, dpi=100, - antialias=True, quality=85, backcolor='#ffffff00'): + antialias=True, quality=85, backcolor='#ffffff00', + pdfdpi=150): """Export plot to filename. color is True or False if color is requested in output file @@ -759,12 +838,15 @@ quality is a quality parameter for jpeg output backcolor is the background color for bitmap files, which is a name or a #RRGGBBAA value (red, green, blue, alpha) + pdfdpi is the dpi to use when exporting eps or pdf files """ - self.document.export(filename, page, color=color, - dpi=dpi, antialias=antialias, - quality=quality, backcolor=backcolor) - + e = export.Export(self.document, filename, page, color=color, + bitmapdpi=dpi, antialias=antialias, + quality=quality, backcolor=backcolor, + pdfdpi=pdfdpi) + e.export() + def Rename(self, widget, newname): """Rename the widget with the path given to the new name. diff -Nru veusz-1.10/document/commandinterpreter.py veusz-1.14/document/commandinterpreter.py --- veusz-1.10/document/commandinterpreter.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/commandinterpreter.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,8 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: commandinterpreter.py 1313 2010-07-04 16:37:16Z jeremysanders $ - """ A module for the execution of user 'macro' code inside a special environment. That way the commandline can be used to interact with @@ -154,15 +152,21 @@ sys.stderr.write(l) else: - # execute the code + # block update signals from document while updating + self.document.suspendUpdates() + try: + # execute the code exec c in self.globals - except Exception: + except: # print out the backtrace to stderr i = sys.exc_info() backtrace = traceback.format_exception( *i ) for l in backtrace: - sys.stderr.write(l) + sys.stderr.write(l) + + # reenable documents + self.document.enableUpdates() # return output streams sys.stdout = temp_stdout @@ -171,15 +175,16 @@ def Load(self, filename): """Replace the document with a new one from the filename.""" - # FIXME: should update filename in main window - # This gives the document a __file__ variable so it - # knows what it is f = open(filename, 'rU') self.document.wipe() self.interface.To('/') oldfile = self.globals['__file__'] self.globals['__file__'] = os.path.abspath(filename) + + self.interface.importpath.append( + os.path.dirname(os.path.abspath(filename))) self.runFile(f) + self.interface.importpath.pop() self.globals['__file__'] = oldfile self.document.setModified() self.document.setModified(False) diff -Nru veusz-1.10/document/dataset_histo.py veusz-1.14/document/dataset_histo.py --- veusz-1.10/document/dataset_histo.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/dataset_histo.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: dataset_histo.py 1449 2010-11-22 09:26:58Z jeremysanders $ - import numpy as N from datasets import Dataset, simpleEvalExpression @@ -293,23 +291,23 @@ if self.outvalues != '': self.oldvaluesds = document.data.get(self.outvalues, None) - document.data[self.outvalues] = gen.getValueDataset() + document.setData(self.outvalues, gen.getValueDataset()) if self.outposns != '': self.oldposnsds = document.data.get(self.outposns, None) - document.data[self.outposns] = gen.getBinDataset() + document.setData(self.outposns, gen.getBinDataset()) def undo(self, document): """Undo creation of datasets.""" if self.oldposnsds is not None: if self.outposns != '': - document.data[self.outposns] = self.oldposnsds + document.setData(self.outposns, self.oldposnsds) else: - del document.data[self.outposns] + document.deleteData(self.outposns) if self.oldvaluesds is not None: if self.outvalues != '': - document.data[self.outvalues] = self.oldvaluesds + document.setData(self.outvalues, self.oldvaluesds) else: - del document.data[self.outvalues] + document.deleteData(self.outvalues) diff -Nru veusz-1.10/document/datasets.py veusz-1.14/document/datasets.py --- veusz-1.10/document/datasets.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/datasets.py 2011-11-22 20:23:31.000000000 +0000 @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2006 Jeremy S. Sanders # Email: Jeremy Sanders # @@ -16,8 +17,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: datasets.py 1471 2010-12-10 22:18:24Z jeremysanders $ - """Classes to represent datasets.""" import re @@ -29,6 +28,7 @@ import operations import readcsv +import veusz.qtall as qt4 import veusz.utils as utils import veusz.setting as setting import veusz.plugins as plugins @@ -95,7 +95,7 @@ # if not a dataset pass - # get indexes of invalid pounts + # get indexes of invalid points indexes = invalid.nonzero()[0].tolist() # no bad points: optimisation @@ -152,7 +152,7 @@ for name, ds in document.data.items(): if ds.linked == self: - del document.data[name] + document.deleteData(name) def _moveReadDatasets(self, tempdoc, document): """Move datasets from tempdoc to document if they do not exist @@ -162,10 +162,9 @@ for name, ds in tempdoc.data.items(): if name not in document.data: read.append(name) - document.data[name] = ds + document.setData(name, ds) ds.document = document ds.linked = self - document.setModified(True) return read def _reloadViaOperation(self, document, op): @@ -356,61 +355,34 @@ class LinkedCSVFile(LinkedFileBase): """A CSV file linked to datasets.""" - def __init__(self, filename, readrows=False, - delimiter=',', textdelimiter='"', - encoding='utf_8', - headerignore=0, - prefix='', suffix=''): + def __init__(self, params): """Read CSV data from filename - - Read across rather than down if readrows - Prepend prefix to dataset names if set. + params is a ParamsCSV object """ - - self.filename = filename - self.readrows = readrows - self.delimiter = delimiter - self.textdelimiter = textdelimiter - self.encoding = encoding - self.headerignore = headerignore - self.prefix = prefix - self.suffix = suffix + self.filename = params.filename + self.params = params def saveToFile(self, fileobj, relpath=None): """Save the link to the document file.""" - params = [repr(self._getSaveFilename(relpath)), - 'linked=True'] - if self.prefix: - params.append('dsprefix=' + repr(self.prefix)) - if self.suffix: - params.append('dssuffix=' + repr(self.suffix)) - if self.readrows: - params.append('readrows=True') - if self.encoding != 'utf_8': - params.append('encoding=' + repr(self.encoding)) - if self.delimiter != ',': - params.append('delimiter=' + repr(self.delimiter)) - if self.textdelimiter != '"': - params.append('textdelimiter=' + repr(self.textdelimiter)) - if self.headerignore > 0: - params.append('headerignore=' + repr(self.headerignore)) + paramsout = [repr(self._getSaveFilename(relpath)), + 'linked=True'] + + # add parameters which aren't defaults + for param, default in sorted(self.params.defaults.items()): + v = getattr(self.params, param) + if v != default: + paramsout.append('%s=%s' % (param, repr(v))) + + fileobj.write('ImportFileCSV(%s)\n' % (', '.join(paramsout))) - fileobj.write('ImportFileCSV(%s)\n' % (', '.join(params))) - def reloadLinks(self, document): """Reload any linked data from the CSV file.""" # again, this is messy as we have to make sure we don't # overwrite any non-linked data - op = operations.OperationDataImportCSV( - self.filename, readrows=self.readrows, - delimiter=self.delimiter, - textdelimiter=self.textdelimiter, - encoding=self.encoding, - headerignore=self.headerignore, - prefix=self.prefix, suffix=self.suffix ) + op = operations.OperationDataImportCSV(self.params) return self._reloadViaOperation(document, op) class LinkedFilePlugin(LinkedFileBase): @@ -461,13 +433,34 @@ # number of dimensions the dataset holds dimensions = 0 - datatype = 'numeric' + + # datatype is fundamental type of data + # displaytype is formatting suggestion for data + datatype = displaytype = 'numeric' + + # dataset type to show to user + dstype = 'Dataset' + + # list of columns in dataset (if any) columns = () + # use descriptions for columns column_descriptions = () # class for representing part of this dataset subsetclass = None + # whether this dataset's columns will change without updating the document's + # changeset + isstable = False + + def __init__(self, linked=None): + """Initialise common members.""" + # document member set when this dataset is set in document + self.document = None + + # file this dataset is linked to + self.linked = linked + def saveLinksToSavedDoc(self, fileobj, savedlinks, relpath=None): '''Save the link to the saved document, if this dataset is linked. @@ -489,13 +482,32 @@ return name raise ValueError('Could not find self in document.data') + def userSize(self): + """Return dimensions of dataset for user.""" + return "" + + def userPreview(self): + """Return a small preview of the dataset for the user, e.g. + 1, 2, 3, ..., 4, 5, 6.""" + return None + def description(self, showlinked=True): """Get description of database.""" return "" - def convertToDataItem(self, val): - """Return a value cast to this dataset data type.""" - return None + def uiConvertToDataItem(self, val): + """Return a value cast to this dataset data type. + We assume here it is a float, so override if not + """ + if isinstance(val, basestring) or isinstance(val, qt4.QString): + val, ok = setting.uilocale.toDouble(val) + if ok: return val + raise ValueError, "Invalid floating point number" + return float(val) + + def uiDataItemToQVariant(self, val): + """Return val converted to QVariant.""" + return qt4.QVariant(float(val)) def _getItemHelper(self, key): """Help get arguments to constructor.""" @@ -549,12 +561,22 @@ """Is it possible to rename this dataset?""" return self.linked is None + def datasetAsText(self, fmt='%g', join='\t'): + """Return dataset as text (for use by user).""" + return '' + class Dataset2D(DatasetBase): '''Represents a two-dimensional dataset.''' # number of dimensions the dataset holds dimensions = 2 + # dataset type + dstype = '2D' + + # the dataset is recreated if its data changes + isstable = True + def __init__(self, data, xrange=None, yrange=None): '''Create a two dimensional dataset based on data. @@ -563,17 +585,22 @@ yrange: a tuple of (start, end) coordinates for y ''' - self.document = None - self.linked = None - self.data = _convertNumpy(data) - - self.xrange = xrange - self.yrange = yrange + DatasetBase.__init__(self) - if not xrange: - self.xrange = (0, data.shape[1]) - if not yrange: - self.yrange = (0, data.shape[0]) + # we don't want these set if a inheriting class uses properties instead + if not hasattr(self, 'data'): + try: + self.data = _convertNumpy(data) + self.xrange = (0, self.data.shape[1]) + self.yrange = (0, self.data.shape[0]) + + if xrange: + self.xrange = xrange + if yrange: + self.yrange = yrange + except AttributeError: + # for some reason hasattr doesn't always work + pass def indexToPoint(self, xidx, yidx): """Convert a set of indices to pixels in integers to @@ -598,31 +625,63 @@ fileobj.write("ImportString2D(%s, '''\n" % repr(name)) fileobj.write("xrange %e %e\n" % self.xrange) fileobj.write("yrange %e %e\n" % self.yrange) + fileobj.write(self.datasetAsText(fmt='%e', join=' ')) + fileobj.write("''')\n") + + def datasetAsText(self, fmt='%g', join='\t'): + """Return dataset as text. + fmt is the format specifier to use + join is the string to separate the items + """ + format = ((fmt+join) * (self.data.shape[1]-1)) + fmt + '\n' # write rows backwards, so lowest y comes first + lines = [] for row in self.data[::-1]: - s = ('%e ' * len(row)) % tuple(row) - fileobj.write("%s\n" % (s[:-1],)) - - fileobj.write("''')\n") + line = format % tuple(row) + lines.append(line) + return ''.join(lines) + + def userSize(self): + """Return dimensions of dataset for user.""" + return u'%i×%i' % self.data.shape + + def userPreview(self): + """Return preview of data.""" + return dsPreviewHelper(self.data.flatten()) def description(self, showlinked=True): """Get description of dataset.""" text = self.name() - text += ' (%ix%i)' % self.data.shape + text += u' (%i×%i)' % self.data.shape text += ', x=%g->%g' % tuple(self.xrange) text += ', y=%g->%g' % tuple(self.yrange) if self.linked and showlinked: text += ', linked to %s' % self.linked.filename return text - def convertToDataItem(self, val): - """Return a value cast to this dataset data type.""" - return float(val) - def returnCopy(self): return Dataset2D( N.array(self.data), self.xrange, self.yrange) +def dsPreviewHelper(d): + """Get preview of numpy data d.""" + if d.shape[0] <= 6: + line1 = ', '.join( ['%.3g' % x for x in d] ) + else: + line1 = ', '.join( ['%.3g' % x for x in d[:3]] + + [ '...' ] + + ['%.3g' % x for x in d[-3:]] ) + + try: + line2 = 'mean: %.3g, min: %.3g, max: %.3g' % ( + N.nansum(d) / N.isfinite(d).sum(), + N.nanmin(d), + N.nanmax(d)) + except (ValueError, ZeroDivisionError): + # nanXXX returns error if no valid data points + return line1 + return line1 + '\n' + line2 + class Dataset(DatasetBase): '''Represents a dataset.''' @@ -630,6 +689,10 @@ dimensions = 1 columns = ('data', 'serr', 'nerr', 'perr') column_descriptions = ('Data', 'Sym. errors', 'Neg. errors', 'Pos. errors') + dstype = '1D' + + # the dataset is recreated if its data changes + isstable = True def __init__(self, data = None, serr = None, nerr = None, perr = None, linked = None): @@ -639,6 +702,8 @@ linked optionally specifies a LinkedFile to link the dataset to ''' + DatasetBase.__init__(self, linked=linked) + # convert data to numpy arrays data = _convertNumpy(data) serr = _convertNumpyAbs(serr) @@ -652,13 +717,25 @@ raise DatasetException('Lengths of error data do not match data') # finally assign data - self.document = None self._invalidpoints = None - self.linked = linked - self.data = data - self.serr = serr - self.perr = perr - self.nerr = nerr + + try: + if not hasattr(self, 'data'): + self.data = data + self.serr = serr + self.perr = perr + self.nerr = nerr + except AttributeError: + # we don't want these set if a inheriting class uses properties instead + pass + + def userSize(self): + """Size of dataset.""" + return str( self.data.shape[0] ) + + def userPreview(self): + """Preview of data.""" + return dsPreviewHelper(self.data) def description(self, showlinked=True): """Get description of dataset.""" @@ -741,7 +818,8 @@ for x in (self.serr, self.nerr, self.perr): assert x is None or x.shape == s - self.document.setModified(True) + # tell the document that we've changed + self.document.modifiedData(self) def saveToFile(self, fileobj, name): '''Save data to file. @@ -752,32 +830,35 @@ return # build up descriptor - datasets = [self.data] - descriptor = datasetNameToDescriptorName(name) + '(numeric)' if self.serr is not None: descriptor += ',+-' - datasets.append(self.serr) if self.perr is not None: descriptor += ',+' - datasets.append(self.perr) if self.nerr is not None: descriptor += ',-' - datasets.append(self.nerr) fileobj.write( "ImportString(%s,'''\n" % repr(descriptor) ) + fileobj.write( self.datasetAsText(fmt='%e', join=' ') ) + fileobj.write( "''')\n" ) - # write line line-by-line - format = '%e ' * len(datasets) - format = format[:-1] + '\n' - for line in izip( *datasets ): - fileobj.write( format % line ) + def datasetAsText(self, fmt='%g', join='\t'): + """Return data as text.""" - fileobj.write( "''')\n" ) + # work out which columns to write + cols = [] + for c in (self.data, self.serr, self.perr, self.nerr): + if c is not None: + cols.append(c) - def convertToDataItem(self, val): - """Return a value cast to this dataset data type.""" - return float(val) + # format statement + format = (fmt + join) * (len(cols)-1) + fmt + '\n' + + # do the conversion + lines = [] + for line in izip(*cols): + lines.append( format % line ) + return ''.join(lines) def deleteRows(self, row, numrows): """Delete numrows rows starting from row. @@ -790,6 +871,7 @@ retn[col] = coldata[row:row+numrows] setattr(self, col, N.delete( coldata, N.s_[row:row+numrows] )) + self.document.modifiedData(self) return retn def insertRows(self, row, numrows, rowdata): @@ -804,6 +886,8 @@ if coldata is not None: setattr(self, col, N.insert(coldata, [row]*numrows, data)) + self.document.modifiedData(self) + def returnCopy(self): """Return version of dataset with no linking.""" return Dataset(data = _copyOrNone(self.data), @@ -811,19 +895,80 @@ perr = _copyOrNone(self.perr), nerr = _copyOrNone(self.nerr)) +class DatasetDateTime(Dataset): + """Dataset holding dates and times.""" + + columns = ('data',) + column_descriptions = ('Data',) + isstable = True + + dstype = 'Date' + displaytype = 'date' + + def __init__(self, data=None, linked=None): + Dataset.__init__(self, data=data, linked=linked) + + def description(self, showlinked=True): + text = '%s (%i date/times)' % (self.name(), len(self.data)) + if self.linked and showlinked: + text += ', linked to %s' % self.linked.filename + return text + + def uiConvertToDataItem(self, val): + """Return a value cast to this dataset data type.""" + if isinstance(val, basestring) or isinstance(val, qt4.QString): + v = utils.dateStringToDate( unicode(val) ) + if not N.isfinite(v): + try: + v = float(val) + except ValueError: + pass + return v + else: + return N.nan + + def uiDataItemToQVariant(self, val): + """Return val converted to QVariant.""" + return qt4.QVariant(utils.dateFloatToString(val)) + + def saveToFile(self, fileobj, name): + '''Save data to file. + ''' + + if self.linked is not None: + # do not save if linked to a file + return + + descriptor = datasetNameToDescriptorName(name) + '(date)' + fileobj.write( "ImportString(%s,'''\n" % repr(descriptor) ) + fileobj.write( self.datasetAsText() ) + fileobj.write( "''')\n" ) + + def datasetAsText(self, fmt=None, join=None): + """Return data as text.""" + lines = [ utils.dateFloatToString(val) for val in self.data ] + lines.append('') + return '\n'.join(lines) + + def returnCopy(self): + """Returns version of dataset with no linking.""" + return DatasetDateTime(data=N.array(self.data)) + class DatasetText(DatasetBase): """Represents a text dataset: holding an array of strings.""" dimensions = 1 - datatype = 'text' + datatype = displaytype = 'text' columns = ('data',) column_descriptions = ('Data',) + dstype = 'Text' + isstable = True def __init__(self, data=None, linked=None): """Initialise dataset with data given. Data are a list of strings.""" + DatasetBase.__init__(self, linked=linked) self.data = list(data) - self.linked = linked def description(self, showlinked=True): text = '%s (%i items)' % (self.name(), len(self.data)) @@ -831,18 +976,26 @@ text += ', linked to %s' % self.linked.filename return text + def userSize(self): + """Size of dataset.""" + return str( len(self.data) ) + def changeValues(self, type, vals): if type == 'data': self.data = list(vals) else: raise ValueError, 'type does not contain an allowed value' - self.document.setModified(True) + self.document.modifiedData(self) - def convertToDataItem(self, val): + def uiConvertToDataItem(self, val): """Return a value cast to this dataset data type.""" return unicode(val) + def uiDataItemToQVariant(self, val): + """Return val converted to QVariant.""" + return qt4.QVariant(unicode(val)) + def saveToFile(self, fileobj, name): '''Save data to file. ''' @@ -859,12 +1012,20 @@ fileobj.write(r) fileobj.write( "''')\n" ) + def datasetAsText(self, fmt=None, join=None): + """Return data as text.""" + lines = list(self.data) + lines.append('') + return '\n'.join(lines) + def deleteRows(self, row, numrows): """Delete numrows rows starting from row. Returns deleted rows as a dict of {column:data, ...} """ retn = {'data': self.data[row:row+numrows]} del self.data[row:row+numrows] + + self.document.modifiedData(self) return retn def insertRows(self, row, numrows, rowdata): @@ -877,6 +1038,8 @@ for d in insdata[::-1]: self.data.insert(row, d) + self.document.modifiedData(self) + def returnCopy(self): """Returns version of dataset with no linking.""" return DatasetText(self.data) @@ -976,6 +1139,8 @@ class DatasetExpression(Dataset): """A dataset which is linked to another dataset by an expression.""" + dstype = 'Expression' + def __init__(self, data=None, serr=None, nerr=None, perr=None, parametric=None): """Initialise the dataset with the expressions given. @@ -983,9 +1148,7 @@ parametric is option and can be (minval, maxval, steps) or None """ - self.document = None - self.linked = None - self._invalidpoints = None + Dataset.__init__(self, data=[]) # store the expressions to use to generate the dataset self.expr = {} @@ -1160,11 +1323,17 @@ class DatasetRange(Dataset): """Dataset consisting of a range of values e.g. 1 to 10 in 10 steps.""" + dstype = 'Range' + isstable = True + def __init__(self, numsteps, data, serr=None, perr=None, nerr=None): """Construct dataset. numsteps: number of steps in range data, serr, perr and nerr are tuples containing (start, stop) values.""" + + Dataset.__init__(self, data=[]) + self.range_data = data self.range_serr = serr self.range_perr = perr @@ -1184,9 +1353,17 @@ vals = None setattr(self, name, vals) - self.document = None - self.linked = None - self._invalidpoints = None + def __getitem__(self, key): + """Return a dataset based on this dataset + + We override this from DatasetBase as it would return a + DatsetExpression otherwise, not chopped sets of data. + """ + return Dataset(**self._getItemHelper(key)) + + def userSize(self): + """Size of dataset.""" + return str( self.numsteps ) def saveToFile(self, fileobj, name): """Save dataset to file.""" @@ -1226,11 +1403,12 @@ (i.e. steps are not all multiples of some mininimum) """ - uniquesorted = N.unique1d(data) + uniquesorted = N.unique(data) + sigfactor = (uniquesorted[-1]-uniquesorted[0])*1e-13 # differences between elements - deltas = N.unique1d( N.ediff1d(uniquesorted) ) + deltas = N.unique( N.ediff1d(uniquesorted) ) mindelta = None for delta in deltas: @@ -1251,14 +1429,14 @@ class Dataset2DXYZExpression(Dataset2D): '''A 2d dataset with expressions for x, y and z.''' + dstype = '2D XYZ' + def __init__(self, exprx, expry, exprz): """Initialise dataset. Parameters are mathematical expressions based on datasets.""" + Dataset2D.__init__(self, []) - self.document = None - self.linked = None - self._invalidpoints = None self.lastchangeset = -1 self.cacheddata = None @@ -1308,7 +1486,7 @@ except Exception, e: raise DatasetExpressionException( "Error evaluating expression: %s\n" - "Error: %s" % (expr, str(e)) ) + "Error: %s" % (expr, unicode(e)) ) minx, maxx, stepx, stepsx = getSpacing(evaluated['exprx']) miny, maxy, stepy, stepsy = getSpacing(evaluated['expry']) @@ -1323,7 +1501,12 @@ ypts = ((1./stepy)*(evaluated['expry']-miny)).astype('int32') # this is ugly - is this really the way to do it? - self.cacheddata.flat [ xpts + ypts*stepsx ] = evaluated['exprz'] + try: + self.cacheddata.flat [ xpts + ypts*stepsx ] = evaluated['exprz'] + except Exception, e: + raise DatasetExpressionException( + "Shape mismatch when constructing dataset\n" + "Error: %s" % unicode(e) ) # update changeset self.lastchangeset = self.document.changeset @@ -1384,10 +1567,13 @@ class Dataset2DExpression(Dataset2D): """Evaluate an expression of 2d datasets.""" + dstype = '2D Expr' + def __init__(self, expr): """Create 2d expression dataset.""" - self.document = None - self.linked = None + + Dataset2D.__init__(self, None) + self.expr = expr self.lastchangeset = -1 self.cachedexpr = None @@ -1490,12 +1676,13 @@ """Return linking information.""" return 'Linked 2D expression: %s' % self.expr - class Dataset2DXYFunc(Dataset2D): """Given a range of x and y, this is a dataset which is a function of this. """ + dstype = '2D f(x,y)' + def __init__(self, xstep, ystep, expr): """Create 2d dataset: @@ -1504,9 +1691,7 @@ expr: expression of x and y """ - self.document = None - self.linked = None - self._invalidpoints = None + Dataset2D.__init__(self, []) self.xstep = xstep self.ystep = ystep @@ -1625,6 +1810,11 @@ self.pluginmanager.saveToFile(fileobj) return + @property + def dstype(self): + """Return type of plugin.""" + return self.pluginmanager.plugin.name + class Dataset1DPlugin(_DatasetPlugin, Dataset): """Return 1D dataset from a plugin.""" @@ -1635,6 +1825,18 @@ def __getitem__(self, key): return Dataset(**self._getItemHelper(key)) + def userSize(self): + """Size of dataset.""" + return str( self.data.shape[0] ) + + def __getitem__(self, key): + """Return a dataset based on this dataset + + We override this from DatasetBase as it would return a + DatsetExpression otherwise, not chopped sets of data. + """ + return Dataset(**self._getItemHelper(key)) + # parent class sets these attributes, so override setattr to do nothing data = property( lambda self: self.getPluginData('data'), lambda self, val: None ) @@ -1674,3 +1876,16 @@ data = property( lambda self: self.getPluginData('data'), lambda self, val: None ) + +class DatasetDateTimePlugin(_DatasetPlugin, DatasetDateTime): + """Return date dataset from plugin.""" + + def __init__(self, manager, ds): + _DatasetPlugin.__init__(self, manager, ds) + DatasetDateTime.__init__(self, []) + + def __getitem__(self, key): + return DatasetDateTime(self.data[key]) + + data = property( lambda self: self.getPluginData('data'), + lambda self, val: None ) diff -Nru veusz-1.10/document/doc.py veusz-1.14/document/doc.py --- veusz-1.10/document/doc.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/doc.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,16 +19,12 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: doc.py 1466 2010-12-02 13:58:20Z jeremysanders $ - """A class to represent Veusz documents, with dataset classes.""" import os.path -import time -import random -import math import re import traceback +import datetime from collections import defaultdict import numpy as N @@ -37,19 +33,11 @@ import widgetfactory import datasets +import painthelper import veusz.utils as utils import veusz.setting as setting -try: - import emf_export - hasemf = True -except ImportError: - hasemf = False - -import svg_export -import selftest_export - # python identifier identifier_re = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$') # for splitting @@ -94,8 +82,20 @@ Document.loadPlugins() Document.pluginsloaded = True - self.changeset = 0 # increased when the document changes - self.suspendupdates = False # if True then do not notify listeners of updates + # change tracking of document as a whole + self.changeset = 0 # increased when the document changes + + # change tracking of datasets + self.datachangeset = 0 # increased whan any dataset changes + self.datachangesets = dict() # each ds has an associated change set + + # if set, do not notify listeners of updates + # wait under enableUpdates + self.suspendupdates = [] + + # default document locale + self.locale = qt4.QLocale() + self.clearHistory() self.wipe() @@ -105,9 +105,13 @@ # store custom functions and constants # consists of tuples of (name, type, value) # type is constant or function + # we use this format to preserve evaluation order self.customs = [] self.updateEvalContext() + # copy default colormaps + self.colormaps = dict(utils.defaultcolormaps) + def wipe(self): """Wipe out any stored data.""" self.data = {} @@ -124,16 +128,17 @@ self.historyredo = [] def suspendUpdates(self): - """Holds sending update messages. This speeds up modification of the document.""" - assert not self.suspendupdates - self.suspendchangeset = self.changeset - self.suspendupdates = True + """Holds sending update messages. + This speeds up modification of the document and prevents the document + from being updated on the screen.""" + self.suspendupdates.append(self.changeset) def enableUpdates(self): """Reenables document updates.""" - assert self.suspendupdates - self.suspendupdates = False - if self.suspendchangeset != self.changeset: + changeset = self.suspendupdates.pop() + if len(self.suspendupdates) == 0 and changeset != self.changeset: + # bump this up as some watchers might ignore this otherwise + self.changeset += 1 self.setModified() def makeDefaultDoc(self): @@ -153,9 +158,18 @@ Operations represent atomic actions which can be done to the document and undone. + + Updates are suspended during the operation. """ - - retn = operation.do(self) + + self.suspendUpdates() + try: + retn = operation.do(self) + self.changeset += 1 + except: + self.enableUpdates() + raise + self.enableUpdates() if self.historybatch: # in batch mode, create an OperationMultiple for all changes @@ -165,7 +179,6 @@ self.historyundo = self.historyundo[-9:] + [operation] self.historyredo = [] - self.setModified() return retn def batchHistory(self, batch): @@ -185,11 +198,17 @@ def undoOperation(self): """Undo the previous operation.""" - + operation = self.historyundo.pop() - operation.undo(self) + self.suspendUpdates() + try: + operation.undo(self) + self.changeset += 1 + except: + self.enableUpdates() + raise + self.enableUpdates() self.historyredo.append(operation) - self.setModified() def canUndo(self): """Returns True if previous operation can be removed.""" @@ -197,11 +216,8 @@ def redoOperation(self): """Redo undone operations.""" - operation = self.historyredo.pop() - operation.do(self) - self.historyundo.append(operation) - self.setModified() + return self.applyOperation(operation) def canRedo(self): """Returns True if previous operation can be redone.""" @@ -254,25 +270,52 @@ """Set data to val, with symmetric or negative and positive errors.""" self.data[name] = dataset dataset.document = self + + # update the change tracking + cs = self.datachangesets.get(name, 0) + self.datachangesets[name] = cs + 1 + self.datachangeset += 1 self.setModified() + + def deleteData(self, name): + """Remove a dataset""" + if name in self.data: + del self.data[name] + + # don't remove the changeset tracker, in case this action is later undone + self.datachangesets[name] += 1 + self.datachangeset += 1 + self.setModified() - def getLinkedFiles(self): - """Get a list of LinkedFile objects used by the document.""" + def modifiedData(self, dataset): + """The named dataset was modified""" + for name, ds in self.data.iteritems(): + if ds is dataset: + self.datachangesets[name] += 1 + self.datachangeset += 1 + self.setModified() + + def getLinkedFiles(self, filenames=None): + """Get a list of LinkedFile objects used by the document. + if filenames is a set, only get the objects with filenames given + """ links = set() for ds in self.data.itervalues(): - if ds.linked: + if ds.linked and (filenames is None or + ds.linked.filename in filenames): links.add(ds.linked) return list(links) - def reloadLinkedDatasets(self): + def reloadLinkedDatasets(self, filenames=None): """Reload linked datasets from their files. + If filenames is a set(), only reload from these filenames Returns a tuple of - List of datasets read - Dict of tuples containing dataset names and number of errors """ - links = self.getLinkedFiles() + links = self.getLinkedFiles(filenames=filenames) read = [] errors = {} @@ -305,6 +348,8 @@ d = self.data[oldname] del self.data[oldname] self.data[newname] = d + # transfer change set to new name + self.datachangesets[newname] = self.datachangesets[oldname] self.setModified() @@ -326,7 +371,7 @@ self.modified = ismodified self.changeset += 1 - if not self.suspendupdates: + if len(self.suspendupdates) == 0: self.emit( qt4.SIGNAL("sigModified"), ismodified ) def isModified(self): @@ -334,33 +379,36 @@ return self.modified @classmethod - def loadPlugins(kls): + def loadPlugins(kls, pluginlist=None): """Load plugins and catch exceptions.""" - for plugin in setting.settingdb.get('plugins', []): + if pluginlist is None: + pluginlist = setting.settingdb.get('plugins', []) + + for plugin in pluginlist: try: execfile(plugin, dict()) except Exception, ex: err = ('Error loading plugin ' + plugin + '\n\n' + traceback.format_exc()) - qt4.QMessageBox.critical(None, "Error loading plugin", - err) + qt4.QMessageBox.critical(None, "Error loading plugin", err) def printTo(self, printer, pages, scaling = 1., dpi = None, antialias = False): """Print onto printing device.""" - painter = Painter(scaling=scaling, dpi=dpi) - - painter.begin( printer ) - painter.setRenderHint(qt4.QPainter.Antialiasing, - antialias) - painter.setRenderHint(qt4.QPainter.TextAntialiasing, - antialias) - + dpi = (printer.logicalDpiX(), printer.logicalDpiY()) + painter = qt4.QPainter(printer) + if antialias: + painter.setRenderHint(qt4.QPainter.Antialiasing, True) + painter.setRenderHint(qt4.QPainter.TextAntialiasing, True) + # This all assumes that only pages can go into the root widget num = len(pages) for count, page in enumerate(pages): - self.basewidget.draw(painter, page) + size = self.pageSize(page, dpi=dpi) + helper = painthelper.PaintHelper(size, dpi=dpi, directpaint=painter) + self.paintTo(helper, page) + painter.restore() # start new pages between each page if count < num-1: @@ -368,13 +416,9 @@ painter.end() - def paintTo(self, painter, page, scaling = 1., dpi = None): - """Paint page specified to the painter.""" - - painter.veusz_scaling = scaling - if dpi is not None: - painter.veusz_pixperpt = dpi / 72. - self.basewidget.draw(painter, page) + def paintTo(self, painthelper, page): + """Paint page specified to the paint helper.""" + self.basewidget.draw(painthelper, page) def getNumberPages(self): """Return the number of pages in the document.""" @@ -388,14 +432,9 @@ """Write a header to a saved file of type.""" fileobj.write('# Veusz %s (version %s)\n' % (type, utils.version())) - try: - fileobj.write('# User: %s\n' % os.environ['LOGNAME'] ) - except KeyError: - pass - fileobj.write('# Date: %s\n\n' % - time.strftime("%a, %d %b %Y %H:%M:%S +0000", - time.gmtime()) ) - + fileobj.write('# Saved at %s\n\n' % + datetime.datetime.utcnow().isoformat()) + def saveCustomDefinitions(self, fileobj): """Save custom constants and functions.""" @@ -447,242 +486,31 @@ fileobj.write( stylesheet.saveText(True, rootname='') ) - def _exportBitmap(self, filename, pagenumber, dpi=100, antialias=True, - quality=85, backcolor='#ffffff00'): - """Export the pagenumber to the requested bitmap filename.""" - - # firstly have to convert dpi to image size - # have to use a temporary bitmap first - tmp = qt4.QPixmap(1, 1) - tmppainter = Painter(tmp) - realdpi = tmppainter.device().logicalDpiY() - width, height = self.basewidget.getSize(tmppainter) - scaling = dpi/float(realdpi) - tmppainter.end() - del tmp, tmppainter - - # work out format - format = os.path.splitext(filename)[1] - if not format: - format = '.png' - # str is required as unicode not supported - format = str(format[1:].lower()) - - # create real output image - pixmap = qt4.QPixmap(int(width*scaling), int(height*scaling)) - backqcolor = utils.extendedColorToQColor(backcolor) - if format != 'png': - # not transparent - backqcolor.setAlpha(255) - pixmap.fill(backqcolor) - - # paint to the image - painter = Painter(pixmap) - painter.setRenderHint(qt4.QPainter.Antialiasing, - antialias) - painter.setRenderHint(qt4.QPainter.TextAntialiasing, - antialias) - self.paintTo(painter, pagenumber, scaling=scaling, dpi=realdpi) - painter.end() - - # write image to disk - writer = qt4.QImageWriter() - writer.setFormat(qt4.QByteArray(format)) - - writer.setFileName(filename) - - if format == 'png': - # min quality for png as it makes no difference to output - # and makes file size smaller - writer.setQuality(0) + def _pagedocsize(self, widget, dpi, scaling, integer): + """Helper for page or doc size.""" + if dpi is None: + p = qt4.QPixmap(1, 1) + dpi = (p.logicalDpiX(), p.logicalDpiY()) + helper = painthelper.PaintHelper( (1,1), dpi=dpi, scaling=scaling ) + w = widget.settings.get('width').convert(helper) + h = widget.settings.get('height').convert(helper) + if integer: + return int(w), int(h) else: - writer.setQuality(quality) - - writer.write( pixmap.toImage() ) - - def _exportPS(self, filename, page, color=True): - """Postscript or eps format.""" - - ext = os.path.splitext(filename)[1] + return w, h - printer = qt4.QPrinter() - printer.setFullPage(True) + def pageSize(self, pagenum, dpi=None, scaling=1., integer=True): + """Get the size of a particular page in pixels. - # set printer parameters - printer.setColorMode( (qt4.QPrinter.GrayScale, qt4.QPrinter.Color)[color] ) - - if ext == '.pdf': - f = qt4.QPrinter.PdfFormat - else: - f = qt4.QPrinter.PostScriptFormat - printer.setOutputFormat(f) - printer.setOutputFileName(filename) - printer.setCreator('Veusz %s' % utils.version()) - - # draw the page - printer.newPage() - painter = Painter(printer) - width, height = self.basewidget.getSize(painter) - self.basewidget.draw(painter, page) - painter.end() - - # fixup eps/pdf file - yuck HACK! - hope qt gets fixed - # this makes the bounding box correct - if ext == '.eps' or ext == '.pdf': - # copy eps to a temporary file - tmpfile = "%s.tmp.%i" % (filename, random.randint(0,1000000)) - fout = open(tmpfile, 'wb') - fin = open(filename, 'rb') - - if ext == '.eps': - # adjust bounding box - for line in fin: - if line[:14] == '%%BoundingBox:': - # replace bounding box line by calculated one - parts = line.split() - widthfactor = float(parts[3]) / printer.width() - origheight = float(parts[4]) - line = "%s %i %i %i %i\n" % ( - parts[0], 0, - int(math.floor(origheight-widthfactor*height)), - int(math.ceil(widthfactor*width)), - int(math.ceil(origheight)) ) - fout.write(line) - - elif ext == '.pdf': - # change pdf bounding box and correct pdf index - text = fin.read() - text = utils.scalePDFMediaBox(text, - printer.width(), - width, height) - text = utils.fixupPDFIndices(text) - fout.write(text) - - fout.close() - fin.close() - os.remove(filename) - os.rename(tmpfile, filename) - - def _getDocSize(self, dpi): - """Get size of document in pixels given dpi.""" - pixmap = qt4.QPixmap(1, 1) - painter = Painter(pixmap, scaling=1., dpi=dpi) - width, height = self.basewidget.getSize(painter) - painter.end() - return width, height - - def _exportSVG(self, filename, page): - """Export document as SVG""" - - dpi = 90. - width, height = self._getDocSize(dpi) - - if qt4.PYQT_VERSION >= 0x40600: - # custom paint devices don't work in old PyQt versions - - f = open(filename, 'w') - paintdev = svg_export.SVGPaintDevice(f, width/dpi, height/dpi) - painter = Painter(paintdev) - self.basewidget.draw(painter, page) - painter.end() - f.close() - - else: - # use built-in svg generation, which doesn't work very well - # (no clipping, font size problems) - import PyQt4.QtSvg - - # actually paint the image - gen = PyQt4.QtSvg.QSvgGenerator() - gen.setFileName(filename) - gen.setResolution(dpi) - gen.setSize( qt4.QSize(int(width), int(height)) ) - painter = Painter(gen) - self.basewidget.draw(painter, page) - painter.end() - - def _exportSelfTest(self, filename, page): - """Export document for testing""" - - dpi = 90. - width, height = self._getDocSize(dpi) - - f = open(filename, 'w') - paintdev = selftest_export.SelfTestPaintDevice(f, width/dpi, height/dpi) - painter = Painter(paintdev) - self.basewidget.draw(painter, page) - painter.end() - f.close() - - def _exportPIC(self, filename, page): - """Export document as SVG""" - - pic = qt4.QPicture() - painter = Painter(pic) - self.basewidget.draw( painter, page ) - painter.end() - pic.save(filename) - - def _exportEMF(self, filename, page): - """Export document as EMF.""" - - dpi = 90. - width, height = self._getDocSize(dpi) - - paintdev = emf_export.EMFPaintDevice(width/dpi, height/dpi, - dpi=dpi) - painter = Painter(paintdev) - self.basewidget.draw( painter, page ) - painter.end() - paintdev.paintEngine().saveFile(filename) - - def export(self, filename, pagenumber, color=True, dpi=100, - antialias=True, quality=85, backcolor='#ffffff00'): - """Export the figure to the filename.""" - - ext = os.path.splitext(filename)[1] - - if ext in ('.eps', '.pdf'): - self._exportPS(filename, pagenumber, color=color) - - elif ext in ('.png', '.jpg', '.jpeg', '.bmp'): - self._exportBitmap(filename, pagenumber, dpi=dpi, - antialias=antialias, quality=quality, - backcolor=backcolor) - - elif ext == '.svg': - self._exportSVG(filename, pagenumber) - - elif ext == '.selftest': - self._exportSelfTest(filename, pagenumber) - - elif ext == '.pic': - self._exportPIC(filename, pagenumber) - - elif ext == '.emf' and hasemf: - self._exportEMF(filename, pagenumber) - - else: - raise RuntimeError, "File type '%s' not supported" % ext - - def getExportFormats(self): - """Get list of export formats: - [ - (['ext'], 'Filename type'), - ... - ] - """ - formats = [(["eps"], "Encapsulated Postscript"), - (["png"], "Portable Network Graphics"), - (["jpg"], "Jpeg bitmap"), - (["bmp"], "Windows bitmap"), - (["pdf"], "Portable Document Format"), - (["svg"], "Scalable Vector Graphics"), - #(["pic"], "QT Pic format"), - ] - if hasemf: - formats.append( (["emf"], "Windows Enhanced Metafile") ) - return formats + If dpi is None, use the default Qt screen dpi + Use dpi if given.""" + return self._pagedocsize(self.basewidget.getPage(pagenum), + dpi=dpi, scaling=scaling, integer=integer) + + def docSize(self, dpi=None, scaling=1., integer=True): + """Get size for document.""" + return self._pagedocsize(self.basewidget, + dpi=dpi, scaling=scaling, integer=integer) def resolveItem(self, fromwidget, where): """Resolve item relative to fromwidget. @@ -902,6 +730,13 @@ else: raise ValueError, 'Invalid custom type' + def customDict(self): + """Return a dictionary mapping custom names to (idx, type, value).""" + retn = {} + for i, (ctype, name, val) in enumerate(self.customs): + retn[name] = (i, ctype, val) + return retn + def evalDatasetExpression(self, expr, part='data'): """Return results of evaluating a 1D dataset expression. part is 'data', 'serr', 'perr' or 'nerr' - these are the @@ -909,22 +744,44 @@ """ return datasets.simpleEvalExpression(self, expr, part=part) -class Painter(qt4.QPainter): - """A painter which allows the program to know which widget it is - currently drawing.""" - - def __init__(self, *args, **argsv): - qt4.QPainter.__init__(self, *args) - - self.veusz_scaling = argsv.get('scaling', 1.) - if 'dpi' in argsv and argsv['dpi'] is not None: - self.veusz_pixperpt = argsv['dpi'] / 72. - - def beginPaintingWidget(self, widget, bounds): - """Keep track of the widget currently being painted.""" - pass - - def endPaintingWidget(self): - """Widget is now finished.""" - pass + def walkNodes(self, tocall, root=None, + nodetypes=('widget', 'setting', 'settings'), + _path=None): + """Walk the widget/settings/setting nodes in the document. + For each one call tocall(path, node). + nodetypes is tuple of possible node types + """ + if root is None: + root = self.basewidget + if _path is None: + _path = root.path + + if root.nodetype in nodetypes: + tocall(_path, root) + + if root.nodetype == 'widget': + # get rid of // at start of path + if _path == '/': + _path = '' + + # do the widget's children + for w in root.children: + self.walkNodes(tocall, root=w, nodetypes=nodetypes, + _path = _path + '/' + w.name) + # then do the widget's settings + self.walkNodes(tocall, root=root.settings, + nodetypes=nodetypes, _path=_path) + elif root.nodetype == 'settings': + # do the settings of the settings + for name, s in sorted(root.setdict.iteritems()): + self.walkNodes(tocall, root=s, nodetypes=nodetypes, + _path = _path + '/' + s.name) + # elif root.nodetype == 'setting': pass + + def getColormap(self, name, invert): + """Get colormap with name given (returning grey if does not exist).""" + cmap = self.colormaps.get(name, self.colormaps['grey']) + if invert: + return cmap[::-1] + return cmap diff -Nru veusz-1.10/document/emf_export.py veusz-1.14/document/emf_export.py --- veusz-1.10/document/emf_export.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/emf_export.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: emf_export.py 1065 2009-09-19 19:33:50Z jeremysanders $ - """A paint engine to produce EMF exports. Requires: PyQt-x11-gpl-4.6-snapshot-20090906.tar.gz @@ -35,450 +33,380 @@ scale = 100 def isStockObject(obj): - """Is this a stock windows object.""" - return (obj & 0x80000000) != 0 + """Is this a stock windows object.""" + return (obj & 0x80000000) != 0 class _EXTCREATEPEN(pyemf._EMR._EXTCREATEPEN): - """Extended pen creation record with custom line style.""" + """Extended pen creation record with custom line style.""" - emr_typedef=[ - ('i','handle',0), - ('i','offBmi',0), - ('i','cbBmi',0), - ('i','offBits',0), - ('i','cbBits',0), - ('i','style'), - ('i','penwidth'), - ('i','brushstyle'), - ('i','color'), - ('i','brushhatch',0), - ('i','numstyleentries')] - - def __init__(self, style=pyemf.PS_SOLID, width=1, color=0, - styleentries=[]): - """Create pen. - styleentries is a list of dash and space lengths.""" - - pyemf._EMR._EXTCREATEPEN.__init__(self) - self.style = style - self.penwidth = width - self.color = pyemf._normalizeColor(color) - self.brushstyle = 0x0 # solid - - if style & pyemf.PS_USERSTYLE == 0: - self.styleentries = [] - else: - self.styleentries = styleentries - - self.numstyleentries = len(self.styleentries) + emr_typedef = [ + ('i','handle',0), + ('i','offBmi',0), + ('i','cbBmi',0), + ('i','offBits',0), + ('i','cbBits',0), + ('i','style'), + ('i','penwidth'), + ('i','brushstyle'), + ('i','color'), + ('i','brushhatch',0), + ('i','numstyleentries')] + + def __init__(self, style=pyemf.PS_SOLID, width=1, color=0, + styleentries=[]): + """Create pen. + styleentries is a list of dash and space lengths.""" + + pyemf._EMR._EXTCREATEPEN.__init__(self) + self.style = style + self.penwidth = width + self.color = pyemf._normalizeColor(color) + self.brushstyle = 0x0 # solid + + if style & pyemf.PS_USERSTYLE == 0: + self.styleentries = [] + else: + self.styleentries = styleentries + + self.numstyleentries = len(self.styleentries) - def sizeExtra(self): - return struct.calcsize("i")*len(self.styleentries) + def sizeExtra(self): + return struct.calcsize("i")*len(self.styleentries) - def serializeExtra(self, fh): - self.serializeList(fh, "i", self.styleentries) + def serializeExtra(self, fh): + self.serializeList(fh, "i", self.styleentries) - def hasHandle(self): - return True + def hasHandle(self): + return True class EMFPaintEngine(qt4.QPaintEngine): - "Custom EMF paint engine.""" - - def __init__(self, width_in, height_in, dpi=75): - qt4.QPaintEngine.__init__(self) - self.width = width_in - self.height = height_in - self.dpi = dpi - - def begin(self, paintdevice): - self.emf = pyemf.EMF(self.width, self.height, self.dpi*scale) - self.pen = self.emf.GetStockObject(pyemf.BLACK_PEN) - self.pencolor = (0, 0, 0) - self.brush = self.emf.GetStockObject(pyemf.NULL_BRUSH) - - self.paintdevice = paintdevice - return True - - def drawLines(self, lines): - """Draw lines to emf output.""" - - for line in lines: - self.emf.Polyline( - [(line.x1()*scale, line.y1()*scale), - (line.x2()*scale, line.y2()*scale)] ) - - def drawPolygon(self, points, mode): - """Draw polygon on output.""" - # print "Polygon" - pts = [(p.x()*scale, p.y()*scale) for p in points] - - if mode == qt4.QPaintEngine.PolylineMode: - self.emf.Polyline(pts) - else: - self.emf.SetPolyFillMode( {qt4.QPaintEngine.WindingMode: - pyemf.WINDING, - qt4.QPaintEngine.OddEvenMode: - pyemf.ALTERNATE, - qt4.QPaintEngine.ConvexMode: - pyemf.WINDING} ) - self.emf.Polygon(pts) - - def drawEllipse(self, rect): - """Draw an ellipse.""" - # print "ellipse" - args = (rect.left()*scale, rect.top()*scale, - rect.right()*scale, rect.bottom()*scale, - rect.left()*scale, rect.top()*scale, - rect.left()*scale, rect.top()*scale) - self.emf.Pie(*args) - self.emf.Arc(*args) - - def drawPoints(self, points): - """Draw points.""" - # print "points" - - for pt in points: - x, y = (pt.x()-0.5)*scale, (pt.y()-0.5)*scale - self.emf.Pie( x, y, - (pt.x()+0.5)*scale, (pt.y()+0.5)*scale, - x, y, x, y ) - - def drawPixmap(self, r, pixmap, sr): - """Draw pixmap to display.""" - - # convert pixmap to BMP format - bytes = qt4.QByteArray() - buffer = qt4.QBuffer(bytes) - buffer.open(qt4.QIODevice.WriteOnly) - pixmap.save(buffer, "BMP") - - # chop off bmp header to get DIB - bmp = str(buffer.data()) - dib = bmp[0xe:] - hdrsize, = struct.unpack(' +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +"""Routines to export the document.""" + +import os.path +import random +import math + +import veusz.qtall as qt4 +import veusz.utils as utils + +try: + import emf_export + hasemf = True +except ImportError: + hasemf = False + +import svg_export +import selftest_export +import painthelper + +# 1m in inch +m_inch = 39.370079 + +class Export(object): + """Class to do the document exporting. + + This is split from document to make that class cleaner. + """ + + formats = [ + (["bmp"], "Windows bitmap"), + (["eps"], "Encapsulated Postscript"), + (["jpg", "jpeg"], "Jpeg bitmap"), + (["pdf"], "Portable Document Format"), + #(["pic"], "QT Pic format"), + (["png"], "Portable Network Graphics"), + (["svg"], "Scalable Vector Graphics"), + (["tiff"], "Tagged Image File Format bitmap"), + (["xpm"], "X Pixmap"), + ] + + if hasemf: + formats.append( (["emf"], "Windows Enhanced Metafile") ) + formats.sort() + + def __init__(self, doc, filename, pagenumber, color=True, bitmapdpi=100, + antialias=True, quality=85, backcolor='#ffffff00', + pdfdpi=150): + """Initialise export class. Parameters are: + doc: document to write + filename: output filename + pagenumber: pagenumber to export + color: use color or try to use monochrome + bitmapdpi: assume this dpi value when writing images + antialias: antialias text and lines when writing bitmaps + quality: compression factor for bitmaps + backcolor: background color default for bitmaps (default transparent). + pdfdpi: dpi for pdf and eps files + """ + + self.doc = doc + self.filename = filename + self.pagenumber = pagenumber + self.color = color + self.bitmapdpi = bitmapdpi + self.antialias = antialias + self.quality = quality + self.backcolor = backcolor + self.pdfdpi = pdfdpi + + def export(self): + """Export the figure to the filename.""" + + ext = os.path.splitext(self.filename)[1].lower() + + if ext in ('.eps', '.pdf'): + self.exportPS(ext) + + elif ext in ('.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.xpm'): + self.exportBitmap(ext) + + elif ext == '.svg': + self.exportSVG() + + elif ext == '.selftest': + self.exportSelfTest() + + elif ext == '.pic': + self.exportPIC() + + elif ext == '.emf' and hasemf: + self.exportEMF() + + else: + raise RuntimeError, "File type '%s' not supported" % ext + + def renderPage(self, size, dpi, painter): + """Render page using paint helper to painter. + This first renders to the helper, then to the painter + """ + helper = painthelper.PaintHelper(size, dpi=dpi, directpaint=painter) + painter.setClipRect( qt4.QRectF( + qt4.QPointF(0,0), qt4.QPointF(*size)) ) + self.doc.paintTo(helper, self.pagenumber) + painter.restore() + painter.end() + + def exportBitmap(self, format): + """Export to a bitmap format.""" + + # get size for bitmap's dpi + dpi = self.bitmapdpi + size = self.doc.pageSize(self.pagenumber, dpi=(dpi,dpi)) + + # create real output image + backqcolor = utils.extendedColorToQColor(self.backcolor) + if format == '.png': + # transparent output + image = qt4.QImage(size[0], size[1], + qt4.QImage.Format_ARGB32_Premultiplied) + else: + # non transparent output + image = qt4.QImage(size[0], size[1], + qt4.QImage.Format_RGB32) + backqcolor.setAlpha(255) + + image.setDotsPerMeterX(dpi*m_inch) + image.setDotsPerMeterY(dpi*m_inch) + if backqcolor.alpha() == 0: + image.fill(qt4.qRgba(0,0,0,0)) + else: + image.fill(backqcolor.rgb()) + + # paint to the image + painter = qt4.QPainter(image) + painter.setRenderHint(qt4.QPainter.Antialiasing, self.antialias) + painter.setRenderHint(qt4.QPainter.TextAntialiasing, self.antialias) + self.renderPage(size, (dpi,dpi), painter) + + # write image to disk + writer = qt4.QImageWriter() + # format below takes extension without dot + writer.setFormat(qt4.QByteArray(format[1:])) + writer.setFileName(self.filename) + + if format == 'png': + # min quality for png as it makes no difference to output + # and makes file size smaller + writer.setQuality(0) + else: + writer.setQuality(self.quality) + + writer.write(image) + + def exportPS(self, ext): + """Export to EPS or PDF format.""" + + printer = qt4.QPrinter() + printer.setFullPage(True) + + # set printer parameters + printer.setColorMode( (qt4.QPrinter.GrayScale, qt4.QPrinter.Color)[ + self.color] ) + + if ext == '.pdf': + fmt = qt4.QPrinter.PdfFormat + else: + fmt = qt4.QPrinter.PostScriptFormat + printer.setOutputFormat(fmt) + printer.setOutputFileName(self.filename) + printer.setCreator('Veusz %s' % utils.version()) + printer.setResolution(self.pdfdpi) + + # setup for printing + printer.newPage() + painter = qt4.QPainter(printer) + + # write to printer with correct dpi + dpi = (printer.logicalDpiX(), printer.logicalDpiY()) + width, height = size = self.doc.pageSize(self.pagenumber, dpi=dpi) + self.renderPage(size, dpi, painter) + + # fixup eps/pdf file - yuck HACK! - hope qt gets fixed + # this makes the bounding box correct + # copy output to a temporary file + tmpfile = "%s.tmp.%i" % (self.filename, random.randint(0,1000000)) + fout = open(tmpfile, 'wb') + fin = open(self.filename, 'rb') + + if ext == '.eps': + # adjust bounding box + for line in fin: + if line[:14] == '%%BoundingBox:': + # replace bounding box line by calculated one + parts = line.split() + widthfactor = float(parts[3]) / printer.width() + origheight = float(parts[4]) + line = "%s %i %i %i %i\n" % ( + parts[0], 0, + int(math.floor(origheight-widthfactor*height)), + int(math.ceil(widthfactor*width)), + int(math.ceil(origheight)) ) + fout.write(line) + + elif ext == '.pdf': + # change pdf bounding box and correct pdf index + text = fin.read() + text = utils.scalePDFMediaBox(text, printer.width(), + width, height) + text = utils.fixupPDFIndices(text) + fout.write(text) + + fout.close() + fin.close() + os.remove(self.filename) + os.rename(tmpfile, self.filename) + + def exportSVG(self): + """Export document as SVG""" + + dpi = svg_export.dpi * 1. + size = self.doc.pageSize(self.pagenumber, dpi=(dpi,dpi), integer=False) + + if qt4.PYQT_VERSION >= 0x40600: + # custom paint devices don't work in old PyQt versions + + f = open(self.filename, 'w') + paintdev = svg_export.SVGPaintDevice(f, size[0]/dpi, size[1]/dpi) + painter = qt4.QPainter(paintdev) + self.renderPage(size, (dpi,dpi), painter) + f.close() + else: + # use built-in svg generation, which doesn't work very well + # (no clipping, font size problems) + import PyQt4.QtSvg + + # actually paint the image + gen = PyQt4.QtSvg.QSvgGenerator() + gen.setFileName(self.filename) + gen.setResolution(dpi) + gen.setSize( qt4.QSize(int(size[0]), int(size[1])) ) + painter = qt4.QPainter(gen) + self.renderPage(size, (dpi,dpi), painter) + + def exportSelfTest(self): + """Export document for testing""" + + dpi = 90. + size = width, height = self.doc.pageSize( + self.pagenumber, dpi=(dpi,dpi), integer=False) + + f = open(self.filename, 'w') + paintdev = selftest_export.SelfTestPaintDevice(f, width/dpi, height/dpi) + painter = qt4.QPainter(paintdev) + self.renderPage(size, (dpi,dpi), painter) + f.close() + + def exportPIC(self): + """Export document as Qt PIC""" + + pic = qt4.QPicture() + painter = qt4.QPainter(pic) + + dpi = (pic.logicalDpiX(), pic.logicalDpiY()) + size = self.doc.pageSize(self.pagenumber, dpi=dpi) + self.renderPage(size, dpi, painter) + pic.save(self.filename) + + def exportEMF(self): + """Export document as EMF.""" + + dpi = 90. + size = self.doc.pageSize(self.pagenumber, dpi=(dpi,dpi), integer=False) + + paintdev = emf_export.EMFPaintDevice(size[0]/dpi, size[1]/dpi, dpi=dpi) + painter = qt4.QPainter(paintdev) + self.renderPage(size, (dpi,dpi), painter) + paintdev.paintEngine().saveFile(self.filename) diff -Nru veusz-1.10/document/__init__.py veusz-1.14/document/__init__.py --- veusz-1.10/document/__init__.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/__init__.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: __init__.py 1224 2010-05-13 21:00:22Z jeremysanders $ - from widgetfactory import * from doc import * from datasets import * @@ -30,3 +28,6 @@ from capture import * from mime import * from dataset_histo import * +from painthelper import * +from export import Export +from readcsv import ParamsCSV diff -Nru veusz-1.10/document/mime.py veusz-1.14/document/mime.py --- veusz-1.10/document/mime.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/mime.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,9 +16,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: mime.py 1449 2010-11-22 09:26:58Z jeremysanders $ - -from itertools import izip +from itertools import izip, count import veusz.qtall as qt4 @@ -26,9 +24,14 @@ import operations import widgetfactory +import StringIO + # mime type for copy and paste widgetmime = 'text/x-vnd.veusz-widget-3' +# dataset mime +datamime = 'text/x-vnd.veusz-data-1' + def generateWidgetsMime(widgets): """Create mime data describing widget and children. format is: @@ -59,6 +62,41 @@ mimedata.setData(widgetmime, qt4.QByteArray(text)) return mimedata +def generateDatasetsMime(datasets, document): + """Generate mime for the list of dataset names given in the document. + + Format is: + repr of names + text to recreate dataset 1 + ... + """ + + mimedata = qt4.QMimeData() + + # just plain text format + output = [] + for name in datasets: + output.append( document.data[name].datasetAsText() ) + text = '\n'.join(output) + mimedata.setData('text/plain', qt4.QByteArray(text)) + + textfile = StringIO.StringIO() + for name in datasets: + # get unlinked copy of dataset + ds = document.data[name].returnCopy() + + # write into a string file + ds.saveToFile(textfile, name) + + mimedata.setData(datamime, textfile.getvalue()) + + return mimedata + +def isDataMime(): + """Returns whether data available on clipboard.""" + mimedata = qt4.QApplication.clipboard().mimeData() + return datamime in mimedata.formats() + def getClipboardWidgetMime(): """Returns widget mime data if clipboard contains mime data or None.""" mimedata = qt4.QApplication.clipboard().mimeData() @@ -85,8 +123,8 @@ paths = [eval(x) for x in lines[3:3+4*numwidgets:4]] return paths -def isMimePastable(parentwidget, mimedata): - """Is mime data suitable to paste at parentwidget?""" +def isWidgetMimePastable(parentwidget, mimedata): + """Is widget mime data suitable to paste at parentwidget?""" if mimedata is None: return False @@ -208,3 +246,46 @@ """Do the import.""" widgets = OperationWidgetPaste.do(self, document) return widgets[0] + +class OperationDataPaste(object): + """Paste dataset from mime data.""" + + descr = 'paste data' + + def __init__(self, mimedata): + """Paste datasets into document.""" + self.data = str(mimedata.data(datamime)) + + def do(self, thisdoc): + """Do the data paste.""" + + import commandinterpreter + + # write data into a temporary document + tempdoc = doc.Document() + # interpreter to create datasets + interpreter = commandinterpreter.CommandInterpreter(tempdoc) + interpreter.runFile(self.data) + + # list of pasted datasets + self.newds = [] + + # now transfer datasets to existing document + for name, ds in sorted(tempdoc.data.iteritems()): + + # get new name + if name not in thisdoc.data: + newname = name + else: + for idx in count(2): + newname = '%s_%s' % (name, idx) + if newname not in thisdoc.data: + break + + thisdoc.setData(newname, ds) + self.newds.append(newname) + + def undo(self, thisdoc): + """Undo pasting datasets.""" + for n in self.newds: + thisdoc.deleteData(n) diff -Nru veusz-1.10/document/operations.py veusz-1.14/document/operations.py --- veusz-1.10/document/operations.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/operations.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: operations.py 1471 2010-12-10 22:18:24Z jeremysanders $ - """Represents atomic operations to take place on a document which can be undone. Rather than the document modified directly, this interface should be used. @@ -62,7 +60,6 @@ def do(self, document): """Apply setting variable.""" - setting = document.resolveFullSettingPath(self.settingpath) if setting.isReference(): self.oldvalue = setting.getReference() @@ -72,7 +69,6 @@ def undo(self, document): """Return old value back...""" - setting = document.resolveFullSettingPath(self.settingpath) setting.set(self.oldvalue) @@ -207,6 +203,7 @@ self.oldwidget = document.resolveFullWidgetPath(self.widgetpath) oldparent = self.oldwidget.parent + self.oldwidget.parent = None self.oldparentpath = oldparent.path self.oldindex = oldparent.children.index(self.oldwidget) oldparent.removeChild(self.oldwidget.name) @@ -215,8 +212,55 @@ """Restore deleted widget.""" oldparent = document.resolveFullWidgetPath(self.oldparentpath) + self.oldwidget.parent = oldparent oldparent.addChild(self.oldwidget, index=self.oldindex) - + +class OperationWidgetsDelete(object): + """Delete mutliple widget.""" + + descr = 'delete' + + def __init__(self, widgets): + """Delete the widget.""" + self.widgetpaths = [w.path for w in widgets] + + def do(self, document): + """Delete widget.""" + + # ignore widgets which share ancestry + # as deleting the parent deletes the child + widgetpaths = list(self.widgetpaths) + widgetpaths.sort( cmp=lambda a, b: len(a)-len(b) ) + i = 0 + while i < len(widgetpaths): + wp = widgetpaths[i] + for j in xrange(i): + if wp[:len(widgetpaths[j])+1] == widgetpaths[j]+'/': + del widgetpaths[i] + break + else: + i += 1 + + self.oldwidgets = [] + self.oldparentpaths = [] + self.oldindexes = [] + + # delete each widget keeping track of details + for path in widgetpaths: + self.oldwidgets.append( document.resolveFullWidgetPath(path) ) + oldparent = self.oldwidgets[-1].parent + self.oldparentpaths.append( oldparent.path ) + self.oldindexes.append( oldparent.children.index(self.oldwidgets[-1]) ) + oldparent.removeChild(self.oldwidgets[-1].name) + + def undo(self, document): + """Restore deleted widget.""" + + # put back widgets in reverse order so that indexes are corrent + for i in xrange(len(self.oldwidgets)-1,-1,-1): + oldparent = document.resolveFullWidgetPath(self.oldparentpaths[i]) + oldparent.addChild(self.oldwidgets[i], index=self.oldindexes[i]) + class OperationWidgetMoveUpDown(object): """Move a widget up or down in the hierarchy.""" @@ -266,6 +310,7 @@ newparent = document.resolveFullWidgetPath(self.newparentpath) self.oldchildindex = oldparent.children.index(child) self.oldparentpath = oldparent.path + self.oldname = None if self.newindex < 0: # convert negative index to normal index @@ -281,12 +326,22 @@ else: # moving to different parent self.movemode = 'differentparent' - # record previous parent and position + # remove from old parent del oldparent.children[self.oldchildindex] + + # current names of children + childnames = newparent.childnames + + # record previous parent and position newparent.children.insert(self.newindex, child) child.parent = newparent + # set a new name, if required + if child.name in childnames: + self.oldname = child.name + child.name = child.chooseName() + self.newchildpath = child.path def undo(self, document): @@ -296,10 +351,16 @@ child = document.resolveFullWidgetPath(self.newchildpath) oldparent = document.resolveFullWidgetPath(self.oldparentpath) + # remove from new parent del newparent.children[self.newindex] + # restore parent oldparent.children.insert(self.oldchildindex, child) child.parent = oldparent + # restore name + if self.oldname is not None: + child.name = self.oldname + class OperationWidgetAdd(object): """Add a widget of specified type to parent.""" @@ -370,7 +431,7 @@ def undo(self, document): """Undo the data setting.""" - del document.data[self.datasetname] + document.deleteData(self.datasetname) if self.olddata is not None: document.setData(self.datasetname, self.olddata) @@ -385,7 +446,7 @@ def do(self, document): """Remove dataset from document, but preserve for undo.""" self.olddata = document.data[self.datasetname] - del document.data[self.datasetname] + document.deleteData(self.datasetname) def undo(self, document): """Put dataset back""" @@ -437,9 +498,9 @@ """Delete the duplicate""" if self.olddata is None: - del document.data[self.duplname] + document.deleteData(self.duplname) else: - document.data[self.duplname] = self.olddata + document.setData(self.duplname, self.olddata) class OperationDatasetUnlinkFile(object): """Remove association between dataset and file.""" @@ -491,9 +552,9 @@ def undo(self, document): """Delete the created dataset.""" - del document.data[self.datasetname] + document.deleteData(self.datasetname) if self.olddataset is not None: - document.data[self.datasetname] = self.olddataset + document.setData(self.datasetname, self.olddataset) class OperationDatasetCreateRange(OperationDatasetCreate): """Create a dataset in a specfied range.""" @@ -658,7 +719,7 @@ def undo(self, document): """Undo dataset creation.""" - del document.data[self.datasetname] + document.deleteData(self.datasetname) if self.olddataset: document.setData(self.datasetname, self.olddataset) @@ -704,6 +765,53 @@ def makeDSClass(self): return datasets.Dataset2DXYFunc(self.xstep, self.ystep, self.expr) +class OperationDatasetUnlinkByFile(object): + """Unlink all datasets associated with file.""" + + descr = "unlink datasets" + + def __init__(self, filename): + """Unlink all datasets associated with filename.""" + self.filename = filename + + def do(self, document): + """Remove links.""" + self.oldlinks = {} + for name, ds in document.data.iteritems(): + if ds.linked is not None and ds.linked.filename == self.filename: + self.oldlinks[name] = ds.linked + ds.linked = None + + def undo(self, document): + """Restore links.""" + for name, link in self.oldlinks.iteritems(): + try: + document.data[name].linked = link + except KeyError: + pass + +class OperationDatasetDeleteByFile(object): + """Delete all datasets associated with file.""" + + descr = "delete datasets" + + def __init__(self, filename): + """Delete all datasets associated with filename.""" + self.filename = filename + + def do(self, document): + """Remove datasets.""" + self.olddatasets = {} + for name, ds in document.data.items(): + if ds.linked is not None and ds.linked.filename == self.filename: + self.olddatasets[name] = ds + document.deleteData(name) + + def undo(self, document): + """Restore datasets.""" + for name, ds in self.olddatasets.iteritems(): + document.setData(name, ds) + ############################################################################### # Import datasets @@ -773,84 +881,69 @@ else: LF = None - # backup datasets in document for undo - # this has possible space issues! - self.olddatasets = dict(document.data) + # remember datasets in document for undo + olddatasets = dict(document.data) # actually set the data in the document - names = self.simpleread.setInDocument(document, linkedfile=LF, - prefix=self.prefix, - suffix=self.suffix) + names = self.simpleread.setInDocument( + document, linkedfile=LF, prefix=self.prefix, suffix=self.suffix) + + # only remember the parts we need + self.olddatasets = dict([ (n, olddatasets.get(n, None)) for n in names]) + return names def undo(self, document): """Undo import.""" - # restore old datasets - document.data = self.olddatasets + for name, ds in self.olddatasets.iteritems(): + if ds is None: + document.deleteData(name) + else: + document.setData(name, ds) class OperationDataImportCSV(object): """Import data from a CSV file.""" descr = 'import CSV data' - def __init__(self, filename, readrows=False, - delimiter=',', textdelimiter='"', - encoding='utf_8', - prefix='', suffix='', - headerignore=0, - linked=False): - """Import CSV data from filename - - If readrows, then read in rows rather than columns. - Prefix is appended to each dataset name. + def __init__(self, params, linked=False): + """Import CSV data + params is a ParamsCSV object Data are linked to file if linked is True. """ - - self.filename = filename - self.readrows = readrows - self.delimiter = delimiter - self.textdelimiter = textdelimiter - self.encoding = encoding - self.prefix = prefix - self.suffix = suffix - self.headerignore = headerignore self.linked = linked + self.params = params def do(self, document): """Do the data import.""" - csvr = readcsv.ReadCSV(self.filename, readrows=self.readrows, - delimiter=self.delimiter, - textdelimiter=self.textdelimiter, - encoding=self.encoding, - headerignore=self.headerignore, - prefix=self.prefix, suffix=self.suffix) + csvr = readcsv.ReadCSV(self.params) csvr.readData() if self.linked: - LF = datasets.LinkedCSVFile(self.filename, readrows=self.readrows, - delimiter=self.delimiter, - textdelimiter=self.textdelimiter, - encoding=self.encoding, - headerignore=self.headerignore, - prefix=self.prefix, suffix=self.suffix) + LF = datasets.LinkedCSVFile(self.params) else: LF = None - # backup datasets in document for undo - # this has possible space issues! - self.olddatasets = dict(document.data) + # remember the old datasets for undo + olddatasets = dict(document.data) # set the data names = csvr.setData(document, linkedfile=LF) + + # only remember the parts we need + self.olddatasets = dict([ (n, olddatasets.get(n, None)) for n in names]) return names def undo(self, document): """Undo import.""" - # restore old datasets - document.data = self.olddatasets + for name, ds in self.olddatasets.iteritems(): + if ds is None: + document.deleteData(name) + else: + document.setData(name, ds) class OperationDataImport2D(object): """Import a 2D matrix from a file.""" @@ -922,9 +1015,9 @@ else: LF = None - # backup datasets in document for undo - self.olddatasets = dict(document.data) - + # remember datasets in document for undo + olddatasets = dict(document.data) + readds = [] for name in self.datasets: sr = simpleread.SimpleRead2D(name) @@ -943,13 +1036,22 @@ readds += sr.setInDocument(document, linkedfile=LF, prefix=self.prefix, suffix=self.suffix) + + # only remember the parts we need + self.olddatasets = dict([ (n, olddatasets.get(n, None)) + for n in self.datasets ]) + return readds def undo(self, document): """Undo import.""" # restore old datasets - document.data = self.olddatasets + for name, ds in self.olddatasets.iteritems(): + if ds is None: + document.deleteData(name) + else: + document.setData(name, ds) class OperationDataImportFITS(object): """Import 1d or 2d data from a fits file.""" @@ -1004,8 +1106,12 @@ # actually create the dataset return datasets.Dataset(data=datav, serr=symv, perr=posv, nerr=negv) - def _import2d(self, hdu): - """Import 2d data from hdu.""" + def _import1dimage(self, hdu): + """Import 1d image data form hdu.""" + return datasets.Dataset(data=hdu.data) + + def _import2dimage(self, hdu): + """Import 2d image data from hdu.""" if ( self.datacol is not None or self.symerrcol is not None or self.poserrcol is not None or self.negerrcol is not None ): @@ -1052,8 +1158,13 @@ ds = self._import1d(hdu) except AttributeError: - ds = self._import2d(hdu) - + naxis = hdu.header.get('NAXIS') + if naxis == 1: + ds = self._import1dimage(hdu) + elif naxis == 2: + ds = self._import2dimage(hdu) + else: + raise RuntimeError, "Cannot import images with %i dimensions" % naxis f.close() if self.linked: @@ -1069,12 +1180,10 @@ def undo(self, document): """Undo the import.""" - - if self.dsname in document.data: - del document.data[self.dsname] - if self.olddataset is not None: document.setData(self.dsname, self.olddataset) + else: + document.deleteData(self.dsname) class OperationDataImportPlugin(object): """Import data using a plugin.""" @@ -1107,11 +1216,32 @@ except KeyError: pass + def addCustoms(self, document, consts): + """Add the customs return by plugins to document.""" + + self.oldconst = None + if len(consts) > 0: + self.oldconst = list(document.customs) + cd = document.customDict() + for item in consts: + if item[1] in cd: + idx, ctype, val = cd[item[1]] + document.customs[idx] = item + else: + document.customs.append(item) + document.updateEvalContext() + def do(self, document): """Do import.""" names = [p.name for p in plugins.importpluginregistry] plugin = plugins.importpluginregistry[names.index(self.pluginname)] + + # if the plugin is a class, make an instance + # the old API is for the plugin to be instances + if isinstance(plugin, type): + plugin = plugin() + plugparams = plugins.ImportPluginParams(self.filename, self.encoding, self.params) @@ -1127,17 +1257,28 @@ self.pluginname, self.filename, self.params, encoding=self.encoding, prefix=self.prefix, suffix=self.suffix) - + + customs = [] + # convert results to real datasets + names = [] for d in results: - if isinstance(d, plugins.ImportDataset1D): + if isinstance(d, plugins.Dataset1D): ds = datasets.Dataset(data=d.data, serr=d.serr, perr=d.perr, nerr=d.nerr) - elif isinstance(d, plugins.ImportDataset2D): + elif isinstance(d, plugins.Dataset2D): ds = datasets.Dataset2D(data=d.data, xrange=d.rangex, yrange=d.rangey) - elif isinstance(d, plugins.ImportDatasetText): + elif isinstance(d, plugins.DatasetText): ds = datasets.DatasetText(data=d.data) + elif isinstance(d, plugins.DatasetDateTime): + ds = datasets.DatasetDateTime(data=d.data) + elif isinstance(d, plugins.Constant): + customs.append( ['constant', d.name, d.val] ) + continue + elif isinstance(d, plugins.Function): + customs.append( ['function', d.name, d.val] ) + continue else: raise RuntimeError("Invalid data set in plugin results") @@ -1153,16 +1294,27 @@ # actually make dataset document.setData(d.name, ds) - self.datasetnames = [d.name for d in results] - return self.datasetnames + names.append(d.name) + + # add constants, functions to doc, if any + self.addCustoms(document, customs) + + # custom strings + custstr = ['%s %s=%s' % tuple(c) for c in customs] + + self.datasetnames = names + return self.datasetnames, custstr def undo(self, document): """Undo import.""" for name in self.datasetnames: - del document.data[name] + document.deleteData(name) for name, dataset in self.olddata.iteritems(): document.setData(name, dataset) + if self.oldconst is not None: + document.customs = self.oldconst + document.updateEvalContext() class OperationDataCaptureSet(object): """An operation for setting the results from a SimpleRead into the @@ -1197,10 +1349,10 @@ for name in self.nameschanged: if name in self.olddata: # replace datasets with what was there previously - document.data[name] = self.olddata[name] + document.setData(name, self.olddata[name]) else: # or delete datasets that weren't there before - del document.data[name] + document.deleteData(name) ############################################################################### # Alter dataset @@ -1258,6 +1410,31 @@ datacol[self.row] = self.oldval ds.changeValues(self.columnname, datacol) +class OperationDatasetSetVal2D(object): + """Set a value in a 2D dataset.""" + + descr = 'change 2D dataset value' + + def __init__(self, datasetname, row, col, val): + """Set row in column columnname to val.""" + self.datasetname = datasetname + self.row = row + self.col = col + self.val = val + + def do(self, document): + """Set the value.""" + ds = document.data[self.datasetname] + self.oldval = ds.data[self.row, self.col] + ds.data[self.row, self.col] = self.val + document.modifiedData(ds) + + def undo(self, document): + """Restore the value.""" + ds = document.data[self.datasetname] + ds.data[self.row, self.col] = self.oldval + document.modifiedData(ds) + class OperationDatasetDeleteRow(object): """Delete a row or several in the dataset.""" @@ -1380,11 +1557,9 @@ e = None try: interpreter.runFile( open(self.filename) ) - except Exception, e: - pass - document.batchHistory(None) - if e: - raise e + except: + document.batchHistory(None) + raise class OperationLoadCustom(OperationLoadStyleSheet): descr = 'load custom definitions' @@ -1411,7 +1586,7 @@ ifc = commandinterface.CommandInterface(document) try: self.plugin.apply(ifc, self.fields) - except Exception: + except: document.batchHistory(None) raise document.batchHistory(None) @@ -1451,7 +1626,7 @@ # add new datasets to document for name, ds in izip(names, manager.veuszdatasets): if name is not None: - document.data[name] = ds + document.setData(name, ds) return names @@ -1465,7 +1640,8 @@ # delete datasets which were created for name in self.datasetnames: if name is not None: - del document.data[name] + document.deleteData(name) # put back old datasets - document.data.update(self.olddata) + for name, ds in self.olddata.iteritems(): + document.setData(name, ds) diff -Nru veusz-1.10/document/painthelper.py veusz-1.14/document/painthelper.py --- veusz-1.10/document/painthelper.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/document/painthelper.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,243 @@ +# Copyright (C) 2011 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +"""Helper for doing the plotting of the document. +""" + +import veusz.qtall as qt4 +import veusz.setting as setting +import veusz.utils as utils + +try: + from veusz.helpers.qtloops import RecordPaintDevice +except ImportError: + # fallback to this if we don't get the native recorded + def RecordPaintDevice(width, height, dpix, dpiy): + return qt4.QPicture() + +class DrawState(object): + """Each widget plotted has a recorded state in this object.""" + + def __init__(self, widget, bounds, clip, helper): + """Initialise state for widget. + bounds: tuple of (x1, y1, x2, y2) + clip: if clipping should be done, another tuple.""" + + self.widget = widget + self.record = RecordPaintDevice( + helper.pagesize[0], helper.pagesize[1], + helper.dpi[0], helper.dpi[1]) + self.bounds = bounds + self.clip = clip + + # controlgraphs belonging to widget + self.cgis = [] + + # list of child widgets states + self.children = [] + +class PaintHelper(object): + """Helper used when painting widgets. + + Provides a QPainter to each widget for plotting. + Records the controlgraphs for each widget. + Holds the scaling, dpi and size of the page. + """ + + def __init__(self, pagesize, scaling=1., dpi=(100, 100), + directpaint=None): + """Initialise using page size (tuple of pixelw, pixelh). + + If directpaint is set to a painter, use this directly rather + than creating separate layers for rendering later. The user + will need to call restore() on the painter before ending, if + using this mode, however. + """ + + self.dpi = dpi + self.scaling = scaling + self.pixperpt = self.dpi[1] / 72. + self.pagesize = pagesize + + # keep track of states of all widgets + self.states = {} + + # axis to plotter mappings + self.axisplottermap = {} + + # whether to directly render to a painter or make new layers + self.directpaint = directpaint + self.directpainting = False + + # state for root widget + self.rootstate = None + + @property + def maxsize(self): + """Return maximum page dimension (using PaintHelper's DPI).""" + return max(*self.pagesize) + + def sizeAtDpi(self, dpi): + """Return a tuple size for the page given an output device dpi.""" + return ( int(self.pagesize[0]/self.dpi[0] * dpi), + int(self.pagesize[1]/self.dpi[1] * dpi) ) + + def updatePageSize(self, pagew, pageh): + """Update page size to value given (in user text units.""" + self.pagesize = ( setting.Distance.convertDistance(self, pagew), + setting.Distance.convertDistance(self, pageh) ) + + def painter(self, widget, bounds, clip=None): + """Return a painter for use when drawing the widget. + widget: widget object + bounds: tuple (x1, y1, x2, y2) of widget bounds + clip: another tuple, if set clips drawing to this rectangle + """ + s = self.states[widget] = DrawState(widget, bounds, clip, self) + if widget.parent is None: + self.rootstate = s + else: + self.states[widget.parent].children.append(s) + + if self.directpaint: + # only paint to one output painter + p = self.directpaint + if self.directpainting: + p.restore() + self.directpainting = True + p.save() + else: + # save to multiple recorded layers + p = qt4.QPainter(s.record) + + p.scaling = self.scaling + p.pixperpt = self.pixperpt + p.pagesize = self.pagesize + p.maxsize = max(*self.pagesize) + p.dpi = self.dpi[1] + + if clip: + p.setClipRect(clip) + + return p + + def setControlGraph(self, widget, cgis): + """Records the control graph list for the widget given.""" + self.states[widget].cgis = cgis + + def renderToPainter(self, painter): + """Render saved output to painter. + """ + self._renderState(self.rootstate, painter) + + def _renderState(self, state, painter): + """Render state to painter.""" + + painter.save() + state.record.play(painter) + painter.restore() + + for child in state.children: + self._renderState(child, painter) + + def identifyWidgetAtPoint(self, x, y, antialias=True): + """What widget has drawn at the point x,y? + + Returns the widget drawn last on the point, or None if it is + an empty part of the page. + root is the root widget to recurse from + if antialias is true, do test for antialiased drawing + """ + + # make a small image filled with a specific color + box = 3 + specialcolor = qt4.QColor(254, 255, 254) + origpix = qt4.QPixmap(2*box+1, 2*box+1) + origpix.fill(specialcolor) + origimg = origpix.toImage() + # store most recent widget here + lastwidget = [None] + + def rendernextstate(state): + """Recursively draw painter. + + Checks whether drawing a widgetchanges the small image + around the point given. + """ + + pixmap = qt4.QPixmap(origpix) + painter = qt4.QPainter(pixmap) + painter.setRenderHint(qt4.QPainter.Antialiasing, antialias) + painter.setRenderHint(qt4.QPainter.TextAntialiasing, antialias) + # this makes the small image draw from x-box->x+box, y-box->y+box + # translate would get overriden by coordinate system playback + painter.setWindow(x-box,y-box,box*2+1,box*2+1) + state.record.play(painter) + painter.end() + newimg = pixmap.toImage() + + if newimg != origimg: + lastwidget[0] = state.widget + + for child in state.children: + rendernextstate(child) + + rendernextstate(self.rootstate) + return lastwidget[0] + + def pointInWidgetBounds(self, x, y, widgettype): + """Which graph widget plots at point x,y? + + Recurse from widget root + widgettype is the class of widget to get + """ + + widget = [None] + + def recursestate(state): + if isinstance(state.widget, widgettype): + b = state.bounds + if x >= b[0] and y >= b[1] and x <= b[2] and y <= b[3]: + # most recent widget drawing on point + widget[0] = state.widget + + for child in state.children: + recursestate(child) + + recursestate(self.rootstate) + return widget[0] + + def widgetBounds(self, widget): + """Return bounds of widget.""" + return self.states[widget].bounds + + def widgetBoundsIterator(self, widgettype=None): + """Returns bounds for each widget. + Set widgettype to be a widget type to filter returns + Yields (widget, bounds) + """ + + # this is a recursive algorithm turned into an iterative one + # which makes creation of a generator easier + stack = [self.rootstate] + while stack: + state = stack[0] + if widgettype is None or isinstance(state.widget, widgettype): + yield state.widget, state.bounds + # remove the widget itself from the stack and insert children + stack = state.children + stack[1:] diff -Nru veusz-1.10/document/readcsv.py veusz-1.14/document/readcsv.py --- veusz-1.10/document/readcsv.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/readcsv.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,15 +16,15 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: readcsv.py 1471 2010-12-10 22:18:24Z jeremysanders $ - """This module contains routines for importing CSV data files in an easy-to-use manner.""" +import re import numpy as N import datasets import veusz.utils as utils +import veusz.qtall as qt4 class _FileReaderCols(object): """Read a CSV file in rows. This acts as an iterator. @@ -34,10 +34,17 @@ def __init__(self, csvreader): self.csvreader = csvreader + self.maxlen = 0 def next(self): """Return next row.""" - return self.csvreader.next() + row = self.csvreader.next() + + # add blank columns up to maximum previously read + self.maxlen = max(self.maxlen, len(row)) + row = row + ['']*(self.maxlen - len(row)) + + return row class _FileReaderRows(object): """Read a CSV file in columns. This acts as an iterator. @@ -83,44 +90,87 @@ ('(number)', 'float'), ) -class ReadCSV(object): - """A class to import data from CSV files.""" +class ParamsCSV(object): + """CSV import parameters.""" - def __init__(self, filename, readrows=False, - delimiter=',', textdelimiter='"', - encoding='utf_8', - headerignore=0, - prefix='', suffix=''): - """Initialise the reader to import data from filename. + defaults = { + 'readrows': False, + 'encoding': 'utf_8', + 'delimiter': ',', + 'textdelimiter': '"', + 'headerignore': 0, + 'rowsignore': 0, + 'blanksaredata': False, + 'numericlocale': 'en_US', + 'dateformat': 'YYYY-MM-DD|T|hh:mm:ss', + 'headermode': 'multi', + 'dsprefix': '', + 'dssuffix': '', + } - If readrows is True, then data are read from columns, rather than - rows + def __init__(self, filename, **argsv): + """Initialise the reader to import data from filename. + If readrows is True, then data are read from rows headerignore is number of lines to ignore after headers + rowsignore is number of lines to ignore at top of file + if blanksaredata is true, treat blank entries as nans + + numericlocale: locale to use for converting numbers + dateformat: format for dates in file + + headermode: one of ('multi', '1st', 'none') + multi: allow multiple headers in file + 1st: first non-blank line is header + none: no headers, assign names - prefix is a prefix to prepend to the name of datasets from this file + dsprefix is a prefix to prepend to the name of datasets from this file + dssuffix is suffix to add to dataset names """ self.filename = filename - self.readrows = readrows - self.delimiter = delimiter - self.textdelimiter = textdelimiter - self.encoding = encoding - self.headerignore = headerignore - self.prefix = prefix - self.suffix = suffix - # datasets. Each name is associated with a list + # set defaults + for k, v in self.defaults.iteritems(): + setattr(self, k, v) + + # set parameters + for k, v in argsv.iteritems(): + if k not in self.defaults: + raise ValueError, "Invalid parameter %s" % k + setattr(self, k, v) + + if self.headermode not in ('multi', '1st', 'none'): + raise ValueError, "Invalid headermode" + +class _NextValue(Exception): + """A class to be raised to move to next value.""" + +class ReadCSV(object): + """A class to import data from CSV files.""" + + def __init__(self, params): + """Initialise the reader. + params is a ParamsCSV object + """ + + self.params = params + self.numericlocale = qt4.QLocale(params.numericlocale) + self.datere = re.compile( + utils.dateStrToRegularExpression(params.dateformat)) + + # created datasets. Each name is associated with a list self.data = {} def _generateName(self, column): """Generate a name for a column.""" - if self.readrows: + if self.params.readrows: prefix = 'row' else: prefix = 'col' - name = '%s%s%i%s' % (self.prefix, prefix, column+1, self.suffix) + name = '%s%s%i%s' % (self.params.dsprefix, prefix, + column+1, self.params.dssuffix) return name def _getNameAndColType(self, colnum, colval): @@ -133,7 +183,9 @@ while prevcol >= 0: n = self.colnames[prevcol] if len(n) > 0 and n[-1] not in "+-": - name = n + name + # we add a \0 here so that there's no chance of the user + # using this as a column name + name = n + '\0' + name return self.coltypes[prevcol], name prevcol -= 1 else: @@ -142,14 +194,13 @@ # examine whether object type is at end of name # convert, and remove, if is - type = 'float' + type = 'unknown' for codename, codetype in typecodes: if name[-len(codename):] == codename: type = codetype name = name[:-len(codename)].strip() break - - return type, self.prefix + name + self.suffix + return type, self.params.dsprefix + name + self.params.dssuffix def _setNameAndType(self, colnum, colname, coltype): """Set a name for column number given column name and type.""" @@ -158,25 +209,154 @@ self.coltypes[colnum] = coltype self.nametypes[colname] = coltype self.colnames[colnum] = colname - self.colignore[colnum] = self.headerignore + self.colignore[colnum] = self.params.headerignore + self.colblanks[colnum] = 0 if colname not in self.data: self.data[colname] = [] + def _guessType(self, val): + """Guess type for new dataset.""" + v, ok = self.numericlocale.toDouble(val) + if ok: + return 'float' + m = self.datere.match(val) + try: + v = utils.dateREMatchToDate(m) + return 'date' + except ValueError: + return 'string' + + def _newValueInBlankColumn(self, colnum, col): + """Handle occurance of new data value in previously blank column. + """ + + if self.params.headermode == '1st': + # just use name of column as title in 1st header mode + coltype, name = self._getNameAndColType(colnum, col) + self._setNameAndType(colnum, name.strip(), coltype) + raise _NextValue() + elif self.params.headermode == 'none': + # no header, so just start a new data set + dtype = self._guessType(col) + self._setNameAndType(colnum, self._generateName(colnum), + dtype) + else: + # see whether it looks like data, not a header + dtype = self._guessType(col) + if dtype == 'string': + # use text as dataset name + coltype, name = self._getNameAndColType(colnum, col) + self._setNameAndType(colnum, name.strip(), coltype) + raise _NextValue() + else: + # use guessed data type and generated name + self._setNameAndType(colnum, self._generateName(colnum), + dtype) + + def _newUnknownDataValue(self, colnum, col): + """Process data value if data type is unknown. + """ + + # blank value + if col.strip() == '': + if self.params.blanksaredata: + # keep track of blanks above autodetected data + self.colblanks[colnum] += 1 + # skip back to next value + raise _NextValue() + + # guess type from data value + t = self._guessType(col) + self.nametypes[self.colnames[colnum]] = t + self.coltypes[colnum] = t + + # add back on blanks if necessary with correct format + for i in xrange(self.colblanks[colnum]): + d = (N.nan, '')[t == 'string'] + self.data[self.colnames[colnum]].append(d) + self.colblanks[colnum] = 0 + + def _handleFailedConversion(self, colnum, col): + """If conversion from text to data type fails.""" + if col.strip() == '': + # skip blanks unless blanksaredata is set + if self.params.blanksaredata: + # assumes a numeric data type + self.data[self.colnames[colnum]].append(N.nan) + else: + if self.params.headermode == '1st': + # no more headers, so fill with invalid number + self.data[self.colnames[colnum]].append(N.nan) + else: + # start a new dataset if conversion failed + coltype, name = self._getNameAndColType(colnum, col) + self._setNameAndType(colnum, name.strip(), coltype) + + def _handleVal(self, colnum, col): + """Handle a value from the file. + colnum: number of column + col: data value + """ + + if colnum not in self.colnames: + # ignore blanks + if col.strip() == '': + return + # process value + self._newValueInBlankColumn(colnum, col) + + # ignore lines after headers + if self.colignore[colnum] > 0: + self.colignore[colnum] -= 1 + return + + # process value if data type unknown + if self.coltypes[colnum] == 'unknown': + self._newUnknownDataValue(colnum, col) + + ctype = self.coltypes[colnum] + try: + # convert text to data type of column + if ctype == 'float': + v, ok = self.numericlocale.toDouble(col) + if not ok: + raise ValueError + elif ctype == 'date': + m = self.datere.match(col) + v = utils.dateREMatchToDate(m) + elif ctype == 'string': + v = col + else: + raise RuntimeError, "Invalid type in CSV reader" + + except ValueError: + self._handleFailedConversion(colnum, col) + + else: + # conversion succeeded - append number to data + self.data[self.colnames[colnum]].append(v) + def readData(self): """Read the data into the document.""" + par = self.params + # open the csv file - csvf = utils.UnicodeCSVReader( open(self.filename), - delimiter=self.delimiter, - quotechar=self.textdelimiter, - encoding=self.encoding ) + csvf = utils.UnicodeCSVReader( par.filename, + delimiter=par.delimiter, + quotechar=par.textdelimiter, + encoding=par.encoding ) # make in iterator for the file - if self.readrows: + if par.readrows: it = _FileReaderRows(csvf) else: it = _FileReaderCols(csvf) + # ignore rows (at top), if requested + for i in xrange(par.rowsignore): + it.next() + # dataset names for each column self.colnames = {} # type of column (float, string or date) @@ -185,6 +365,9 @@ self.nametypes = {} # ignore lines after headers self.colignore = {} + # keep track of how many blank values before 1st data for auto + # type detection + self.colblanks = {} # iterate over each line (or column) while True: @@ -195,52 +378,10 @@ # iterate over items on line for colnum, col in enumerate(line): - - if colnum >= len(self.coltypes) or self.coltypes[colnum] == '': - ctype = 'float' - else: - ctype = self.coltypes[colnum] - - # ignore lines after headers - if colnum < len(self.coltypes) and self.colignore[colnum] > 0: - self.colignore[colnum] -= 1 - continue - try: - # do any necessary conversion - if ctype == 'float': - v = float(col) - elif ctype == 'date': - v = utils.dateStringToDate(col) - elif ctype == 'string': - v = col - else: - raise RuntimeError, "Invalid type in CSV reader" - - except ValueError: - # skip blanks - if col.strip() == '': - continue - if ( colnum in self.colnames and - len(self.data[self.colnames[colnum]]) == 0 ): - # if dataset is empty, convert to a string dataset - self._setNameAndType(colnum, self.colnames[colnum], 'string') - self.data[self.colnames[colnum]].append(col) - else: - # start a new dataset if conversion failed - coltype, name = self._getNameAndColType(colnum, col) - self._setNameAndType(colnum, name.strip(), coltype) - - else: - # generate a name if required - if colnum not in self.colnames: - self._setNameAndType(colnum, self._generateName(colnum), - 'float') - - # conversion okay - # append number to data - coldata = self.data[self.colnames[colnum]] - coldata.append(v) + self._handleVal(colnum, col) + except _NextValue: + pass def setData(self, document, linkedfile=None): """Set the read-in datasets in the document.""" @@ -250,14 +391,15 @@ for name in self.data.iterkeys(): # skip error data here, they are used below - if name[-1] in '+-': + # error data name contains \0 + if name.find('\0') >= 0: continue dsnames.append(name) # get data and errors (if any) data = [] - for k in (name, name+'+-', name+'+', name+'-'): + for k in (name, name+'\0+-', name+'\0+', name+'\0-'): data.append( self.data.get(k, None) ) # make them have a maximum length by adding NaNs @@ -268,12 +410,16 @@ ( data[i], N.zeros(maxlen-len(data[i]))*N.nan ) ) # create dataset - if self.nametypes[name] == 'string': + dstype = self.nametypes[name] + if dstype == 'string': ds = datasets.DatasetText(data=data[0], linked=linkedfile) + elif dstype == 'date': + ds = datasets.DatasetDateTime(data=data[0], linked=linkedfile) else: ds = datasets.Dataset(data=data[0], serr=data[1], perr=data[2], nerr=data[3], linked=linkedfile) + document.setData(name, ds) dsnames.sort() diff -Nru veusz-1.10/document/selftest_export.py veusz-1.14/document/selftest_export.py --- veusz-1.10/document/selftest_export.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/selftest_export.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,11 +16,10 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: selftest_export.py 1405 2010-09-12 14:26:35Z jeremysanders $ - """A paint engine for doing self-tests.""" import sys +import struct import veusz.qtall as qt4 import svg_export @@ -33,20 +32,20 @@ in inches.""" svg_export.SVGPaintEngine.__init__(self, width_in, height_in) - self.imageformat = 'bmp' + # ppm images are simple and should be same on all platforms + self.imageformat = 'ppm' def drawTextItem(self, pt, textitem): - """Convert text to a path and draw it. - """ - self.doStateUpdate() - self.fileobj.write( - '%s\n' % ( - svg_export.fltStr(pt.x()), - svg_export.fltStr(pt.y()), - textitem.font().pointSize(), - self.pen.color().name(), - textitem.text().toLatin1()) - ) + """Write text directly in self test mode.""" + + text = unicode(textitem.text()).encode('ascii', 'xmlcharrefreplace') + svg_export.SVGElement(self.celement, 'text', + 'x="%s" y="%s" font-size="%gpt" fill="%s"' % + (svg_export.fltStr(pt.x()), + svg_export.fltStr(pt.y()), + textitem.font().pointSize(), + self.pen.color().name()), + text=text) class SelfTestPaintDevice(svg_export.SVGPaintDevice): """Paint device for SVG paint engine.""" diff -Nru veusz-1.10/document/simpleread.py veusz-1.14/document/simpleread.py --- veusz-1.10/document/simpleread.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/simpleread.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: simpleread.py 1355 2010-08-12 19:38:13Z jeremysanders $ - """SimpleRead: a class for the reading of data formatted in a simple way To read the data it takes a descriptor which takes the form of @@ -240,7 +238,6 @@ val = stream.nextColumn() if val is None: return - # append a suffix to specify whether error or value # \0 is used as the user cannot enter it fullname = '%s\0%s' % (name, col) @@ -314,7 +311,7 @@ minlength = len(ds) for ds in vals, pos, neg, sym: if ds is not None and len(ds) != minlength: - ds = ds[:minlength] + del ds[minlength:] # only remember last N values if tail is not None: @@ -324,10 +321,13 @@ if neg is not None: neg = neg[-tail:] # create the dataset - if self.datatype in ('float', 'date'): - ds = datasets.Dataset(data = vals, serr = sym, - nerr = neg, perr = pos, - linked = linkedfile) + if self.datatype == 'float': + ds = datasets.Dataset( data = vals, serr = sym, + nerr = neg, perr = pos, + linked = linkedfile ) + elif self.datatype == 'date': + ds = datasets.DatasetDateTime( data=vals, + linked=linkedfile ) elif self.datatype == 'string': ds = datasets.DatasetText( data=vals, linked = linkedfile ) @@ -343,19 +343,22 @@ return names class Stream(object): + """This object reads through an input data source (override + readLine) and interprets data from the source.""" - # this regular expression is for splitting up the stream into words + # this is a regular expression for finding data items in data stream # I'll try to explain this bit-by-bit (these are ORd, and matched in order) - split_re = re.compile( r''' - `.+?`[^ \t\n#!%;]* | # match dataset name quoted in back-ticks - # we also need to match following characters to catch - # corner cases in the descriptor + find_re = re.compile( r''' + `.+?`[^ \t\n\r#!%;]* | # match dataset name quoted in back-ticks + # we also need to match following characters to catch + # corner cases in the descriptor u?"" | # match empty double-quoted string u?".*?[^\\]" | # match double-quoted string, ignoring escaped quotes u?'' | # match empty single-quoted string u?'.*?[^\\]' | # match single-quoted string, ignoring escaped quotes + [#!%;](?=descriptor) | # match separately comment char before descriptor [#!%;].* | # match comment to end of line - [^ \t\n#!%;]+ # match normal space/tab separated items + [^ \t\n\r#!%;]+ # match normal space/tab separated items ''', re.VERBOSE ) def __init__(self): @@ -394,8 +397,8 @@ return False # break up and append to buffer (removing comments) - self.remainingline += [ x for x in self.split_re.findall(line) if - x[0] not in '#!%;' ] + cmpts = self.find_re.findall(line) + self.remainingline += [ x for x in cmpts if x[0] not in '#!%;'] if self.remainingline and self.remainingline[-1] == '\\': # this is a continuation: drop this item and read next line @@ -432,9 +435,16 @@ tail attribute if set says to only use last tail data points when setting ''' - + def __init__(self, descriptor): + # convert descriptor to part objects + descriptor = descriptor.strip() self._parseDescriptor(descriptor) + + # construct data names automatically + self.autodescr = (descriptor == '') + + # get read for reading data self.clearState() def clearState(self): @@ -442,10 +452,9 @@ self.datasets = {} self.blocks = None self.tail = None - + def _parseDescriptor(self, descriptor): """Take a descriptor, and parse it into its individual parts.""" - self.parts = interpretDescriptor(descriptor) def readData(self, stream, useblocks=False, ignoretext=False): @@ -465,7 +474,7 @@ def _readDataUnblocked(self, stream, ignoretext): """Read in that data from the stream.""" - allparts = self.parts + allparts = list(self.parts) # loop over lines while stream.newLine(): @@ -474,6 +483,7 @@ descriptor = ' '.join(stream.remainingline[1:]) self._parseDescriptor(descriptor) allparts += self.parts + self.autodescr = False elif ( self.ignoretext and len(stream.remainingline) > 0 and text_start_re.match(stream.remainingline[0]) and len(self.parts) > 0 and @@ -486,6 +496,16 @@ # normal text for p in self.parts: p.readFromStream(stream, self.datasets) + + # automatically create parts if data are remaining + if self.autodescr: + while len(stream.remainingline) > 0: + p = DescriptorPart( + str(len(self.parts)+1), None, 'D', None ) + p.readFromStream(stream, self.datasets) + self.parts.append(p) + allparts.append(p) + stream.flushLine() self.parts = allparts @@ -497,11 +517,11 @@ blocks = {} block = 1 while stream.newLine(): - l = stream.remainingline + line = stream.remainingline # if this is a blank line, separating data then advance to a new # block - if len(l) == 0 or l[0].lower() == 'no': + if len(line) == 0 or line[0].lower() == 'no': # blank lines separate blocks if block in blocks: block += 1 @@ -509,6 +529,16 @@ # read in data for p in self.parts: p.readFromStream(stream, self.datasets, block=block) + + # automatically create parts if data are remaining + if self.autodescr: + while len(stream.remainingline) > 0: + p = DescriptorPart( + str(len(self.parts)+1), None, 'D', None ) + p.readFromStream(stream, self.datasets, block=block) + self.parts.append(p) + allparts.append(p) + blocks[block] = True # lose remaining data @@ -548,14 +578,19 @@ else: blocks = self.blocks + # if automatically making parts, use a prefix/suffix if not set + if self.autodescr and prefix == '' and suffix == '': + prefix = 'col' + names = [] for block in blocks: for part in self.parts: - names += part.setInDocument(self.datasets, document, - block=block, - linkedfile=linkedfile, - prefix=prefix, suffix=suffix, - tail=self.tail) + names += part.setInDocument( + self.datasets, document, + block=block, + linkedfile=linkedfile, + prefix=prefix, suffix=suffix, + tail=self.tail) return names diff -Nru veusz-1.10/document/svg_export.py veusz-1.14/document/svg_export.py --- veusz-1.10/document/svg_export.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/svg_export.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,12 +16,11 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: svg_export.py 1406 2010-09-12 14:35:03Z jeremysanders $ - """A home-brewed SVG paint engine for doing svg with clipping and exporting text as paths for WYSIWYG.""" import sys +import re import veusz.qtall as qt4 dpi = 90. @@ -31,9 +30,22 @@ """Change a float to a string, using a maximum number of decimal places but removing trailing zeros.""" - val = ('%.'+str(prec)+'f') % round(v, prec) + # this is to get consistent rounding to get the self test correct... yuck + # decimal would work, but that drags in loads of code + # convert float to string with prec decimal places + + fmt = '% 10.' + str(prec) + 'f' + v1 = fmt % (v-1e-6) + v2 = fmt % (v+1e-6) + + # always round down + if v1 < v2: + val = v1 + else: + val = v2 + # drop any trailing zeros - val = val.rstrip('0').rstrip('.') + val = val.rstrip('0').lstrip(' ').rstrip('.') # get rid of -0s (platform differences here) if val == '-0': val = '0' @@ -73,6 +85,44 @@ i += 1 return ''.join(p) +class SVGElement(object): + """SVG element in output. + This represents the XML tree in memory + """ + + def __init__(self, parent, eltype, attrb, text=None): + """Intialise element. + parent: parent element or None + eltype: type (e.g. 'polyline') + attrb: attribute string appended to output + text: text to output between this and closing element. + """ + self.eltype = eltype + self.attrb = attrb + self.children = [] + self.parent = parent + self.text = text + + if parent: + parent.children.append(self) + + def write(self, fileobj): + """Write element and its children to the output file.""" + fileobj.write('<%s' % self.eltype) + if self.attrb: + fileobj.write(' ' + self.attrb) + + if self.text: + fileobj.write('>%s\n' % (self.text, self.eltype)) + elif self.children: + fileobj.write('>\n') + for c in self.children: + c.write(fileobj) + fileobj.write('\n' % self.eltype) + else: + # simple close tag if not children or text + fileobj.write('/>\n') + class SVGPaintEngine(qt4.QPaintEngine): """Paint engine class for writing to svg files.""" @@ -88,7 +138,7 @@ qt4.QPaintEngine.PixmapTransform | qt4.QPaintEngine.AlphaBlend ) - + self.width = width_in self.height = height_in @@ -97,7 +147,6 @@ def begin(self, paintdevice): """Start painting.""" self.device = paintdevice - self.fileobj = paintdevice.fileobj self.pen = qt4.QPen() self.brush = qt4.QBrush() @@ -105,48 +154,60 @@ self.clipnum = 0 self.existingclips = {} self.matrix = qt4.QMatrix() - - self.lastclip = None - self.laststate = None - - self.defs = [] - - self.fileobj.write(''' - - -Veusz output document -''' % (fltStr(self.width*dpi), fltStr(self.height*dpi))) - - # as defaults use qt defaults - self.fileobj.write('\n') + + # svg root element for qt defaults + self.rootelement = SVGElement( + None, 'svg', + ('width="%spx" height="%spx" version="1.1"\n' + ' xmlns="http://www.w3.org/2000/svg"\n' + ' xmlns:xlink="http://www.w3.org/1999/xlink"') % + (fltStr(self.width*dpi), fltStr(self.height*dpi))) + SVGElement(self.rootelement, 'desc', '', 'Veusz output document') + + # definitions, for clips, etc. + self.defs = SVGElement(self.rootelement, 'defs', '') + + # this is where all the drawing goes + self.celement = SVGElement( + self.rootelement, 'g', + 'stroke-linejoin="bevel" stroke-linecap="square" ' + 'stroke="#000000" fill-rule="evenodd"') + + # previous transform, stroke and clip states + self.oldstate = [None, None, None] + + # cache paths to avoid duplication + self.pathcache = {} + self.pathcacheidx = 0 return True + def pruneEmptyGroups(self): + """Take the element tree and remove any empty group entries.""" + + def recursive(root): + children = list(root.children) + # remove any empty children first + for c in children: + recursive(c) + if root.eltype == 'g' and len(root.children) == 0: + # safe to remove + index = root.parent.children.index(root) + del root.parent.children[index] + + recursive(self.rootelement) + def end(self): - # close any existing groups - if self.laststate is not None: - self.fileobj.write('\n') - if self.lastclip is not None: - self.fileobj.write('\n') - - # close defaults - self.fileobj.write('\n') - - # write any defined objects - if self.defs: - self.fileobj.write('\n') - for d in self.defs: - self.fileobj.write(d) - self.fileobj.write('\n') + self.pruneEmptyGroups() + + fileobj = self.device.fileobj + fileobj.write('\n' + '\n') + + # write all the elements + self.rootelement.write(fileobj) - # end svg file - self.fileobj.write('\n') return True def _updateClipPath(self, clippath, clipoperation): @@ -160,31 +221,76 @@ elif clipoperation == qt4.Qt.UniteClip: self.clippath = self.clippath.unite(clippath) else: - print clipoperation + assert False def updateState(self, state): """Examine what has changed in state and call apropriate function.""" - self.updatedstate = True ss = state.state() + + # state is a list of transform, stroke/fill and clip states + statevec = list(self.oldstate) if ss & qt4.QPaintEngine.DirtyPen: self.pen = state.pen() + statevec[1] = self.strokeFillState() if ss & qt4.QPaintEngine.DirtyBrush: self.brush = state.brush() + statevec[1] = self.strokeFillState() if ss & qt4.QPaintEngine.DirtyClipPath: self._updateClipPath(state.clipPath(), state.clipOperation()) + statevec[2] = self.clipState() if ss & qt4.QPaintEngine.DirtyClipRegion: path = qt4.QPainterPath() path.addRegion(state.clipRegion()) self._updateClipPath(path, state.clipOperation()) + statevec[2] = self.clipState() if ss & qt4.QPaintEngine.DirtyTransform: self.matrix = state.matrix() + statevec[0] = self.transformState() - def getSVGState(self): - """Get state as svg group.""" - # these are the values to write into the attribute - vals = {} + # work out which state differs first + pop = 0 + for i in xrange(2, -1, -1): + if statevec[i] != self.oldstate[i]: + pop = i+1 + break + + # go back up the tree the required number of times + for i in xrange(pop): + if self.oldstate[i]: + self.celement = self.celement.parent + + # create new elements for changed states + for i in xrange(pop-1, -1, -1): + if statevec[i]: + self.celement = SVGElement( + self.celement, 'g', ' '.join(statevec[i])) + + self.oldstate = statevec + + def clipState(self): + """Get SVG clipping state. This is in the form of an svg group""" + + if self.clippath is None: + return () + + path = createPath(self.clippath, 1.0) - # PEN UPDATE + if path in self.existingclips: + url = 'url(#c%i)' % self.existingclips[path] + else: + clippath = SVGElement(self.defs, 'clipPath', + 'id="c%i"' % self.clipnum) + SVGElement(clippath, 'path', 'd="%s"' % path) + url = 'url(#c%i)' % self.clipnum + self.existingclips[path] = self.clipnum + self.clipnum += 1 + + return ('clip-path="%s"' % url,) + + def strokeFillState(self): + """Return stroke-fill state.""" + + vals = {} p = self.pen # - color color = p.color().name() @@ -232,100 +338,69 @@ if b.color().alphaF() != 1.0: vals['fill-opacity'] = '%.3g' % b.color().alphaF() - # MATRIX + items = ['%s="%s"' % x for x in sorted(vals.items())] + return tuple(items) + + def transformState(self): if not self.matrix.isIdentity(): m = self.matrix dx, dy = m.dx(), m.dy() if (m.m11(), m.m12(), m.m21(), m.m22()) == (1., 0., 0., 1): - vals['transform'] = 'translate(%s, %s)' % (fltStr(dx), - fltStr(dy)) + out = ('transform="translate(%s,%s)"' % (fltStr(dx), fltStr(dy)) ,) else: - vals['transform'] = 'matrix(%s %s %s %s %s %s)' % ( - fltStr(m.m11(), 4), fltStr(m.m12(), 4), - fltStr(m.m21(), 4), fltStr(m.m22(), 4), - fltStr(dx), fltStr(dy) ) - - # build up group for state - t = ['\n' - return state - - def getClipState(self): - """Get SVG clipping state. This is in the form of an svg group""" - - if self.clippath is None: - return None - - path = createPath(self.clippath, 1.0) - - if path in self.existingclips: - url = 'url(#c%i)' % self.existingclips[path] + out = ('transform="matrix(%s %s %s %s %s %s)"' % ( + fltStr(m.m11(), 4), fltStr(m.m12(), 4), + fltStr(m.m21(), 4), fltStr(m.m22(), 4), + fltStr(dx), fltStr(dy) ),) else: - clippath = '\n' % ( - self.clipnum, path) - - self.defs.append(clippath) - url = 'url(#c%i)' % self.clipnum - self.existingclips[path] = self.clipnum - self.clipnum += 1 - - return '\n' % url - - def doStateUpdate(self): - """Handle changes of state, starting and stopping - groups to modify clipping and attributes.""" - if not self.updatedstate: - return - - clipgrp = self.getClipState() - state = self.getSVGState() - - if clipgrp == self.lastclip and state == self.laststate: - # do nothing if everything is unchanged - pass - elif clipgrp == self.lastclip: - # if state has only changed - if self.laststate is not None: - self.fileobj.write('\n') - self.fileobj.write(state) - self.laststate = state - else: - # clip and state have changed - if self.laststate is not None: - self.fileobj.write('\n') - if self.lastclip is not None: - self.fileobj.write('\n') - self.fileobj.write(clipgrp) - self.fileobj.write(state) - self.laststate = state - self.lastclip = clipgrp + out = () + return out def drawPath(self, path): """Draw a path on the output.""" - self.doStateUpdate() p = createPath(path, 1.) - self.fileobj.write('\n') + attrb += ' fill-rule="nonzero"' + + if attrb in self.pathcache: + element, num = self.pathcache[attrb] + if num is None: + # this is the first time an element has been referenced again + # assign it an id for use below + num = self.pathcacheidx + self.pathcacheidx += 1 + self.pathcache[attrb] = element, num + # add an id attribute + element.attrb += ' id="p%i"' % num + + # if the parent is a translation, swallow this into the use element + m = re.match('transform="translate\(([-0-9.]+),([-0-9.]+)\)"', + self.celement.attrb) + if m: + SVGElement(self.celement.parent, 'use', + 'xlink:href="#p%i" x="%s" y="%s"' % ( + num, m.group(1), m.group(2))) + else: + SVGElement(self.celement, 'use', 'xlink:href="#p%i"' % num) + else: + pathel = SVGElement(self.celement, 'path', attrb) + self.pathcache[attrb] = [pathel, None] def drawTextItem(self, pt, textitem): """Convert text to a path and draw it. """ - self.doStateUpdate() path = qt4.QPainterPath() path.addText(pt, textitem.font(), textitem.text()) p = createPath(path, 1.) - self.fileobj.write('\n' % ( - p, self.pen.color().name(), self.pen.color().alphaF() )) + SVGElement( + self.celement, 'path', + 'd="%s" fill="%s" stroke="none" fill-opacity="%.3g"' % ( + p, self.pen.color().name(), self.pen.color().alphaF()) ) def drawLines(self, lines): """Draw multiple lines.""" - self.doStateUpdate() paths = [] for line in lines: path = 'M%s,%sl%s,%s' % ( @@ -333,123 +408,96 @@ fltStr(line.x2()-line.x1()), fltStr(line.y2()-line.y1())) paths.append(path) - self.fileobj.write('\n' % (''.join(paths))) + SVGElement(self.celement, 'path', 'd="%s"' % ''.join(paths)) def drawPolygon(self, points, mode): """Draw polygon on output.""" - self.doStateUpdate() pts = [] for p in points: pts.append( '%s,%s' % (fltStr(p.x()), fltStr(p.y())) ) if mode == qt4.QPaintEngine.PolylineMode: - self.fileobj.write('\n' % - ' '.join(pts)) + SVGElement(self.celement, 'polyline', + 'fill="none" points="%s"' % ' '.join(pts)) else: - self.fileobj.write('\n') + attrb += ' fill-rule="nonzero"' + SVGElement(self.celement, 'polygon', attrb) def drawEllipse(self, rect): """Draw an ellipse to the svg file.""" - self.doStateUpdate() - self.fileobj.write('\n' % - (fltStr(rect.center().x()), fltStr(rect.center().y()), - fltStr(rect.width()*0.5), fltStr(rect.height()*0.5))) + SVGElement(self.celement, 'ellipse', + 'cx="%s" cy="%s" rx="%s" ry="%s"' % + (fltStr(rect.center().x()), fltStr(rect.center().y()), + fltStr(rect.width()*0.5), fltStr(rect.height()*0.5))) def drawPoints(self, points): """Draw points.""" - self.doStateUpdate() for pt in points: - self.fileobj.write( '\n' % - fltStr(pt.x()), fltStr(pt.y()), - fltStr(pt.x()), fltStr(pt.y()) ) + SVGElement(self.celement, 'line', + ('x1="%s" y1="%s" x2="%s" y2="%s" ' + 'stroke-linecap="round"') % + fltStr(pt.x()), fltStr(pt.y()), + fltStr(pt.x()), fltStr(pt.y()) ) + + def drawImage(self, r, img, sr, flags): + """Draw image. + As the pixmap method uses the same code, just call this.""" + self.drawPixmap(r, img, sr) def drawPixmap(self, r, pixmap, sr): - """Draw pixmap to file. + """Draw pixmap svg item. - This is converted to a PNG and embedded in the output + This is converted to a bitmap and embedded in the output """ - self.doStateUpdate() - self.fileobj.write( '\n') + attrb = [ 'x="%s" y="%s" ' % (fltStr(r.x()), fltStr(r.y())), + 'width="%s" ' % fltStr(r.width()), + 'height="%s" ' % fltStr(r.height()), + 'xlink:href="data:image/%s;base64,' % self.imageformat, + str(data.toBase64()), + '" preserveAspectRatio="none"' ] + SVGElement(self.celement, 'image', ''.join(attrb)) class SVGPaintDevice(qt4.QPaintDevice): - """Paint device for SVG paint engine.""" - - def __init__(self, fileobj, width_in, height_in): - qt4.QPaintDevice.__init__(self) - self.engine = SVGPaintEngine(width_in, height_in) - self.fileobj = fileobj - - def paintEngine(self): - return self.engine - - def width(self): - return self.engine.width*dpi - - def widthMM(self): - return int(self.width() * inch_mm) - - def height(self): - return self.engine.height*dpi - - def heightMM(self): - return int(self.height() * inch_mm) - - def logicalDpiX(self): - return dpi - - def logicalDpiY(self): - return dpi - - def physicalDpiX(self): - return dpi - - def physicalDpiY(self): - return dpi - - def depth(self): - return 24 - - def numColors(self): - return 2147483647 - - def metric(self, m): - if m & qt4.QPaintDevice.PdmWidth: - return self.width() - elif m & qt4.QPaintDevice.PdmHeight: - return self.height() - elif m & qt4.QPaintDevice.PdmWidthMM: - return self.widthMM() - elif m & qt4.QPaintDevice.PdmHeightMM: - return self.heightMM() - elif m & qt4.QPaintDevice.PdmNumColors: - return self.numColors() - elif m & qt4.QPaintDevice.PdmDepth: - return self.depth() - elif m & qt4.QPaintDevice.PdmDpiX: - return self.logicalDpiX() - elif m & qt4.QPaintDevice.PdmDpiY: - return self.logicalDpiY() - elif m & qt4.QPaintDevice.PdmPhysicalDpiX: - return self.physicalDpiX() - elif m & qt4.QPaintDevice.PdmPhysicalDpiY: - return self.physcialDpiY() + """Paint device for SVG paint engine.""" + def __init__(self, fileobj, width_in, height_in): + qt4.QPaintDevice.__init__(self) + self.engine = SVGPaintEngine(width_in, height_in) + self.fileobj = fileobj + + def paintEngine(self): + return self.engine + + def metric(self, m): + """Return the metrics of the painter.""" + if m == qt4.QPaintDevice.PdmWidth: + return int(self.engine.width * dpi) + elif m == qt4.QPaintDevice.PdmHeight: + return int(self.engine.height * dpi) + elif m == qt4.QPaintDevice.PdmWidthMM: + return int(self.engine.width * inch_mm) + elif m == qt4.QPaintDevice.PdmHeightMM: + return int(self.engine.height * inch_mm) + elif m == qt4.QPaintDevice.PdmNumColors: + return 2147483647 + elif m == qt4.QPaintDevice.PdmDepth: + return 24 + elif m == qt4.QPaintDevice.PdmDpiX: + return int(dpi) + elif m == qt4.QPaintDevice.PdmDpiY: + return int(dpi) + elif m == qt4.QPaintDevice.PdmPhysicalDpiX: + return int(dpi) + elif m == qt4.QPaintDevice.PdmPhysicalDpiY: + return int(dpi) diff -Nru veusz-1.10/document/widgetfactory.py veusz-1.14/document/widgetfactory.py --- veusz-1.10/document/widgetfactory.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/document/widgetfactory.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,8 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: widgetfactory.py 1140 2010-01-30 11:58:36Z jeremysanders $ - class WidgetFactory(object): """Class to help produce any type of widget you want by name.""" @@ -47,7 +45,7 @@ # allow subsettings to be set using __ -> syntax name = name.replace('__', '/') - w.prefLookup(name).val = val + w.prefLookup(name).set(val) if autoadd: w.addDefaultSubWidgets() diff -Nru veusz-1.10/Documents/document_api.py veusz-1.14/Documents/document_api.py --- veusz-1.10/Documents/document_api.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/Documents/document_api.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: document_api.py 1197 2010-03-17 20:04:57Z jeremysanders $ - """ Document veusz widget types and settings Creates an xml file designed to be processed into a web page using xsl diff -Nru veusz-1.10/Documents/generate_manual.sh veusz-1.14/Documents/generate_manual.sh --- veusz-1.10/Documents/generate_manual.sh 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/Documents/generate_manual.sh 2011-11-22 20:23:31.000000000 +0000 @@ -29,6 +29,3 @@ release=$(cat ../VERSION) pod2man --release=${release} --center="Veusz" veusz.pod > veusz.1 pod2man --release=${release} --center="Veusz" veusz_listen.pod > veusz_listen.1 - -################################################################### -# $Id: generate_manual.sh 1465 2010-12-01 17:20:48Z jeremysanders $ diff -Nru veusz-1.10/Documents/manual.html veusz-1.14/Documents/manual.html --- veusz-1.10/Documents/manual.html 2010-12-12 12:41:42.000000000 +0000 +++ veusz-1.14/Documents/manual.html 2011-11-22 20:24:16.000000000 +0000 @@ -34,7 +34,7 @@ >Jeremy SandersCopyright © 2011

This document is released under the GNU Free Documention - Licence, Version 1.1 or any later version published by the Free - Software Foundation; with no Invariant Sections, no Front-Cover - Texts. and no Back-Cover Texts.

This document is licensed under the GNU General Public + License, version 2 or greater. Please see the file COPYING for + details, or see http://www.gnu.org/licenses/gpl-2.0.html.

Introduction
Veusz
Terminology
Widget
Measurements
Settings
Axis numbers
Installation
The main window
My first plot
Reading data
Reading CSV files
Reading FITS files
Reading other data formats
Manipulating datasets
Using dataset plugins
Using expressions to create new datasets
Linking datasets to expressions
Splitting data
Defining new constants or functions
Dataset plugins
Introduction
Commands
ForceUpdate
Security
Using Veusz from other programs
Non-Qt Python programs
PyQt4 programs
Non Python programs
C, C++ and Fortran

Introduction

Veusz


Terminology


Widget

- plot distribution of points in a dataset.

  • polar - plot polar data or + functions. This is a non-orthogonal plot and is placed + directly on the page rather than in a graph.

  • ternary - plot data of three + variables which add up to 100 per cent.This is a + non-orthogonal plot and is placed directly on the page + rather than in a graph.


  • Measurements


    Settings



    Axis numbers

    C-style number formatting is used with a few Veusz specific extensions. Text can be mixed with format specifiers, @@ -1226,7 +1262,7 @@ >


    Installation


    The main window

    In recent versions there also exists a dataset browsing + window, by default to the right of the screen. This window + allows you to view the datasets currently loaded, their + dimensions and type. Hovering a mouse over the size of the + dataset will give you a preview of the data.


    My first plot


    Reading data

    Currently Veusz supports reading data from a text file, FITS - format files or from CSV files. Reading data is supported using - the "Data, Import" dialog, or using - the ImportFile @@ -1432,8 +1474,8 @@ commands which read data from files or an existing Python string (allowing data to be embedded in a Python script). In addition, the user can load or write plugins in Python which load data into - Veusz in an arbitrary format. At the moment QDP files are - supported with this method. + Veusz in an arbitrary format. At the moment QDP, binary and + NPY/NPZ files are supported with this method.


    If a descriptor is left blank, Veusz will automatically + create dataset names. If the prefix and suffix settings are + blank, they are assigned names col1, col2, etc. If prefix and + suffix are not blank, the datasets are called + prefix+number+suffix.

    When reading in data, Veusz treats any whitespace as separating columns. The columns do not actually need to be in columns! Furthermore a "\" symbol can be placed at the end of @@ -1680,7 +1728,7 @@ >


    Reading CSV files

    Text datasets are not yet autodetected for CSV files. You - can specify a text dataset by using "(text)" after the name of the - dataset at the top of the column. A future release show allow a - more general method for autodetecting data or specifying, as the - standard Veusz file format currently does.

    The data type in CSV files are automatically detected unless + specified. The data type can be given in brackets after the column + name, e.g. "name (text)", where the data type is "date", "numeric" + or "text". Explicit data types are needed if the data look like a + different data type (e.g. a text item of "1.23"). The date format + in CSV files can be specified in the import dialog box - see the + examples given. In addition CSV files support numbers in European + format (e.g. 2,34 rather than 2.34), depending on the setting in + the dialog box.


    Manipulating datasets


    Using dataset plugins


    Using expressions to create new datasets


    Linking datasets to expressions


    Splitting data


    Defining new constants or functions


    Dataset plugins

    In addition to creating datasets based on expressions, a + variety of dataset plugins exist, which make certain + operations on datasets much more convenient. See the Data, + Operations menu for a list of the default plugins. The user + can easily create new plugins. See + http://barmag.net/veusz-wiki/DatasetPlugins for details. +


    Introduction


    Commands




    AddCustom(name, type, value)AddCustom(type, name, value)

    Add a custom function or value for evaluating - expressions, or import external python functions.

    Add a custom definition for evaluation of + expressions. This can define a constant (can be in terms of + other constants), a function of 1 or more variables, or a + function imported from an external python module.

    For a constant value name should be the name of the - constant. type should be 'constant'. value is the value of the - constant.

    ctype is "constant", "function" or "import".

    For a function, name should be of the format - 'functionname(x,y,z)' where x, y and z are its arguments. type - should be 'function'. value is the definition of the function, - e.g. 'x+y+z'.

    name is name of constant, or "function(x, y, ...)" or + module name.

    For a Python import, name should be the module to import - from. type is 'import'. value is the name of the symbol to - import.

    val is definition for constant or function (both are + _strings_), or is a list of symbols for a module (comma + separated items in a string).

    If mode is 'appendalways', the custom value is appended + to the end of the list even if there is one with the same + name. If mode is 'replace', it replaces any existing + definition in the same place in the list or is appended + otherwise. If mode is 'append', then an existing definition is + deleted, and the new one appended to the end.








    Export(filename, color=True, - page=0)

    Export the page given to the filename given. The - filename must end with the correct extension - to get the right sort of output file. Currrenly supported - extensions are '.eps', '.pdf', '.svg', '.jpg', '.jpeg', '.bmp' - and '.png'. If - must end with the correct + extension to get the right sort of output file. Currrenly + supported extensions are '.eps', '.pdf', '.svg', '.jpg', + '.jpeg', '.bmp' and '.png'. If color is True, then the output is in colour, - else greyscale. is + True, then the output is in colour, else + greyscale. page is the page number of - the document to export (starting from 0 for the first - page!).

    is the page number of the + document to export (starting from 0 for the first page!). + dpi is the number of dots per inch for + bitmap output files. antialias - + antialiases output if True. quality is a + quality parameter for jpeg + output. backcolor is the background color + for bitmap files, which is a name or a #RRGGBBAA value (red, + green, blue, alpha). pdfdpi is the dpi to + use when exporting EPS or PDF files. +


    ForceUpdate

    ForceUpdate()

    Force the window to be updated to reflect the current + state of the document. Often used when periodic updates have + been disabled (see SetUpdateInterval). This command is only + supported in embedded mode or from veusz_listen.



































    Tells window to update every interval milliseconds at most. The value 0 disables updates until this function is - called with a positive value.

    Note: this command is only supported in the embedding interface or veusz_listen.








    Security


    Using Veusz from other programs


    PyQt4 programs


    Non Python programs


    C, C++ and Fortran

    - - Veusz - a scientific plotting package @@ -17,18 +15,18 @@ - 2010 + 2011 - This document is released under the GNU Free Documention - Licence, Version 1.1 or any later version published by the Free - Software Foundation; with no Invariant Sections, no Front-Cover - Texts. and no Back-Cover Texts. + + This document is licensed under the GNU General Public + License, version 2 or greater. Please see the file COPYING for + details, or see . - Introduction @@ -336,6 +334,19 @@ of points in a dataset. + + polar - plot polar data or + functions. This is a non-orthogonal plot and is placed + directly on the page rather than in a graph. + + + + ternary - plot data of three + variables which add up to 100 per cent.This is a + non-orthogonal plot and is placed directly on the page + rather than in a graph. + + @@ -450,7 +461,8 @@ appropriate date formatting is used so that the interval shown is correct. A format can be given for an axis in the axis number formatting panel can be given to explicitly choose a - format. + format. Some examples are given in the drop down axis + menu. Hold the mouse over the example for detail. C-style number formatting is used with a few Veusz specific extensions. Text can be mixed with format specifiers, @@ -555,6 +567,12 @@ history like many Unix shells. Press the up and down cursor keys to browse through the history. Command line completion is not available yet! + + In recent versions there also exists a dataset browsing + window, by default to the right of the screen. This window + allows you to view the datasets currently loaded, their + dimensions and type. Hovering a mouse over the size of the + dataset will give you a preview of the data.

    @@ -645,20 +663,19 @@ - Reading data Currently Veusz supports reading data from a text file, FITS - format files or from CSV files. Reading data is supported using - the "Data, Import" dialog, or using - the ImportFile + format files, CSV files, QDP files, binary files and NPY/NPZ + files. Reading data is supported using the "Data, Import" dialog, + or using the ImportFile and ImportString commands which read data from files or an existing Python string (allowing data to be embedded in a Python script). In addition, the user can load or write plugins in Python which load data into - Veusz in an arbitrary format. At the moment QDP files are - supported with this method. + Veusz in an arbitrary format. At the moment QDP, binary and + NPY/NPZ files are supported with this method. @@ -761,6 +778,12 @@ errors. The signs on positive or negative errors are automatically set to be correct. + If a descriptor is left blank, Veusz will automatically + create dataset names. If the prefix and suffix settings are + blank, they are assigned names col1, col2, etc. If prefix and + suffix are not blank, the datasets are called + prefix+number+suffix. + When reading in data, Veusz treats any whitespace as separating columns. The columns do not actually need to be in columns! Furthermore a "\" symbol can be placed at the end of @@ -870,11 +893,15 @@ updated when the file changes. See the example CSV import for details. - Text datasets are not yet autodetected for CSV files. You - can specify a text dataset by using "(text)" after the name of the - dataset at the top of the column. A future release show allow a - more general method for autodetecting data or specifying, as the - standard Veusz file format currently does. + The data type in CSV files are automatically detected unless + specified. The data type can be given in brackets after the column + name, e.g. "name (text)", where the data type is "date", "numeric" + or "text". Explicit data types are needed if the data look like a + different data type (e.g. a text item of "1.23"). The date format + in CSV files can be specified in the import dialog box - see the + examples given. In addition CSV files support numbers in European + format (e.g. 2,34 rather than 2.34), depending on the setting in + the dialog box.
    @@ -990,6 +1017,7 @@ dataset to an expression, so if the expression changes the dataset changes with it, like in a spreadsheet. +
    Splitting data @@ -1021,11 +1049,22 @@ box.
    +
    + Dataset plugins + In addition to creating datasets based on expressions, a + variety of dataset plugins exist, which make certain + operations on datasets much more convenient. See the Data, + Operations menu for a list of the default plugins. The user + can easily create new plugins. See + for details. +
    + + - <anchor id="Commands" />Command line interface @@ -1112,23 +1151,28 @@
    <anchor id="Command.AddCustom" />AddCustom - AddCustom(name, type, value) + AddCustom(type, name, value) - Add a custom function or value for evaluating - expressions, or import external python functions. - - For a constant value name should be the name of the - constant. type should be 'constant'. value is the value of the - constant. - - For a function, name should be of the format - 'functionname(x,y,z)' where x, y and z are its arguments. type - should be 'function'. value is the definition of the function, - e.g. 'x+y+z'. - - For a Python import, name should be the module to import - from. type is 'import'. value is the name of the symbol to - import. + Add a custom definition for evaluation of + expressions. This can define a constant (can be in terms of + other constants), a function of 1 or more variables, or a + function imported from an external python module. + + ctype is "constant", "function" or "import". + + name is name of constant, or "function(x, y, ...)" or + module name. + + val is definition for constant or function (both are + _strings_), or is a list of symbols for a module (comma + separated items in a string). + + If mode is 'appendalways', the custom value is appended + to the end of the list even if there is one with the same + name. If mode is 'replace', it replaces any existing + definition in the same place in the list or is appended + otherwise. If mode is 'append', then an existing definition is + deleted, and the new one appended to the end.
    @@ -1210,17 +1254,38 @@ <anchor id="Command.Export" />Export Export(filename, color=True, - page=0) + page=0 dpi=100, + antialias=True, quality=85, backcolor='#ffffff00', + pdfdpi=150) Export the page given to the filename given. The - filename must end with the correct extension - to get the right sort of output file. Currrenly supported - extensions are '.eps', '.pdf', '.svg', '.jpg', '.jpeg', '.bmp' - and '.png'. If - color is True, then the output is in colour, - else greyscale. page is the page number of - the document to export (starting from 0 for the first - page!). + filename must end with the correct + extension to get the right sort of output file. Currrenly + supported extensions are '.eps', '.pdf', '.svg', '.jpg', + '.jpeg', '.bmp' and '.png'. If color is + True, then the output is in colour, else + greyscale. page is the page number of the + document to export (starting from 0 for the first page!). + dpi is the number of dots per inch for + bitmap output files. antialias - + antialiases output if True. quality is a + quality parameter for jpeg + output. backcolor is the background color + for bitmap files, which is a name or a #RRGGBBAA value (red, + green, blue, alpha). pdfdpi is the dpi to + use when exporting EPS or PDF files. + +
    + +
    + <anchor id="Command.ForceUpdate" />ForceUpdate + + ForceUpdate() + + Force the window to be updated to reflect the current + state of the document. Often used when periodic updates have + been disabled (see SetUpdateInterval). This command is only + supported in embedded mode or from veusz_listen.
    @@ -1775,7 +1840,11 @@ Tells window to update every interval milliseconds at most. The value 0 disables updates until this function is - called with a positive value. + called with a non-zero. The value -1 tells Veusz to update the + window every time the document has changed. This will make + things slow if repeated changes are made to the + document. Disabling updates and using the ForceUpdate command + will allow the user to control updates directly. Note: this command is only supported in the embedding interface or veusz_listen. @@ -1877,7 +1946,6 @@ - Using Veusz from other programs @@ -1964,7 +2032,7 @@ command line by doing something like: -veusz_listen < in.vsz +veusz_listen < in.vsz where in.vsz contains: diff -Nru veusz-1.10/Documents/veusz.1 veusz-1.14/Documents/veusz.1 --- veusz-1.10/Documents/veusz.1 2010-12-12 12:41:53.000000000 +0000 +++ veusz-1.14/Documents/veusz.1 2011-11-22 20:24:17.000000000 +0000 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 2.23 (Pod::Simple 3.14) +.\" Automatically generated by Pod::Man 2.25 (Pod::Simple 3.16) .\" .\" Standard preamble: .\" ======================================================================== @@ -124,7 +124,7 @@ .\" ======================================================================== .\" .IX Title "VEUSZ 1" -.TH VEUSZ 1 "2010-12-12" "1.10" "Veusz" +.TH VEUSZ 1 "2011-11-22" "1.13.999" "Veusz" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -168,9 +168,14 @@ .IX Item "--export=FILE" Export the next Veusz document file on the command line to the graphics file \fI\s-1FILE\s0\fR. Supported file types include \s-1EPS\s0, \s-1PDF\s0, \s-1SVG\s0, -\&\s-1PNG\s0, \s-1BMP\s0 and \s-1JPG\s0. The extension of the output file is used to +\&\s-1PNG\s0, \s-1BMP\s0, \s-1JPG\s0 and \s-1XPM\s0. The extension of the output file is used to determine the output file format. There should be as many export options specified as input Veusz documents on the command line. +.IP "\fB\-\-plugin\fR=\fI\s-1FILE\s0\fR" 8 +.IX Item "--plugin=FILE" +Loads the Veusz plugin \fI\s-1FILE\s0\fR when starting Veusz. This option +provides a per-session alternative to adding the plugin in the +preferences dialog box. .IP "\fB\-\-help\fR" 8 .IX Item "--help" Displays the options to the program and exits. @@ -187,7 +192,7 @@ This manual page was written by Jeremy Sanders . .SH "COPYRIGHT" .IX Header "COPYRIGHT" -Copyright (C) 2003\-2010 Jeremy Sanders . +Copyright (C) 2003\-2011 Jeremy Sanders . .PP This program is free software; you can redistribute it and/or modify it under the terms of the \s-1GNU\s0 General Public License as published by the diff -Nru veusz-1.10/Documents/veusz_listen.1 veusz-1.14/Documents/veusz_listen.1 --- veusz-1.10/Documents/veusz_listen.1 2010-12-12 12:41:53.000000000 +0000 +++ veusz-1.14/Documents/veusz_listen.1 2011-11-22 20:24:17.000000000 +0000 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 2.23 (Pod::Simple 3.14) +.\" Automatically generated by Pod::Man 2.25 (Pod::Simple 3.16) .\" .\" Standard preamble: .\" ======================================================================== @@ -124,7 +124,7 @@ .\" ======================================================================== .\" .IX Title "VEUSZ_LISTEN 1" -.TH VEUSZ_LISTEN 1 "2010-12-12" "1.10" "Veusz" +.TH VEUSZ_LISTEN 1 "2011-11-22" "1.13.999" "Veusz" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -159,7 +159,7 @@ This manual page was written by Jeremy Sanders . .SH "COPYRIGHT" .IX Header "COPYRIGHT" -Copyright (C) 2003\-2010 Jeremy Sanders . +Copyright (C) 2003\-2011 Jeremy Sanders . .PP This program is free software; you can redistribute it and/or modify it under the terms of the \s-1GNU\s0 General Public License as published by the diff -Nru veusz-1.10/Documents/veusz_listen.pod veusz-1.14/Documents/veusz_listen.pod --- veusz-1.10/Documents/veusz_listen.pod 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/Documents/veusz_listen.pod 2011-11-22 20:23:31.000000000 +0000 @@ -35,7 +35,7 @@ =head1 COPYRIGHT -Copyright (C) 2003-2010 Jeremy Sanders . +Copyright (C) 2003-2011 Jeremy Sanders . This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the diff -Nru veusz-1.10/Documents/veusz.pod veusz-1.14/Documents/veusz.pod --- veusz-1.10/Documents/veusz.pod 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/Documents/veusz.pod 2011-11-22 20:23:31.000000000 +0000 @@ -46,10 +46,16 @@ Export the next Veusz document file on the command line to the graphics file I. Supported file types include EPS, PDF, SVG, -PNG, BMP and JPG. The extension of the output file is used to +PNG, BMP, JPG and XPM. The extension of the output file is used to determine the output file format. There should be as many export options specified as input Veusz documents on the command line. +=item B<--plugin>=I + +Loads the Veusz plugin I when starting Veusz. This option +provides a per-session alternative to adding the plugin in the +preferences dialog box. + =item B<--help> Displays the options to the program and exits. @@ -72,7 +78,7 @@ =head1 COPYRIGHT -Copyright (C) 2003-2010 Jeremy Sanders . +Copyright (C) 2003-2011 Jeremy Sanders . This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the diff -Nru veusz-1.10/embed.py veusz-1.14/embed.py --- veusz-1.10/embed.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/embed.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: embed.py 1331 2010-07-15 20:08:41Z jeremysanders $ - """This module allows veusz to be embedded within other Python programs. For example: @@ -49,9 +47,9 @@ import new import cPickle import socket -import random import subprocess import time +import uuid # check remote process has this API version API_VERSION = 1 @@ -196,14 +194,14 @@ # here is the list of commands to try possiblecommands = [ - [findpython, os.path.join(thisdir, 'embed_remote.py')], + [findpython, os.path.join(thisdir, 'veusz_main.py')], [findexe] ] else: # try embed_remote.py in this directory, veusz in this directory # or veusz on the path in order possiblecommands = [ [sys.executable, - os.path.join(thisdir, 'embed_remote.py')], + os.path.join(thisdir, 'veusz_main.py')], [os.path.join(thisdir, 'veusz')], [findOnPath('veusz')] ] @@ -251,17 +249,15 @@ if waitaccept: cls.serv_socket, address = cls.serv_socket.accept() - # send a secret to the remote program by secure route and - # check it comes back - # this is to check that no program has secretly connected - # on our port, which isn't really useful for AF_UNIX sockets - secret = ''.join([random.choice('ABCDEFGHUJKLMNOPQRSTUVWXYZ' - 'abcdefghijklmnopqrstuvwxyz' - '0123456789') - for i in xrange(16)]) + '\n' + # Send a secret to the remote program by secure route and + # check it comes back. This is to check that no program has + # secretly connected on our port, which isn't really useful + # for AF_UNIX sockets. + secret = str(uuid.uuid4()) + '\n' stdin.write(secret) secretback = cls.readLenFromSocket(cls.serv_socket, len(secret)) - assert secret == secretback + if secret != secretback: + raise RuntimeError, "Security between client and server broken" # packet length for command bytes cls.cmdlen = struct.calcsize('>sys.stderr, ("This program must be run from " - "the Veusz embedding module") - sys.exit(1) - +def runremote(): + """Run remote end of embedding module.""" # get connection parameters params = sys.stdin.readline().split() @@ -277,6 +283,3 @@ app = EmbedApplication(listensocket, []) app.setQuitOnLastWindowClosed(False) app.exec_() - -if __name__ == '__main__': - main() diff -Nru veusz-1.10/examples/coloredpoints.vsz veusz-1.14/examples/coloredpoints.vsz --- veusz-1.10/examples/coloredpoints.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/examples/coloredpoints.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,360 @@ +# Veusz saved document (version 1.13.99) +# Saved at 2011-11-09T19:53:17.746203 + +ImportString('c(numeric)',''' +1.459606e+02 +1.702896e+02 +7.934817e+01 +1.227687e+01 +5.599601e+03 +1.539531e+01 +1.828166e+04 +5.054989e+03 +3.191051e+02 +7.949710e+00 +2.216535e+03 +5.143240e+03 +3.148290e+01 +4.965444e+01 +7.777005e+02 +3.185130e+02 +1.588387e+04 +2.813152e+02 +1.339360e+02 +4.215790e+03 +6.843036e+03 +8.415999e+01 +1.362926e+02 +6.243027e+03 +6.179793e+02 +1.897636e+03 +2.796312e+02 +1.026779e+01 +4.765936e+03 +1.570359e+04 +1.467323e+02 +1.164478e+03 +1.902282e+02 +1.520207e+04 +2.973087e+00 +2.337408e+00 +4.134565e+02 +7.166886e+03 +4.217587e+03 +9.953222e+02 +8.267064e+01 +5.035324e+00 +9.556994e+00 +8.546561e+01 +1.500701e+01 +1.351431e+01 +4.917555e+03 +2.496430e+00 +2.103088e+01 +1.077053e+03 +1.563147e+02 +3.163246e+03 +1.626294e+04 +4.627788e+03 +8.483016e+03 +5.805220e+03 +1.600690e+01 +2.339657e+02 +1.526946e+02 +4.282832e+02 +2.799937e+02 +3.737549e+02 +2.842098e+01 +1.275867e+03 +4.526153e+04 +2.403380e+04 +4.756101e+01 +2.977493e+04 +5.519853e+01 +2.365612e+02 +4.460124e+03 +2.131700e+03 +6.597176e+03 +8.787685e+02 +3.755261e+04 +5.750869e+02 +7.043527e+02 +8.845572e+03 +1.043050e+02 +5.741879e+00 +2.603017e+02 +1.178276e+01 +9.850848e+00 +9.117799e+02 +1.102115e+02 +9.833058e+01 +2.374284e+02 +1.129614e+04 +2.973567e+03 +2.594940e+02 +3.478364e+01 +2.132164e+00 +7.268064e+03 +1.879249e+01 +1.321168e+02 +1.057914e+03 +7.070878e+01 +4.039144e+00 +7.826971e+01 +1.123204e+04 +''') +SetDataExpression(u'sizef', u'abs(x-0.5)+0.5', linked=True) +ImportString('x(numeric)',''' +3.259640e-01 +2.936538e-01 +3.162201e-01 +2.638131e-01 +8.803033e-01 +2.577980e-01 +9.479319e-01 +5.931048e-01 +5.222732e-01 +1.018496e-01 +7.321032e-01 +7.753362e-01 +2.971560e-01 +3.875175e-01 +4.714681e-01 +5.461489e-01 +8.912572e-01 +3.138063e-01 +3.838737e-01 +7.173045e-01 +9.487288e-01 +3.029883e-01 +4.863407e-01 +8.589109e-01 +5.710536e-01 +7.576283e-01 +5.049896e-01 +2.308246e-01 +8.570804e-01 +8.612760e-01 +4.759630e-01 +5.248471e-01 +4.719874e-01 +7.990987e-01 +9.636075e-02 +6.375893e-02 +6.057062e-01 +9.102558e-01 +7.864667e-01 +6.260576e-01 +4.087506e-01 +1.741066e-01 +1.165042e-01 +3.446296e-01 +2.170302e-01 +9.573020e-02 +8.219256e-01 +9.182756e-02 +2.018576e-01 +6.840850e-01 +3.888053e-01 +8.064366e-01 +9.381433e-01 +8.748492e-01 +8.716737e-01 +8.351529e-01 +1.047454e-01 +4.707636e-01 +4.919583e-01 +5.672392e-01 +4.050533e-01 +4.783146e-01 +3.282565e-01 +6.755866e-01 +9.435113e-01 +9.404685e-01 +2.656279e-01 +9.456492e-01 +3.985884e-01 +5.263482e-01 +6.925591e-01 +6.756365e-01 +9.412488e-01 +6.768923e-01 +8.147229e-01 +5.731504e-01 +5.591458e-01 +8.806983e-01 +4.030426e-01 +1.638092e-01 +4.160340e-01 +2.343386e-01 +1.942402e-01 +6.532868e-01 +3.740195e-01 +3.338149e-01 +3.336983e-01 +8.617688e-01 +7.426878e-01 +4.504087e-01 +2.690837e-01 +5.143151e-02 +9.142272e-01 +1.996254e-01 +5.223122e-01 +7.060125e-01 +3.945721e-01 +1.191278e-01 +3.037703e-01 +8.536895e-01 +''') +ImportString('y(numeric)',''' +1.720759e+00 +-2.113146e+00 +-1.269313e+00 +6.767068e-02 +-4.538878e-01 +-3.123929e-01 +-9.405761e-01 +-2.662602e+00 +-8.296818e-01 +-9.859058e-01 +-8.345237e-01 +1.219784e+00 +6.189014e-01 +2.917761e-01 +-2.009880e+00 +-6.370632e-01 +1.271855e+00 +-2.387936e+00 +-1.182805e+00 +1.511322e+00 +8.066724e-02 +-1.426305e+00 +-3.782192e-01 +-7.195032e-01 +-1.013519e+00 +-4.953992e-01 +-8.532545e-01 +-1.763571e-01 +4.996532e-01 +-1.501790e+00 +-5.253474e-01 +-1.933486e+00 +7.826508e-01 +1.971016e+00 +1.755292e-01 +2.273975e-01 +3.872100e-01 +-4.286146e-01 +9.583945e-01 +-9.874666e-01 +5.646978e-01 +1.120199e-02 +1.028609e+00 +-1.106546e+00 +6.163468e-01 +-1.495746e+00 +-8.080937e-01 +6.001841e-02 +-1.030854e+00 +-5.917945e-01 +-1.277557e+00 +-5.487731e-01 +-9.172515e-01 +3.319534e-01 +-8.837110e-01 +8.464141e-01 +1.570651e+00 +-9.721957e-01 +-4.319809e-01 +7.255485e-01 +1.653870e+00 +-1.318657e+00 +2.812261e-01 +-8.069181e-01 +-1.763368e+00 +-1.237897e+00 +-1.229479e+00 +-1.382508e+00 +2.951479e-01 +-5.371013e-01 +-1.758221e+00 +-1.252360e+00 +-1.087257e-01 +-4.726106e-01 +-2.631497e+00 +-9.342638e-01 +1.222414e+00 +-8.478654e-01 +8.122695e-01 +2.076344e-01 +-1.502682e+00 +2.677853e-01 +4.330256e-01 +-6.934856e-01 +1.092298e+00 +1.314858e+00 +-2.081479e+00 +-1.211710e+00 +1.005053e+00 +-1.224985e+00 +-9.300805e-01 +2.461890e-01 +-4.090199e-01 +9.509655e-01 +-6.341827e-02 +4.008008e-01 +5.423699e-01 +2.595563e-01 +1.357025e+00 +-1.271401e+00 +''') +Set('StyleSheet/Font/font', u'Arial') +Set('StyleSheet/axis/Line/width', u'1pt') +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Set('Background/color', u'#e5e9ff') +Set('Border/width', u'1pt') +Add('colorbar', name='colorbar1', autoadd=False) +To('colorbar1') +Set('widgetName', u'xy1') +Set('label', u'Power (W)') +Set('autoExtend', False) +Set('autoExtendZero', False) +Set('lowerPosition', 0.0) +Set('upperPosition', 1.0) +Set('otherPosition', 0.0) +Set('TickLabels/format', u'%VE') +Set('horzPosn', u'centre') +Set('vertPosn', u'top') +Set('width', u'8cm') +To('..') +Add('xy', name='xy1', autoadd=False) +To('xy1') +Set('marker', u'circle') +Set('markerSize', u'5pt') +Set('scalePoints', u'sizef') +Set('Color/points', u'c') +Set('Color/min', 2.0) +Set('Color/max', 4500.0) +Set('Color/scaling', u'log') +Set('PlotLine/hide', True) +Set('MarkerFill/colorMap', u'complement') +To('..') +Add('axis', name='x', autoadd=False) +To('x') +Set('label', u'Time (yr)') +Set('GridLines/hide', False) +To('..') +Add('axis', name='y', autoadd=False) +To('y') +Set('label', u'Offset (m)') +Set('min', -3.0) +Set('max', 3.0) +Set('autoExtend', False) +Set('direction', 'vertical') +Set('GridLines/hide', False) +To('..') +To('..') +To('..') diff -Nru veusz-1.10/examples/embedexample.py veusz-1.14/examples/embedexample.py --- veusz-1.10/examples/embedexample.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/examples/embedexample.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: embedexample.py 964 2009-05-10 11:26:12Z jeremysanders $ - """An example embedding program. Veusz needs to be installed into the Python path for this to work (use setup.py) diff -Nru veusz-1.10/examples/linked_datasets.vsz veusz-1.14/examples/linked_datasets.vsz --- veusz-1.10/examples/linked_datasets.vsz 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/examples/linked_datasets.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -1,127 +1,33 @@ -# Veusz saved document (version 0.10.cvs) -# User: jss -# Date: Wed, 14 Jun 2006 20:08:16 +0000 +# Veusz saved document (version 1.12.999) +# Saved at 2011-08-18T18:35:23.805605 -SetDataExpression(u'xhalf', u'x/2*y', linked=True) +SetDataRange(u't', 100, (-3.141592, 3.141592), linked=True) +SetDataExpression(u'x', u'sin(t)', linked=True) SetDataExpression(u'x2', u'sin(t*8)', linked=True) -ImportString(u't',''' --3.141593e+00 --3.078126e+00 --3.014660e+00 --2.951193e+00 --2.887727e+00 --2.824260e+00 --2.760793e+00 --2.697327e+00 --2.633860e+00 --2.570394e+00 --2.506927e+00 --2.443461e+00 --2.379994e+00 --2.316528e+00 --2.253061e+00 --2.189595e+00 --2.126128e+00 --2.062662e+00 --1.999195e+00 --1.935729e+00 --1.872262e+00 --1.808796e+00 --1.745329e+00 --1.681863e+00 --1.618396e+00 --1.554930e+00 --1.491463e+00 --1.427997e+00 --1.364530e+00 --1.301064e+00 --1.237597e+00 --1.174131e+00 --1.110664e+00 --1.047198e+00 --9.837310e-01 --9.202645e-01 --8.567980e-01 --7.933315e-01 --7.298649e-01 --6.663984e-01 --6.029319e-01 --5.394654e-01 --4.759989e-01 --4.125324e-01 --3.490658e-01 --2.855993e-01 --2.221328e-01 --1.586663e-01 --9.519978e-02 --3.173326e-02 -3.173326e-02 -9.519978e-02 -1.586663e-01 -2.221328e-01 -2.855993e-01 -3.490658e-01 -4.125324e-01 -4.759989e-01 -5.394654e-01 -6.029319e-01 -6.663984e-01 -7.298649e-01 -7.933315e-01 -8.567980e-01 -9.202645e-01 -9.837310e-01 -1.047198e+00 -1.110664e+00 -1.174131e+00 -1.237597e+00 -1.301064e+00 -1.364530e+00 -1.427997e+00 -1.491463e+00 -1.554930e+00 -1.618396e+00 -1.681863e+00 -1.745329e+00 -1.808796e+00 -1.872262e+00 -1.935729e+00 -1.999195e+00 -2.062662e+00 -2.126128e+00 -2.189595e+00 -2.253061e+00 -2.316528e+00 -2.379994e+00 -2.443461e+00 -2.506927e+00 -2.570394e+00 -2.633860e+00 -2.697327e+00 -2.760793e+00 -2.824260e+00 -2.887727e+00 -2.951193e+00 -3.014660e+00 -3.078126e+00 -3.141593e+00 -''') SetDataExpression(u'x3', u'sin(x*2)', linked=True) +SetDataExpression(u'xhalf', u'x/2*y', linked=True) SetDataExpression(u'y', u'cos(t)', linked=True) -SetDataExpression(u'x', u'sin(t)', linked=True) SetDataExpression(u'y2', u'cos(t*16)', linked=True) SetDataExpression(u'yhalf', u'y/2', linked=True) +Set('StyleSheet/Line/width', u'1pt') +Set('StyleSheet/Font/font', u'Arial') +Set('StyleSheet/axis/Label/size', u'18pt') +Set('StyleSheet/axis/MajorTicks/number', 8) +Set('StyleSheet/xy/markerSize', u'4pt') Add('page', name='page1', autoadd=False) To('page1') Add('graph', name='graph1', autoadd=False) To('graph1') +Set('Background/color', u'#e9ffff') Add('axis', name='x', autoadd=False) To('x') Set('label', u'Experiments with linked datasets') +Set('autoExtend', False) To('..') Add('axis', name='y', autoadd=False) To('y') -Set('label', u'Funky chicken') +Set('label', u'Another axis') +Set('autoExtend', False) Set('direction', 'vertical') To('..') Add('xy', name='xy1', autoadd=False) @@ -133,14 +39,18 @@ To('xy2') Set('xData', u'x2') Set('yData', u'y2') +Set('PlotLine/color', u'#00aa00') Set('MarkerFill/color', u'green') To('..') Add('xy', name='xy3', autoadd=False) To('xy3') Set('xData', u'x3') Set('marker', u'star') +Set('markerSize', u'6pt') Set('PlotLine/color', u'red') Set('PlotLine/width', u'1pt') +Set('MarkerLine/hide', False) +Set('MarkerFill/color', u'red') To('..') Add('xy', name='xy4', autoadd=False) To('xy4') diff -Nru veusz-1.10/examples/starchart.vsz veusz-1.14/examples/starchart.vsz --- veusz-1.10/examples/starchart.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/examples/starchart.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,1268 @@ +# Veusz saved document (version 1.12.99) +# Saved at 2011-08-17T21:38:56.668330 + +ImportString('size(numeric)',''' +4.050e-01 +3.455e-01 +5.800e-01 +8.497e-01 +8.651e-01 +8.397e-01 +7.715e-01 +3.617e-01 +2.565e-01 +6.906e-01 +5.932e-01 +8.708e-01 +2.335e-01 +2.521e-01 +4.786e-01 +7.606e-01 +3.874e-01 +3.600e-01 +5.842e-01 +7.647e-01 +4.191e-01 +7.555e-01 +8.471e-01 +4.407e-01 +4.300e-01 +1.249e-01 +6.256e-02 +4.905e-01 +2.311e-02 +3.583e-01 +7.830e-01 +5.041e-01 +3.003e-01 +6.946e-01 +4.093e-01 +3.897e-01 +4.539e-01 +6.352e-01 +8.445e-01 +2.948e-01 +5.420e-01 +4.801e-01 +8.155e-01 +4.433e-02 +3.789e-01 +5.625e-01 +6.695e-02 +7.941e-01 +3.804e-01 +9.344e-01 +8.059e-01 +1.631e-01 +4.861e-02 +6.394e-01 +3.932e-01 +2.929e-01 +8.481e-01 +3.624e-01 +3.574e-02 +1.552e-01 +3.909e-01 +4.646e-02 +8.587e-01 +1.063e-02 +9.837e-01 +7.219e-01 +7.637e-01 +6.057e-01 +8.209e-01 +4.198e-01 +6.915e-01 +9.860e-01 +9.955e-01 +3.070e-01 +4.891e-01 +9.027e-02 +1.901e-01 +4.686e-01 +4.856e-01 +8.366e-01 +2.897e-02 +9.239e-01 +7.923e-01 +4.719e-01 +7.960e-01 +9.449e-01 +5.453e-01 +6.638e-01 +7.995e-01 +9.712e-02 +3.511e-01 +5.018e-01 +1.632e-01 +1.294e-02 +6.344e-01 +6.543e-02 +7.970e-01 +2.727e-01 +2.513e-01 +8.460e-01 +3.325e-02 +7.681e-02 +5.476e-01 +7.590e-01 +8.837e-01 +1.755e-01 +9.289e-01 +1.040e-01 +4.746e-01 +6.847e-02 +6.449e-01 +6.703e-01 +5.274e-02 +4.158e-01 +9.168e-01 +1.963e-01 +4.417e-01 +4.567e-01 +2.895e-01 +5.929e-03 +9.152e-01 +8.444e-01 +8.529e-01 +5.076e-01 +4.550e-01 +6.566e-01 +7.721e-01 +3.514e-01 +3.746e-01 +9.664e-01 +1.667e-01 +9.201e-02 +4.317e-01 +4.654e-01 +6.015e-01 +6.814e-01 +4.866e-01 +9.389e-01 +8.908e-01 +4.130e-02 +4.716e-01 +5.987e-01 +4.767e-02 +8.905e-01 +8.800e-01 +7.935e-01 +7.342e-01 +3.535e-01 +9.705e-01 +1.858e-01 +9.075e-01 +9.458e-02 +1.833e-01 +2.194e-01 +1.657e-01 +2.087e-01 +4.359e-01 +3.506e-01 +9.515e-01 +8.851e-01 +4.048e-02 +1.991e-01 +3.447e-01 +2.877e-01 +1.286e-01 +3.182e-01 +4.290e-01 +7.130e-01 +5.366e-01 +5.687e-01 +5.497e-01 +7.495e-01 +7.154e-01 +2.551e-01 +4.554e-01 +6.916e-01 +6.407e-01 +2.440e-01 +9.261e-01 +5.933e-02 +8.469e-01 +3.196e-01 +8.569e-01 +6.384e-01 +2.937e-01 +5.292e-02 +1.931e-01 +2.760e-01 +2.912e-02 +3.069e-01 +9.244e-01 +1.239e-01 +2.516e-01 +4.644e-01 +9.013e-02 +9.745e-01 +3.796e-01 +2.707e-02 +9.780e-01 +2.674e-01 +''') +ImportString('size2(numeric)',''' +4.149e-01 +6.146e-01 +2.448e-01 +7.628e-02 +1.768e-01 +2.063e-01 +5.393e-01 +2.900e-01 +3.645e-01 +8.713e-02 +5.542e-01 +8.222e-02 +4.418e-01 +6.340e-01 +6.466e-01 +6.498e-01 +6.988e-01 +1.329e-01 +3.229e-01 +5.532e-01 +4.700e-01 +1.371e-01 +3.681e-01 +5.719e-01 +6.000e-01 +5.295e-01 +3.881e-01 +2.361e-01 +1.955e-01 +1.751e-01 +7.426e-01 +5.149e-01 +3.111e-01 +2.939e-01 +1.782e-01 +3.758e-01 +6.307e-01 +5.059e-01 +7.406e-01 +4.687e-01 +8.772e-03 +6.461e-02 +4.382e-02 +9.037e-02 +4.053e-01 +5.267e-01 +9.926e-02 +5.119e-01 +4.667e-01 +2.564e-01 +1.839e-01 +3.433e-01 +3.153e-01 +3.838e-01 +5.806e-01 +4.922e-01 +3.729e-02 +1.960e-01 +6.638e-01 +5.121e-01 +6.760e-01 +2.950e-01 +2.766e-01 +2.515e-01 +7.347e-01 +1.599e-02 +2.976e-01 +1.024e-01 +3.245e-01 +2.576e-02 +4.938e-01 +5.979e-01 +6.476e-01 +5.961e-01 +2.611e-01 +5.783e-01 +4.673e-01 +3.781e-01 +7.286e-01 +2.363e-01 +6.622e-01 +4.862e-01 +7.304e-01 +3.534e-01 +6.367e-01 +1.068e-01 +3.883e-01 +6.653e-01 +5.703e-01 +2.747e-01 +3.201e-01 +5.755e-01 +6.744e-01 +3.886e-01 +7.911e-02 +8.700e-02 +3.535e-01 +1.245e-01 +1.195e-01 +5.841e-01 +6.398e-01 +1.762e-01 +5.250e-01 +5.847e-01 +4.941e-01 +6.708e-01 +3.526e-01 +4.299e-01 +4.609e-01 +4.045e-02 +3.338e-01 +4.018e-01 +5.591e-01 +2.480e-01 +3.281e-01 +2.539e-01 +6.600e-01 +7.943e-02 +3.481e-01 +1.625e-02 +5.123e-01 +3.390e-01 +3.904e-01 +4.350e-01 +5.448e-01 +4.653e-01 +4.141e-01 +1.425e-01 +4.031e-01 +3.517e-01 +6.214e-01 +7.483e-01 +6.904e-01 +5.430e-02 +2.179e-01 +4.177e-01 +1.834e-01 +5.792e-02 +2.297e-01 +5.871e-01 +2.647e-01 +1.269e-01 +2.579e-01 +7.247e-01 +4.171e-01 +2.275e-01 +7.145e-01 +6.673e-01 +3.532e-01 +6.283e-01 +9.327e-02 +1.819e-01 +6.711e-01 +2.019e-01 +5.201e-01 +3.551e-01 +8.790e-02 +4.421e-01 +6.433e-01 +5.816e-01 +4.612e-01 +5.547e-02 +2.555e-01 +6.520e-01 +2.823e-01 +4.777e-01 +4.286e-01 +3.108e-01 +1.322e-01 +4.525e-01 +3.051e-01 +1.148e-01 +6.799e-01 +7.144e-01 +1.421e-01 +2.629e-01 +1.992e-01 +4.288e-01 +2.898e-01 +5.219e-01 +1.484e-01 +6.617e-01 +2.670e-01 +7.308e-01 +4.514e-01 +5.690e-01 +2.678e-01 +7.160e-02 +3.514e-02 +2.425e-01 +3.181e-01 +3.302e-01 +3.846e-01 +2.463e-01 +5.676e-02 +5.476e-01 +2.593e-01 +1.812e-01 +2.168e-01 +5.940e-01 +''') +ImportString('x(numeric)',''' +-1.717564e-01 +4.242809e-01 +1.582449e+00 +9.042384e-01 +-7.558175e-01 +9.180096e-01 +1.688422e+00 +-5.624878e-01 +1.148113e+00 +-6.757747e-01 +-7.184862e-01 +9.559501e-02 +-7.735163e-01 +-1.368390e+00 +2.839300e-01 +3.863967e-01 +-1.018726e+00 +-1.057551e+00 +1.480676e+00 +-1.436318e+00 +-6.077504e-01 +-6.134922e-01 +-1.451637e+00 +6.367692e-01 +-2.051966e+00 +-1.061056e-01 +1.132593e+00 +-2.332081e+00 +-1.483644e-01 +-1.709232e+00 +7.715420e-01 +1.825963e-01 +-1.242801e+00 +5.126108e-01 +7.855827e-01 +-1.391835e+00 +3.679549e-01 +-1.126583e+00 +7.381246e-01 +-7.205989e-01 +2.466142e-01 +-1.842360e+00 +-1.307182e+00 +7.427424e-01 +-3.075417e-01 +1.075113e+00 +-1.109696e+00 +-3.353667e-02 +-1.087587e+00 +1.517819e-03 +1.402492e+00 +-4.629109e-01 +-2.314234e-01 +-1.404300e+00 +-7.136651e-01 +1.143241e-01 +-2.593171e-01 +5.261581e-01 +7.312414e-01 +9.634989e-01 +6.653014e-02 +-1.525272e+00 +-1.979862e+00 +8.790669e-02 +-3.367087e-01 +-1.487463e+00 +-6.323704e-01 +-2.691609e-01 +-1.192839e+00 +7.129419e-01 +-2.026876e+00 +-1.670084e+00 +-1.105418e+00 +4.942608e-01 +1.440158e+00 +9.895613e-01 +-5.584712e-01 +3.488847e-02 +-5.603513e-03 +9.674879e-01 +6.670954e-02 +9.170016e-01 +-1.313607e+00 +-2.246257e+00 +2.042009e+00 +1.025452e+00 +-8.301401e-01 +4.508979e-01 +5.296303e-01 +1.255025e+00 +1.236309e+00 +-8.178501e-01 +1.658664e+00 +-1.020029e+00 +-7.597083e-01 +1.642019e+00 +1.163782e+00 +3.648611e-01 +1.205375e+00 +-2.208854e+00 +1.961078e-01 +2.150545e-01 +1.335152e+00 +2.279445e-01 +-3.136242e-02 +9.497465e-01 +2.207430e-01 +-7.895290e-01 +-8.275847e-01 +7.666461e-01 +-1.008892e-01 +-1.343381e+00 +-1.484571e+00 +5.167052e-01 +-3.572661e-01 +1.440624e+00 +9.023146e-01 +-3.818540e-01 +-1.613737e+00 +-1.362967e-01 +7.986278e-01 +5.329052e-01 +-7.147625e-02 +1.977507e+00 +6.095448e-01 +-1.774797e+00 +-5.755160e-01 +-6.896392e-01 +3.003489e-01 +1.888296e-01 +7.108283e-01 +8.797144e-01 +2.526157e-01 +3.947440e-01 +8.264080e-01 +-2.686278e-01 +-5.568894e-01 +-8.562743e-01 +-6.682676e-01 +1.613319e-01 +8.180479e-02 +7.551501e-01 +1.345002e-01 +-5.441405e-01 +-4.750194e-01 +-1.144256e+00 +-3.917083e-02 +5.153497e-01 +-3.864621e-01 +-9.947071e-01 +4.151202e-01 +1.694649e-01 +8.304631e-01 +-2.744186e+00 +-5.189704e-01 +5.950722e-01 +4.409983e-01 +-3.860202e-01 +2.654908e+00 +-4.632929e-01 +3.676008e-02 +3.220285e-01 +-1.910937e-01 +-7.468510e-01 +-8.650068e-01 +-9.713170e-01 +-1.130643e-01 +-3.678296e-01 +-4.921382e-01 +1.181965e-01 +6.255350e-01 +-1.242620e+00 +1.156373e+00 +7.030362e-01 +9.894105e-02 +5.791196e-01 +-8.927444e-01 +1.267695e+00 +-7.141264e-01 +2.025816e+00 +-1.293909e+00 +-1.477149e+00 +-3.851663e-02 +-6.038046e-01 +-5.637100e-01 +-7.411827e-01 +-1.302341e+00 +-9.121214e-01 +1.058442e+00 +1.873947e+00 +-6.876215e-01 +-9.217540e-02 +-1.163100e+00 +1.145855e+00 +-4.506662e-01 +-1.423829e+00 +1.236884e-01 +2.303835e-01 +1.388673e+00 +9.321516e-01 +''') +ImportString('x2(numeric)',''' +-5.904152e-01 +6.036018e-01 +1.861803e-01 +-1.182390e-01 +4.273179e-01 +-5.329131e-01 +3.477783e-01 +-4.442659e-01 +4.112825e-01 +-6.975073e-01 +-7.894340e-02 +3.737914e-01 +5.502770e-01 +-1.807715e-01 +2.749290e-01 +8.347531e-01 +-2.566171e-01 +4.480129e-02 +-1.661492e-01 +-1.005103e+00 +6.827219e-02 +1.267272e-01 +-7.289300e-02 +7.991350e-02 +-4.951967e-01 +1.395421e+00 +-1.094929e+00 +3.143240e-01 +1.436579e-01 +5.987043e-01 +8.988077e-04 +6.491476e-01 +-1.945068e-01 +-1.464254e-01 +1.318037e-01 +5.736032e-01 +-6.424045e-01 +1.929405e-02 +1.083004e-01 +-4.876157e-01 +-1.975081e-01 +-9.470392e-01 +7.348370e-02 +-3.555848e-01 +-4.303004e-01 +-1.937558e-01 +-3.060530e-01 +-1.239214e+00 +-7.159538e-01 +-1.180139e-01 +-8.582931e-01 +9.332523e-01 +8.186905e-02 +1.658889e-01 +3.878644e-01 +4.596426e-01 +-2.580098e-01 +1.466808e-01 +-2.334527e-01 +-7.628955e-01 +-4.923554e-02 +-1.866111e-01 +-1.203111e-01 +7.535110e-01 +3.363780e-01 +-7.143939e-01 +9.262992e-01 +4.826713e-02 +-3.288597e-01 +-3.829378e-01 +6.682712e-01 +-7.421102e-01 +4.511552e-02 +-7.936474e-01 +5.949329e-01 +6.510491e-02 +-9.386907e-02 +7.775805e-01 +1.218719e-01 +-1.494402e-02 +-6.653168e-03 +-1.096615e+00 +-5.639730e-01 +-2.858281e-01 +4.137544e-01 +9.787053e-02 +-3.736432e-02 +1.178890e-01 +8.644677e-02 +8.907128e-01 +-2.575110e-01 +8.681292e-01 +3.192787e-01 +-4.730755e-02 +3.308247e-02 +5.196318e-01 +1.612190e-02 +-9.430100e-02 +-3.501541e-01 +1.988158e-01 +-6.610444e-02 +3.183876e-01 +9.110975e-01 +3.537435e-01 +2.405701e-02 +-1.046873e+00 +3.043448e-01 +1.020531e-01 +-5.537825e-01 +-1.344010e-01 +-4.608990e-01 +9.335337e-01 +-9.321411e-02 +3.717192e-01 +3.558881e-01 +-2.581769e-01 +7.042958e-01 +4.890943e-02 +-3.936620e-01 +-1.006861e+00 +-4.333877e-01 +7.150886e-01 +-4.349794e-01 +4.448926e-01 +4.621154e-01 +1.543000e-01 +-1.210074e-01 +-9.640434e-02 +6.018297e-01 +-1.054106e-02 +-9.582333e-02 +-1.221224e+00 +-2.510498e-01 +8.605007e-01 +-2.235232e-01 +6.005776e-01 +5.389756e-02 +2.774058e-01 +-7.967761e-02 +-2.286575e-01 +-2.433566e-01 +-1.338816e-01 +-1.915733e-01 +-9.346132e-02 +-3.945893e-01 +-1.000764e+00 +-4.389132e-01 +-5.512491e-01 +-3.093361e-01 +-5.310395e-01 +5.232963e-01 +-4.543161e-01 +1.269040e-01 +-2.244547e-01 +6.759115e-01 +2.601059e-01 +-4.742974e-01 +7.627344e-02 +1.575795e-01 +2.050366e-01 +3.363180e-01 +5.956497e-01 +7.390307e-01 +6.510501e-01 +-7.047546e-01 +8.470191e-01 +-2.368321e-01 +-2.557962e-01 +9.486894e-01 +-3.198666e-01 +-6.815602e-02 +-2.756185e-01 +9.079499e-01 +6.653649e-01 +1.566721e-01 +-1.089555e+00 +-1.078753e-01 +-1.503253e-01 +1.093731e+00 +6.795102e-01 +9.591976e-01 +7.418597e-02 +-4.870427e-01 +-1.311627e-01 +3.575294e-01 +2.585647e-01 +-1.156918e-01 +-9.740063e-02 +7.856365e-01 +7.415529e-01 +2.188530e-01 +5.858654e-01 +6.431388e-01 +7.472439e-01 +-3.185660e-01 +1.466183e-01 +-3.765560e-01 +-2.363125e-01 +-7.311099e-03 +4.543543e-01 +''') +ImportString('y(numeric)',''' +-1.179418e+00 +9.963364e-01 +1.035245e+00 +-4.930856e-01 +-3.079140e-01 +-8.363301e-01 +3.772350e-01 +-1.793423e+00 +-3.955435e-01 +-7.903513e-01 +2.248519e-01 +8.868708e-01 +1.385193e+00 +-9.564458e-02 +-2.326000e-01 +-1.571620e-01 +6.207154e-01 +3.515783e-01 +-6.039606e-01 +-4.724230e-01 +-2.933388e-01 +-8.633148e-01 +-1.216204e-01 +-9.216055e-01 +-1.218777e+00 +7.417790e-01 +-5.619947e-02 +5.039686e-01 +-2.296762e-01 +-6.494386e-01 +1.621411e+00 +-2.070276e-01 +3.945420e-01 +-6.584078e-02 +-1.396862e-01 +1.662038e+00 +-1.202367e+00 +2.562593e-01 +-2.478231e-01 +-8.922410e-01 +1.175726e+00 +2.447118e-01 +1.086628e+00 +-1.745885e+00 +1.342561e+00 +-9.685250e-01 +1.514767e+00 +-8.541269e-01 +-7.186263e-01 +-2.286539e-01 +-4.432723e-03 +-2.748991e-01 +-1.319403e-01 +-1.297866e+00 +-1.293355e+00 +-2.800920e+00 +4.857350e-01 +1.273159e+00 +-8.956792e-01 +1.418659e+00 +1.314727e+00 +9.482291e-01 +8.534508e-01 +2.193894e+00 +8.981459e-01 +-8.840747e-02 +-8.619552e-01 +4.299827e-01 +-4.838336e-01 +1.537284e+00 +-1.290272e+00 +4.619874e-01 +-1.756571e-01 +1.502658e+00 +-1.084621e+00 +1.366688e+00 +6.150953e-01 +-1.909314e+00 +1.499255e+00 +-5.910319e-01 +1.289383e-01 +-1.935533e+00 +-7.818810e-02 +1.060331e+00 +-8.665276e-01 +3.874504e-01 +5.042166e-01 +6.286272e-01 +1.567413e+00 +3.106100e-01 +7.147362e-01 +-1.696396e+00 +9.068055e-01 +-1.004093e+00 +-5.714048e-01 +-9.721016e-01 +5.559200e-01 +2.595251e+00 +4.741763e-01 +-4.348529e-01 +-1.789185e+00 +-3.293816e-01 +-6.850946e-01 +2.887457e-01 +6.801073e-01 +-3.200756e-01 +5.279586e-01 +-1.002568e+00 +-1.523962e+00 +-8.351697e-01 +1.209814e+00 +-7.876147e-01 +6.289090e-01 +-2.341422e-01 +-3.740775e-01 +2.385541e-01 +-8.140004e-02 +9.481686e-02 +1.225121e+00 +-3.815732e-01 +-7.840012e-01 +2.927405e+00 +9.265183e-01 +7.812431e-01 +1.302915e+00 +6.347094e-01 +5.027754e-01 +-7.110444e-01 +-4.297201e-01 +1.828868e+00 +-4.414072e-02 +-1.110879e+00 +3.847165e-01 +-4.791328e-01 +3.638377e-01 +-1.728797e+00 +-4.163380e-02 +2.300275e-01 +-8.915757e-01 +-6.725092e-01 +9.569900e-01 +-4.703625e-01 +1.274738e+00 +1.264137e+00 +-4.649065e-01 +-4.831641e-02 +-1.410860e+00 +6.752751e-01 +-2.005785e+00 +-8.789505e-01 +-5.581799e-01 +6.318567e-01 +-7.021317e-01 +2.698140e-01 +-2.171639e+00 +-4.838061e-01 +-4.254585e-02 +7.594293e-01 +1.628929e+00 +-7.460201e-01 +-3.770777e-01 +8.849620e-02 +6.164952e-02 +1.475529e+00 +-1.113757e+00 +-1.621159e+00 +4.006426e-01 +1.077418e-01 +-1.913829e+00 +-1.317677e+00 +4.880339e-01 +-4.312087e-01 +-1.442873e-01 +-2.004930e-02 +5.019437e-01 +-7.684092e-01 +1.541577e+00 +-3.599171e-01 +-9.827931e-01 +-1.514225e-01 +3.947186e-02 +1.175757e+00 +1.097290e+00 +-5.108434e-01 +-2.445287e+00 +-7.330248e-02 +-4.411181e-01 +9.852479e-01 +-1.161989e+00 +-1.082814e+00 +-5.290654e-02 +1.425348e+00 +-2.933696e-01 +-8.431636e-01 +1.201905e+00 +7.220561e-02 +-6.565752e-02 +2.359271e+00 +4.880444e-01 +1.560229e+00 +''') +ImportString('y2(numeric)',''' +3.277343e-01 +-4.791839e-01 +-5.622894e-01 +-8.147905e-01 +1.461145e-01 +-4.588977e-01 +3.968568e-01 +1.423664e-01 +5.319692e-01 +5.065758e-01 +-6.637735e-01 +-2.946463e-01 +-4.624716e-01 +6.319409e-01 +4.678516e-02 +2.219609e-01 +1.170330e+00 +7.035864e-01 +-3.346529e-01 +1.734514e-03 +7.248274e-01 +6.135434e-01 +1.028304e-01 +-1.206416e-01 +2.208649e-02 +6.919419e-01 +3.821196e-01 +2.178115e-01 +1.770095e-01 +-1.652230e-02 +9.046649e-01 +-4.432710e-01 +2.206628e-01 +-4.543656e-01 +1.301324e+00 +5.292525e-01 +-4.944746e-01 +2.174136e-01 +-3.947546e-01 +4.118183e-01 +3.404201e-01 +6.837868e-01 +8.153092e-01 +-1.681122e-01 +-8.328832e-01 +-9.128235e-02 +-7.871977e-01 +1.963190e-01 +6.868842e-01 +7.185369e-01 +-1.860621e-01 +9.277049e-01 +-5.712796e-01 +9.867310e-01 +-7.430959e-01 +-4.200795e-01 +-3.312437e-01 +3.417390e-01 +5.875478e-02 +-8.900409e-01 +4.459047e-01 +-2.478977e-01 +2.576785e-01 +-1.180131e+00 +3.503088e-01 +2.095792e-01 +-5.624632e-01 +-1.504682e-01 +1.082096e-01 +2.748657e-02 +-2.188184e-01 +-3.769354e-01 +3.687838e-01 +-1.177511e-01 +-2.714600e-01 +-1.006103e-01 +-1.509157e-01 +7.425458e-02 +1.376503e-01 +-4.981824e-01 +-4.858082e-01 +2.773604e-01 +1.011194e-02 +7.998077e-01 +5.213352e-01 +-2.153188e-01 +7.149105e-01 +-4.479349e-02 +4.948704e-02 +-3.989437e-01 +-2.229656e-01 +2.417801e-01 +4.207912e-01 +-2.746693e-01 +2.094705e-01 +1.712305e-01 +-5.440275e-01 +2.324411e-01 +1.052073e+00 +-1.872281e-01 +6.171029e-01 +1.811711e-02 +3.791602e-01 +-8.040386e-02 +2.399947e-01 +-3.413228e-01 +1.895484e-01 +-2.544584e-01 +-4.259011e-02 +6.655630e-01 +-7.508104e-01 +3.565060e-01 +6.546992e-01 +-5.122739e-01 +-5.014537e-01 +4.045582e-01 +9.928116e-01 +6.045285e-01 +5.509190e-02 +6.477872e-01 +4.653931e-01 +3.743647e-01 +8.731676e-01 +2.466262e-01 +1.092800e-01 +-2.989571e-02 +1.348799e-01 +1.836956e-01 +2.409095e-01 +4.300016e-01 +-1.057074e+00 +-6.740874e-01 +-9.931708e-02 +-1.897434e-01 +8.633123e-01 +1.121861e+00 +7.219913e-01 +5.390399e-01 +-1.552721e-02 +-2.794775e-01 +1.651937e-01 +3.403011e-01 +-4.625169e-01 +2.946608e-01 +2.377226e-01 +-3.711845e-01 +1.413453e-01 +2.804835e-01 +-1.872180e-01 +4.300357e-01 +-4.654751e-01 +4.160271e-01 +1.007708e+00 +-1.936004e-02 +2.207483e-01 +-6.654413e-01 +-4.479407e-01 +-1.364038e-01 +4.005023e-01 +-2.469230e-01 +-4.057847e-01 +-4.814272e-01 +-4.971257e-01 +-2.414501e-01 +-1.648537e-01 +8.348442e-02 +-1.048533e-01 +3.140404e-01 +3.173624e-01 +2.430033e-02 +-2.236744e-01 +-2.959903e-01 +-2.569196e-02 +5.527547e-01 +-2.281922e-01 +-2.492468e-01 +4.654103e-01 +7.154733e-01 +-5.553536e-01 +-6.881153e-01 +8.689605e-01 +6.762373e-01 +-1.865834e-01 +3.385609e-01 +1.006165e-01 +-1.133610e+00 +3.109142e-01 +1.346475e-01 +-2.651954e-01 +7.159829e-02 +-4.473595e-01 +-8.366994e-01 +4.135409e-01 +-6.879046e-01 +-2.671874e-01 +5.195351e-01 +-2.111901e-01 +5.907788e-01 +6.770766e-02 +-5.751893e-02 +''') +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Set('leftMargin', u'0.2cm') +Set('bottomMargin', u'0.2cm') +Add('axis', name='x', autoadd=False) +To('x') +Set('TickLabels/hide', True) +To('..') +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +Set('TickLabels/hide', True) +To('..') +Add('xy', name='xy1', autoadd=False) +To('xy1') +Set('scalePoints', u'size') +Set('PlotLine/hide', True) +To('..') +Add('xy', name='xy2', autoadd=False) +To('xy2') +Set('xData', u'x2') +Set('yData', u'y2') +Set('marker', u'circle') +Set('scalePoints', u'size2') +Set('PlotLine/hide', True) +Set('MarkerLine/hide', True) +Set('MarkerFill/color', u'#5555ff') +To('..') +Add('function', name=u'horz', autoadd=False) +To(u'horz') +Set('function', u'0') +Set('Line/color', u'lightgrey') +To('..') +Add('function', name=u'vert', autoadd=False) +To(u'vert') +Set('function', u'0') +Set('variable', u'y') +Set('Line/color', u'lightgrey') +To('..') +Add('ellipse', name='ellipse1', autoadd=False) +To('ellipse1') +Set('xPos', [0.5]) +Set('yPos', [0.5]) +Set('width', [0.8]) +Set('height', [0.8]) +Set('rotate', [0.0]) +Set('Border/color', u'grey') +Set('Border/width', u'0.25pt') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/examples/ternary.vsz veusz-1.14/examples/ternary.vsz --- veusz-1.10/examples/ternary.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/examples/ternary.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,435 @@ +# Veusz saved document (version 1.12.99) +# Saved at 2011-08-14T11:00:24.842710 + +ImportString('a1(numeric)',''' +3.785032e+01 +2.887586e+01 +3.090787e+01 +3.991348e+01 +2.194688e+01 +6.907754e+00 +3.731477e+01 +2.729718e+01 +4.962433e+01 +3.593354e+01 +2.980925e+01 +1.883289e+01 +2.864022e+01 +2.385115e+01 +1.916651e+01 +1.163294e+01 +3.201094e+01 +4.940456e+01 +2.211055e+01 +5.347282e+01 +3.966645e+01 +1.129016e+01 +2.900517e+01 +2.285825e+01 +1.434235e+01 +2.491757e+01 +4.324933e+01 +-5.615836e-01 +1.333683e+01 +1.169526e+01 +5.702774e+01 +-4.698968e+00 +2.472287e+01 +3.090273e+01 +2.121293e+01 +2.293004e+00 +3.181916e+01 +3.576081e+01 +2.548760e+01 +3.387097e+01 +1.090547e+01 +4.670771e+01 +3.679762e+01 +1.870816e+01 +4.207707e+01 +8.373339e+00 +1.622552e+01 +4.240925e+01 +1.243271e+01 +9.907227e+00 +''') +ImportString('a2(numeric)',''' +3.927892e+01 +3.147607e+01 +3.585074e+01 +2.532616e+01 +2.748505e+01 +3.089898e+01 +3.265939e+01 +2.699355e+01 +2.724263e+01 +3.627490e+01 +3.543455e+01 +2.613919e+01 +2.955639e+01 +2.698287e+01 +3.423267e+01 +2.446838e+01 +3.444203e+01 +4.034653e+01 +2.890829e+01 +2.838100e+01 +2.620180e+01 +3.366264e+01 +2.950382e+01 +3.507761e+01 +2.963094e+01 +3.659981e+01 +2.953002e+01 +3.010142e+01 +2.482832e+01 +4.350083e+01 +2.834519e+01 +2.657412e+01 +3.719120e+01 +3.122923e+01 +3.240051e+01 +2.650656e+01 +3.104269e+01 +3.162769e+01 +3.344840e+01 +3.266331e+01 +2.874867e+01 +2.454526e+01 +3.835166e+01 +3.302628e+01 +3.290526e+01 +3.631759e+01 +3.109008e+01 +3.474628e+01 +3.992190e+01 +3.665463e+01 +''') +ImportString('a3(numeric)',''' +2.974402e+01 +2.992560e+01 +2.879212e+01 +2.289509e+01 +3.151959e+01 +2.514551e+01 +2.878709e+01 +3.809324e+01 +2.142433e+01 +3.253007e+01 +3.495113e+01 +3.219936e+01 +3.676403e+01 +2.577995e+01 +2.996285e+01 +2.957746e+01 +2.770419e+01 +2.726770e+01 +3.137323e+01 +3.328274e+01 +2.981133e+01 +2.227267e+01 +3.234312e+01 +2.694048e+01 +2.859402e+01 +3.514191e+01 +3.444068e+01 +2.544261e+01 +2.895935e+01 +2.397857e+01 +2.591309e+01 +3.282064e+01 +2.463678e+01 +3.006554e+01 +3.302107e+01 +3.165531e+01 +3.265907e+01 +2.977074e+01 +2.642066e+01 +2.612206e+01 +2.921875e+01 +3.187261e+01 +2.956203e+01 +2.660354e+01 +3.243911e+01 +3.434163e+01 +3.642871e+01 +2.220277e+01 +3.788404e+01 +2.830237e+01 +''') +ImportString('a4(numeric)',''' +6.023658e+01 +6.951139e+01 +6.130916e+01 +5.480901e+01 +5.694097e+01 +5.796104e+01 +5.963835e+01 +5.135586e+01 +6.062245e+01 +5.609685e+01 +5.564031e+01 +6.290610e+01 +5.559147e+01 +5.874261e+01 +6.085174e+01 +5.703215e+01 +5.848376e+01 +6.636230e+01 +6.031409e+01 +5.848922e+01 +6.486602e+01 +5.198642e+01 +5.658263e+01 +6.206651e+01 +6.050066e+01 +6.178179e+01 +5.837051e+01 +6.618575e+01 +6.036681e+01 +6.307698e+01 +5.658911e+01 +5.888594e+01 +6.263513e+01 +6.270467e+01 +6.305208e+01 +5.303999e+01 +4.733556e+01 +6.382467e+01 +5.859786e+01 +6.703914e+01 +5.636757e+01 +5.305304e+01 +6.576543e+01 +4.626336e+01 +5.763185e+01 +5.699938e+01 +6.279347e+01 +6.506666e+01 +5.402032e+01 +5.895224e+01 +''') +ImportString('b1(numeric)',''' +6.650787e+00 +1.687328e+01 +1.010450e+01 +1.502892e+01 +8.124262e+00 +1.579514e+01 +2.195101e+01 +3.239890e+00 +1.600109e+01 +4.296486e+00 +2.970698e+00 +9.968865e+00 +1.679330e+01 +6.402929e+00 +1.273309e+01 +1.358886e+01 +1.144324e+01 +-1.422098e+00 +2.708607e+00 +1.120078e+01 +8.676094e+00 +9.079920e+00 +9.543885e+00 +1.468984e+01 +1.288232e+01 +8.349164e+00 +1.372535e+01 +5.718161e+00 +1.638648e+01 +7.551304e+00 +1.155270e+01 +1.067230e+01 +8.879579e+00 +1.171511e+01 +6.194712e+00 +1.445493e+01 +8.503971e+00 +1.397565e+01 +8.748016e+00 +1.188018e+01 +1.910491e+01 +3.901901e+00 +4.879108e+00 +1.863455e+01 +1.608425e+01 +1.645056e+01 +-5.258673e+00 +8.866189e+00 +3.559357e-01 +6.020759e+00 +''') +ImportString('b2(numeric)',''' +2.738027e+01 +1.836565e+01 +1.566644e+01 +1.609443e+01 +2.403639e+01 +8.289908e+00 +3.651296e+01 +5.184957e+01 +2.844856e+00 +5.319719e+00 +1.482401e+01 +3.813406e+01 +8.649367e+00 +-1.039173e+01 +2.710390e+01 +2.282277e+01 +2.110841e+01 +2.748859e+01 +2.756449e+01 +2.152050e+01 +1.344263e+01 +2.089343e+01 +1.775140e+01 +1.806021e+01 +4.594484e+01 +9.522575e+00 +2.831632e+01 +2.687223e+01 +5.372986e+01 +3.397279e+01 +1.318980e+01 +2.891465e+00 +2.525563e+01 +2.537575e+01 +2.810802e+01 +3.799986e+01 +2.522245e+01 +3.272764e+01 +1.361898e+01 +3.343623e+01 +2.053961e+01 +3.908754e+01 +5.705844e+01 +2.483616e+01 +3.003679e+01 +9.125044e+00 +1.994561e+01 +2.138206e+01 +1.927962e+01 +1.461585e+01 +''') +ImportString(u'label(text)',r''' +u'Nougat' +u'Chocolate' +''') +ImportString(u'lx(numeric)',''' +6.000000e+01 +4.500000e+01 +''') +ImportString(u'ly(numeric)',''' +2.500000e+01 +5.000000e+01 +''') +ImportString('sizes(numeric)',''' +3.703217e-01 +4.291845e-01 +2.342960e-01 +1.000000e-01 +7.013704e-01 +4.164477e-01 +1.065102e+00 +7.756953e-01 +9.482757e-01 +3.618510e-01 +7.314487e-01 +4.392592e-01 +2.367437e-01 +1.065933e-01 +6.584510e-01 +4.378377e-01 +1.532375e-01 +7.235843e-01 +3.666900e-01 +7.430943e-01 +4.697697e-01 +4.473618e-01 +3.818643e-01 +5.482129e-01 +3.484666e-01 +2.090011e-01 +2.950832e-01 +2.944881e-01 +4.601784e-01 +7.396155e-01 +4.672722e-01 +1.195206e-01 +2.994085e-01 +3.087374e-01 +5.446024e-01 +3.683946e-01 +4.009373e-01 +8.466384e-01 +2.609215e-01 +5.970062e-01 +4.180349e-01 +5.768238e-01 +2.538321e-01 +8.176581e-01 +7.842167e-01 +6.022510e-01 +3.519089e-01 +4.928984e-01 +8.822681e-01 +3.058641e-01 +''') +Set('width', '15cm') +Set('height', '13.9cm') +Add('page', name='page1', autoadd=False) +To('page1') +Add('ternary', name='ternary1', autoadd=False) +To('ternary1') +Set('topMargin', '0.72cm') +Set('bottomMargin', '1.28cm') +Set('labelbottom', u'Earth') +Set('labelleft', u'Air') +Set('labelright', u'Fire') +Set('Label/size', u'20pt') +Set('Label/italic', True) +Add('nonorthpoint', name='nonorthpoint4', autoadd=False) +To('nonorthpoint4') +Set('marker', u'star') +Set('markerSize', u'5pt') +Set('data1', u'lx') +Set('data2', u'ly') +Set('labels', u'label') +Set('PlotLine/hide', True) +Set('Label/size', u'18pt') +To('..') +Add('nonorthpoint', name='nonorthpoint1', autoadd=False) +To('nonorthpoint1') +Set('data1', u'a1') +Set('data2', u'a2') +Set('PlotLine/hide', True) +Set('MarkerFill/color', u'#aaffff') +To('..') +Add('nonorthpoint', name='nonorthpoint2', autoadd=False) +To('nonorthpoint2') +Set('marker', u'diamond') +Set('data1', u'a3') +Set('data2', u'a4') +Set('PlotLine/hide', True) +Set('MarkerFill/color', u'red') +To('..') +Add('nonorthpoint', name='nonorthpoint3', autoadd=False) +To('nonorthpoint3') +Set('markerSize', u'5pt') +Set('data1', u'b2') +Set('data2', u'b1') +Set('scalePoints', u'sizes') +Set('PlotLine/hide', True) +Set('MarkerFill/color', u'blue') +To('..') +Add('nonorthfunc', name='nonorthfunc1', autoadd=False) +To('nonorthfunc1') +Set('function', u'40') +Set('PlotLine/color', u'#aa00ff') +Set('PlotLine/width', u'1.5pt') +Set('PlotLine/style', u'dotted') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/examples/tutorialdata.csv veusz-1.14/examples/tutorialdata.csv --- veusz-1.10/examples/tutorialdata.csv 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/examples/tutorialdata.csv 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,7 @@ +"alpha","beta","gamma" +1,2,4 +2,5,6 +3,6,5 +4,13,10 +5,9,6 +6,3,14 diff -Nru veusz-1.10/helpers/__init__.py veusz-1.14/helpers/__init__.py --- veusz-1.10/helpers/__init__.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/helpers/__init__.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,7 +16,5 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: __init__.py 872 2008-12-29 12:51:59Z jeremysanders $ - """Helper compiled routines.""" diff -Nru veusz-1.10/helpers/src/isnan.h veusz-1.14/helpers/src/isnan.h --- veusz-1.10/helpers/src/isnan.h 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/helpers/src/isnan.h 2011-11-22 20:23:31.000000000 +0000 @@ -22,6 +22,7 @@ * for the code itself. */ +#include #include /* You might try changing the above to if you have problems. * Whether you use math.h or cmath, you may need to edit the .cpp file @@ -30,7 +31,7 @@ #if defined(__isnan) # define isNaN(_a) (__isnan(_a)) /* MacOSX/Darwin definition < 10.4 */ -#elif defined(WIN32) || defined(_isnan) +#elif defined(WIN32) || defined(_isnan) || defined(_MSC_VER) # define isNaN(_a) (_isnan(_a)) /* Win32 definition */ #elif defined(isnan) || defined(__FreeBSD__) || defined(__osf__) # define isNaN(_a) (isnan(_a)) /* GNU definition */ @@ -45,6 +46,8 @@ #if defined(__isfinite) # define isFinite(_a) (__isfinite(_a)) /* MacOSX/Darwin definition < 10.4 */ +#elif defined(WIN32) || defined(_finite) || defined(_MSC_VER) +# define isFinite(_a) (_finite(_a)) /* Win32 definition */ #elif defined(__sgi) # define isFinite(_a) (_isfinite(_a)) #elif defined(isfinite) diff -Nru veusz-1.10/helpers/src/_nc_cntr.c veusz-1.14/helpers/src/_nc_cntr.c --- veusz-1.10/helpers/src/_nc_cntr.c 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/helpers/src/_nc_cntr.c 2011-11-22 20:23:31.000000000 +0000 @@ -10,8 +10,6 @@ the entirely different framework of a Python type. It was written by following the Python "Extending and Embedding" tutorial. - - $Id: _nc_cntr.c 1280 2010-06-13 14:53:17Z jeremysanders $ */ /* diff -Nru veusz-1.10/helpers/src/paintelement.h veusz-1.14/helpers/src/paintelement.h --- veusz-1.10/helpers/src/paintelement.h 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/helpers/src/paintelement.h 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,30 @@ +// Copyright (C) 2011 Jeremy S. Sanders +// Email: Jeremy Sanders +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +///////////////////////////////////////////////////////////////////////////// + +#ifndef PAINTELEMENT_H +#define PAINTELEMENT_H + +class QPainter; + +class PaintElement { +public: + virtual ~PaintElement() {}; + virtual void paint(QPainter& painter) = 0; +}; + +#endif diff -Nru veusz-1.10/helpers/src/polylineclip.cpp veusz-1.14/helpers/src/polylineclip.cpp --- veusz-1.10/helpers/src/polylineclip.cpp 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/helpers/src/polylineclip.cpp 2011-11-22 20:23:31.000000000 +0000 @@ -138,7 +138,7 @@ // is difference between points very small? inline bool smallDelta(const QPointF& pt1, const QPointF& pt2) { - return fabs(pt1.x() - pt2.x()) < 0.01 and + return fabs(pt1.x() - pt2.x()) < 0.01 && fabs(pt1.y()- pt2.y()) < 0.01; } } @@ -154,6 +154,10 @@ const QPolygonF& poly, bool autoexpand) { + // exit if fewer than 2 points in polygon + if ( poly.size() < 2 ) + return; + // if autoexpand, expand rectangle by line width if ( autoexpand ) { @@ -183,14 +187,14 @@ { // add first line pout << p1; - if( not smallDelta(p1, p2) ) + if( ! smallDelta(p1, p2) ) pout << p2; } else { if( p1 == pout.last() ) { - if( not smallDelta(p1, p2) ) + if( ! smallDelta(p1, p2) ) // extend polyline pout << p2; } @@ -203,7 +207,7 @@ // start new line pout.clear(); pout << p1; - if( not smallDelta(p1, p2) ) + if( ! smallDelta(p1, p2) ) pout << p2; } } diff -Nru veusz-1.10/helpers/src/qtloops.cpp veusz-1.14/helpers/src/qtloops.cpp --- veusz-1.10/helpers/src/qtloops.cpp 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/helpers/src/qtloops.cpp 2011-11-22 20:23:31.000000000 +0000 @@ -26,13 +26,15 @@ #include #include #include +#include +#include namespace { // is difference between points very small? inline bool smallDelta(const QPointF& pt1, const QPointF& pt2) { - return fabs(pt1.x() - pt2.x()) < 0.01 and + return fabs(pt1.x() - pt2.x()) < 0.01 && fabs(pt1.y()- pt2.y()) < 0.01; } @@ -69,7 +71,7 @@ if( row < d.dims[col] && row < d.dims[col+1] ) { const QPointF pt(d.data[col][row], d.data[col+1][row]); - if( not smallDelta(pt, lastpt) ) + if( ! smallDelta(pt, lastpt) ) { poly << pt; lastpt = pt; @@ -85,7 +87,9 @@ void plotPathsToPainter(QPainter& painter, QPainterPath& path, const Numpy1DObj& x, const Numpy1DObj& y, - const QRectF* clip) + const Numpy1DObj* scaling, + const QRectF* clip, + const QImage* colorimg) { QRectF cliprect( QPointF(-32767,-32767), QPointF(32767,32767) ); if( clip != 0 ) @@ -98,16 +102,43 @@ cliprect.adjust(pathbox.left(), pathbox.top(), pathbox.bottom(), pathbox.right()); - const int size = min(x.dim, y.dim); + // keep track of duplicate points QPointF lastpt(-1e6, -1e6); + // keep original transformation for restoration after each iteration + QTransform origtrans(painter.worldTransform()); + + // number of iterations + int size = min(x.dim, y.dim); + + // if few color points, trim down number of paths + if( colorimg != 0 ) + size = min(size, colorimg->width()); + // too few scaling points + if( scaling != 0 ) + size = min(size, scaling->dim); + + // draw each path for(int i = 0; i < size; ++i) { const QPointF pt(x(i), y(i)); - if( cliprect.contains(pt) and not smallDelta(lastpt, pt) ) + if( cliprect.contains(pt) && ! smallDelta(lastpt, pt) ) { painter.translate(pt); + if( scaling != 0 ) + { + // scale point if requested + const qreal s = (*scaling)(i); + painter.scale(s, s); + } + if( colorimg != 0 ) + { + // get color from pixel and create a new brush + QBrush b( QColor::fromRgba(colorimg->pixel(i, 0)) ); + painter.setBrush(b); + } + painter.drawPath(path); - painter.translate(-pt); + painter.setWorldTransform(origtrans); lastpt = pt; } } @@ -122,7 +153,7 @@ // if autoexpand, expand rectangle by line width QRectF clipcopy; - if ( clip != 0 and autoexpand ) + if ( clip != 0 && autoexpand ) { const qreal lw = painter.pen().widthF(); qreal x1, y1, x2, y2; @@ -158,7 +189,7 @@ { // if autoexpand, expand rectangle by line width QRectF clipcopy(QPointF(-32767,-32767), QPointF(32767,32767)); - if ( clip != 0 and autoexpand ) + if ( clip != 0 && autoexpand ) { const qreal lw = painter.pen().widthF(); qreal x1, y1, x2, y2; diff -Nru veusz-1.10/helpers/src/qtloops.h veusz-1.14/helpers/src/qtloops.h --- veusz-1.10/helpers/src/qtloops.h 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/helpers/src/qtloops.h 2011-11-22 20:23:31.000000000 +0000 @@ -37,9 +37,16 @@ void addNumpyToPolygonF(QPolygonF& poly, const Tuple2Ptrs& v); +// plot paths to painter +// x and y locations are given in x and y +// if scaling is not 0, is an array to scale the data points by +// if colorimg is not 0, is a Nx1 image containing color points for path fills +// clip is a clipping rectangle if set void plotPathsToPainter(QPainter& painter, QPainterPath& path, const Numpy1DObj& x, const Numpy1DObj& y, - const QRectF* clip = 0); + const Numpy1DObj* scaling = 0, + const QRectF* clip = 0, + const QImage* colorimg = 0); void plotLinesToPainter(QPainter& painter, const Numpy1DObj& x1, const Numpy1DObj& y1, diff -Nru veusz-1.10/helpers/src/qtloops_helpers.h veusz-1.14/helpers/src/qtloops_helpers.h --- veusz-1.10/helpers/src/qtloops_helpers.h 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/helpers/src/qtloops_helpers.h 2011-11-22 20:23:31.000000000 +0000 @@ -58,7 +58,7 @@ inline double operator()(const int x) const { - if( DEBUG and (x < 0 or x >= dim) ) + if( DEBUG && (x < 0 || x >= dim) ) throw "Invalid index in array"; return data[x]; } @@ -79,7 +79,7 @@ inline double operator()(const int x, const int y) const { - if( DEBUG and (x < 0 or x >= dims[0] or y < 0 or y >= dims[1]) ) + if( DEBUG && (x < 0 || x >= dims[0] || y < 0 || y >= dims[1]) ) throw "Invalid index in array"; return data[x+y*dims[1]]; } @@ -100,7 +100,7 @@ inline int operator()(const int x, const int y) const { - if( DEBUG and (x < 0 or x >= dims[0] or y < 0 or y >= dims[1]) ) + if( DEBUG && (x < 0 || x >= dims[0] || y < 0 || y >= dims[1]) ) throw "Invalid index in array"; return data[x+y*dims[1]]; } diff -Nru veusz-1.10/helpers/src/qtloops.sip veusz-1.14/helpers/src/qtloops.sip --- veusz-1.10/helpers/src/qtloops.sip 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/helpers/src/qtloops.sip 2011-11-22 20:23:31.000000000 +0000 @@ -59,20 +59,34 @@ %End void plotPathsToPainter(QPainter&, QPainterPath&, SIP_PYOBJECT, SIP_PYOBJECT, - const QRectF* clip=0); + SIP_PYOBJECT, + const QRectF* clip=0, + const QImage* colorimg=0); %MethodCode - { - try - { - Numpy1DObj x(a2); - Numpy1DObj y(a3); - plotPathsToPainter(*a0, *a1, x, y, a4); - } - catch( const char *msg ) - { - sipIsErr = 1; PyErr_SetString(PyExc_TypeError, msg); - } - } +{ + Numpy1DObj* scaling = 0; + + try + { + // x and y coordinates + Numpy1DObj x(a2); + Numpy1DObj y(a3); + + // a4 is scaling or None + if (a4 != Py_None) { + scaling = new Numpy1DObj(a4); + } + + plotPathsToPainter(*a0, *a1, x, y, scaling, a5, a6); + } + catch( const char *msg ) + { + sipIsErr = 1; PyErr_SetString(PyExc_TypeError, msg); + } + + if( scaling ) + delete scaling; +} %End void plotLinesToPainter(QPainter& painter, @@ -169,3 +183,20 @@ QPolygonF bezier_fit_cubic_multi(const QPolygonF& data, double error, unsigned max_beziers); + +class RecordPaintDevice : QPaintDevice + { +%TypeHeaderCode +#include +%End + +public: + RecordPaintDevice(int width, int height, int dpix, int dpiy); + ~RecordPaintDevice(); + void play(QPainter& painter); + + QPaintEngine* paintEngine() const; + + int metric(QPaintDevice::PaintDeviceMetric metric) const; + int drawItemCount() const; + }; diff -Nru veusz-1.10/helpers/src/recordpaintdevice.cpp veusz-1.14/helpers/src/recordpaintdevice.cpp --- veusz-1.10/helpers/src/recordpaintdevice.cpp 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/helpers/src/recordpaintdevice.cpp 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,76 @@ +// Copyright (C) 2011 Jeremy S. Sanders +// Email: Jeremy Sanders +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +///////////////////////////////////////////////////////////////////////////// + +#include +#include +#include "recordpaintdevice.h" +#include "recordpaintengine.h" + +#define INCH_MM 25.4 + +RecordPaintDevice::RecordPaintDevice(int width, int height, + int dpix, int dpiy) + :_width(width), _height(height), _dpix(dpix), _dpiy(dpiy), + _engine(new RecordPaintEngine) +{ +} + +RecordPaintDevice::~RecordPaintDevice() +{ + delete _engine; + qDeleteAll(_elements); +} + +QPaintEngine* RecordPaintDevice::paintEngine() const +{ + return _engine; +} + +int RecordPaintDevice::metric(QPaintDevice::PaintDeviceMetric metric) const +{ + switch(metric) { + case QPaintDevice::PdmWidth: + return _width; + case QPaintDevice::PdmHeight: + return _height; + case QPaintDevice::PdmWidthMM: + return int(_width * INCH_MM / _dpix); + case QPaintDevice::PdmHeightMM: + return int(_height * INCH_MM / _dpiy); + case QPaintDevice::PdmNumColors: + return std::numeric_limits::max(); + case QPaintDevice::PdmDepth: + return 24; + case QPaintDevice::PdmDpiX: + case QPaintDevice::PdmPhysicalDpiX: + return _dpix; + case QPaintDevice::PdmDpiY: + case QPaintDevice::PdmPhysicalDpiY: + return _dpiy; + default: + return -1; + } +} + +void RecordPaintDevice::play(QPainter& painter) +{ + foreach(PaintElement* el, _elements) + { + el->paint(painter); + } +} diff -Nru veusz-1.10/helpers/src/recordpaintdevice.h veusz-1.14/helpers/src/recordpaintdevice.h --- veusz-1.10/helpers/src/recordpaintdevice.h 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/helpers/src/recordpaintdevice.h 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,57 @@ +// Copyright (C) 2011 Jeremy S. Sanders +// Email: Jeremy Sanders +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +///////////////////////////////////////////////////////////////////////////// + +#ifndef RECORD_PAINT_DEVICE__H +#define RECORD_PAINT_DEVICE__H + +#include +#include +#include "paintelement.h" +#include "recordpaintengine.h" + +class RecordPaintDevice : public QPaintDevice +{ +public: + RecordPaintDevice(int width, int height, int dpix, int dpiy); + ~RecordPaintDevice(); + QPaintEngine* paintEngine() const; + + // play back all + void play(QPainter& painter); + + int metric(QPaintDevice::PaintDeviceMetric metric) const; + + int drawItemCount() const { return _engine->drawItemCount(); } + +public: + friend class RecordPaintEngine; + +private: + // add an element to the list of maintained elements + void addElement(PaintElement* el) + { + _elements.push_back(el); + } + +private: + int _width, _height, _dpix, _dpiy; + RecordPaintEngine* _engine; + QVector _elements; +}; + +#endif diff -Nru veusz-1.10/helpers/src/recordpaintengine.cpp veusz-1.14/helpers/src/recordpaintengine.cpp --- veusz-1.10/helpers/src/recordpaintengine.cpp 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/helpers/src/recordpaintengine.cpp 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,601 @@ +// Copyright (C) 2011 Jeremy S. Sanders +// Email: Jeremy Sanders +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +///////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include + +#include "paintelement.h" +#include "recordpaintengine.h" +#include "recordpaintdevice.h" + +namespace { + + ////////////////////////////////////////////////////////////// + // Drawing Elements + // these are defined for each type of painting + // the QPaintEngine does + + // draw an ellipse (QRect and QRectF) + template + class ellipseElement : public PaintElement { + public: + ellipseElement(const T &rect) : _ellipse(rect) {} + + void paint(QPainter& painter) + { + painter.drawEllipse(_ellipse); + } + + private: + T _ellipse; + }; + typedef ellipseElement EllipseElement; + typedef ellipseElement EllipseFElement; + + // draw QImage + class ImageElement : public PaintElement { + public: + ImageElement(const QRectF& rect, const QImage& image, + const QRectF& sr, Qt::ImageConversionFlags flags) + : _image(image), _rect(rect), _sr(sr), _flags(flags) + {} + + void paint(QPainter& painter) + { + painter.drawImage(_rect, _image, _sr, _flags); + } + + private: + QImage _image; + QRectF _rect, _sr; + Qt::ImageConversionFlags _flags; + }; + + // draw lines + // this is for painting QLine and QLineF + template + class lineElement : public PaintElement { + public: + lineElement(const T *lines, int linecount) + { + for(int i = 0; i < linecount; i++) + _lines << lines[i]; + } + + void paint(QPainter& painter) + { + painter.drawLines(_lines); + } + + private: + QVector _lines; + }; + // specific Line and LineF variants + typedef lineElement LineElement; + typedef lineElement LineFElement; + + // draw QPainterPath + class PathElement : public PaintElement { + public: + PathElement(const QPainterPath& path) + : _path(path) {} + + void paint(QPainter& painter) + { + painter.drawPath(_path); + } + + private: + QPainterPath _path; + }; + + // draw Pixmap + class PixmapElement : public PaintElement { + public: + PixmapElement(const QRectF& r, const QPixmap& pm, + const QRectF& sr) : + _r(r), _pm(pm), _sr(sr) {} + + void paint(QPainter& painter) + { + painter.drawPixmap(_r, _pm, _sr); + } + + private: + QRectF _r; + QPixmap _pm; + QRectF _sr; + }; + + // draw points (QPoint and QPointF) + template + class pointElement : public PaintElement { + public: + pointElement(const T* points, int pointcount) + { + for(int i=0; i PointElement; + typedef pointElement PointFElement; + + // for QPolygon and QPolygonF + template + class polyElement: public PaintElement { + public: + polyElement(const T* points, int pointcount, + QPaintEngine::PolygonDrawMode mode) + : _mode(mode) + { + for(int i=0; i PolygonElement; + typedef polyElement PolygonFElement; + + // for QRect and QRectF + template + class rectElement : public PaintElement { + public: + rectElement(const T* rects, int rectcount) + { + for(int i=0; i _rects; + }; + typedef rectElement RectElement; + typedef rectElement RectFElement; + + // draw Text + class TextElement : public PaintElement { + public: + TextElement(const QPointF& pt, const QTextItem& txt) + : _pt(pt), _text(txt.text()) + {} + + void paint(QPainter& painter) + { + painter.drawText(_pt, _text); + } + + private: + QPointF _pt; + QString _text; + }; + + class TiledPixmapElement : public PaintElement { + public: + TiledPixmapElement(const QRectF& rect, const QPixmap& pixmap, + const QPointF& pt) + : _rect(rect), _pixmap(pixmap), _pt(pt) + {} + + void paint(QPainter& painter) + { + painter.drawTiledPixmap(_rect, _pixmap, _pt); + } + + private: + QRectF _rect; + QPixmap _pixmap; + QPointF _pt; + }; + + /////////////////////////////////////////////////////////////////// + // State paint elements + + // these define and change the state of the painter + + class BackgroundBrushElement : public PaintElement { + public: + BackgroundBrushElement(const QBrush& brush) + : _brush(brush) + {} + + void paint(QPainter& painter) + { + painter.setBackground(_brush); + } + + private: + QBrush _brush; + }; + + class BackgroundModeElement : public PaintElement { + public: + BackgroundModeElement(Qt::BGMode mode) + : _mode(mode) + {} + + void paint(QPainter& painter) + { + painter.setBackgroundMode(_mode); + } + + private: + Qt::BGMode _mode; + }; + + class BrushElement : public PaintElement { + public: + BrushElement(const QBrush& brush) + : _brush(brush) + {} + + void paint(QPainter& painter) + { + painter.setBrush(_brush); + } + + private: + QBrush _brush; + }; + + class BrushOriginElement : public PaintElement { + public: + BrushOriginElement(const QPointF& origin) + : _origin(origin) + {} + + void paint(QPainter& painter) + { + painter.setBrushOrigin(_origin); + } + + private: + QPointF _origin; + }; + + class ClipRegionElement : public PaintElement { + public: + ClipRegionElement(Qt::ClipOperation op, + const QRegion& region) + : _op(op), _region(region) + {} + + void paint(QPainter& painter) + { + painter.setClipRegion(_region, _op); + } + + private: + Qt::ClipOperation _op; + QRegion _region; + }; + + class ClipPathElement : public PaintElement { + public: + ClipPathElement(Qt::ClipOperation op, + const QPainterPath& region) + : _op(op), _region(region) + {} + + void paint(QPainter& painter) + { + painter.setClipPath(_region, _op); + } + + private: + Qt::ClipOperation _op; + QPainterPath _region; + }; + + class CompositionElement : public PaintElement { + public: + CompositionElement(QPainter::CompositionMode mode) + : _mode(mode) + {} + + void paint(QPainter& painter) + { + painter.setCompositionMode(_mode); + } + + private: + QPainter::CompositionMode _mode; + }; + + class FontElement : public PaintElement { + public: + FontElement(const QFont& font) + : _font(font) + {} + + void paint(QPainter& painter) + { + painter.setFont(_font); + } + + private: + QFont _font; + }; + + class TransformElement : public PaintElement { + public: + TransformElement(const QTransform& t) + : _t(t) + {} + + void paint(QPainter& painter) + { + painter.setWorldTransform(_t); + } + + private: + QTransform _t; + }; + + class ClipEnabledElement : public PaintElement { + public: + ClipEnabledElement(bool enabled) + : _enabled(enabled) + {} + + void paint(QPainter& painter) + { + painter.setClipping(_enabled); + } + + private: + bool _enabled; + }; + + class PenElement : public PaintElement { + public: + PenElement(const QPen& pen) + : _pen(pen) + {} + + void paint(QPainter& painter) + { + painter.setPen(_pen); + } + + private: + QPen _pen; + }; + + class HintsElement : public PaintElement { + public: + HintsElement(QPainter::RenderHints hints) + : _hints(hints) + {} + + void paint(QPainter& painter) + { + painter.setRenderHints(_hints); + } + + private: + QPainter::RenderHints _hints; + }; + + + // end anonymous block +} + +/////////////////////////////////////////////////////////////////// +// Paint engine follows + +RecordPaintEngine::RecordPaintEngine() + : QPaintEngine(QPaintEngine::AllFeatures), + _drawitemcount(0), + _pdev(0) +{ +} + +bool RecordPaintEngine::begin(QPaintDevice* pdev) +{ + // old style C cast - probably should use dynamic_cast + _pdev = (RecordPaintDevice*)(pdev); + + // signal started ok + return 1; +} + +// for each type of drawing command we add a new element +// to the list maintained by the device + +void RecordPaintEngine::drawEllipse(const QRectF& rect) +{ + _pdev->addElement( new EllipseFElement(rect) ); + _drawitemcount++; +} + +void RecordPaintEngine::drawEllipse(const QRect& rect) +{ + _pdev->addElement( new EllipseElement(rect) ); + _drawitemcount++; +} + +void RecordPaintEngine::drawImage(const QRectF& rectangle, + const QImage& image, + const QRectF& sr, + Qt::ImageConversionFlags flags) +{ + _pdev->addElement( new ImageElement(rectangle, image, sr, flags) ); + _drawitemcount++; +} + +void RecordPaintEngine::drawLines(const QLineF* lines, int lineCount) +{ + _pdev->addElement( new LineFElement(lines, lineCount) ); + _drawitemcount += lineCount; +} + +void RecordPaintEngine::drawLines(const QLine* lines, int lineCount) +{ + _pdev->addElement( new LineElement(lines, lineCount) ); + _drawitemcount += lineCount; +} + +void RecordPaintEngine::drawPath(const QPainterPath& path) +{ + _pdev->addElement( new PathElement(path) ); + _drawitemcount++; +} + +void RecordPaintEngine::drawPixmap(const QRectF& r, + const QPixmap& pm, const QRectF& sr) +{ + _pdev->addElement( new PixmapElement(r, pm, sr) ); + _drawitemcount++; +} + +void RecordPaintEngine::drawPoints(const QPointF* points, int pointCount) +{ + _pdev->addElement( new PointFElement(points, pointCount) ); + _drawitemcount += pointCount; +} + +void RecordPaintEngine::drawPoints(const QPoint* points, int pointCount) +{ + _pdev->addElement( new PointElement(points, pointCount) ); + _drawitemcount += pointCount; +} + +void RecordPaintEngine::drawPolygon(const QPointF* points, int pointCount, + QPaintEngine::PolygonDrawMode mode) +{ + _pdev->addElement( new PolygonFElement(points, pointCount, mode) ); + _drawitemcount += pointCount; +} + +void RecordPaintEngine::drawPolygon(const QPoint* points, int pointCount, + QPaintEngine::PolygonDrawMode mode) +{ + _pdev->addElement( new PolygonElement(points, pointCount, mode) ); + _drawitemcount += pointCount; +} + +void RecordPaintEngine::drawRects(const QRectF* rects, int rectCount) +{ + _pdev->addElement( new RectFElement( rects, rectCount ) ); + _drawitemcount += rectCount; +} + +void RecordPaintEngine::drawRects(const QRect* rects, int rectCount) +{ + _pdev->addElement( new RectElement( rects, rectCount ) ); + _drawitemcount += rectCount; +} + +void RecordPaintEngine::drawTextItem(const QPointF& p, + const QTextItem& textItem) +{ + _pdev->addElement( new TextElement(p, textItem) ); + _drawitemcount += textItem.text().length(); +} + +void RecordPaintEngine::drawTiledPixmap(const QRectF& rect, + const QPixmap& pixmap, + const QPointF& p) +{ + _pdev->addElement( new TiledPixmapElement(rect, pixmap, p) ); + _drawitemcount += 1; +} + +bool RecordPaintEngine::end() +{ + // signal finished ok + return 1; +} + +QPaintEngine::Type RecordPaintEngine::type () const +{ + // some sort of random number for the ID of the engine type + return QPaintEngine::Type(int(QPaintEngine::User)+34); +} + +void RecordPaintEngine::updateState(const QPaintEngineState& state) +{ + // we add a new element for each change of state + // these are replayed later + const int flags = state.state(); + if( flags & QPaintEngine::DirtyBackground ) + _pdev->addElement( new BackgroundBrushElement( state.backgroundBrush() ) ); + if( flags & QPaintEngine::DirtyBackgroundMode ) + _pdev->addElement( new BackgroundModeElement( state.backgroundMode() ) ); + if( flags & QPaintEngine::DirtyBrush ) + _pdev->addElement( new BrushElement( state.brush() ) ); + if( flags & QPaintEngine::DirtyBrushOrigin ) + _pdev->addElement( new BrushOriginElement( state.brushOrigin() ) ); + if( flags & QPaintEngine::DirtyClipRegion ) + _pdev->addElement( new ClipRegionElement( state.clipOperation(), + state.clipRegion() ) ); + if( flags & QPaintEngine::DirtyClipPath ) + _pdev->addElement( new ClipPathElement( state.clipOperation(), + state.clipPath() ) ); + if( flags & QPaintEngine::DirtyCompositionMode ) + _pdev->addElement( new CompositionElement( state.compositionMode() ) ); + if( flags & QPaintEngine::DirtyFont ) + _pdev->addElement( new FontElement( state.font() ) ); + if( flags & QPaintEngine::DirtyTransform ) + _pdev->addElement( new TransformElement( state.transform() ) ); + if( flags & QPaintEngine::DirtyClipEnabled ) + _pdev->addElement( new ClipEnabledElement( state.isClipEnabled() ) ); + if( flags & QPaintEngine::DirtyPen ) + _pdev->addElement( new PenElement( state.pen() ) ); + if( flags & QPaintEngine::DirtyHints ) + _pdev->addElement( new HintsElement( state.renderHints() ) ); +} diff -Nru veusz-1.10/helpers/src/recordpaintengine.h veusz-1.14/helpers/src/recordpaintengine.h --- veusz-1.10/helpers/src/recordpaintengine.h 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/helpers/src/recordpaintengine.h 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,75 @@ +// Copyright (C) 2011 Jeremy S. Sanders +// Email: Jeremy Sanders +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +///////////////////////////////////////////////////////////////////////////// + +#ifndef RECORD_PAINT_ENGINE__H +#define RECORD_PAINT_ENGINE__H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class RecordPaintDevice; + +class RecordPaintEngine : public QPaintEngine +{ +public: + RecordPaintEngine(); + + // standard methods to be overridden in engine + bool begin(QPaintDevice* pdev); + + void drawEllipse(const QRectF& rect); + void drawEllipse(const QRect& rect); + void drawImage(const QRectF& rectangle, const QImage& image, + const QRectF& sr, + Qt::ImageConversionFlags flags = Qt::AutoColor); + void drawLines(const QLineF* lines, int lineCount); + void drawLines(const QLine* lines, int lineCount); + void drawPath(const QPainterPath& path); + void drawPixmap(const QRectF& r, const QPixmap& pm, const QRectF& sr); + void drawPoints(const QPointF* points, int pointCount); + void drawPoints(const QPoint* points, int pointCount); + void drawPolygon(const QPointF* points, int pointCount, + QPaintEngine::PolygonDrawMode mode); + void drawPolygon(const QPoint* points, int pointCount, + QPaintEngine::PolygonDrawMode mode); + void drawRects(const QRectF* rects, int rectCount); + void drawRects(const QRect* rects, int rectCount); + void drawTextItem(const QPointF& p, const QTextItem& textItem); + void drawTiledPixmap(const QRectF& rect, const QPixmap& pixmap, + const QPointF& p); + bool end (); + QPaintEngine::Type type () const; + void updateState(const QPaintEngineState& state); + + // return an estimate of number of items drawn + int drawItemCount() const { return _drawitemcount; } + +private: + int _drawitemcount; + RecordPaintDevice* _pdev; +}; + +#endif diff -Nru veusz-1.10/__init__.py veusz-1.14/__init__.py --- veusz-1.10/__init__.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/__init__.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,6 +16,4 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: __init__.py 872 2008-12-29 12:51:59Z jeremysanders $ - """Main veusz module.""" diff -Nru veusz-1.10/INSTALL veusz-1.14/INSTALL --- veusz-1.10/INSTALL 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/INSTALL 2011-11-22 20:23:31.000000000 +0000 @@ -1,31 +1,70 @@ Veusz Installation ================== -Veusz uses distutils for its installation. See below for how to use -it. +1. INSTALLING FROM SOURCE +************************* + +Veusz uses distutils for its installation. See below for how to use it. Requirements: python >= 2.4 http://www.python.org/ PyQt >= 4.3 http://www.riverbankcomputing.co.uk/pyqt/ numpy >= 1.0 http://numpy.scipy.org/ -(PyQt requires +PyQt requires Qt4 http://www.trolltech.com/products/qt/ (free version) - version >= 4.3 recommended - SIP http://www.riverbankcomputing.co.uk/sip/ ) + version >= 4.4 recommended + SIP http://www.riverbankcomputing.co.uk/sip/ Optional requirements: PyFITS>=1.1 http://www.stsci.edu/resources/software_hardware/pyfits pyemf >= 2.0.0 http://pyemf.sourceforge.net/ Corefonts http://corefonts.sourceforge.net/ + PyMinuit http://code.google.com/p/pyminuit/ + +1.1 Full installation with distutils +==================================== +There are a number of ways to install programs using distutils. I will +list a few of the possible method here: + +To install on linux to the standard location on the hard disk + +# cd veusz-1.14 +# python setup.py build +# su +[enter root password] +# python setup.py install +# exit + +If you do not have a root account (as is default on Ubuntu), do +# sudo python setup.py install +instead of the final three lines + +On Windows, it should just be a matter of running the python setup.py +build and install steps with the requirements installed. + +1.2 Testing +=========== +After veusz has been installed into the Python path (in the standard +location or in PYTHONPATH), you can run the runselftest.py executable +in the tests directory. This will compare the generated output of +example documents with the expected output. The return code of the +runselftest.py script is the number of tests that have failed (0 for +success). + +On Unix/Linux, Qt requires the DISPLAY environment to be set to an X11 +server for the self test to run. In a non graphical environment Xvfb +can be used to create a hidden X11 server: +# xvfb-run -a --server-args "-screen 0 640x480x24" \ + python tests/runselftest.py -Simple source use (if requirements installed) -============================================= +1.3 Simple source use (if requirements installed) +================================================= If you don't want to bother installing veusz fully, it can be run from its own directory (at the moment). Simply do: -# tar xzf veusz-1.9.tar.gz [change version here] -# cd veusz-1.9 +# tar xzf veusz-1.14.tar.gz [change version here] +# cd veusz-1.14 # ./veusz_main.py Certain features will be disabled if you do this. You will not be able @@ -37,55 +76,28 @@ # python setup.py build # cp build/*/veusz/helpers/*.so helpers/ -Full installation with distutils -================================ -There are a number of ways to install programs using distutils. I will -list a few of the possible method here: +2. BINARY INSTALL +***************** -To install to the standard location on the hard disk (it's better to use -rpms if you have an rpm-based linux distibution) ------------------------------------------------------------------------- -# cd veusz-1.9 -# python setup.py build -# su -[enter root password] -# python setup.py install -# exit - -Linux binary -============ -If you do not have the requirements, you can use the Linux binary -instead (if available). Note that this may not work on all -distributions due to glibc/library incompatibilities. You need to -simply unpack the tar file and run the main executable: +2.1 Linux binary +================ +If your distribution does not include an up to date package, you can +use the Linux binary instead (for i386/x86_64). Note that this may not +work on all distributions due to glibc/library +incompatibilities. Simply unpack the tar file and run the main +executable: -# tar xzf veusz-linux-i386-1.9.tar.gz [change version here] -# cd veusz-linux-i386-1.9 +# tar xzf veusz-linux-i386-1.14.tar.gz [change version here] +# cd veusz-linux-i386-1.14 # ./veusz -Installing in Windows -===================== -Simply run the setup.exe binary installer. This does not provide the -embedding interface, however. - -Installing on Mac OS X -====================== -A binary is available for Mac OS X. It does not provide the embedding -interface. Simply drag the Veusz application into your Applications -directory. - -Veusz can also be installed from source on Mac OS X. The requirements -can be obtained using a system such as MacPorts. You can install them -with MacPorts using: - -$ sudo port install qt4-mac -$ sudo port install py-numpy - -Once these have successfully built and installed, you can unpack veusz -and install as above. - -Qt is available from TrollTech as a binary, but SIP and PyQt are not -available as a binary. - -------------------------------------------------------------------------------- -$Id: INSTALL 1375 2010-08-22 19:14:45Z jeremysanders $ +2.2 Installing in Windows +========================= +Simply run the setup.exe binary installer. Add the location of the +embed.py file to your PYTHONPATH if you want to use the embedding +module. + +2.3 Installing on Mac OS X +========================== +A binary is available for Mac OS X. Simply drag the Veusz application +into your Applications directory. diff -Nru veusz-1.10/MANIFEST.in veusz-1.14/MANIFEST.in --- veusz-1.10/MANIFEST.in 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/MANIFEST.in 2011-11-22 20:32:03.000000000 +0000 @@ -1,10 +1,9 @@ include VERSION AUTHORS ChangeLog COPYING INSTALL README include MANIFEST.in setup.py setup.cfg include scripts/veusz scripts/veusz_listen -recursive-include tests *.py *.sh *.vsz +recursive-include tests *.py *.sh *.vsz *.selftest *.csv *.dat *.npy *.npz recursive-include Documents *.xml *.sh *.png *.txt *.pdf *.html *.xsl *.py *.pod *.1 recursive-include windows *.png *.ico *.svg README recursive-include dialogs *.ui recursive-include examples *.vsz *.py *.csv *.dat -recursive-include widgets *.dat recursive-include helpers *.c *.cpp *.h README LICENSE_* diff -Nru veusz-1.10/PKG-INFO veusz-1.14/PKG-INFO --- veusz-1.10/PKG-INFO 2010-12-12 12:41:59.000000000 +0000 +++ veusz-1.14/PKG-INFO 2011-11-22 20:32:14.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: veusz -Version: 1.10 +Version: 1.14 Summary: A scientific plotting package Home-page: http://home.gna.org/veusz/ Author: Jeremy Sanders diff -Nru veusz-1.10/plugins/datasetplugin.py veusz-1.14/plugins/datasetplugin.py --- veusz-1.10/plugins/datasetplugin.py 2010-12-12 12:41:06.000000000 +0000 +++ veusz-1.14/plugins/datasetplugin.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,14 +18,14 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: datasetplugin.py 1373 2010-08-22 18:43:32Z jeremysanders $ - """Plugins for creating datasets.""" import numpy as N from itertools import izip import field +import veusz.utils as utils + # add an instance of your class to this list to be registered datasetpluginregistry = [] @@ -104,6 +104,44 @@ import veusz.document as document return document.Dataset2DPlugin(manager, self) +class DatasetDateTime(object): + """Date-time dataset for ImportPlugin or DatasetPlugin.""" + + def __init__(self, name, data=[]): + """A date dataset + name: name of dataset + data: list of datetime objects + """ + self.name = name + self.update(data=data) + + def update(self, data=[]): + self.data = N.array(data) + + @staticmethod + def datetimeToFloat(datetimeval): + """Return a python datetime object to the required float type.""" + return utils.datetimeToFloat(datetimeval) + + @staticmethod + def dateStringToFloat(text): + """Try to convert an iso or local date time to the float type.""" + return utils.dateStringToDate(text) + + @staticmethod + def floatToDateTime(val): + """Convert float format datetime to Python datetime.""" + return utils.floatToDateTime(val) + + def _null(self): + """Empty data contents.""" + self.data = N.array([]) + + def _makeVeuszDataset(self, manager): + """Make a Veusz dataset from the plugin dataset.""" + import veusz.document as document + return document.DatasetDatePlugin(manager, self) + class DatasetText(object): """Text dataset for ImportPlugin or DatasetPlugin.""" def __init__(self, name, data=[]): @@ -126,6 +164,26 @@ import veusz.document as document return document.DatasetTextPlugin(manager, self) +class Constant(object): + """Dataset to return to set a Veusz constant after import. + This is only useful in an ImportPlugin, not a DatasetPlugin + """ + def __init__(self, name, val): + """Map string value val to name. + Convert float vals to strings first!""" + self.name = name + self.val = val + +class Function(object): + """Dataset to return to set a Veusz function after import.""" + def __init__(self, name, val): + """Map string value val to name. + name is "funcname(param,...)", val is a text expression of param. + This is only useful in an ImportPlugin, not a DatasetPlugin + """ + self.name = name + self.val = val + # class to pass to plugin to give parameters class DatasetPluginHelper(object): """Helpers to get existing datasets for plugins.""" @@ -152,6 +210,13 @@ return [name for name, ds in self._doc.data.iteritems() if (ds.dimensions == 1 and ds.datatype == 'text')] + @property + def datasetsdatetime(self): + """Return list of existing date-time datesets""" + import veusz.document as document + return [name for name, ds in self._doc.data.iteritems() if + isinstance(ds, document.DatasetDateTime)] + def evaluateExpression(self, expr, part='data'): """Return results of evaluating a 1D dataset expression. part is 'data', 'serr', 'perr' or 'nerr' - these are the @@ -169,6 +234,7 @@ name not found: raise a DatasetPluginException dimensions not right: raise a DatasetPluginException """ + import veusz.document as document try: ds = self._doc.data[name] except KeyError: @@ -181,7 +247,9 @@ raise DatasetPluginException( "Dataset '%s' is not a numerical dataset" % name) - if ds.dimensions == 1: + if isinstance(ds, document.DatasetDateTime): + return DatasetDateTime(name, data=ds.data) + elif ds.dimensions == 1: return Dataset1D(name, data=ds.data, serr=ds.serr, perr=ds.perr, nerr=ds.nerr) elif ds.dimensions == 2: diff -Nru veusz-1.10/plugins/field.py veusz-1.14/plugins/field.py --- veusz-1.10/plugins/field.py 2010-12-12 12:41:06.000000000 +0000 +++ veusz-1.14/plugins/field.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: field.py 1433 2010-11-04 19:12:00Z jeremysanders $ - """Data entry fields for plugins.""" import veusz.qtall as qt4 diff -Nru veusz-1.10/plugins/importplugin.py veusz-1.14/plugins/importplugin.py --- veusz-1.10/plugins/importplugin.py 2010-12-12 12:41:06.000000000 +0000 +++ veusz-1.14/plugins/importplugin.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,24 +16,13 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: importplugin.py 1422 2010-09-28 09:15:27Z jeremysanders $ - """Import plugin base class and helpers.""" import numpy as N import veusz.utils as utils -from field import Field as ImportField -from field import FieldBool as ImportFieldCheck -from field import FieldText as ImportFieldText -from field import FieldFloat as ImportFieldFloat -from field import FieldInt as ImportFieldInt -from field import FieldCombo as ImportFieldCombo import field - -from datasetplugin import Dataset1D as ImportDataset1D -from datasetplugin import Dataset2D as ImportDataset2D -from datasetplugin import DatasetText as ImportDatasetText +import datasetplugin # add an instance of your class to this list to get it registered importpluginregistry = [] @@ -55,17 +44,26 @@ class ImportPlugin(object): """Define a plugin to read data in a particular format. - override doImport and optionally getPreview to define a new plugin - register the class by adding to the importpluginregistry list + Override doImport and optionally getPreview to define a new plugin. + Register the class by adding it to the importpluginregistry list. + Of promote_tab is set to some text, put the plugin on its own tab + in the import dialog using that text as the tab name. """ name = 'Import plugin' author = '' description = '' + # if set to some text, use this plugin on its own tab + promote_tab = None + + # set these to get focus if a file is selected with these extensions + # include the dot in the extension names + file_extensions = set() + def __init__(self): """Override this to declare a list of input fields if required.""" - # a list of ImportField objects to display + # a list of Field objects to display self.fields = [] def getPreview(self, params): @@ -79,7 +77,7 @@ def doImport(self, params): """Actually import data params is a ImportPluginParams object. - Return a list of ImportDataset1D, ImportDataset2D objects + Return a list of datasetplugin.Dataset1D, datasetplugin.Dataset2D objects """ return [] @@ -95,32 +93,64 @@ def __init__(self): self.fields = [ - ImportFieldText("name", descr="Dataset name", default="name"), - ImportFieldCheck("invert", descr="invert values"), - ImportFieldFloat("mult", descr="Multiplication factor", default=1), - ImportFieldInt("skip", descr="Skip N lines", + field.FieldText("name", descr="Dataset name", default="name"), + field.FieldBool("invert", descr="invert values"), + field.FieldFloat("mult", descr="Multiplication factor", default=1), + field.FieldInt("skip", descr="Skip N lines", default=0, minval=0), - ImportFieldCombo("subtract", items=("0", "1", "2"), + field.FieldCombo("subtract", items=("0", "1", "2"), editable=False, default="0") ] def doImport(self, params): """Actually import data params is a ImportPluginParams object. - Return a list of ImportDataset1D, ImportDataset2D objects + Return a list of datasetplugin.Dataset1D, datasetplugin.Dataset2D objects + """ + try: + f = params.openFileWithEncoding() + data = [] + mult = params.field_results["mult"] + sub = float(params.field_results["subtract"]) + if params.field_results["invert"]: + mult *= -1 + for i in xrange(params.field_results["skip"]): + f.readline() + for line in f: + data += [float(x)*mult-sub for x in line.split()] + + return [datasetplugin.Dataset1D(params.field_results["name"], data), + datasetplugin.Constant("testconst", "42"), + datasetplugin.Function("testfunc(x)", "testconst*x**2")] + except Exception, e: + raise ImportPluginException(unicode(e)) + +class ImportPluginDateTime(ImportPlugin): + """An example plugin for reading a set of iso date-times from a + file.""" + + name = "Example plugin for date/times" + author = "Jeremy Sanders" + description = "Reads a list of ISO date times in a text file" + + def __init__(self): + self.fields = [ + field.FieldText("name", descr="Dataset name", default="name"), + ] + + def doImport(self, params): + """Actually import data + params is a ImportPluginParams object. + Return a list of datasetplugin.Dataset1D, datasetplugin.Dataset2D objects """ f = params.openFileWithEncoding() data = [] - mult = params.field_results["mult"] - sub = float(params.field_results["subtract"]) - if params.field_results["invert"]: - mult *= -1 - for i in xrange(params.field_results["skip"]): - f.readline() for line in f: - data += [float(x)*mult-sub for x in line.split()] - - return [ImportDataset1D(params.field_results["name"], data)] + data.append( datasetplugin.DatasetDateTime. + dateStringToFloat(line.strip()) ) + return [ datasetplugin.DatasetDateTime(params.field_results["name"], + data) ] +#importpluginregistry.append( ImportPluginDateTime ) class QdpFile(object): """Handle reading of a Qdp file.""" @@ -202,16 +232,16 @@ a = N.array(self.data[i]) if len(a.shape) == 1: # no error bars - ds = ImportDataset1D(name, data=a) + ds = datasetplugin.Dataset1D(name, data=a) elif a.shape[1] == 2: # serr - ds = ImportDataset1D(name, data=a[:,0], serr=a[:,1]) + ds = datasetplugin.Dataset1D(name, data=a[:,0], serr=a[:,1]) elif a.shape[1] == 3: # perr/nerr p = N.where(a[:,1] < a[:,2], a[:,2], a[:,1]) n = N.where(a[:,1] < a[:,2], a[:,1], a[:,2]) - ds = ImportDataset1D(name, data=a[:,0], perr=p, nerr=n) + ds = datasetplugin.Dataset1D(name, data=a[:,0], perr=p, nerr=n) else: raise RuntimeError @@ -296,6 +326,7 @@ name = "QDP import" author = "Jeremy Sanders" description = "Reads datasets from QDP files" + file_extensions = set(['.qdp']) def __init__(self): self.fields = [ @@ -306,7 +337,7 @@ def doImport(self, params): """Actually import data params is a ImportPluginParams object. - Return a list of ImportDataset1D, ImportDataset2D objects + Return a list of datasetplugin.Dataset1D, datasetplugin.Dataset2D objects """ names = [x.strip() for x in params.field_results["names"] if x.strip()] @@ -318,7 +349,237 @@ return rqdp.retndata +def cnvtImportNumpyArray(name, val, errorsin2d=True): + """Convert a numpy array to plugin returns.""" + + try: + val.shape + except AttributeError: + raise ImportPluginException("Not the correct format file") + try: + val + 0. + val = val.astype(N.float64) + except TypeError: + raise ImportPluginException("Unsupported array type") + + if val.ndim == 1: + return datasetplugin.Dataset1D(name, val) + elif val.ndim == 2: + if errorsin2d and val.shape[1] in (2, 3): + # return 1d array + if val.shape[1] == 2: + # use as symmetric errors + return datasetplugin.Dataset1D(name, val[:,0], serr=val[:,1]) + else: + # asymmetric errors + # unclear on ordering here... + return datasetplugin.Dataset1D(name, val[:,0], perr=val[:,1], + nerr=val[:,2]) + else: + return datasetplugin.Dataset2D(name, val) + else: + raise ImportPluginException("Unsupported dataset shape") + +class ImportPluginNpy(ImportPlugin): + """For reading single datasets from NPY numpy saved files.""" + + name = "Numpy NPY import" + author = "Jeremy Sanders" + description = "Reads a 1D/2D numeric dataset from a Numpy NPY file" + file_extensions = set(['.npy']) + + def __init__(self): + self.fields = [ + field.FieldText("name", descr="Dataset name", + default=''), + field.FieldBool("errorsin2d", + descr="Treat 2 and 3 column 2D arrays as\n" + "data with error bars", + default=True), + ] + + def getPreview(self, params): + """Get data to show in a text box to show a preview. + params is a ImportPluginParams object. + Returns (text, okaytoimport) + """ + try: + retn = N.load(params.filename) + except Exception, e: + return "Cannot read file", False + + try: + text = 'Array shape: %s\n' % str(retn.shape) + text += 'Array datatype: %s (%s)\n' % (retn.dtype.str, + str(retn.dtype)) + text += str(retn) + return text, True + except AttributeError: + return "Not an NPY file", False + + def doImport(self, params): + """Actually import data. + """ + + name = params.field_results["name"].strip() + if not name: + raise ImportPluginException("Please provide a name for the dataset") + + try: + retn = N.load(params.filename) + except Exception, e: + raise ImportPluginException("Error while reading file: %s" % + unicode(e)) + + return [ cnvtImportNumpyArray( + name, retn, errorsin2d=params.field_results["errorsin2d"]) ] + +class ImportPluginNpz(ImportPlugin): + """For reading single datasets from NPY numpy saved files.""" + + name = "Numpy NPZ import" + author = "Jeremy Sanders" + description = "Reads datasets from a Numpy NPZ file." + file_extensions = set(['.npz']) + + def __init__(self): + self.fields = [ + field.FieldBool("errorsin2d", + descr="Treat 2 and 3 column 2D arrays as\n" + "data with error bars", + default=True), + ] + + def getPreview(self, params): + """Get data to show in a text box to show a preview. + params is a ImportPluginParams object. + Returns (text, okaytoimport) + """ + try: + retn = N.load(params.filename) + except Exception, e: + return "Cannot read file", False + + # npz files should define this attribute + try: + retn.files + except AttributeError: + return "Not an NPZ file", False + + text = [] + for f in sorted(retn.files): + a = retn[f] + text.append('Name: %s' % f) + text.append(' Shape: %s' % str(a.shape)) + text.append(' Datatype: %s (%s)' % (a.dtype.str, str(a.dtype))) + text.append('') + return '\n'.join(text), True + + def doImport(self, params): + """Actually import data. + """ + + try: + retn = N.load(params.filename) + except Exception, e: + raise ImportPluginException("Error while reading file: %s" % + unicode(e)) + + try: + retn.files + except AttributeError: + raise ImportPluginException("File is not in NPZ format") + + # convert each of the imported arrays + out = [] + for f in sorted(retn.files): + out.append( cnvtImportNumpyArray( + f, retn[f], errorsin2d=params.field_results["errorsin2d"]) ) + + return out + +class ImportPluginBinary(ImportPlugin): + + name = "Binary import" + author = "Jeremy Sanders" + description = "Reads numerical binary files." + file_extensions = set(['.bin']) + + def __init__(self): + self.fields = [ + field.FieldText("name", descr="Dataset name", + default=""), + field.FieldCombo("datatype", descr="Data type", + items = ("float32", "float64", + "int8", "int16", "int32", "int64", + "uint8", "uint16", "uint32", "uint64"), + default="float64", editable=False), + field.FieldCombo("endian", descr="Endian (byte order)", + items = ("little", "big"), editable=False), + field.FieldInt("offset", descr="Offset (bytes)", default=0, minval=0), + field.FieldInt("length", descr="Length (values)", default=-1) + ] + + def getNumpyDataType(self, params): + """Convert params to numpy datatype.""" + t = N.dtype(str(params.field_results["datatype"])) + return t.newbyteorder( {"little": "<", "big": ">"} [ + params.field_results["endian"]] ) + + def getPreview(self, params): + """Preview of data files.""" + try: + f = open(params.filename, "rb") + data = f.read() + f.close() + except IOError: + return "Cannot read file", False + + text = ['File length: %i bytes' % len(data)] + + def filtchr(c): + """Filtered character to ascii range.""" + if ord(c) <= 32 or ord(c) > 127: + return '.' + else: + return c + + # do a hex dump (like in CP/M) + for i in xrange(0, min(65536, len(data)), 16): + hdr = '%04X ' % i + subset = data[i:i+16] + hexdata = ('%02X '*len(subset)) % tuple([ord(x) for x in subset]) + chrdata = ''.join([filtchr(c) for c in subset]) + + text.append(hdr+hexdata + ' ' + chrdata) + + return '\n'.join(text), True + + def doImport(self, params): + """Import the data.""" + + name = params.field_results["name"].strip() + if not name: + raise ImportPluginException("Please provide a name for the dataset") + + try: + f = open(params.filename, "rb") + f.seek( params.field_results["offset"] ) + retn = f.read() + f.close() + except IOError, e: + raise ImportPluginException("Error while reading file: %s" % + unicode(e)) + + data = N.fromstring(retn, dtype=self.getNumpyDataType(params), + count=params.field_results["length"]) + data = data.astype(N.float64) + return [ datasetplugin.Dataset1D(name, data) ] + importpluginregistry += [ - ImportPluginQdp(), - ImportPluginExample(), + ImportPluginNpy, + ImportPluginNpz, + ImportPluginQdp, + ImportPluginBinary, + ImportPluginExample, ] diff -Nru veusz-1.10/plugins/__init__.py veusz-1.14/plugins/__init__.py --- veusz-1.10/plugins/__init__.py 2010-12-12 12:41:06.000000000 +0000 +++ veusz-1.14/plugins/__init__.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,9 +16,18 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: __init__.py 1334 2010-07-19 20:25:08Z jeremysanders $ - from field import * from datasetplugin import * from importplugin import * from toolsplugin import * + +# backward compatibility +ImportDataset1D = Dataset1D +ImportDataset2D = Dataset2D +ImportDatasetText = DatasetText +ImportField = Field +ImportFieldCheck = FieldBool +ImportFieldText = FieldText +ImportFieldFloat = FieldFloat +ImportFieldInt = FieldInt +ImportFieldCombo = FieldCombo diff -Nru veusz-1.10/plugins/toolsplugin.py veusz-1.14/plugins/toolsplugin.py --- veusz-1.10/plugins/toolsplugin.py 2010-12-12 12:41:06.000000000 +0000 +++ veusz-1.14/plugins/toolsplugin.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: toolsplugin.py 1408 2010-09-16 19:33:07Z jeremysanders $ - """Plugins for general operations.""" import random @@ -183,7 +181,7 @@ idx += 1 class ColorsReplace(ToolsPlugin): - """Randomize the colors used in plotting.""" + """Replace one color by another.""" menu = ('Colors', 'Replace') name = 'Replace colors' @@ -228,6 +226,55 @@ fromwidget = ifc.Root.fromPath(fields['widget']) walkNodes(fromwidget) +class ColorsSwap(ToolsPlugin): + """Swap colors used in plotting.""" + + menu = ('Colors', 'Swap') + name = 'Swap colors' + description_short = 'Swap two colors' + description_full = 'Swaps two colors in the plot' + + def __init__(self): + """Construct plugin.""" + self.fields = [ + field.FieldWidget("widget", descr="Start from widget", + default="/"), + field.FieldBool("follow", descr="Change references and defaults", + default=True), + field.FieldColor('color1', descr="First color", + default='black'), + field.FieldColor('color2', descr="Second color", + default='red'), + ] + + def apply(self, ifc, fields): + """Do the color search and replace.""" + + col1 = qt4.QColor(fields['color1']) + col2 = qt4.QColor(fields['color2']) + + def walkNodes(node): + """Walk nodes, changing values.""" + if node.type == 'setting' and node.settingtype == 'color': + # only follow references if requested + if node.isreference: + if fields['follow']: + node = node.resolveReference() + else: + return + + # evaluate into qcolor to make sure is a true match + if qt4.QColor(node.val) == col1: + node.val = fields['color2'] + elif qt4.QColor(node.val) == col2: + node.val = fields['color1'] + else: + for c in node.children: + walkNodes(c) + + fromwidget = ifc.Root.fromPath(fields['widget']) + walkNodes(fromwidget) + class TextReplace(ToolsPlugin): """Randomize the colors used in plotting.""" @@ -469,6 +516,7 @@ ColorsRandomize, ColorsSequence, ColorsReplace, + ColorsSwap, TextReplace, WidgetsClone, FontSizeIncrease, diff -Nru veusz-1.10/qtall.py veusz-1.14/qtall.py --- veusz-1.10/qtall.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/qtall.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: qtall.py 1243 2010-05-29 13:35:43Z jeremysanders $ - """A convenience module to import both the used Qt symbols from.""" import sys @@ -28,4 +26,5 @@ from PyQt4.QtCore import * from PyQt4.QtGui import * +from PyQt4.QtSvg import * from PyQt4.uic import loadUi diff -Nru veusz-1.10/qtwidgets/datasetbrowser.py veusz-1.14/qtwidgets/datasetbrowser.py --- veusz-1.10/qtwidgets/datasetbrowser.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/qtwidgets/datasetbrowser.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,677 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2011 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################### + +"""A widget for navigating datasets.""" + +import os.path +import numpy as N +import textwrap + +import veusz.qtall as qt4 +import veusz.setting as setting +import veusz.document as document +import veusz.utils as utils + +from lineeditwithclear import LineEditWithClear +from veusz.utils.treemodel import TMNode, TreeModel + +def datasetLinkFile(ds): + """Get a linked filename from a dataset.""" + if ds.linked is None: + return "/" + else: + return ds.linked.filename + +class DatasetNode(TMNode): + """Node for a dataset.""" + + def __init__(self, doc, dsname, cols, parent): + ds = doc.data[dsname] + data = [] + assert cols[0] == "name" + for c in cols: + if c == "name": + data.append( dsname ) + elif c == "size": + data.append( ds.userSize() ) + elif c == "type": + data.append( ds.dstype ) + elif c == "linkfile": + data.append( os.path.basename(datasetLinkFile(ds)) ) + + TMNode.__init__(self, tuple(data), parent) + self.doc = doc + self.cols = cols + + def getPreviewPixmap(self, ds): + """Get a preview pixmap for a dataset.""" + size = (140, 70) + if ds.dimensions != 1 or ds.datatype != "numeric": + return None + + pixmap = qt4.QPixmap(*size) + pixmap.fill(qt4.Qt.transparent) + p = qt4.QPainter(pixmap) + p.setRenderHint(qt4.QPainter.Antialiasing) + + # calculate data points + try: + if len(ds.data) < size[1]: + y = ds.data + else: + intvl = len(ds.data)/size[1]+1 + y = ds.data[::intvl] + x = N.arange(len(y)) + + # plot data points on image + minval, maxval = N.nanmin(y), N.nanmax(y) + y = (y-minval) / (maxval-minval) * size[1] + finite = N.isfinite(y) + x, y = x[finite], y[finite] + x = x * (1./len(x)) * size[0] + + poly = qt4.QPolygonF() + utils.addNumpyToPolygonF(poly, x, size[1]-y) + p.setPen( qt4.QPen(qt4.Qt.blue) ) + p.drawPolyline(poly) + + # draw x axis if span 0 + p.setPen( qt4.QPen(qt4.Qt.black) ) + if minval <= 0 and maxval > 0: + y0 = size[1] - (0-minval)/(maxval-minval)*size[1] + p.drawLine(x[0], y0, x[-1], y0) + else: + p.drawLine(x[0], size[1], x[-1], size[1]) + p.drawLine(x[0], 0, x[0], size[1]) + + except (ValueError, ZeroDivisionError): + # zero sized array after filtering or min == max, so return None + p.end() + return None + + p.end() + return pixmap + + def toolTip(self, column): + """Return tooltip for column.""" + try: + ds = self.doc.data[self.data[0]] + except KeyError: + return qt4.QVariant() + + c = self.cols[column] + if c == "name": + return qt4.QVariant(textwrap.fill(ds.description(), 40)) + elif c == "size" or (c == 'type' and 'size' not in self.cols): + text = ds.userPreview() + # add preview of dataset if possible + pix = self.getPreviewPixmap(ds) + if pix: + text = text.replace("\n", "
    ") + text = "%s
    %s" % (text, utils.pixmapAsHtml(pix)) + return qt4.QVariant(text) + elif c == "linkfile" or c == "type": + return qt4.QVariant(textwrap.fill(ds.linkedInformation(), 40)) + return qt4.QVariant() + + def dataset(self): + """Get associated dataset.""" + try: + return self.doc.data[self.data[0]] + except KeyError: + return None + + def datasetName(self): + """Get dataset name.""" + return self.data[0] + + def cloneTo(self, newroot): + """Make a clone of self at the root given.""" + return self.__class__(self.doc, self.data[0], self.cols, newroot) + +class FilenameNode(TMNode): + """A special node for holding filenames of files.""" + + def nodeData(self, column): + """basename of filename for data.""" + if column == 0: + if self.data[0] == "/": + return qt4.QVariant("/") + else: + return qt4.QVariant(os.path.basename(self.data[0])) + return qt4.QVariant() + + def filename(self): + """Return filename.""" + return self.data[0] + + def toolTip(self, column): + """Full filename for tooltip.""" + if column == 0: + return qt4.QVariant(self.data[0]) + return qt4.QVariant() + +def treeFromList(nodelist, rootdata): + """Construct a tree from a list of nodes.""" + tree = TMNode( rootdata, None ) + for node in nodelist: + tree.insertChildSorted(node) + return tree + +class DatasetRelationModel(TreeModel): + """A model to show how the datasets are related to each file.""" + def __init__(self, doc, grouping="filename", readonly=False, + filterdims=None, filterdtype=None): + """Model parameters: + doc: document + group: how to group datasets + readonly: no modification of data + filterdims/filterdtype: filter dimensions and datatypes. + """ + + TreeModel.__init__(self, ("Dataset", "Size", "Type")) + self.doc = doc + self.linkednodes = {} + self.grouping = grouping + self.filter = "" + self.readonly = readonly + self.filterdims = filterdims + self.filterdtype = filterdtype + self.refresh() + + self.connect(doc, qt4.SIGNAL("sigModified"), self.refresh) + + def datasetFilterOut(self, ds, node): + """Should dataset be filtered out by filter options.""" + filterout = False + + # is filter text not in node text + if ( self.filter != "" and + all([d.find(self.filter)<0 for d in node.data]) ): + filterout = True + if ( self.filterdims is not None and + ds.dimensions not in self.filterdims ): + filterout = True + if ( self.filterdtype is not None and + ds.datatype not in self.filterdtype ): + filterout = True + + return filterout + + def makeGrpTreeNone(self): + """Make tree with no grouping.""" + tree = TMNode( ("Dataset", "Size", "Type", "File"), None ) + for name, ds in self.doc.data.iteritems(): + child = DatasetNode( self.doc, name, + ("name", "size", "type", "linkfile"), + None ) + + # add if not filtered for filtering + if not self.datasetFilterOut(ds, child): + tree.insertChildSorted(child) + return tree + + def makeGrpTree(self, coltitles, colitems, grouper, GrpNodeClass): + """Make a tree grouping with function: + coltitles: tuple of titles of columns for user + colitems: tuple of items to lookup in DatasetNode + grouper: function of dataset to return text for grouping + GrpNodeClass: class for creating grouping nodes + """ + grpnodes = {} + for name, ds in self.doc.data.iteritems(): + child = DatasetNode(self.doc, name, colitems, None) + + # check whether filtered out + if not self.datasetFilterOut(ds, child): + # get group + grp = grouper(ds) + if grp not in grpnodes: + grpnodes[grp] = GrpNodeClass( (grp,), None ) + # add to group + grpnodes[grp].insertChildSorted(child) + + return treeFromList(grpnodes.values(), coltitles) + + def makeGrpTreeFilename(self): + """Make a tree of datasets grouped by linked file.""" + return self.makeGrpTree( + ("Dataset", "Size", "Type"), + ("name", "size", "type"), + lambda ds: datasetLinkFile(ds), + FilenameNode + ) + + def makeGrpTreeSize(self): + """Make a tree of datasets grouped by dataset size.""" + return self.makeGrpTree( + ("Dataset", "Type", "Filename"), + ("name", "type", "linkfile"), + lambda ds: ds.userSize(), + TMNode + ) + + def makeGrpTreeType(self): + """Make a tree of datasets grouped by dataset type.""" + return self.makeGrpTree( + ("Dataset", "Size", "Filename"), + ("name", "size", "linkfile"), + lambda ds: ds.dstype, + TMNode + ) + + def flags(self, idx): + """Return model flags for index.""" + f = TreeModel.flags(self, idx) + # allow dataset names to be edited + if ( idx.isValid() and isinstance(self.objFromIndex(idx), DatasetNode) + and not self.readonly and idx.column() == 0 ): + f |= qt4.Qt.ItemIsEditable + return f + + def setData(self, idx, data, role): + """Rename dataset.""" + dsnode = self.objFromIndex(idx) + newname = unicode(data.toString()) + if not utils.validateDatasetName(newname) or newname in self.doc.data: + return False + + self.doc.applyOperation( + document.OperationDatasetRename(dsnode.data[0], newname)) + self.emit( + qt4.SIGNAL("dataChanged(const QModelIndex &, const QModelIndex &)"), + idx, idx) + return True + + def refresh(self): + """Update tree of datasets when document changes.""" + + header = self.root.data + tree = { + "none": self.makeGrpTreeNone, + "filename": self.makeGrpTreeFilename, + "size": self.makeGrpTreeSize, + "type": self.makeGrpTreeType, + }[self.grouping]() + + self.syncTree(tree) + +class DatasetsNavigatorTree(qt4.QTreeView): + """Tree view for dataset names.""" + + def __init__(self, doc, mainwin, grouping, parent, + readonly=False, filterdims=None, filterdtype=None): + """Initialise the dataset tree view. + doc: veusz document + mainwin: veusz main window (or None if readonly) + grouping: grouping mode of datasets + parent: parent window or None + filterdims: if set, only show datasets with dimensions given + filterdtype: if set, only show datasets with type given + """ + + qt4.QTreeView.__init__(self, parent) + self.doc = doc + self.mainwindow = mainwin + self.model = DatasetRelationModel(doc, grouping, readonly=readonly, + filterdims=filterdims, + filterdtype=filterdtype) + + self.setModel(self.model) + self.setSelectionBehavior(qt4.QTreeView.SelectRows) + self.setUniformRowHeights(True) + self.setContextMenuPolicy(qt4.Qt.CustomContextMenu) + if not readonly: + self.connect(self, qt4.SIGNAL("customContextMenuRequested(QPoint)"), + self.showContextMenu) + self.model.refresh() + self.expandAll() + + # stretch of columns + hdr = self.header() + hdr.setStretchLastSection(False) + hdr.setResizeMode(0, qt4.QHeaderView.Stretch) + for col in xrange(1, 3): + hdr.setResizeMode(col, qt4.QHeaderView.ResizeToContents) + + # when documents have finished opening, expand all nodes + if mainwin is not None: + self.connect(mainwin, qt4.SIGNAL("documentopened"), self.expandAll) + + # keep track of selection + self.connect( self.selectionModel(), + qt4.SIGNAL("selectionChanged(const QItemSelection&, " + "const QItemSelection&)"), + self.slotNewSelection ) + + # expand nodes by default + self.connect( self.model, + qt4.SIGNAL("rowsInserted(const QModelIndex&, int, int)"), + self.slotNewRow ) + + def changeGrouping(self, grouping): + """Change the tree grouping behaviour.""" + self.model.grouping = grouping + self.model.refresh() + self.expandAll() + + def changeFilter(self, filtertext): + """Change filtering text.""" + self.model.filter = filtertext + self.model.refresh() + self.expandAll() + + def selectDataset(self, dsname): + """Find, and if possible select dataset name.""" + + matches = self.model.match( + self.model.index(0, 0, qt4.QModelIndex()), + qt4.Qt.DisplayRole, qt4.QVariant(dsname), -1, + qt4.Qt.MatchFixedString | qt4.Qt.MatchCaseSensitive | + qt4.Qt.MatchRecursive ) + for idx in matches: + if isinstance(self.model.objFromIndex(idx), DatasetNode): + self.selectionModel().setCurrentIndex( + idx, qt4.QItemSelectionModel.SelectCurrent | + qt4.QItemSelectionModel.Clear | + qt4.QItemSelectionModel.Rows ) + + def showContextMenu(self, pt): + """Context menu for nodes.""" + idx = self.currentIndex() + if idx.isValid(): + node = self.model.objFromIndex(idx) + else: + node = None + + menu = qt4.QMenu() + if isinstance(node, DatasetNode): + self.datasetContextMenu(node, menu) + elif isinstance(node, FilenameNode): + self.filenameContextMenu(node, menu) + + def _paste(): + """Paste dataset.""" + if document.isDataMime(): + mime = qt4.QApplication.clipboard().mimeData() + self.doc.applyOperation(document.OperationDataPaste(mime)) + + # if there is data to paste, add menu item + if document.isDataMime(): + menu.addAction("Paste", _paste) + + if len( menu.actions() ) != 0: + menu.exec_(self.mapToGlobal(pt)) + + def datasetContextMenu(self, dsnode, menu): + """Return context menu for datasets.""" + import veusz.dialogs.dataeditdialog as dataeditdialog + dataset = dsnode.dataset() + dsname = dsnode.datasetName() + + def _edit(): + """Open up dialog box to recreate dataset.""" + dataeditdialog.recreate_register[type(dataset)]( + self.mainwindow, self.doc, dataset, dsname) + def _edit_data(): + """Open up data edit dialog.""" + dialog = self.mainwindow.slotDataEdit(editdataset=dsname) + def _delete(): + """Simply delete dataset.""" + self.doc.applyOperation(document.OperationDatasetDelete(dsname)) + def _unlink_file(): + """Unlink dataset from file.""" + self.doc.applyOperation(document.OperationDatasetUnlinkFile(dsname)) + def _unlink_relation(): + """Unlink dataset from relation.""" + self.doc.applyOperation(document.OperationDatasetUnlinkRelation(dsname)) + def _copy(): + """Copy data to clipboard.""" + mime = document.generateDatasetsMime([dsname], self.doc) + qt4.QApplication.clipboard().setMimeData(mime) + + if type(dataset) in dataeditdialog.recreate_register: + menu.addAction("Edit", _edit) + else: + menu.addAction("Edit data", _edit_data) + + menu.addAction("Delete", _delete) + if dataset.canUnlink(): + if dataset.linked: + menu.addAction("Unlink file", _unlink_file) + else: + menu.addAction("Unlink relation", _unlink_relation) + + menu.addAction("Copy", _copy) + + useasmenu = menu.addMenu("Use as") + if dataset is not None: + self.getMenuUseAs(useasmenu, dataset) + + def filenameContextMenu(self, node, menu): + """Return context menu for filenames.""" + + from veusz.dialogs.reloaddata import ReloadData + filename = node.filename() + if filename == '/': + # non linked filename node + return None + + def _reload(): + """Reload data in this file.""" + d = ReloadData(self.doc, self.mainwindow, filenames=set([filename])) + self.mainwindow.showDialog(d) + def _unlink_all(): + """Unlink all datasets associated with file.""" + self.doc.applyOperation( + document.OperationDatasetUnlinkByFile(filename)) + def _delete_all(): + """Delete all datasets associated with file.""" + self.doc.applyOperation( + document.OperationDatasetDeleteByFile(filename)) + + menu.addAction("Reload", _reload) + menu.addAction("Unlink all", _unlink_all) + menu.addAction("Delete all", _delete_all) + + def getMenuUseAs(self, menu, dataset): + """Build up menu of widget settings to use dataset in.""" + + def addifdatasetsetting(path, setn): + def _setdataset(): + self.doc.applyOperation( + document.OperationSettingSet( + path, self.doc.datasetName(dataset)) ) + + if ( isinstance(setn, setting.Dataset) and + setn.dimensions == dataset.dimensions and + setn.datatype == dataset.datatype and + path[:12] != "/StyleSheet/" ): + menu.addAction(path, _setdataset) + + self.doc.walkNodes(addifdatasetsetting, nodetypes=("setting",)) + + def keyPressEvent(self, event): + """Enter key selects widget.""" + if event.key() in (qt4.Qt.Key_Return, qt4.Qt.Key_Enter): + self.emit(qt4.SIGNAL("updateitem")) + return + qt4.QTreeView.keyPressEvent(self, event) + + def mouseDoubleClickEvent(self, event): + """Emit updateitem signal if double clicked.""" + retn = qt4.QTreeView.mouseDoubleClickEvent(self, event) + self.emit(qt4.SIGNAL("updateitem")) + return retn + + def slotNewSelection(self, selected, deselected): + """Emit selecteditem signal on new selection.""" + self.emit(qt4.SIGNAL("selecteditem"), self.getSelectedDataset()) + + def slotNewRow(self, parent, start, end): + """Expand parent if added.""" + self.expand(parent) + + def getSelectedDataset(self): + """Return selected dataset.""" + name = None + sel = self.selectionModel().selection() + try: + modelidx = sel.indexes()[0] + node = self.model.objFromIndex(modelidx) + if isinstance(node, DatasetNode): + name = node.datasetName() + except IndexError: + pass + return name + +class DatasetBrowser(qt4.QWidget): + """Widget which shows the document's datasets.""" + + # how datasets can be grouped + grpnames = ("none", "filename", "type", "size") + grpentries = { + "none": "None", + "filename": "Filename", + "type": "Type", + "size": "Size" + } + + def __init__(self, thedocument, mainwin, parent, readonly=False, + filterdims=None, filterdtype=None): + """Initialise widget: + thedocument: document to show + mainwin: main window of application (or None if readonly) + parent: parent of widget. + readonly: for choosing datasets only + filterdims: if set, only show datasets with dimensions given + filterdtype: if set, only show datasets with type given + """ + + qt4.QWidget.__init__(self, parent) + self.layout = qt4.QVBoxLayout() + self.setLayout(self.layout) + + # options for navigator are in this layout + self.optslayout = qt4.QHBoxLayout() + + # grouping options - use a menu to choose the grouping + self.grpbutton = qt4.QPushButton("Group") + self.grpmenu = qt4.QMenu() + self.grouping = setting.settingdb.get("navtree_grouping", "filename") + self.grpact = qt4.QActionGroup(self) + self.grpact.setExclusive(True) + for name in self.grpnames: + a = self.grpmenu.addAction(self.grpentries[name]) + a.grpname = name + a.setCheckable(True) + if name == self.grouping: + a.setChecked(True) + self.grpact.addAction(a) + self.connect(self.grpact, qt4.SIGNAL("triggered(QAction*)"), + self.slotGrpChanged) + self.grpbutton.setMenu(self.grpmenu) + self.grpbutton.setToolTip("Group datasets with property given") + self.optslayout.addWidget(self.grpbutton) + + # filtering by entering text + self.optslayout.addWidget(qt4.QLabel("Filter")) + self.filteredit = LineEditWithClear() + self.filteredit.setToolTip("Enter text here to filter datasets") + self.connect(self.filteredit, qt4.SIGNAL("textChanged(const QString&)"), + self.slotFilterChanged) + self.optslayout.addWidget(self.filteredit) + + self.layout.addLayout(self.optslayout) + + # the actual widget tree + self.navtree = DatasetsNavigatorTree( + thedocument, mainwin, self.grouping, None, + readonly=readonly, filterdims=filterdims, filterdtype=filterdtype) + self.layout.addWidget(self.navtree) + + def slotGrpChanged(self, action): + """Grouping changed by user.""" + self.navtree.changeGrouping(action.grpname) + setting.settingdb["navtree_grouping"] = action.grpname + + def slotFilterChanged(self, filtertext): + """Filtering changed by user.""" + self.navtree.changeFilter(unicode(filtertext)) + + def selectDataset(self, dsname): + """Find, and if possible select dataset name.""" + self.navtree.selectDataset(dsname) + +class DatasetBrowserPopup(DatasetBrowser): + """Popup window for dataset browser for selecting datasets. + This is used by setting.controls.Dataset + """ + + def __init__(self, document, dsname, parent, + filterdims=None, filterdtype=None): + """Open popup window for document + dsname: dataset name + parent: window parent + filterdims: if set, only show datasets with dimensions given + filterdtype: if set, only show datasets with type given + """ + + DatasetBrowser.__init__(self, document, None, parent, readonly=True, + filterdims=filterdims, filterdtype=filterdtype) + self.setWindowFlags(qt4.Qt.Popup) + self.setAttribute(qt4.Qt.WA_DeleteOnClose) + self.spacing = self.fontMetrics().height() + + utils.positionFloatingPopup(self, parent) + self.selectDataset(dsname) + self.installEventFilter(self) + + self.navtree.setFocus() + + self.connect(self.navtree, qt4.SIGNAL("updateitem"), + self.slotUpdateItem) + + def eventFilter(self, node, event): + """Grab clicks outside this window to close it.""" + if ( isinstance(event, qt4.QMouseEvent) and + event.buttons() != qt4.Qt.NoButton ): + frame = qt4.QRect(0, 0, self.width(), self.height()) + if not frame.contains(event.pos()): + self.close() + return True + return qt4.QTextEdit.eventFilter(self, node, event) + + def sizeHint(self): + """A reasonable size for the text editor.""" + return qt4.QSize(self.spacing*30, self.spacing*20) + + def closeEvent(self, event): + """Tell the calling widget that we are closing.""" + self.emit(qt4.SIGNAL("closing")) + event.accept() + + def slotUpdateItem(self): + """Emit new dataset signal.""" + selected = self.navtree.selectionModel().currentIndex() + if selected.isValid(): + n = self.navtree.model.objFromIndex(selected) + if isinstance(n, DatasetNode): + self.emit(qt4.SIGNAL("newdataset"), n.data[0]) + self.close() diff -Nru veusz-1.10/qtwidgets/historycheck.py veusz-1.14/qtwidgets/historycheck.py --- veusz-1.10/qtwidgets/historycheck.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/qtwidgets/historycheck.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,63 @@ +# Copyright (C) 2009 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +import veusz.qtall as qt4 +import veusz.setting as setting + +class HistoryCheck(qt4.QCheckBox): + """Checkbox remembers its setting between calls + """ + + def __init__(self, *args): + qt4.QCheckBox.__init__(self, *args) + self.default = False + + def getSettingName(self): + """Get name for saving in settings.""" + # get dialog for widget + dialog = self.parent() + while not isinstance(dialog, qt4.QDialog): + dialog = dialog.parent() + + # combine dialog and object names to make setting + return '%s_%s_HistoryCheck' % ( dialog.objectName(), + self.objectName() ) + + def loadHistory(self): + """Load contents of HistoryCheck from settings.""" + checked = setting.settingdb.get(self.getSettingName(), self.default) + # this is to ensure toggled() signals get sent + self.setChecked(not checked) + self.setChecked(checked) + + def saveHistory(self): + """Save contents of HistoryCheck to settings.""" + setting.settingdb[self.getSettingName()] = self.isChecked() + + def showEvent(self, event): + """Show HistoryCheck and load history.""" + qt4.QCheckBox.showEvent(self, event) + # we do this now rather than in __init__ because the widget + # has no name set at __init__ + self.loadHistory() + + def hideEvent(self, event): + """Save history as widget is hidden.""" + qt4.QCheckBox.hideEvent(self, event) + self.saveHistory() + diff -Nru veusz-1.10/qtwidgets/historycombo.py veusz-1.14/qtwidgets/historycombo.py --- veusz-1.10/qtwidgets/historycombo.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/qtwidgets/historycombo.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,134 @@ +# Copyright (C) 2009 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +"""A combobox which remembers its history. + +The history is stored in the Veusz settings database. +""" + +import veusz.qtall as qt4 +import veusz.setting as setting + +class HistoryCombo(qt4.QComboBox): + """This combobox records what items have been entered into it so the + user can choose them again. + + Duplicates and blanks are ignored. + """ + + def __init__(self, *args): + qt4.QComboBox.__init__(self, *args) + + # sane defaults + self.setEditable(True) + self.setAutoCompletion(True) + self.setMaxCount(50) + self.setInsertPolicy(qt4.QComboBox.InsertAtTop) + self.setDuplicatesEnabled(False) + self.setSizePolicy( qt4.QSizePolicy(qt4.QSizePolicy.MinimumExpanding, + qt4.QSizePolicy.Fixed) ) + + # stops combobox readjusting in size to fit contents + self.setSizeAdjustPolicy( + qt4.QComboBox.AdjustToMinimumContentsLengthWithIcon) + + self.default = [] + self.hasshown = False + + def text(self): + """Get text in combobox + - this gives it the same interface as QLineEdit.""" + return self.currentText() + + def setText(self, text): + """Set text in combobox + - gives same interface as QLineEdit.""" + self.lineEdit().setText(text) + + def hasAcceptableInput(self): + """Input valid? + - gives same interface as QLineEdit.""" + return self.lineEdit().hasAcceptableInput() + + def replaceAndAddHistory(self, item): + """Replace the text and place item at top of history.""" + + self.lineEdit().setText(item) + index = self.findText(item) # lookup for existing item (if any) + if index != -1: + # remove any old items matching this + self.removeItem(index) + + # put new item in + self.insertItem(0, item) + # set selected item in drop down list match current item + self.setCurrentIndex(0) + + def getSettingName(self): + """Get name for saving in settings.""" + + # get dialog for widget + dialog = self.parent() + while not isinstance(dialog, qt4.QDialog): + dialog = dialog.parent() + + # combine dialog and object names to make setting + return '%s_%s_HistoryCombo' % ( dialog.objectName(), + self.objectName() ) + + def loadHistory(self): + """Load contents of history combo from settings.""" + self.clear() + history = setting.settingdb.get(self.getSettingName(), self.default) + self.insertItems(0, history) + + self.hasshown = True + + def saveHistory(self): + """Save contents of history combo to settings.""" + + # only save history if it has been loaded + if not self.hasshown: + return + + # collect current items + history = [ unicode(self.itemText(i)) for i in xrange(self.count()) ] + history.insert(0, unicode(self.currentText())) + + # remove dups + histout = [] + histset = set() + for item in history: + if item not in histset: + histout.append(item) + histset.add(item) + + # save the history + setting.settingdb[self.getSettingName()] = histout + + def showEvent(self, event): + """Show HistoryCombo and load history.""" + qt4.QComboBox.showEvent(self, event) + # we do this now rather than in __init__ because the widget + # has no name set at __init__ + self.loadHistory() + + def hideEvent(self, event): + """Save history as widget is hidden.""" + qt4.QComboBox.hideEvent(self, event) + self.saveHistory() diff -Nru veusz-1.10/qtwidgets/historygroupbox.py veusz-1.14/qtwidgets/historygroupbox.py --- veusz-1.10/qtwidgets/historygroupbox.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/qtwidgets/historygroupbox.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,76 @@ +# Copyright (C) 2010 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +import veusz.qtall as qt4 +import veusz.setting as setting + +class HistoryGroupBox(qt4.QGroupBox): + """Group box remembers settings of radio buttons inside it. + + emits radioClicked(radiowidget) when clicked + """ + + def getSettingName(self): + """Get name for saving in settings.""" + # get dialog for widget + dialog = self.parent() + while not isinstance(dialog, qt4.QDialog): + dialog = dialog.parent() + + # combine dialog and object names to make setting + return '%s_%s_HistoryGroup' % ( dialog.objectName(), + self.objectName() ) + + def loadHistory(self): + """Load from settings.""" + # connect up radio buttons to emit clicked signal + for w in self.children(): + if isinstance(w, qt4.QRadioButton): + def doemit(w=w): + self.emit(qt4.SIGNAL("radioClicked"), w) + self.connect( w, qt4.SIGNAL('clicked()'), doemit) + + # set item to be checked + checked = setting.settingdb.get(self.getSettingName(), "") + for w in self.children(): + if isinstance(w, qt4.QRadioButton) and ( + w.objectName() == checked or checked == ""): + w.click() + return + + def getRadioChecked(self): + """Get name of radio button checked.""" + for w in self.children(): + if isinstance(w, qt4.QRadioButton) and w.isChecked(): + return w + return None + + def saveHistory(self): + """Save to settings.""" + name = unicode(self.getRadioChecked().objectName()) + setting.settingdb[self.getSettingName()] = name + + def showEvent(self, event): + """Show and load history.""" + qt4.QGroupBox.showEvent(self, event) + self.loadHistory() + + def hideEvent(self, event): + """Save history as widget is hidden.""" + qt4.QGroupBox.hideEvent(self, event) + self.saveHistory() diff -Nru veusz-1.10/qtwidgets/historyspinbox.py veusz-1.14/qtwidgets/historyspinbox.py --- veusz-1.10/qtwidgets/historyspinbox.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/qtwidgets/historyspinbox.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,57 @@ +# Copyright (C) 2010 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +import veusz.qtall as qt4 +import veusz.setting as setting + +class HistorySpinBox(qt4.QSpinBox): + """A SpinBox which remembers its setting between calls.""" + + def __init__(self, *args): + qt4.QSpinBox.__init__(self, *args) + self.default = 0 + + def getSettingName(self): + """Get name for saving in settings.""" + # get dialog for widget + dialog = self.parent() + while not isinstance(dialog, qt4.QDialog): + dialog = dialog.parent() + + # combine dialog and object names to make setting + return "%s_%s_HistorySpinBox" % ( dialog.objectName(), + self.objectName() ) + + def loadHistory(self): + """Load contents of HistorySpinBox from settings.""" + num = setting.settingdb.get(self.getSettingName(), self.default) + self.setValue(num) + + def saveHistory(self): + """Save contents of HistorySpinBox to settings.""" + setting.settingdb[self.getSettingName()] = self.value() + + def showEvent(self, event): + """Show HistorySpinBox and load history.""" + qt4.QSpinBox.showEvent(self, event) + self.loadHistory() + + def hideEvent(self, event): + """Save history as widget is hidden.""" + qt4.QSpinBox.hideEvent(self, event) + self.saveHistory() diff -Nru veusz-1.10/qtwidgets/historyvaluecombo.py veusz-1.14/qtwidgets/historyvaluecombo.py --- veusz-1.10/qtwidgets/historyvaluecombo.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/qtwidgets/historyvaluecombo.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,92 @@ +# Copyright (C) 2009 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +"""A combobox which remembers previous setting +""" + +import veusz.qtall as qt4 +import veusz.setting as setting + +class HistoryValueCombo(qt4.QComboBox): + """This combobox records what value was previously saved + """ + + def __init__(self, *args): + qt4.QComboBox.__init__(self, *args) + self.defaultlist = [] + self.defaultval = None + self.hasshown = False + + def getSettingName(self): + """Get name for saving in settings.""" + + # get dialog for widget + dialog = self.parent() + while not isinstance(dialog, qt4.QDialog): + dialog = dialog.parent() + + # combine dialog and object names to make setting + return '%s_%s_HistoryValueCombo' % ( dialog.objectName(), + self.objectName() ) + + def saveHistory(self): + """Save contents of history combo to settings.""" + + # only save history if it has been loaded + if not self.hasshown: + return + + # collect current items + history = [ unicode(self.itemText(i)) for i in xrange(self.count()) ] + history.insert(0, unicode(self.currentText())) + + # remove dups + histout = [] + histset = set() + for item in history: + if item not in histset: + histout.append(item) + histset.add(item) + + # save the history + setting.settingdb[self.getSettingName()] = histout + + def showEvent(self, event): + """Show HistoryCombo and load history.""" + qt4.QComboBox.showEvent(self, event) + + self.clear() + self.addItems(self.defaultlist) + text = setting.settingdb.get(self.getSettingName(), self.defaultval) + if text is not None: + indx = self.findText(text) + if indx < 0: + if self.isEditable(): + self.insertItem(0, text) + indx = 0 + self.setCurrentIndex(indx) + self.hasshown = True + + def hideEvent(self, event): + """Save history as widget is hidden.""" + qt4.QComboBox.hideEvent(self, event) + + if self.hasshown: + text = unicode(self.currentText()) + setting.settingdb[self.getSettingName()] = text + diff -Nru veusz-1.10/qtwidgets/__init__.py veusz-1.14/qtwidgets/__init__.py --- veusz-1.10/qtwidgets/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/qtwidgets/__init__.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,38 @@ +# Copyright (C) 2011 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +"""Veusz qtwidgets module.""" + +# insert history combo into the list of modules so that it can be found +# by loadUi - yuck +import sys +import historycombo +import historycheck +import historyvaluecombo +import historygroupbox +import historyspinbox +import recentfilesbutton +import lineeditwithclear + +sys.modules['historycombo'] = historycombo +sys.modules['historycheck'] = historycheck +sys.modules['historyvaluecombo'] = historyvaluecombo +sys.modules['historygroupbox'] = historygroupbox +sys.modules['historyspinbox'] = historyspinbox +sys.modules['recentfilesbutton'] = recentfilesbutton +sys.modules['lineeditwithclear'] = lineeditwithclear diff -Nru veusz-1.10/qtwidgets/lineeditwithclear.py veusz-1.14/qtwidgets/lineeditwithclear.py --- veusz-1.10/qtwidgets/lineeditwithclear.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/qtwidgets/lineeditwithclear.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2011 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################### + +import veusz.qtall as qt4 +import veusz.utils as utils + +class LineEditWithClear(qt4.QLineEdit): + """This is a line edit widget which supplies a clear button + to delete the text if it is clicked. + + Adapted from: + http://labs.qt.nokia.com/2007/06/06/lineedit-with-a-clear-button/ + """ + + def __init__(self, *args): + """Initialise the line edit.""" + qt4.QLineEdit.__init__(self, *args) + + # the clear button itself, with no padding + self.clearbutton = cb = qt4.QToolButton(self) + cb.setIcon( utils.getIcon('kde-edit-delete') ) + cb.setCursor(qt4.Qt.ArrowCursor) + cb.setStyleSheet('QToolButton { border: none; padding: 0px; }') + cb.setToolTip("Clear text") + cb.hide() + + # make clicking on the button clear the text + self.connect(cb, qt4.SIGNAL('clicked()'), self, qt4.SLOT("clear()")) + + # button should appear if there is text + self.connect(self, qt4.SIGNAL('textChanged(const QString&)'), + self.updateCloseButton) + + # positioning of the button + fw = self.style().pixelMetric(qt4.QStyle.PM_DefaultFrameWidth) + self.setStyleSheet("QLineEdit { padding-right: %ipx; } " % + (cb.sizeHint().width() + fw + 1)) + msz = self.minimumSizeHint() + mx = cb.sizeHint().height()+ fw*2 + 2 + self.setMinimumSize( max(msz.width(), mx), max(msz.height(), mx) ) + + def resizeEvent(self, evt): + """Move button if widget resized.""" + sz = self.clearbutton.sizeHint() + fw = self.style().pixelMetric(qt4.QStyle.PM_DefaultFrameWidth) + r = self.rect() + self.clearbutton.move( r.right() - fw - sz.width(), + (r.bottom() + 1 - sz.height())/2 ) + + def updateCloseButton(self, text): + """Button should only appear if there is text.""" + self.clearbutton.setVisible( not text.isEmpty() ) diff -Nru veusz-1.10/qtwidgets/recentfilesbutton.py veusz-1.14/qtwidgets/recentfilesbutton.py --- veusz-1.10/qtwidgets/recentfilesbutton.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/qtwidgets/recentfilesbutton.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,81 @@ +# Copyright (C) 2009 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +import os.path + +import veusz.qtall as qt4 +import veusz.setting as setting + +def removeBadRecents(itemlist): + """Remove duplicates from list and bad entries.""" + previous = set() + i = 0 + while i < len(itemlist): + if itemlist[i] in previous: + del itemlist[i] + elif not os.path.exists(itemlist[i]): + del itemlist[i] + else: + previous.add(itemlist[i]) + i += 1 + + # trim list + del itemlist[10:] + +class RecentFilesButton(qt4.QPushButton): + """A button for remembering recent files. + + emits filechosen(filename) if a file is chosen + """ + + def __init__(self, *args): + qt4.QPushButton.__init__(self, *args) + + self.menu = qt4.QMenu() + self.setMenu(self.menu) + self.settingname = None + + def setSetting(self, name): + """Specify settings to use when loading menu. + Should be called before use.""" + self.settingname = name + self.fillMenu() + + def fillMenu(self): + """Add filenames to menu.""" + self.menu.clear() + recent = setting.settingdb.get(self.settingname, []) + removeBadRecents(recent) + setting.settingdb[self.settingname] = recent + + for filename in recent: + if os.path.exists(filename): + act = self.menu.addAction( os.path.basename(filename) ) + def loadRecentFile(filename=filename): + self.emit(qt4.SIGNAL('filechosen'), filename) + self.connect( act, qt4.SIGNAL('triggered()'), + loadRecentFile ) + + def addFile(self, filename): + """Add filename to list of recent files.""" + recent = setting.settingdb.get(self.settingname, []) + recent.insert(0, os.path.abspath(filename)) + setting.settingdb[self.settingname] = recent + self.fillMenu() + + diff -Nru veusz-1.10/README veusz-1.14/README --- veusz-1.10/README 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/README 2011-11-22 20:23:31.000000000 +0000 @@ -1,10 +1,12 @@ -Veusz 1.10 +Veusz 1.14 ---------- Velvet Ember Under Sky Zenith ----------------------------- http://home.gna.org/veusz/ -Veusz is Copyright (C) 2003-2010 Jeremy Sanders +Copyright (C) 2003-2011 Jeremy Sanders +and contributors. + Licenced under the GPL (version 2 or greater). Veusz is a Qt4 based scientific plotting package. It is written in @@ -16,41 +18,63 @@ Veusz provides a GUI, command line, embedding and scripting interface (based on Python) to its plotting facilities. It also allows for manipulation and editing of datasets. Data can be captured from -external sources such as internet sockets or other programs. +external sources such as Internet sockets or other programs. -Changes in 1.10: - * Box plot widget added, which can be given statistics to plot or - calculated from datasets - * Polar plot widget added - * Datasets are now easier to construct and edit in the Data->Edit - dialog box - * CSV reader will assume a text dataset if it cannot convert first item - to a number - * Add color sequence plugin for making a range of widget colors - * Import plugin for QDP files added - * Date and times can be also written in local formats - * Reload data dialog box can reload at intervals and is now non-modal - * 2D datasets can be created based on expressions of other 2D datasets - -Minor changes: - * Option to change size of ends of error bars - * Margin size option added for key widget - * Add --listen option to veusz command to replace veusz_listen. - * Add --quiet option to run commands without displaying a window - * Add --export option to export documents to graphics files and exit - * PNG export compression increased - * Add option to ignore number of lines after headers in CSV files - -Bug fixes: - * Multiple datasets can now be properly created from dataset plugin dialog - * X and Y ranges of 2D datasets are now correct when converted from - X,Y,Z 1D datasets - * Bounding boxes of resizing rectangles, ellipses and images are fixed - * min and max coordinate range now works for plotting functions of y - * Remove duplicate linked files when using import plugins - * Several crash reports fixed - * More robust code in data->edit dialog box - * veusz_listen now works in Windows (not in binary package yet) +Changes in 1.14: + * Added interactive tutorial + * Points in graphs can be colored depending on another dataset and + the scale shown in a colorbar widget + * Improved CSV import + - better data type detection + - locale-specific numeric and date formats + - single/multiple/none header modes + - option to skip lines at top of file + - better handling of missing values + * Data can be imported from clipboard + * Substantially reduced size of output SVG files + * In standard data import, descriptor can be left blank to generate + dataset names colX + * Axis plotting range can be interactively manipulated + * If axis is in date-time format, show and allow the min and max + values to be in date-time format + * ImageFile widget can have image data embedded in document file + * Fit widget can update the fit parameters and fit quality to a + label widget + * Allow editing of 2D datasets in data edit dialog + * Add copy and paste dataset command to dataset browser context menu + +Minor and API changes: + * Examples added to help menu + * Picker shows date values as dates + * Allow descriptor statement in standard data files after a comment + character, e.g. "#descriptor x y" + * Added some further color maps + * Draw key symbols for vector field widget + * Import plugin changes + - Register classes rather than instances (backward compatibility + is retained) + - Plugins can return constants and functions (see Constant and + Function types) + - Add DatasetDateTime for returning date-time datasets + * Custom definitions + - Add RemoveCustom API to remove custom definitions + - AddCustom API can specify order where custom definition is added + * C++ code to speed up plotting points of different sizes / colors + * Expand files by default in data navigator window + * Select created datasets in data edit dialog + * Tooltip wrapping used in data navigator window + * Grid lines are dropped if they overlap with edge of graph + +Bug fixes + * Fix initial extension in export dialog + * Fix crash on hiding pages + * Fixed validation for numeric values + * Position of grid lines in perpendicular direction for non default + positions + * Catch errors in example import plugin + * Fix crash for non existent key symbols + * Fix crash when mismatch of dataset sizes when combining 1D datasets + to make 2D dataset Features of package: * X-Y plots (with errorbars) @@ -62,6 +86,7 @@ * Vector field plots * Box plots * Polar plots + * Ternary plots * Plotting dates * Fitting functions to data * Stacked plots and arrays of plots @@ -73,18 +98,21 @@ * Scripting interface * Dataset creation/manipulation * Embed Veusz within other programs - * Text, CSV, FITS and user-plugin importing + * Text, CSV, FITS, NPY/NPZ, QDP, binary and user-plugin importing * Data can be captured from external sources * User defined functions, constants and can import external Python functions * Plugin interface to allow user to write or load code to - import data using new formats - make new datasets, optionally linked to existing datasets - arbitrarily manipulate the document + * Data picker + * Interactive tutorial + * Multithreaded rendering Requirements for source install: Python (2.4 or greater required) http://www.python.org/ - Qt >= 4.3 (free edition) + Qt >= 4.4 (free edition) http://www.trolltech.com/products/qt/ PyQt >= 4.3 (SIP is required to be installed first) http://www.riverbankcomputing.co.uk/pyqt/ @@ -99,8 +127,11 @@ http://www.stsci.edu/resources/software_hardware/pyfits pyemf >= 2.0.0 (optional for EMF export) http://pyemf.sourceforge.net/ + PyMinuit >= 1.1.2 (optional improved fitting) + http://code.google.com/p/pyminuit/ For EMF and better SVG export, PyQt >= 4.6 or better is required, to fix a bug in the C++ wrapping + For documentation on using Veusz, see the "Documents" directory. The manual is in PDF, HTML and text format (generated from docbook). The @@ -109,23 +140,15 @@ Issues with the current version: - * Plots can sometimes be slow using antialiasing. Go to the - preferences dialog or right click on the plot to disable - antialiasing. - * Some recent versions of PyQt/SIP will causes crashes when exporting SVG files. Update to 4.7.4 (if released) or a recent snapshot to solve this problem. -If you enjoy using Veusz, I would love to hear from you. Please join +If you enjoy using Veusz, we would love to hear from you. Please join the mailing lists at https://gna.org/mail/?group=veusz to discuss new features or if you'd like to contribute code. The -latest code can always be found in the SVN repository. - -Jeremy Sanders - -------------------------------------------------------------------------------- -$Id: README 1472 2010-12-11 21:31:00Z jeremysanders $ +latest code can always be found in the Git repository +at https://github.com/jeremysanders/veusz.git. diff -Nru veusz-1.10/scripts/veusz veusz-1.14/scripts/veusz --- veusz-1.10/scripts/veusz 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/scripts/veusz 2011-11-22 20:23:31.000000000 +0000 @@ -20,7 +20,5 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: veusz 872 2008-12-29 12:51:59Z jeremysanders $ - import veusz.veusz_main veusz.veusz_main.run() diff -Nru veusz-1.10/scripts/veusz_listen veusz-1.14/scripts/veusz_listen --- veusz-1.10/scripts/veusz_listen 2010-12-12 12:41:07.000000000 +0000 +++ veusz-1.14/scripts/veusz_listen 2011-11-22 20:23:31.000000000 +0000 @@ -20,7 +20,5 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: veusz_listen 872 2008-12-29 12:51:59Z jeremysanders $ - import veusz.veusz_listen veusz.veusz_listen.run() diff -Nru veusz-1.10/setting/collections.py veusz-1.14/setting/collections.py --- veusz-1.10/setting/collections.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/setting/collections.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: collections.py 1449 2010-11-22 09:26:58Z jeremysanders $ - """Collections of predefined settings for common settings.""" import veusz.qtall as qt4 @@ -51,14 +49,14 @@ descr = 'Hide the line', usertext='Hide') ) - def makeQPen(self, painter): + def makeQPen(self, painthelper): '''Make a QPen from the description. This currently ignores the hide attribute ''' color = qt4.QColor(self.color) color.setAlphaF( (100-self.transparency) / 100.) - width = self.get('width').convert(painter) + width = self.get('width').convert(painthelper) style, dashpattern = setting.LineStyle._linecnvt[self.style] pen = qt4.QPen( color, width, style ) @@ -67,12 +65,12 @@ return pen - def makeQPenWHide(self, painter): + def makeQPenWHide(self, painthelper): """Make a pen, taking account of hide attribute.""" if self.hide: return qt4.QPen(qt4.Qt.NoPen) else: - return self.makeQPen(painter) + return self.makeQPen(painthelper) class XYPlotLine(Line): '''A plot line for plotting data, allowing histogram-steps @@ -238,10 +236,10 @@ c.families = self.families return c - def makeQFont(self, painter): + def makeQFont(self, painthelper): '''Return a qt4.QFont object corresponding to the settings.''' - size = self.get('size').convertPts(painter) + size = self.get('size').convertPts(painthelper) weight = qt4.QFont.Normal if self.bold: weight = qt4.QFont.Bold @@ -249,7 +247,7 @@ f = qt4.QFont(self.font, size, weight, self.italic) if self.underline: f.setUnderline(True) - f.setStyleHint( qt4.QFont.Times, qt4.QFont.PreferDevice ) + f.setStyleHint(qt4.QFont.Times) return f diff -Nru veusz-1.10/setting/controls.py veusz-1.14/setting/controls.py --- veusz-1.10/setting/controls.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/setting/controls.py 2011-11-22 20:23:31.000000000 +0000 @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2005 Jeremy S. Sanders # Email: Jeremy Sanders # @@ -16,8 +17,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: controls.py 1469 2010-12-08 22:15:00Z jeremysanders $ - """Module for creating QWidgets for the settings, to enable their values to be changed. @@ -27,10 +26,12 @@ from itertools import izip import re +import numpy as N import veusz.qtall as qt4 import setting + import veusz.utils as utils def styleClear(widget): @@ -42,6 +43,15 @@ widget.setStyleSheet("background-color: " + setting.settingdb.color('error').name() ) +class DotDotButton(qt4.QPushButton): + """A button for opening up more complex editor.""" + def __init__(self, tooltip=None, checkable=True): + qt4.QPushButton.__init__(self, "..", flat=True, checkable=checkable, + maximumWidth=16) + if tooltip: + self.setToolTip(tooltip) + self.setSizePolicy(qt4.QSizePolicy.Maximum, qt4.QSizePolicy.Maximum) + class Edit(qt4.QLineEdit): """Main control for editing settings which are text.""" @@ -69,11 +79,7 @@ try: val = self.setting.fromText(text) styleClear(self) - - # value has changed - if self.setting.val != val: - self.emit( qt4.SIGNAL('settingChanged'), - self, self.setting, val ) + self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, val ) except setting.InvalidType: styleError(self) @@ -107,7 +113,7 @@ if readonly: self.setReadOnly(True) - self.positionSelf(parent) + utils.positionFloatingPopup(self, parent) self.installEventFilter(self) @@ -138,42 +144,13 @@ """A reasonable size for the text editor.""" return qt4.QSize(self.spacing*40, self.spacing*3) - def positionSelf(self, widget): - """Open the edit box below the widget.""" - - pos = widget.parentWidget().mapToGlobal( widget.pos() ) - desktop = qt4.QApplication.desktop() - - # recalculates out position so that size is correct below - self.adjustSize() - - # is there room to put this widget besides the widget? - if pos.y() + self.height() + 1 < desktop.height(): - # put below - y = pos.y() + 1 - else: - # put above - y = pos.y() - self.height() - 1 - - # is there room to the left for us? - if ( (pos.x() + widget.width() + self.width() < desktop.width()) or - (pos.x() + widget.width() < desktop.width()/2) ): - # put left justified with widget - x = pos.x() + widget.width() - else: - # put extending to left - x = pos.x() - self.width() - 1 - - self.move(x, y) - self.setFocus() - def closeEvent(self, event): """Tell the calling widget that we are closing, and provide the new text.""" text = unicode(self.toPlainText()) text = text.replace('\n', '') - self.emit( qt4.SIGNAL('closing'), text) + self.emit(qt4.SIGNAL('closing'), text) event.accept() class String(qt4.QWidget): @@ -191,11 +168,7 @@ self.edit = qt4.QLineEdit() layout.addWidget(self.edit) - b = self.button = qt4.QPushButton('..') - b.setFlat(True) - b.setSizePolicy(qt4.QSizePolicy.Maximum, qt4.QSizePolicy.Maximum) - b.setMaximumWidth(16) - b.setCheckable(True) + b = self.button = DotDotButton(tooltip="Edit text") layout.addWidget(b) # set the text of the widget to the @@ -242,10 +215,7 @@ try: val = self.setting.fromText(text) styleClear(self.edit) - - # value has changed - if self.setting.val != val: - self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, val) + self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, val ) except setting.InvalidType: styleError(self.edit) @@ -260,6 +230,7 @@ def __init__(self, setting, parent): qt4.QSpinBox.__init__(self, parent) + self.ignorechange = False self.setting = setting self.setMinimum(setting.minval) self.setMaximum(setting.maxval) @@ -273,11 +244,15 @@ def slotChanged(self, value): """If check box changes.""" - self.emit(qt4.SIGNAL('settingChanged'), self, self.setting, value) + # this is emitted by setValue, so ignore onModified doing this + if not self.ignorechange: + self.emit(qt4.SIGNAL('settingChanged'), self, self.setting, value) def onModified(self, mod): """called when the setting is changed remotely""" + self.ignorechange = True self.setValue( self.setting.val ) + self.ignorechange = False class Bool(qt4.QCheckBox): """A check box for changing a bool setting.""" @@ -285,6 +260,7 @@ def __init__(self, setting, parent): qt4.QCheckBox.__init__(self, parent) + self.ignorechange = False self.setting = setting self.setChecked(setting.val) @@ -299,11 +275,15 @@ def slotToggled(self, state): """Emitted when checkbox toggled.""" - self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, state ) + # this is emitted by setChecked, so ignore onModified doing this + if not self.ignorechange: + self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, state ) def onModified(self, mod): """called when the setting is changed remotely""" + self.ignorechange = True self.setChecked( self.setting.val ) + self.ignorechange = False class BoolSwitch(Bool): """Bool for switching off/on other settings.""" @@ -374,6 +354,10 @@ if setting.readonly: self.setEnabled(False) + # make completion case sensitive (to help fix case typos) + if self.completer(): + self.completer().setCaseSensitivity(qt4.Qt.CaseSensitive) + def focusOutEvent(self, *args): """Allows us to check the contents of the widget.""" qt4.QComboBox.focusOutEvent(self, *args) @@ -386,10 +370,7 @@ try: val = self.setting.fromText(text) styleClear(self) - - # value has changed - if self.setting.val != val: - self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, val ) + self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, val ) except setting.InvalidType: styleError(self) @@ -431,10 +412,7 @@ try: val = self.setting.fromText(text) styleClear(self) - - # value has changed - if self.setting.val != val: - self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, val ) + self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, val ) except setting.InvalidType: styleError(self) @@ -448,7 +426,7 @@ # used to remove non-numerics from the string # we also remove X/ from X/num - stripnumre = re.compile(r"[0-9]*/|[^0-9.]") + stripnumre = re.compile(r"[0-9]*/|[^0-9.,]") # remove spaces stripspcre = re.compile(r"\s") @@ -519,7 +497,7 @@ '''Initialise with blank list, then populate with sensible units.''' Choice.__init__(self, setting, True, DistancePt.points, parent) -class Dataset(Choice): +class Dataset(qt4.QWidget): """Allow the user to choose between the possible datasets.""" def __init__(self, setting, document, dimensions, datatype, parent): @@ -529,13 +507,33 @@ Changes on the document refresh the list of datasets.""" - Choice.__init__(self, setting, True, [], parent) + qt4.QWidget.__init__(self, parent) + + self.choice = Choice(setting, True, [], None) + self.connect( self.choice, qt4.SIGNAL("settingChanged"), + self.slotSettingChanged ) + + b = self.button = DotDotButton(tooltip="Select using dataset browser") + self.connect(b, qt4.SIGNAL("toggled(bool)"), + self.slotButtonToggled) + self.document = document self.dimensions = dimensions self.datatype = datatype self.lastdatasets = None self._populateEntries() - self.connect(document, qt4.SIGNAL('sigModified'), self.slotModified) + self.connect(document, qt4.SIGNAL("sigModified"), self.slotModified) + + layout = qt4.QHBoxLayout() + layout.setSpacing(0) + layout.setMargin(0) + layout.addWidget(self.choice) + layout.addWidget(b) + self.setLayout(layout) + + def slotSettingChanged(self, *args): + """Reemit setting changed signal if combo box changes.""" + self.emit( qt4.SIGNAL("settingChanged"), *args ) def _populateEntries(self): """Put the list of datasets into the combobox.""" @@ -548,67 +546,69 @@ datasets.sort() if datasets != self.lastdatasets: - utils.populateCombo(self, datasets) + utils.populateCombo(self.choice, datasets) self.lastdatasets = datasets def slotModified(self, modified): """Update the list of datasets if the document is modified.""" self._populateEntries() -class DatasetOrString(qt4.QWidget): + def slotButtonToggled(self, on): + """Bring up list of datasets.""" + if on: + from veusz.qtwidgets.datasetbrowser import DatasetBrowserPopup + d = DatasetBrowserPopup(self.document, + unicode(self.choice.currentText()), + self.button, + filterdims=set((self.dimensions,)), + filterdtype=set((self.datatype,)) ) + self.connect(d, qt4.SIGNAL("closing"), self.boxClosing) + self.connect(d, qt4.SIGNAL("newdataset"), self.newDataset) + d.show() + + def boxClosing(self): + """Called when the popup edit box closes.""" + self.button.setChecked(False) + + def newDataset(self, dsname): + """New dataset selected.""" + self.emit( qt4.SIGNAL("settingChanged"), self, + self.choice.setting, dsname ) + +class DatasetOrString(Dataset): """Allow use to choose a dataset or enter some text.""" def __init__(self, setting, document, dimensions, datatype, parent): - qt4.QWidget.__init__(self, parent) - self.datachoose = Dataset(setting, document, dimensions, datatype, - None) - - b = self.button = qt4.QPushButton('..') - b.setFlat(True) - b.setSizePolicy(qt4.QSizePolicy.Maximum, qt4.QSizePolicy.Maximum) - b.setMaximumHeight(self.datachoose.height()) - b.setMaximumWidth(16) - b.setCheckable(True) - - layout = qt4.QHBoxLayout() - self.setLayout(layout) - layout.setSpacing(0) - layout.setMargin(0) - layout.addWidget(self.datachoose) - layout.addWidget(b) + Dataset.__init__(self, setting, document, dimensions, datatype, parent) - self.connect(b, qt4.SIGNAL('toggled(bool)'), - self.buttonToggled) - self.connect(self.datachoose, qt4.SIGNAL('settingChanged'), - self.slotSettingChanged) + b = self.textbutton = DotDotButton() + b.setCheckable(True) + self.layout().addWidget(b) + self.connect(b, qt4.SIGNAL('toggled(bool)'), self.textButtonToggled) - def slotSettingChanged(self, *args): - """When datachoose changes, inform any listeners.""" - self.emit( qt4.SIGNAL('settingChanged'), *args ) - - def buttonToggled(self, on): + def textButtonToggled(self, on): """Button is pressed to bring popup up / down.""" # if button is down and there's no existing popup, bring up a new one if on: - e = _EditBox( unicode(self.datachoose.currentText()), - self.datachoose.setting.readonly, self.button) + e = _EditBox( unicode(self.choice.currentText()), + self.choice.setting.readonly, self.textbutton) # we get notified with text when the popup closes - self.connect(e, qt4.SIGNAL('closing'), self.boxClosing) + self.connect(e, qt4.SIGNAL("closing"), self.textBoxClosing) e.show() - def boxClosing(self, text): + def textBoxClosing(self, text): """Called when the popup edit box closes.""" + self.textbutton.setChecked(False) + # update the text if we can - if not self.datachoose.setting.readonly: - self.datachoose.setEditText(text) - self.datachoose.setFocus() + if not self.choice.setting.readonly: + self.choice.setEditText(text) + self.choice.setFocus() self.parentWidget().setFocus() - self.datachoose.setFocus() - - self.button.setChecked(False) + self.choice.setFocus() class FillStyle(Choice): """For choosing between fill styles.""" @@ -735,6 +735,7 @@ # import later for dependency issues import veusz.setting.collections + import veusz.document icons = [] size = cls.size @@ -745,12 +746,15 @@ for lstyle in cls._lines: pix = qt4.QPixmap(*size) pix.fill() + + ph = veusz.document.PaintHelper( (1, 1) ) + painter = qt4.QPainter(pix) painter.setRenderHint(qt4.QPainter.Antialiasing) setn.get('style').set(lstyle) - painter.setPen( setn.makeQPen(painter) ) + painter.setPen( setn.makeQPen(ph) ) painter.drawLine( int(size[0]*0.1), size[1]/2, int(size[0]*0.9), size[1]/2 ) painter.end() @@ -843,19 +847,14 @@ if col.isValid(): # change setting val = unicode( col.name() ) - if self.setting.val != val: - self.emit( qt4.SIGNAL('settingChanged'), self, - self.setting, val) + self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, val ) def slotActivated(self, val): """A different value is selected.""" text = unicode(self.combo.currentText()) val = self.setting.fromText(text) - - # value has changed - if self.setting.val != val: - self.emit(qt4.SIGNAL('settingChanged'), self, self.setting, val) + self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, val ) def setColor(self, color): """Update control with color given.""" @@ -894,8 +893,8 @@ """Update list of axes.""" self._populateEntries() -class Image(WidgetSelector): - """Choose an image.""" +class WidgetChoice(WidgetSelector): + """Choose a widget.""" def __init__(self, setting, document, parent): """Initialise and populate combobox.""" @@ -904,12 +903,12 @@ self._populateEntries() def _populateEntries(self): - """Build up a list of images for combobox.""" + """Build up a list of widgets for combobox.""" - images = self.setting.getImageList() + widgets = self.setting.getWidgetList() # we only need the list of names - names = images.keys() + names = widgets.keys() names.sort() utils.populateCombo(self, names) @@ -1483,12 +1482,9 @@ self.edit.setText( setting.toText() ) layout.addWidget(self.edit) - # get a sensible shape for the button - yawn - b = self.button = qt4.QPushButton('..') - b.setFlat(True) + b = self.button = DotDotButton(checkable=False, + tooltip="Browse for file") layout.addWidget(b) - b.setSizePolicy(qt4.QSizePolicy.Maximum, qt4.QSizePolicy.Maximum) - b.setMaximumWidth(16) # connect up signals self.connect(self.edit, qt4.SIGNAL('editingFinished()'), @@ -1524,9 +1520,7 @@ if filename: val = unicode(filename) - if self.setting.val != val: - self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, - val ) + self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, val ) def validateAndSet(self): """Check the text is a valid setting and update it.""" @@ -1535,11 +1529,7 @@ try: val = self.setting.fromText(text) styleClear(self.edit) - - # value has changed - if self.setting.val != val: - self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, - val ) + self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, val ) except setting.InvalidType: styleError(self.edit) @@ -1579,8 +1569,7 @@ def slotActivated(self, val): """Update setting if a different item is chosen.""" newval = unicode(self.currentText()) - if self.setting.val != newval: - self.emit(qt4.SIGNAL('settingChanged'), self, self.setting, newval) + self.emit( qt4.SIGNAL('settingChanged'), self, self.setting, newval ) def onModified(self, mod): """Make control reflect chosen setting.""" @@ -1605,3 +1594,73 @@ cls._icons = [] for errstyle in cls._errorstyles: cls._icons.append( utils.getIcon('error_%s' % errstyle) ) + +class Colormap(Choice): + """Give the user a preview of colormaps. + + Based on Choice to make life easier + """ + + _icons = {} + + size = (32, 12) + + def __init__(self, setn, document, parent): + names = sorted(document.colormaps.keys()) + + icons = Colormap._generateIcons(document, names) + setting.controls.Choice.__init__(self, setn, True, + names, parent, + icons=icons) + self.setIconSize( qt4.QSize(*self.size) ) + + @classmethod + def _generateIcons(kls, document, names): + """Generate a list of icons for drop down menu.""" + + # create a fake dataset smoothly varying from 0 to size[0]-1 + size = kls.size + fakedataset = N.fromfunction(lambda x, y: y, (size[1], size[0])) + + # keep track of icons to return + retn = [] + + # iterate over colour maps + for name in names: + val = document.colormaps.get(name, None) + if val in kls._icons: + icon = kls._icons[val] + else: + if val is None: + # empty icon + pixmap = qt4.QPixmap(*size) + pixmap.fill(qt4.Qt.transparent) + else: + # generate icon + image = utils.applyColorMap(val, 'linear', + fakedataset, + 0., size[0]-1., 0) + pixmap = qt4.QPixmap.fromImage(image) + icon = qt4.QIcon(pixmap) + kls._icons[val] = icon + retn.append(icon) + return retn + +class AxisBound(Choice): + """Control for setting bounds of axis. + + This is to allow dates etc + """ + + def __init__(self, setting, *args): + Choice.__init__(self, setting, True, ['Auto'], *args) + + modesetn = setting.parent.get('mode') + modesetn.setOnModified(self.modeChange) + + def modeChange(self, changed): + """Called if the mode of the axis changes. + Re-set text as float or date.""" + + if unicode(self.currentText()).lower() != 'auto': + self.setEditText( self.setting.toText() ) diff -Nru veusz-1.10/setting/__init__.py veusz-1.14/setting/__init__.py --- veusz-1.10/setting/__init__.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/setting/__init__.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: __init__.py 1023 2009-07-11 17:52:27Z jeremysanders $ - from settingdb import * from reference import Reference from setting import * diff -Nru veusz-1.10/setting/reference.py veusz-1.14/setting/reference.py --- veusz-1.10/setting/reference.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/setting/reference.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: reference.py 1056 2009-09-05 16:51:59Z jeremysanders $ - class Reference(object): """A value a setting can have to point to another setting. diff -Nru veusz-1.10/setting/settingdb.py veusz-1.14/setting/settingdb.py --- veusz-1.10/setting/settingdb.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/setting/settingdb.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,12 +16,9 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: settingdb.py 1320 2010-07-09 21:18:52Z jeremysanders $ - """A database for default values of settings.""" import sys -import atexit import veusz.qtall as qt4 @@ -29,14 +26,16 @@ defaultValues = { # export options 'export_DPI': 100, + 'export_DPI_PDF': 150, 'export_color': True, 'export_antialias': True, 'export_quality': 85, 'export_background': '#ffffff00', # plot options - 'plot_updateinterval': 500, + 'plot_updatepolicy': -1, # update on document changed 'plot_antialias': True, + 'plot_numthreads': 2, # recent files list 'main_recentfiles': [], @@ -56,8 +55,14 @@ # further ui options 'toolbar_size': 24, + # if set to true, do UI formatting in US/English + 'ui_english': False, + # use cwd as starting directory 'dirname_usecwd': False, + + # ask tutorial before? + 'ask_tutorial': False, } class _SettingDB(object): @@ -144,7 +149,7 @@ def writeSettings(self): """Write the settings using QSettings. - This is called by the atexit handler below + This is called by the mainwindow on close """ s = qt4.QSettings(self.domain, self.product) @@ -154,6 +159,11 @@ for key, value in self.database.iteritems(): cleankey = key.replace('/', self.sepchars) cleankeys.append(cleankey) + + # repr doesn't work on QStrings + if isinstance(value, qt4.QString): + value = unicode(value) + s.setValue(cleankey, qt4.QVariant(repr(value))) # now remove all the values which have been removed @@ -189,5 +199,16 @@ # (e.g. disable safe mode) transient_settings = {} -# write out settings at exit -atexit.register(settingdb.writeSettings) +def updateUILocale(): + """Update locale to one given in preferences.""" + global uilocale + + if settingdb['ui_english']: + uilocale = qt4.QLocale.c() + else: + uilocale = qt4.QLocale.system() + uilocale.setNumberOptions(qt4.QLocale.OmitGroupSeparator) + + qt4.QLocale.setDefault(uilocale) + +updateUILocale() diff -Nru veusz-1.10/setting/setting.py veusz-1.14/setting/setting.py --- veusz-1.10/setting/setting.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/setting/setting.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: setting.py 1469 2010-12-08 22:15:00Z jeremysanders $ - """Module for holding setting values. e.g. @@ -34,7 +32,7 @@ import veusz.qtall as qt4 import controls -from settingdb import settingdb +from settingdb import settingdb, uilocale from reference import Reference import veusz.utils as utils @@ -44,6 +42,11 @@ pass class Setting(object): + """A class to store a value with a particular type.""" + + # differentiate widgets, settings and setting + nodetype = 'setting' + typename = 'setting' def __init__(self, name, value, descr='', usertext='', @@ -55,6 +58,7 @@ descr: description of the setting usertext: name of setting for user formatting: whether setting applies to formatting + hidden: hide widget from user """ self.readonly = False self.parent = None @@ -62,6 +66,7 @@ self.descr = descr self.usertext = usertext self.formatting = formatting + self.hidden = hidden self.default = value self.onmodified = qt4.QObject() self._val = None @@ -92,6 +97,7 @@ opt['descr'] = self.descr opt['usertext'] = self.usertext opt['formatting'] = self.formatting + opt['hidden'] = self.hidden obj = self.__class__(*args, **opt) obj.readonly = self.readonly @@ -445,7 +451,6 @@ def makeControl(self, *args): return controls.Bool(self, *args) - # Storing integers class Int(Setting): """Integer settings.""" @@ -481,21 +486,30 @@ raise InvalidType def toText(self): - return str(self.val) + return unicode( uilocale.toString(self.val) ) def fromText(self, text): - try: - i = int(text) - if i >= self.minval and i <= self.maxval: - return i - else: - raise InvalidType, 'Out of range allowed' - except ValueError: - raise InvalidType + i, ok = uilocale.toLongLong(text) + if not ok: + raise ValueError + + if i >= self.minval and i <= self.maxval: + return i + else: + raise InvalidType, 'Out of range allowed' def makeControl(self, *args): return controls.Int(self, *args) +def _finiteRangeFloat(f, minval=-1e300, maxval=1e300): + """Return a finite float in range or raise exception otherwise.""" + f = float(f) + if not N.isfinite(f): + raise InvalidType, 'Finite values only allowed' + if f < minval or f > maxval: + raise InvalidType, 'Out of range allowed' + return f + # for storing floats class Float(Setting): """Float settings.""" @@ -524,19 +538,16 @@ def convertTo(self, val): if isinstance(val, int) or isinstance(val, float): - if val >= self.minval and val <= self.maxval: - return float(val) - else: - raise InvalidType, 'Out of range allowed' + return _finiteRangeFloat(val, + minval=self.minval, maxval=self.maxval) raise InvalidType def toText(self): - return str(self.val) + return unicode(uilocale.toString(self.val)) def fromText(self, text): - try: - f = float(text) - except ValueError: + f, ok = uilocale.toDouble(text) + if not ok: # try to evaluate f = self.safeEvalHelper(text) return self.convertTo(f) @@ -551,7 +562,7 @@ def convertTo(self, val): if type(val) in (int, float): - return float(val) + return _finiteRangeFloat(val) elif isinstance(val, basestring) and val.strip().lower() == 'auto': return None else: @@ -564,18 +575,20 @@ return val def toText(self): - if self.val is None: + if self.val is None or (isinstance(self.val, basestring) and + self.val.lower() == 'auto'): return 'Auto' else: - return str(self.val) + return unicode(uilocale.toString(self.val)) def fromText(self, text): if text.strip().lower() == 'auto': return 'Auto' else: - try: - return float(text) - except ValueError: + f, ok = uilocale.toDouble(text) + if ok: + return self.convertTo(f) + else: # try to evaluate return self.safeEvalHelper(text) @@ -602,19 +615,20 @@ return val def toText(self): - if self.val is None: + if self.val is None or (isinstance(self.val, basestring) and + self.val.lower() == 'auto'): return 'Auto' else: - return str(self.val) + return unicode( uilocale.toString(self.val) ) def fromText(self, text): if text.strip().lower() == 'auto': return 'Auto' else: - try: - return int(text) - except ValueError: + i, ok = uilocale.toLongLong(text) + if not ok: raise InvalidType + return i def makeControl(self, *args): return controls.Choice(self, True, ['Auto'], *args) @@ -622,128 +636,124 @@ # 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.""" - - dpi = painter.device().logicalDpiY() - if dpi == 0: dpi = 72 - painter.veusz_pixperpt = dpi / 72. - def _distPhys(match, painter, mult): """Convert a physical unit measure in multiples of points.""" - - if not hasattr(painter, 'veusz_pixperpt'): - _calcPixPerPt(painter) - - return (painter.veusz_pixperpt * mult * - float(match.group(1)) * painter.veusz_scaling) + return (painter.pixperpt * mult * + float(match.group(1)) * painter.scaling) def _distInvPhys(pixdist, painter, mult, unit): """Convert number of pixels into physical distance.""" - dist = pixdist / (mult * painter.veusz_pixperpt * - painter.veusz_scaling) + dist = pixdist / (mult * painter.pixperpt * painter.scaling) return "%.3g%s" % (dist, unit) -def _distPerc(match, painter, maxsize): +def _distPerc(match, painter): """Convert from a percentage of maxsize.""" - return maxsize * 0.01 * float(match.group(1)) + return painter.maxsize * 0.01 * float(match.group(1)) -def _distInvPerc(pixdist, painter, maxsize): +def _distInvPerc(pixdist, painter): """Convert pixel distance into percentage.""" - perc = pixdist * 100. / maxsize + perc = pixdist * 100. / painter.maxsize return "%.3g%%" % perc -def _distFrac(match, painter, maxsize): +def _distFrac(match, painter): """Convert from a fraction a/b of maxsize.""" - return maxsize * float(match.group(1)) / float(match.group(2)) + try: + return painter.maxsize * float(match.group(1))/float(match.group(4)) + except ZeroDivisionError: + return 0. -def _distRatio(match, painter, maxsize): +def _distRatio(match, painter): """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 maxsize * float(match.group(1)) + return painter.maxsize * float(match.group(1)) + +# regular expression to match distances +distre_expr = r'''^ + [ ]* # optional whitespace + + (\.?[0-9]+|[0-9]+\.[0-9]*) # a floating point number + + [ ]* # whitespace + + (cm|pt|mm|inch|in|"|%|| # ( unit, no unit, + (?P/) ) # or / ) + + (?(slash)[ ]* # if it was a slash, match any whitespace + (\.?[0-9]+|[0-9]+\.[0-9]*)) # and match following fp number + + [ ]* # optional whitespace +$''' class Distance(Setting): """A veusz distance measure, e.g. 1pt or 3%.""" typename = 'distance' - # mappings from regular expressions to function to convert distance - # the recipient function takes regexp match, - # painter and maximum size of frac - - # the second function is to do the inverse calculation - distregexp = [ - # cm distance - ( re.compile('^([0-9\.]+) *cm$'), - lambda match, painter, t: - _distPhys(match, painter, 28.452756), - lambda pixdist, painter, t: - _distInvPhys(pixdist, painter, 28.452756, 'cm') ), - - # point size - ( re.compile('^([0-9\.]+) *pt$'), - lambda match, painter, t: - _distPhys(match, painter, 1.), - lambda pixdist, painter, t: - _distInvPhys(pixdist, painter, 1., 'pt') ), - - # mm distance - ( re.compile('^([0-9\.]+) *mm$'), - lambda match, painter, t: - _distPhys(match, painter, 2.8452756), - lambda pixdist, painter, t: - _distInvPhys(pixdist, painter, 2.8452756, 'mm') ), - - # inch distance - ( re.compile('^([0-9\.]+) *(inch|in|")$'), - lambda match, painter, t: - _distPhys(match, painter, 72.27), - lambda pixdist, painter, t: - _distInvPhys(pixdist, painter, 72.27, 'in') ), - - # plain fraction - ( re.compile('^([0-9\.]+)$'), - _distRatio, - _distInvPerc ), - - # percentage - ( re.compile('^([0-9\.]+) *%$'), - _distPerc, - _distInvPerc ), - - # fractional - ( re.compile('^([0-9\.]+) */ *([0-9\.]+)$'), - _distFrac, - _distInvPerc ), - ] + # match a distance + distre = re.compile(distre_expr, re.VERBOSE) + + # functions to convert from unit values to pixels + unit_func = { + 'cm': lambda match, painter: + _distPhys(match, painter, 28.452756), + 'pt': lambda match, painter: + _distPhys(match, painter, 1.), + 'mm': lambda match, painter: + _distPhys(match, painter, 2.8452756), + 'in': lambda match, painter: + _distPhys(match, painter, 72.27), + 'inch': lambda match, painter: + _distPhys(match, painter, 72.27), + '"': lambda match, painter: + _distPhys(match, painter, 72.27), + '%': _distPerc, + '/': _distFrac, + '': _distRatio + } + + # inverse functions for converting pixels to units + inv_unit_func = { + 'cm': lambda match, painter: + _distInvPhys(match, painter, 28.452756, 'cm'), + 'pt': lambda match, painter: + _distInvPhys(match, painter, 1., 'pt'), + 'mm': lambda match, painter: + _distInvPhys(match, painter, 2.8452756, 'mm'), + 'in': lambda match, painter: + _distInvPhys(match, painter, 72.27, 'in'), + 'inch': lambda match, painter: + _distInvPhys(match, painter, 72.27, 'in'), + '"': lambda match, painter: + _distInvPhys(match, painter, 72.27, 'in'), + '%': _distInvPerc, + '/': _distInvPerc, + '': _distInvPerc + } @classmethod def isDist(kls, dist): """Is the text a valid distance measure?""" - dist = dist.strip() - for reg, fn, fninv in kls.distregexp: - if reg.match(dist): - return True - - return False + return kls.distre.match(dist) is not None def convertTo(self, val): - if self.isDist(val): + if self.distre.match(val) is not None: return val else: raise InvalidType def toText(self): - return self.val + # convert decimal point to display locale + return self.val.replace('.', qt4.QString(uilocale.decimalPoint())) def fromText(self, text): + # convert decimal point from display locale + text = text.replace(qt4.QString(uilocale.decimalPoint()), '.') + if self.isDist(text): return text else: @@ -753,7 +763,7 @@ return controls.Distance(self, *args) @classmethod - def convertDistance(kls, painter, distance): + def convertDistance(kls, painter, dist): '''Convert a distance to plotter units. dist: eg 0.1 (fraction), 10% (percentage), 1/10 (fraction), @@ -762,26 +772,12 @@ painter: painter to get metrics to convert physical sizes ''' - # we set a scaling variable in the painter if it's not set - if not hasattr(painter, 'veusz_scaling'): - 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 = distance.strip() - - # compare string against each regexp - for reg, fn, fninv in kls.distregexp: - m = reg.match(dist) - - # if there's a match, then call the appropriate conversion fn - if m: - return fn(m, painter, maxsize) + # match distance against expression + m = kls.distre.match(dist) + if m is not None: + # lookup function to call to do conversion + func = kls.unit_func[m.group(2)] + return func(m, painter) # none of the regexps match raise ValueError( "Cannot convert distance in form '%s'" % @@ -789,37 +785,26 @@ def convert(self, painter): """Convert this setting's distance as above""" - return self.convertDistance(painter, self.val) def convertPts(self, painter): """Get the distance in points.""" - if not hasattr(painter, 'veusz_pixperpt'): - _calcPixPerPt(painter) - - return self.convert(painter) / painter.veusz_pixperpt + return self.convert(painter) / painter.pixperpt def convertInverse(self, distpix, painter): """Convert distance in pixels into units of this distance. - - Not that great coding as takes "painter" containing veusz - scaling parameters. Should be cleaned up. """ - # identify units and get inverse mapping - v = self.val - inversefn = None - for reg, fn, fninv in self.distregexp: - if reg.match(v): - inversefn = fninv - break - if not inversefn: - inversefn = self.distregexp[0][2] - - maxsize = max( *painter.veusz_page_size ) + m = self.distre.match(self.val) + if m is not None: + # if it matches convert back + inversefn = self.inv_unit_func[m.group(2)] + else: + # otherwise force unit + inversefn = self.inv_unit_func['cm'] # do inverse mapping - return inversefn(distpix, painter, maxsize) + return inversefn(distpix, painter) class DistancePt(Distance): """For a distance in points.""" @@ -832,7 +817,7 @@ typename = 'distance-or-auto' - distregexp = Distance.distregexp + [(re.compile('^Auto$'), None, None)] + distre = re.compile( distre_expr + r'|^Auto$', re.VERBOSE ) def isAuto(self): return self.val == 'Auto' @@ -935,7 +920,7 @@ keys = self.val.keys() keys.sort() - text = ['%s = %g' % (key, self.val[key]) for key in keys] + text = ['%s = %s' % (k, uilocale.toString(self.val[k])) for k in keys] return '\n'.join(text) def fromText(self, text): @@ -954,9 +939,8 @@ if len(p) != 2: raise InvalidType - try: - v = float(p[1]) - except ValueError: + v, ok = uilocale.toDouble(p[1]) + if not ok: raise InvalidType out[ p[0].strip() ] = v @@ -970,8 +954,6 @@ typename = 'float-list' - list_re = re.compile(r'[\t\n, ]+') - def convertTo(self, val): if type(val) not in (list, tuple): raise InvalidType @@ -987,17 +969,28 @@ def toText(self): """Make a string a, b, c.""" - return ', '.join( [str(i) for i in self.val] ) + # can't use the comma for splitting if used as a decimal point + + join = ', ' + if uilocale.decimalPoint() == qt4.QChar(','): + join = '; ' + return join.join( [unicode(uilocale.toString(x)) for x in self.val] ) def fromText(self, text): """Convert from a, b, c or a b c.""" + # don't use commas if it is the decimal separator + splitre = r'[\t\n, ]+' + if uilocale.decimalPoint() == qt4.QChar(','): + splitre = r'[\t\n; ]+' + out = [] - for x in self.list_re.split(text.strip()): + for x in re.split(splitre, text.strip()): if x: - try: - out.append(float(x)) - except ValueError: + f, ok = uilocale.toDouble(x) + if ok: + out.append(f) + else: out.append( self.safeEvalHelper(x) ) return out @@ -1031,7 +1024,7 @@ {'relativetoparent': self.relativetoparent, 'allowedwidgets': self.allowedwidgets}) - def getWidget(self, val = None): + def getReferredWidget(self, val = None): """Get the widget referred to. We double-check here to make sure it's the one. @@ -1194,10 +1187,6 @@ typename = 'dataset-or-floatlist' - # a list of numbers separated by spaces or tabs - # (requires number at end of line) - numbers_re = re.compile(r'^([-+]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?[ \t,$]+)+$') - def convertTo(self, val): """Check is a string (dataset name) or a list of floats (numbers). @@ -1217,20 +1206,31 @@ if isinstance(self.val, basestring): return self.val else: - return ', '.join( [str(x) for x in self.val] ) + # join based on , or ; depending on decimal point + join = ', ' + if uilocale.decimalPoint() == qt4.QChar(','): + join = '; ' + return join.join( [ unicode(uilocale.toString(x)) + for x in self.val ] ) def fromText(self, text): - text = text.strip() - try: - out = [] - for x in FloatList.list_re.split(text): - try: - out.append(float(x)) - except ValueError: - out.append( self.safeEvalHelper(x) ) - return out - except InvalidType: - return text + """Convert from text.""" + + # split based on , or ; depending on decimal point + splitre = r'[\t\n, ]+' + if uilocale.decimalPoint() == qt4.QChar(','): + splitre = r'[\t\n; ]+' + + out = [] + for x in re.split(splitre, text.strip()): + if x: + f, ok = uilocale.toDouble(x) + if ok: + out.append(f) + else: + # fail conversion, so exit with text + return text + return out def getFloatArray(self, doc): """Get a numpy of values or None.""" @@ -1250,6 +1250,10 @@ return (isinstance(self.val, basestring) and doc.data.get(self.val)) + def isEmpty(self): + """Is this unset?""" + return self.val == [] + def getData(self, doc): """Return veusz dataset""" if isinstance(self.val, basestring): @@ -1437,16 +1441,25 @@ """Allows user to choose an axis or enter a name.""" return controls.Axis(self, self.getDocument(), self.direction, *args) -class Image(Str): - """Hold the name of a child image.""" +class WidgetChoice(Str): + """Hold the name of a child widget.""" + + typename = 'widget-choice' + + def __init__(self, name, val, widgettypes={}, **args): + """Choose widgets from (named) type given.""" + Setting.__init__(self, name, val, **args) + self.widgettypes = widgettypes - typename = 'image-widget' + def copy(self): + """Make a copy of the setting.""" + return self._copyHelper((), (), + {'widgettypes': self.widgettypes}) - @staticmethod - def buildImageList(level, widget, outdict): - """A recursive helper to build up a list of possible image widgets. + def buildWidgetList(self, level, widget, outdict): + """A recursive helper to build up a list of possible widgets. - This iterates over widget's children, and adds Image widgets as tuples + This iterates over widget's children, and adds widgets as tuples to outdict using outdict[name] = (widget, level) Lower level images of the same name outweigh other images further down @@ -1454,14 +1467,14 @@ """ for child in widget.children: - if child.typename == 'image': + if child.typename in self.widgettypes: if (child.name not in outdict) or (outdict[child.name][1]>level): outdict[child.name] = (child, level) else: - Image.buildImageList(level+1, child, outdict) + self.buildWidgetList(level+1, child, outdict) - def getImageList(self): - """Return a dict of valid image names and the corresponding objects.""" + def getWidgetList(self): + """Return a dict of valid widget names and the corresponding objects.""" # find widget which contains setting widget = self.parent @@ -1472,10 +1485,10 @@ if widget is not None: widget = widget.parent - # get list of images from recursive find + # get list of widgets from recursive find images = {} if widget is not None: - Image.buildImageList(0, widget, images) + self.buildWidgetList(0, widget, images) # turn (object, level) pairs into object outdict = {} @@ -1484,15 +1497,15 @@ return outdict - def findImage(self): + def findWidget(self): """Find the image corresponding to this setting. Returns Image object if succeeds or None if fails """ - images = self.getImageList() + widgets = self.getWidgetList() try: - return images[self.get()] + return widgets[self.get()] except KeyError: return None @@ -1502,7 +1515,7 @@ def makeControl(self, *args): """Allows user to choose an image widget or enter a name.""" - return controls.Image(self, self.getDocument(), *args) + return controls.WidgetChoice(self, self.getDocument(), *args) class Marker(Choice): """Choose a marker type from one allowable.""" @@ -1687,6 +1700,7 @@ 'boxfill', 'fillvert', 'fillhorz', 'linevert', 'linehorz', + 'linevertbar', 'linehorzbar' ) controls.ErrorStyle._errorstyles = _errorstyles @@ -1768,3 +1782,70 @@ def copy(self): return self._copyHelper((), (), {'settingsfalse': self.sfalse, 'settingstrue': self.strue}) + +class RotateInterval(Choice): + '''Rotate a label with intervals given.''' + + def __init__(self, name, val, **args): + Choice.__init__(self, name, + ('-180', '-135', '-90', '-45', + '0', '45', '90', '135', '180'), + val, **args) + + def convertTo(self, val): + """Store rotate angle.""" + # backward compatibility with rotate option + # False: angle 0 + # True: angle 90 + if val == False: + val = '0' + elif val == True: + val = '90' + return Choice.convertTo(self, val) + + def copy(self): + """Make a copy of the setting.""" + return self._copyHelper((), (), {}) + +class Colormap(Str): + """A setting to set the color map used in an image. + This is based on a Str rather than Choice as the list might + change later. + """ + + def makeControl(self, *args): + return controls.Colormap(self, self.getDocument(), *args) + +class AxisBound(FloatOrAuto): + """Axis bound - either numeric, Auto or date.""" + + typename = 'axis-bound' + + def makeControl(self, *args): + return controls.AxisBound(self, *args) + + def toText(self): + """Convert to text, taking into account mode of Axis. + Displays datetimes in date format if used + """ + + try: + mode = self.parent.mode + except AttributeError: + mode = None + + v = self.val + if ( not isinstance(v, basestring) and v is not None and + mode == 'datetime' ): + return utils.dateFloatToString(v) + + return FloatOrAuto.toText(self) + + def fromText(self, txt): + """Convert from text, allowing datetimes.""" + + v = utils.dateStringToDate(txt) + if N.isfinite(v): + return v + else: + return FloatOrAuto.fromText(self, txt) diff -Nru veusz-1.10/setting/settings.py veusz-1.14/setting/settings.py --- veusz-1.10/setting/settings.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/setting/settings.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: settings.py 1325 2010-07-12 13:04:13Z jeremysanders $ - """Module for holding collections of settings.""" from reference import Reference @@ -25,21 +23,36 @@ class Settings(object): """A class for holding collections of settings.""" - def __init__(self, name, descr = '', usertext='', pixmap=''): - """A new Settings with a name.""" + # differentiate widgets, settings and setting + nodetype = 'settings' + + def __init__(self, name, descr = '', usertext='', pixmap='', + setnsmode='formatting'): + """A new Settings with a name. + + name: name in hierarchy + descr: description (for user) + usertext: name for user of class + pixmap: pixmap to show in tab (if appropriate) + setnsmode: type of Settings class, one of + ('formatting', 'groupedsetting', 'widgetsettings', 'stylesheet') + """ self.__dict__['setdict'] = {} self.name = name self.descr = descr - self.pixmap = pixmap self.usertext = usertext + self.pixmap = pixmap + self.setnsmode = setnsmode self.setnames = [] # a list of names self.parent = None def copy(self): """Make a copy of the settings and its subsettings.""" - s = Settings(self.name, descr=self.descr, usertext=self.usertext, - pixmap=self.pixmap) + + s = Settings( + self.name, descr=self.descr, usertext=self.usertext, + pixmap=self.pixmap, setnsmode=self.setnsmode ) for name in self.setnames: s.add( self.setdict[name].copy() ) return s @@ -66,6 +79,20 @@ return [self.setdict[n] for n in self.setnames if isinstance(self.setdict[n], Settings)] + def getNames(self): + """Return list of names.""" + return self.setnames + + def getSettingNames(self): + """Get list of setting names.""" + return [n for n in self.setnames + if not isinstance(self.setdict[n], Settings)] + + def getSettingsNames(self): + """Get list of settings names.""" + return [n for n in self.setnames + if isinstance(self.setdict[n], Settings)] + def isSetting(self, name): """Is the name a supported setting?""" return name in self.setdict @@ -234,5 +261,3 @@ setn.default = ref except Reference.ResolveException: pass - - diff -Nru veusz-1.10/setting/stylesheet.py veusz-1.14/setting/stylesheet.py --- veusz-1.10/setting/stylesheet.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/setting/stylesheet.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: stylesheet.py 1387 2010-08-29 15:24:57Z jeremysanders $ - import sys from settings import Settings @@ -45,7 +43,7 @@ def __init__(self, **args): """Create the default settings.""" - Settings.__init__(self, 'StyleSheet', **args) + Settings.__init__(self, 'StyleSheet', setnsmode='stylesheet', **args) self.pixmap = 'settings_stylesheet' for subset in self.registeredsettings: diff -Nru veusz-1.10/setup.py veusz-1.14/setup.py --- veusz-1.10/setup.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/setup.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: setup.py 1285 2010-06-15 20:50:10Z jeremysanders $ - """ Veusz distutils setup script see the file INSTALL for details on how to install Veusz @@ -92,6 +90,14 @@ 'scripts': ['scripts/veusz', 'scripts/veusz_listen'] } +def findData(dirname, extns): + """Return tuple for directory name and list of file extensions for data.""" + files = [] + for extn in extns: + files += glob.glob(os.path.join(dirname, '*.'+extn)) + files.sort() + return ( os.path.join('veusz', dirname), files ) + setup(name = 'veusz', version = version, description = 'A scientific plotting package', @@ -111,27 +117,29 @@ 'veusz.dialogs': 'dialogs', 'veusz.document': 'document', 'veusz.helpers': 'helpers', + 'veusz.plugins': 'plugins', + 'veusz.qtwidgets': 'qtwidgets', 'veusz.setting': 'setting', + 'veusz.tests': 'tests', 'veusz.utils': 'utils', 'veusz.widgets': 'widgets', 'veusz.windows': 'windows', - 'veusz.plugins': 'plugins', - 'veusz.tests': 'tests' }, + }, data_files = [ ('veusz', ['VERSION']), - ('veusz/dialogs', glob.glob('dialogs/*.ui')), - ('veusz/widgets/data', glob.glob('widgets/data/*.dat')), - ('veusz/windows/icons', - glob.glob('windows/icons/*.png')+ - glob.glob('windows/icons/*.svg')) ], + findData('dialogs', ('ui',)), + findData('windows/icons', ('png', 'svg')), + findData('examples', ('vsz', 'py', 'csv', 'dat')), + ], packages = [ 'veusz', 'veusz.dialogs', 'veusz.document', + 'veusz.helpers', + 'veusz.plugins', + 'veusz.qtwidgets', 'veusz.setting', 'veusz.utils', 'veusz.widgets', - 'veusz.helpers', 'veusz.windows', - 'veusz.plugins', ], ext_modules = [ @@ -148,6 +156,8 @@ 'helpers/src/polylineclip.cpp', 'helpers/src/beziers.cpp', 'helpers/src/beziers_qtwrap.cpp', + 'helpers/src/recordpaintdevice.cpp', + 'helpers/src/recordpaintengine.cpp', 'helpers/src/qtloops.sip'], language="c++", include_dirs=['/helpers/src', diff -Nru veusz-1.10/tests/check_all.sh veusz-1.14/tests/check_all.sh --- veusz-1.10/tests/check_all.sh 2010-12-12 12:41:10.000000000 +0000 +++ veusz-1.14/tests/check_all.sh 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright (C) 2009 Jeremy S. Sanders -# Email: Jeremy Sanders -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -############################################################################## - -# $Id: check_all.sh 964 2009-05-10 11:26:12Z jeremysanders $ - -# run pychecker on all script files - -for f in `find . -name "*.py"`; do - pychecker $f >> pychecker-out.txt -done diff -Nru veusz-1.10/tests/comparison/1dto2d.vsz.selftest veusz-1.14/tests/comparison/1dto2d.vsz.selftest --- veusz-1.10/tests/comparison/1dto2d.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/1dto2d.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,92 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +2 +4 +6 +8 + + + + + + + + + + +0 +2 +4 +6 +8 + + + + diff -Nru veusz-1.10/tests/comparison/autodetect.vsz.selftest veusz-1.14/tests/comparison/autodetect.vsz.selftest --- veusz-1.10/tests/comparison/autodetect.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/autodetect.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,129 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +hello +foo +bar +xxx +aaa + + + + + + + + + + + + +2010-01-01 +2010-01-15 +2010-01-29 +2010-02-12 +2010-02-26 +2010-03-12 +2010-03-26 +2010-04-09 + + + + + + + + + + +1 +2 +3 +4 +5 + + + + + + + + + + + + + + +xxx +aaa + + + + + + + + + + + + +4 +4.2 +4.4 +4.6 +4.8 +5 + + + + + + + + + + +1000 +10 +4 + + + + diff -Nru veusz-1.10/tests/comparison/bar_labels.vsz.selftest veusz-1.14/tests/comparison/bar_labels.vsz.selftest --- veusz-1.10/tests/comparison/bar_labels.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/bar_labels.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,165 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + +Spring + + + + + +Summer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Number of balloons + +0 +1 +2 +3 +4 +5 +6 + + + + + + + + + + +Colour +Red +Green +Blue + + + + + + + + + + +Red +Green +Blue + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Summer + +2.5 +3 +3.5 +4 +4.5 + + + + + + + +Spring +2 +3 +4 +5 + + + + diff -Nru veusz-1.10/tests/comparison/barplots.vsz.selftest veusz-1.14/tests/comparison/barplots.vsz.selftest --- veusz-1.10/tests/comparison/barplots.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/barplots.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,355 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + +a + + + + + +b +stacked +mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +−1.5 +−1 +−0.5 +0 +0.5 +1 +1.5 + + + + + + + + + + +0 +5 +10 +15 +20 + + + + + +grouped +mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +−1 +−0.5 +0 +0.5 +1 + + + + + + + + + + +0 +5 +10 +15 +20 + + + + + +error bars + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +−1 +−0.5 +0 +0.5 +1 + + + + + + + + + + +0 +5 +10 +15 +20 + + + + + +horizontal +with values + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1 +1000 +10 +6 +10 +9 + + + + + + + + + + + + + +−1 +0 +0.5 +1 + + + + diff -Nru veusz-1.10/tests/comparison/blockeddata.vsz.selftest veusz-1.14/tests/comparison/blockeddata.vsz.selftest --- veusz-1.10/tests/comparison/blockeddata.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/blockeddata.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,79 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +2 +4 +6 +8 +10 + + + + + + + + + + +0 +2 +4 +6 +8 +10 + + + + diff -Nru veusz-1.10/tests/comparison/boxplot.vsz.selftest veusz-1.14/tests/comparison/boxplot.vsz.selftest --- veusz-1.10/tests/comparison/boxplot.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/boxplot.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,133 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Number of insects + +0 +5 +10 +15 +20 + + + + + + + + + + +Bees +Butterflys + + + + diff -Nru veusz-1.10/tests/comparison/coloredpoints.vsz.selftest veusz-1.14/tests/comparison/coloredpoints.vsz.selftest --- veusz-1.10/tests/comparison/coloredpoints.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/coloredpoints.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,590 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Offset (m) + +−3 +−2 +−1 +0 +1 +2 +3 + + + + + + + + + + + + + + + + + + + +Time (yr) +0 +0.2 +0.4 +0.6 +0.8 +1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Power (W) +10 +100 +1k + + + + diff -Nru veusz-1.10/tests/comparison/contour.vsz.selftest veusz-1.14/tests/comparison/contour.vsz.selftest --- veusz-1.10/tests/comparison/contour.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/contour.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,108 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +20 +40 +60 +80 +100 + + + + + + + + + + +0 +20 +40 +60 +80 +100 + + + + diff -Nru veusz-1.10/tests/comparison/csv1.vsz.selftest veusz-1.14/tests/comparison/csv1.vsz.selftest --- veusz-1.10/tests/comparison/csv1.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/csv1.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,83 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +fdfd +dsfh +bgtr +RR +ZZ +AA + + + + + + + + + + + + +1 +2 +3 +4 +5 +6 + + + + + + + + + + +2 +3 +4 +5 +6 +7 + + + + diff -Nru veusz-1.10/tests/comparison/csv_locale.vsz.selftest veusz-1.14/tests/comparison/csv_locale.vsz.selftest --- veusz-1.10/tests/comparison/csv_locale.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/csv_locale.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,98 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + +2012-12-06 +2011-03-01 +2011-04-05 +2012-01-01 + + + + + + + + + + + + + +1,23 +100,3 +1.001,2 +10 + + + + + + + + + + + + +2008-09 +2009-03 +2009-09 +2010-03 +2010-09 +2011-03 +2011-09 +2012-03 +2012-09 +2013-03 + + + + + + + + + + +0 +250 +500 +750 +1000 +1250 +1500 + + + + diff -Nru veusz-1.10/tests/comparison/csv_missing.vsz.selftest veusz-1.14/tests/comparison/csv_missing.vsz.selftest --- veusz-1.10/tests/comparison/csv_missing.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/csv_missing.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,106 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +hello +foo + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 + + + + + + + + + + +0 +2 +4 +6 +8 + + + + diff -Nru veusz-1.10/tests/comparison/custom.vsz.selftest veusz-1.14/tests/comparison/custom.vsz.selftest --- veusz-1.10/tests/comparison/custom.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/custom.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,66 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +10 +20 +30 +40 + + + + + + + + + + +0 +0.2 +0.4 +0.6 +0.8 +1 + + + + diff -Nru veusz-1.10/tests/comparison/dataset_operations.vsz.selftest veusz-1.14/tests/comparison/dataset_operations.vsz.selftest --- veusz-1.10/tests/comparison/dataset_operations.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/dataset_operations.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,437 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +data + + + + + + + + + + + + + + + + +add + + + + + + + + + + + + + + +plus + + + + + + + + + + + + + + + + +mean + + + + + + + + + + + + + + + + +sub + + + + + + + + + + + + + + + + +scale + + + + + + + + + + + + + + + + + + + + + + +extremes + + + + + + + + + + + + + + +thin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Using data operations to combine datasets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +−2.5 +0 +2.5 +5 + + + + + + + + + + +0 +5 +10 +15 +20 + + + + diff -Nru veusz-1.10/tests/comparison/datebar.vsz.selftest veusz-1.14/tests/comparison/datebar.vsz.selftest --- veusz-1.10/tests/comparison/datebar.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/datebar.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,120 @@ + + + +Veusz output document + + + + + + + + + + + +A graph title + + + + + + + + + + + + + + + + + + + + + +Look! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Crazy bar value + +0 +1 +2 +3 +4 + + + + + + + + + + +Date + +2009-03-10 + + +2009-03-11 + + +2009-03-12 + + +2009-03-13 + + +2009-03-14 + + +2009-03-15 + + +2009-03-16 + + + + + diff -Nru veusz-1.10/tests/comparison/example_csv.vsz.selftest veusz-1.14/tests/comparison/example_csv.vsz.selftest --- veusz-1.10/tests/comparison/example_csv.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/example_csv.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,150 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Line plot + + + + + + + + + + + + + + + + + + +Histogram + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Data read into from CSV + +0 +1 +2 +3 +4 +5 +6 +7 + + + + + + + + + + +Imported CSV file example +0 +2 +4 +6 +8 + + + + diff -Nru veusz-1.10/tests/comparison/example_import.vsz.selftest veusz-1.14/tests/comparison/example_import.vsz.selftest --- veusz-1.10/tests/comparison/example_import.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/example_import.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,400 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +y +1 +0 +1 +2 +3 +4 +5 +6 + + + + + + + + + + +This is an +x-axis +0 +2.5 +5 +7.5 +10 +12.5 +15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +y +2 +−1 +0 +1 +2 +3 + + + + + + + + + + +Another +x-axis +0 +2.5 +5 +7.5 +10 +12.5 +15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +y +3 +0 +0.5 +1 +1.5 +2 +2.5 + + + + + + + + + + +Final \underline +x axis +0 +5 +10 +15 +20 + + + + diff -Nru veusz-1.10/tests/comparison/fit.vsz.selftest veusz-1.14/tests/comparison/fit.vsz.selftest --- veusz-1.10/tests/comparison/fit.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/fit.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,123 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +data + + + + + +fit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +A dubious +y + axis + +0 +5 +10 +15 +20 + + + + + + + + + + +A wonderful +x + axis +1 +10 + + + + diff -Nru veusz-1.10/tests/comparison/functions.vsz.selftest veusz-1.14/tests/comparison/functions.vsz.selftest --- veusz-1.10/tests/comparison/functions.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/functions.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,107 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Another axis + +10 +−2 +10 +0 +δ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Left axis + +0 +5 +10 +15 +20 +25 +30 + + + + + + + + + + +x + axis +−1.5 +−0.5 +0 +0.5 +1 +1.5 + + + + diff -Nru veusz-1.10/tests/comparison/histo.vsz.selftest veusz-1.14/tests/comparison/histo.vsz.selftest --- veusz-1.10/tests/comparison/histo.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/histo.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,105 @@ + + + +Veusz output document + + + + + + +Example +histogram + + + + + + + + + + + + + + + + + + + + + + + + +Fit to histogram + + + + + + + + + + + + + +Histogram + + + + + + + + + + + + + + + + + +Dragons + + +0 +100 +200 +300 +400 +500 + + + + + + + + + + +Wingspan (m) +0 +50 +100 +150 +200 + + + + + + + + + diff -Nru veusz-1.10/tests/comparison/inside.vsz.selftest veusz-1.14/tests/comparison/inside.vsz.selftest --- veusz-1.10/tests/comparison/inside.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/inside.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,437 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +All the cheese in the world + +−50 +−25 +0 +25 +50 +75 +100 + + + + + + + + + + +Random axis, maybe something interesting +2 +... +0 +50 +100 +150 +200 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +−8 +−4 +0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +−1 +0 +1 +2 + + + + + + + + + + +0 +2 +4 +6 +8 +10 + + + + + + + + + + + + + + + + + + + + + + + + +sin 2 +π x + +−1 +−0.5 +0 +0.5 +1 + + + + + + + + + + +an x-axis +0 +0.2 +0.4 +0.6 +0.8 +1 + + + + diff -Nru veusz-1.10/tests/comparison/isolatedaxes.vsz.selftest veusz-1.14/tests/comparison/isolatedaxes.vsz.selftest --- veusz-1.10/tests/comparison/isolatedaxes.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/isolatedaxes.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,61 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + +y +-axis +(cm +-3 +) +0 +0.2 +0.4 +0.6 +0.8 +1 + + + + + + + +x +-axis +(erg) +0.01 +0.1 +1 + + + + diff -Nru veusz-1.10/tests/comparison/labels.vsz.selftest veusz-1.14/tests/comparison/labels.vsz.selftest --- veusz-1.10/tests/comparison/labels.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/labels.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,98 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + +dataset 2 + + +dataset 2 + + +dataset 2 + + + + + + + + + + + + + + +A test +test2 +A +200 +α +β + +γ + + + + + + + + + + + + +Y axis + + +0 +2 +4 +6 +8 + + + + + + + + + + +X axis +0 +1 +2 +3 +4 +5 + + + + + + + + diff -Nru veusz-1.10/tests/comparison/linked_datasets.vsz.selftest veusz-1.14/tests/comparison/linked_datasets.vsz.selftest --- veusz-1.10/tests/comparison/linked_datasets.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/linked_datasets.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,500 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Another axis + +−0.8 +−0.6 +−0.4 +0 +0.2 +0.4 +0.6 +0.8 + + + + + + + + + + +Experiments with linked datasets +−0.8 +−0.4 +0 +0.2 +0.4 +0.6 +0.8 + + + + diff -Nru veusz-1.10/tests/comparison/mandelbrot.vsz.selftest veusz-1.14/tests/comparison/mandelbrot.vsz.selftest --- veusz-1.10/tests/comparison/mandelbrot.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/mandelbrot.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,81 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1 +10 +The Mandelbrot Set + + + + + + + + + + +0.25 +0.5 +0.75 +1 +1.25 +1.5 +1.75 +2 + + + + + + + + + + +−2 +−1 +0 +1 + + + + diff -Nru veusz-1.10/tests/comparison/markerspolygon.vsz.selftest veusz-1.14/tests/comparison/markerspolygon.vsz.selftest --- veusz-1.10/tests/comparison/markerspolygon.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/markerspolygon.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,866 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Outward ticks on this y axis + +−2 +0 +1 +2 + + + + + + + + + + +Outward ticks on this x axis +−2 +−1 +0 +1 +2 + + + + diff -Nru veusz-1.10/tests/comparison/multiaxes.vsz.selftest veusz-1.14/tests/comparison/multiaxes.vsz.selftest --- veusz-1.10/tests/comparison/multiaxes.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/multiaxes.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,242 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Optical surface brightness + + + + + +Electron density + + + + + + + + + + + + + + + + +Iron +Chandra + Western + + + + + + + + + + + + + + + + +Iron +Chandra + Eastern + + + + + + + + + + + + + + + + +Iron +XMM + + + + + + + + +Electron density (cm +-3 +) + +10 +−3 +0.01 + + + + + + + + + + + + + + + + + + + + + + + + + + + +Iron metallicity (solar units) + +0.4 +0.6 +0.8 +1 +1.2 +1.4 +1.6 +1.8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Optical surface brightness (mag arcsec +-2 +) + +22 +24 +26 +28 +30 + + + + + + + + + + +Radius (kpc) +10 +100 + + + + diff -Nru veusz-1.10/tests/comparison/multixy.vsz.selftest veusz-1.14/tests/comparison/multixy.vsz.selftest --- veusz-1.10/tests/comparison/multixy.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/multixy.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,230 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +The joy of plots + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Valkyries + + + + + + + + + + + + + + + + +Swindon + + + + + + + + + + + + + + + + +Discworld + + + + + +Model + + + + + + + + + + + +Death rate + +−5 +−2.5 +0 +2.5 +5 +7.5 + + + + + + + + + + +Winged warriors +0 +5 +10 +15 +20 + + + + diff -Nru veusz-1.10/tests/comparison/noheader.vsz.selftest veusz-1.14/tests/comparison/noheader.vsz.selftest --- veusz-1.10/tests/comparison/noheader.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/noheader.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,84 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + +hello +5 + + + + + + + + + +2 +foo + + + + + + + + + + + + +1 +2 +3 +4 +5 + + + + + + + + + + +2009-01-01 +00:00 +2009-01-01 +06:00 +2009-01-01 +12:00 +2009-01-01 +18:00 + + + + diff -Nru veusz-1.10/tests/comparison/polar.vsz.selftest veusz-1.14/tests/comparison/polar.vsz.selftest --- veusz-1.10/tests/comparison/polar.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/polar.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,163 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1 +1.5 +2 +2.5 +3 + +330° +300° +270° +240° +210° +180° +150° +120° +90° +60° +30° + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +100 +200 +300 + + + + + + + + + + +2 +2.2 +2.4 +2.6 +2.8 + + + + diff -Nru veusz-1.10/tests/comparison/profile.vsz.selftest veusz-1.14/tests/comparison/profile.vsz.selftest --- veusz-1.10/tests/comparison/profile.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/profile.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,457 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Chandra deprojected + + + + + + + + + + + + + + + + + + + +Chandra projected + + + + + + + + + + + + + + + + + + + + + + + + + +XMM projected + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Temperature (keV) + +10 +5 + + + + + + + + + + + + + + +Density + + + + +Cooling time + + + + + + + + + +Cooling time (yr) + +10 +8 +10 +9 +10 +10 +10 +11 +10 +12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Electron density (cm +-3 +) + +0.01 +0.1 + + + + + + + + + + +Radius (kpc) +10 +100 +1000 + + + + diff -Nru veusz-1.10/tests/comparison/rangeds.vsz.selftest veusz-1.14/tests/comparison/rangeds.vsz.selftest --- veusz-1.10/tests/comparison/rangeds.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/rangeds.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,126 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +−2.5 +0 +2.5 +5 +7.5 +10 +12.5 + + + + + + + + + + +0 +2 +4 +6 +8 +10 + + + + diff -Nru veusz-1.10/tests/comparison/shapes.vsz.selftest veusz-1.14/tests/comparison/shapes.vsz.selftest --- veusz-1.10/tests/comparison/shapes.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/shapes.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,130 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Some shapes +α + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +0.25 +0.5 +0.75 +1 +1.25 +1.5 + + + + + + + + + + +0 +0.2 +0.4 +0.6 +0.8 +1 + + + + diff -Nru veusz-1.10/tests/comparison/sin_byhand.vsz.selftest veusz-1.14/tests/comparison/sin_byhand.vsz.selftest --- veusz-1.10/tests/comparison/sin_byhand.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/sin_byhand.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,93 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +sin +x + +−1 +−0.5 +0 +0.5 +1 + + + + + + + + + + +x +0 +1 +2 +3 +4 +5 +6 +7 + + + + diff -Nru veusz-1.10/tests/comparison/sin.vsz.selftest veusz-1.14/tests/comparison/sin.vsz.selftest --- veusz-1.10/tests/comparison/sin.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/sin.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,93 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +sin +x + +−1 +−0.5 +0 +0.5 +1 + + + + + + + + + + +x +0 +1 +2 +3 +4 +5 +6 +7 + + + + diff -Nru veusz-1.10/tests/comparison/sizetest.vsz.selftest veusz-1.14/tests/comparison/sizetest.vsz.selftest --- veusz-1.10/tests/comparison/sizetest.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/sizetest.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,143 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1 +1.5 +2 +2.5 +3 +3.5 +4 + + + + + + + + + + +0.5 +1 +1.5 +2 +2.5 +3 + + + + diff -Nru veusz-1.10/tests/comparison/spectrum.vsz.selftest veusz-1.14/tests/comparison/spectrum.vsz.selftest --- veusz-1.10/tests/comparison/spectrum.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/spectrum.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,356 @@ + + + +Veusz output document + + + + + + +Fe +XXI + + +Fe +XXIII +- +XXIV + + +Fe +XXIV + + +Mg +XII + + +Ne +X + + +Fe +XXIV + + +Fe +XXII +- +XXIII +Ne +X +, Fe +XVII +- +XVIII + + +Fe +XVIII + + +Fe +XVIII + + +Fe +XVII + + +Fe +XVII + + +Fe +XX +- +XXII + + +Fe +XIX + + + + + + + + + +Si +XIII + + +Si +XIV + + + + + + + + + + + + + + + + +99% PSF + + + + + + + + + + + + + +90% PSF + + + + + + + + + + + + + +99% - 90% + + + + + + + + + + + + +Mg +XII + + +N +VII + + +Fe +XVII + + +Fe +XVII + + +O +VIII + Fe +XVIII + + +O +VIII + + + + + + + + + + + + + + + + + + + + +Flux (10 +-3 + photon cm +-2 + s +-1 + + +-1 +) + + +0 +0.5 +1 +1.5 +2 +2.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.5 keV model spectrum + + + + + + + + + + + + + +0.7 keV model spectrum + + + + + + + + + + + + + +1.0 keV model spectrum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Arbitrary units + + +0 +0.1 +0.2 +0.3 +0.4 +0.5 +0.6 + + + + + + + + + + +Wavelength ( + +) +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 + + + + + + + + + + + + + + + diff -Nru veusz-1.10/tests/comparison/stackedxy.vsz.selftest veusz-1.14/tests/comparison/stackedxy.vsz.selftest --- veusz-1.10/tests/comparison/stackedxy.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/stackedxy.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,217 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Valkyries + +−2 +0 +1 +2 +3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Swindon + +−1 +1 +2 +3 +4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Discworld + +−5 +0 +2.5 +5 +7.5 + + + + + + + + + + +Traffic police +0 +5 +10 +15 +20 + + + + diff -Nru veusz-1.10/tests/comparison/starchart.vsz.selftest veusz-1.14/tests/comparison/starchart.vsz.selftest --- veusz-1.10/tests/comparison/starchart.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/starchart.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,1264 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru veusz-1.10/tests/comparison/ternary.vsz.selftest veusz-1.14/tests/comparison/ternary.vsz.selftest --- veusz-1.10/tests/comparison/ternary.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/ternary.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,363 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + +0 +10 +20 +30 +40 +50 +60 +70 +80 +90 +100 +0 +10 +20 +30 +40 +50 +60 +70 +80 +90 +100 +0 +10 +20 +30 +40 +50 +60 +70 +80 +90 +100 +Earth + +Air + + +Fire + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Nougat +Chocolate + + + + diff -Nru veusz-1.10/tests/comparison/testcontour.vsz.selftest veusz-1.14/tests/comparison/testcontour.vsz.selftest --- veusz-1.10/tests/comparison/testcontour.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/testcontour.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,70 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +2.5 +5 +7.5 +10 +12.5 + + + + + + + + + + +0 +2 +4 +6 +8 +10 + + + + diff -Nru veusz-1.10/tests/comparison/testcsverr.vsz.selftest veusz-1.14/tests/comparison/testcsverr.vsz.selftest --- veusz-1.10/tests/comparison/testcsverr.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/testcsverr.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,145 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1 +2 +3 +4 +5 +6 + + + + + + + + + + +0 +2 +4 +6 +8 +10 + + + + diff -Nru veusz-1.10/tests/comparison/test_npy_npz.vsz.selftest veusz-1.14/tests/comparison/test_npy_npz.vsz.selftest --- veusz-1.10/tests/comparison/test_npy_npz.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/test_npy_npz.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,86 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +25 +50 +75 +100 +125 + + + + + + + + + + +0 +2 +4 +6 +8 +10 + + + + diff -Nru veusz-1.10/tests/comparison/vectorfield.vsz.selftest veusz-1.14/tests/comparison/vectorfield.vsz.selftest --- veusz-1.10/tests/comparison/vectorfield.vsz.selftest 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/comparison/vectorfield.vsz.selftest 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,4472 @@ + + + +Veusz output document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +−1 +−0.5 +0 +0.5 +1 + + + + + + + + + + +−1 +−0.5 +0 +0.5 +1 + + + + diff -Nru veusz-1.10/tests/pychecker_all.sh veusz-1.14/tests/pychecker_all.sh --- veusz-1.10/tests/pychecker_all.sh 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/pychecker_all.sh 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,24 @@ +#!/bin/bash +# Copyright (C) 2009 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +# run pychecker on all script files + +for f in `find . -name "*.py"`; do + pychecker $f >> pychecker-out.txt +done diff -Nru veusz-1.10/tests/runselftest.py veusz-1.14/tests/runselftest.py --- veusz-1.10/tests/runselftest.py 2010-12-12 12:41:10.000000000 +0000 +++ veusz-1.14/tests/runselftest.py 2011-11-22 20:23:31.000000000 +0000 @@ -1,3 +1,42 @@ +#!/usr/bin/env python + +# Copyright (C) 2011 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +"""A program to self test the Veusz installation. + +This code compares the output of example + self test input files with +expected output. It returns 0 if the tests succeeded, otherwise the +number of tests failed. If you use an argument "regenerate" to the +program, the comparison files will be recreated. + +This program requires the veusz module to be on the PYTHONPATH. + +On Unix/Linux, Qt requires the DISPLAY environment to be set to an X11 +server for the self test to run. In a non graphical environment Xvfb +can be used to create a hidden X11 server. + +The comparison files are close to being SVG files, but use XPM for any +images and use a fixed (hacked) font metric to give the same results +on each platform. In addition Unicode characters are expanded to their +Unicode code to work around different font handling on platforms. +""" + import glob import os.path import sys @@ -8,11 +47,18 @@ import veusz.setting as setting import veusz.windows.mainwindow +# these tests fail for some reason which haven't been debugged +# it appears the failures aren't important however excluded_tests = set([ - # the 2pi x in the axis gives different positions depending on font - 'inside.vsz', - # for some reason more points in polyline: clipping issue? - 'histo.vsz', + + # fails on Windows + 'histo.vsz', # duplicate in long list of values + 'spectrum.vsz', # angstrom is split into two on linux + + # fails on Mac OS X + 'histo.vsz', # somewhere in long list of values + 'spectrum.vsz', # symbol issue + 'labels.vsz' # symbol issue ]) class StupidFontMetrics(object): @@ -40,6 +86,13 @@ def boundingRect(self, c): return qt4.QRectF(0, 0, self.height()*0.5, self.height()) +_pt = veusz.utils.textrender.PartText +class PartTextAscii(_pt): + """Text renderer which converts text to ascii.""" + def __init__(self, text): + text = unicode(text).encode('ascii', 'xmlcharrefreplace') + _pt.__init__(self, text) + def renderTest(invsz, outfile): """Render vsz document to create outfile.""" @@ -58,18 +111,31 @@ exec open(invsz) in cmds ifc.Export(outfile) + +class Dirs(object): + """Directories and files object.""" + def __init__(self): + self.thisdir = os.path.dirname(__file__) + self.exampledir = os.path.join(self.thisdir, '..', 'examples') + self.testdir = os.path.join(self.thisdir, 'selftests') + self.comparisondir = os.path.join(self.thisdir, 'comparison') + + files = ( glob.glob( os.path.join(self.exampledir, '*.vsz') ) + + glob.glob( os.path.join(self.testdir, '*.vsz') ) ) + + self.invszfiles = [ f for f in files if + os.path.basename(f) not in excluded_tests ] + def renderAllTests(): + """Check documents produce same output as in comparison directory.""" + print "Regenerating all test output" - thisdir = os.path.dirname(__file__) - exampledir = os.path.join(thisdir, '..', 'examples' ) - for vsz in glob.glob( os.path.join(exampledir, '*.vsz') ): + d = Dirs() + for vsz in d.invszfiles: base = os.path.basename(vsz) - if base in excluded_tests: - continue print base - - outfile = os.path.join(thisdir, 'comparison', base + '.selftest') + outfile = os.path.join(d.comparisondir, base + '.selftest') renderTest(vsz, outfile) def runTests(): @@ -78,18 +144,15 @@ fails = 0 passes = 0 - thisdir = os.path.dirname(__file__) - exampledir = os.path.join(thisdir, '..', 'examples' ) - for vsz in glob.glob( os.path.join(exampledir, '*.vsz') ): + d = Dirs() + for vsz in sorted(d.invszfiles): base = os.path.basename(vsz) - if base in excluded_tests: - continue print base - outfile = os.path.join(thisdir, base + '.temp.selftest') + outfile = os.path.join(d.thisdir, base + '.temp.selftest') renderTest(vsz, outfile) - comparfile = os.path.join(thisdir, 'comparison', base + '.selftest') + comparfile = os.path.join(d.thisdir, 'comparison', base + '.selftest') t1 = open(outfile, 'rU').read() t2 = open(comparfile, 'rU').read() @@ -101,12 +164,13 @@ passes += 1 os.unlink(outfile) + print if fails == 0: print "All tests %i/%i PASSED" % (passes, passes) sys.exit(0) else: print "%i/%i tests FAILED" % (fails, passes+fails) - sys.exit(1) + sys.exit(fails) if __name__ == '__main__': app = qt4.QApplication([]) @@ -114,8 +178,11 @@ veusz.setting.transient_settings['unsafe_mode'] = True # hack metrics object to always return same metrics + # and replace text renderer with one that encodes unicode symbols veusz.utils.textrender.FontMetrics = StupidFontMetrics veusz.utils.FontMetrics = StupidFontMetrics + #veusz.utils.Renderer = AsciiRenderer + veusz.utils.textrender.PartText = PartTextAscii # nasty hack to remove underlining del veusz.utils.textrender.part_commands[r'\underline'] diff -Nru veusz-1.10/tests/selftests/1dto2d.vsz veusz-1.14/tests/selftests/1dto2d.vsz --- veusz-1.10/tests/selftests/1dto2d.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/1dto2d.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,40 @@ +# Veusz saved document (version 1.12.99) +# Saved at 2011-08-16T14:20:29.340311 + +AddImportPath(u'/data/jss/veusz/code/veusz/tests/selftests') + +xv = [] +yv = [] +zv = [] +for x in xrange(10): + for y in xrange(10): + z = sqrt((x-5.)**2 + (y-5.)**2) + xv.append(x) + yv.append(y) + zv.append(z) +SetData("x", xv) +SetData("y", yv) +SetData("z", zv) + +SetData2DExpressionXYZ(u'data2d', u'x', u'y', u'z', linked=True) + +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +To('x') +Set('autoExtend', False) +To('..') +Add('axis', name='y', autoadd=False) +To('y') +Set('autoExtend', False) +Set('direction', 'vertical') +To('..') +Add('contour', name='contour1', autoadd=False) +To('contour1') +Set('data', u'data2d') +Set('SubLines/hide', False) +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/selftests/autodetect.csv veusz-1.14/tests/selftests/autodetect.csv --- veusz-1.10/tests/selftests/autodetect.csv 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/autodetect.csv 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,6 @@ +a,b,c,,d,e (text) +1,01/01/10,hello,,1.00E+001,01/01/10 +2,01/02/10,foo,,1.00E+002,02/01/11 +3,20/02/10,bar,,1.00E+003,03/02/12 +4,15/03/10,xxx,,1.00E+004,01/11/11 +5,10/04/10,aaa,,1.00E+003,11/11/11 diff -Nru veusz-1.10/tests/selftests/autodetect.vsz veusz-1.14/tests/selftests/autodetect.vsz --- veusz-1.10/tests/selftests/autodetect.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/autodetect.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,43 @@ +# Veusz saved document (version 1.13) +# Saved at 2011-10-29T16:58:13.552813 + +ImportFileCSV(u'autodetect.csv', linked=True, blanksaredata=True, dateformat=u'DD/MM/YY| |hh:mm:ss', headermode='1st', numericlocale='en_GB') +ImportFileCSV(u'autodetect.csv', linked=True, blanksaredata=True, dateformat=u'DD/MM/YY| |hh:mm:ss', dsprefix=u'p_', headerignore=1, headermode='1st', numericlocale='en_GB', rowsignore=2) +Add('page', name='page1', autoadd=False) +To('page1') +Add('grid', name='grid1', autoadd=False) +To('grid1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +Add('axis', name='y', autoadd=False) +To('y') +Set('mode', u'datetime') +Set('direction', 'vertical') +To('..') +Add('xy', name='xy1', autoadd=False) +To('xy1') +Set('xData', u'a') +Set('yData', u'b') +Set('labels', u'c') +To('..') +To('..') +Add('graph', name='graph2', autoadd=False) +To('graph2') +Add('axis', name='x', autoadd=False) +To('x') +Set('log', True) +To('..') +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +To('..') +Add('xy', name='xy2', autoadd=False) +To('xy2') +Set('xData', u'p_1.00E+002') +Set('yData', u'p_2') +Set('labels', u'p_foo') +To('..') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/selftests/blockeddata.dat veusz-1.14/tests/selftests/blockeddata.dat --- veusz-1.10/tests/selftests/blockeddata.dat 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/blockeddata.dat 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,8 @@ +1 2 +4 5 +6 7 +8 9 + +10 2 +1 3 +3 6 diff -Nru veusz-1.10/tests/selftests/blockeddata.vsz veusz-1.14/tests/selftests/blockeddata.vsz --- veusz-1.10/tests/selftests/blockeddata.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/blockeddata.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,25 @@ +# Veusz saved document (version 1.13) +# Saved at 2011-11-06T14:18:14.450714 + +ImportFile(u'blockeddata.dat', u'x y', linked=True, ignoretext=True, useblocks=True) +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +To('..') +Add('xy', name='xy1', autoadd=False) +To('xy1') +Set('xData', u'x_1') +Set('yData', u'y_1') +To('..') +Add('xy', name='xy2', autoadd=False) +To('xy2') +Set('xData', u'x_2') +Set('yData', u'y_2') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/selftests/csv1.csv veusz-1.14/tests/selftests/csv1.csv --- veusz-1.10/tests/selftests/csv1.csv 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/csv1.csv 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,7 @@ +"test","foo","+-","aerg" +1,7,0.1,"fdfd" +2,4,0.1,"dsfh" +3,3,0.1,"bgtr" +4,2,0.1,"RR" +5,3,0.2,"ZZ" +6,4,0.2,"AA" diff -Nru veusz-1.10/tests/selftests/csv1.vsz veusz-1.14/tests/selftests/csv1.vsz --- veusz-1.10/tests/selftests/csv1.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/csv1.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,22 @@ +# Veusz saved document (version 1.12) +# User: jss +# Date: Sat, 06 Aug 2011 10:31:14 +0000 + +ImportFileCSV(u'csv1.csv', linked=True) +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +To('..') +Add('xy', name='xy1', autoadd=False) +To('xy1') +Set('xData', u'foo') +Set('yData', u'test') +Set('labels', u'aerg') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/selftests/csv_locale.csv veusz-1.14/tests/selftests/csv_locale.csv --- veusz-1.10/tests/selftests/csv_locale.csv 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/csv_locale.csv 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,5 @@ +a b c d +1,23 3.21 01/01/10 2012-12-06 +100,3 200.1 02/02/11 2011-03-01 +1.001,2 1,500.30 23/03/09 2011-04-05 +10 66 05/05/10 2012-01-01 diff -Nru veusz-1.10/tests/selftests/csv_locale.vsz veusz-1.14/tests/selftests/csv_locale.vsz --- veusz-1.10/tests/selftests/csv_locale.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/csv_locale.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,29 @@ +# Veusz saved document (version 1.13) +# Saved at 2011-10-29T17:11:42.391345 + +ImportFileCSV(u'csv_locale.csv', linked=True, delimiter='\t', dsprefix=u'a_') +ImportFileCSV(u'csv_locale.csv', linked=True, dateformat=u'DD/MM/YY| |hh:mm:ss', delimiter='\t', dsprefix=u'b_', numericlocale='de_DE') +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +Add('axis', name='y', autoadd=False) +To('y') +Set('mode', u'datetime') +Set('direction', 'vertical') +To('..') +Add('xy', name='xy1', autoadd=False) +To('xy1') +Set('xData', u'a_b') +Set('yData', u'a_d') +Set('labels', u'a_a') +To('..') +Add('xy', name='xy2', autoadd=False) +To('xy2') +Set('xData', u'b_a') +Set('yData', u'b_c') +Set('labels', u'b_d') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/selftests/csv_missing.csv veusz-1.14/tests/selftests/csv_missing.csv --- veusz-1.10/tests/selftests/csv_missing.csv 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/csv_missing.csv 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,5 @@ +a,b,c,d,e (numeric) +,,,6, +2,,00:02:33.40,,nan +3,hello,01:32:44,4,3 +4,foo,01:01:01,8,invalid diff -Nru veusz-1.10/tests/selftests/csv_missing.vsz veusz-1.14/tests/selftests/csv_missing.vsz --- veusz-1.10/tests/selftests/csv_missing.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/csv_missing.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,41 @@ +# Veusz saved document (version 1.13) +# Saved at 2011-10-29T18:48:52.635479 + +ImportFileCSV(u'csv_missing.csv', linked=True, blanksaredata=True, dateformat=u'DD/MM/YY| |hh:mm:ss', headermode='1st', numericlocale='en_GB') +SetDataExpression(u'cdiv', u'c/1000.', linked=True) +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +To('..') +Add('xy', name='xy1', autoadd=False) +To('xy1') +Set('xData', u'a') +Set('yData', []) +Set('labels', u'b') +To('..') +Add('xy', name='xy2', autoadd=False) +To('xy2') +Set('xData', u'd') +Set('yData', []) +Set('marker', u'diamond') +To('..') +Add('xy', name='xy3', autoadd=False) +To('xy3') +Set('xData', u'e') +Set('yData', []) +Set('markerSize', u'30pt') +Set('MarkerFill/hide', True) +To('..') +Add('xy', name='xy4', autoadd=False) +To('xy4') +Set('xData', []) +Set('yData', u'cdiv') +Set('marker', u'cross') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/selftests/custom.vsz veusz-1.14/tests/selftests/custom.vsz --- veusz-1.10/tests/selftests/custom.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/custom.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,32 @@ +# Veusz saved document (version 1.13) +# Saved at 2011-09-06T18:47:34.442485 + +AddImportPath(u'/home/jss/code/veusz/veusz/tests/selftests') +AddCustom('constant', u'myconst', u'10') + +AddCustom('constant', 'myconst', '41', mode='replace') +AddCustom('constant', 'myconst', '42', mode='append') + +AddCustom('function', 'myfunc(x)', 'myconst*x**2') +AddCustom(u'import', u'numpy.linalg', u'inv') + +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +To('..') +Add('function', name='function1', autoadd=False) +To('function1') +Set('function', u'inv([[1,0],[0,1]])[0,0] * x') +To('..') +Add('function', name='function2', autoadd=False) +To('function2') +Set('function', u'myfunc(x)') +Set('Line/color', u'red') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/selftests/noheader.csv veusz-1.14/tests/selftests/noheader.csv --- veusz-1.10/tests/selftests/noheader.csv 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/noheader.csv 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,5 @@ +1,hello,10:20:30 +2,5,20:30:10 +3,2.2, +4,2,04:20:10.1010 +5,foo,10:10:10 diff -Nru veusz-1.10/tests/selftests/noheader.vsz veusz-1.14/tests/selftests/noheader.vsz --- veusz-1.10/tests/selftests/noheader.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/noheader.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,24 @@ +# Veusz saved document (version 1.13) +# Saved at 2011-10-29T19:22:19.911232 + +ImportFileCSV(u'noheader.csv', linked=True, blanksaredata=True, headermode='none', numericlocale='en_GB') +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +To('x') +Set('mode', u'datetime') +To('..') +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +To('..') +Add('xy', name='xy1', autoadd=False) +To('xy1') +Set('xData', u'col3') +Set('yData', u'col1') +Set('labels', u'col2') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/selftests/rangeds.vsz veusz-1.14/tests/selftests/rangeds.vsz --- veusz-1.10/tests/selftests/rangeds.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/rangeds.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,34 @@ +# Veusz saved document (version 1.11) +# User: jss +# Date: Fri, 10 Jun 2011 19:21:55 +0000 + +AddImportPath(u'/home/jss/code/veusz/veusz/tests/selftests') +SetDataExpression(u'para', u't**2', parametric=(0.0, 1.0, 10), linked=True) +SetDataRange(u'x', 10, (0.0, 10.0), linked=True) +SetDataRange(u'x2', 10, (2.0, 5.0), poserr=(0.1, 0.1), negerr=(-0.1, -0.1), linked=True) +SetDataExpression(u'y', u'(para**2 + x**3)/100', symerr=u'2', linked=True) +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +To('..') +Add('xy', name='xy1', autoadd=False) +To('xy1') +Set('yData', u'para') +To('..') +Add('xy', name='xy2', autoadd=False) +To('xy2') +Set('marker', u'plus') +Set('errorStyle', u'linevertbar') +To('..') +Add('xy', name='xy3', autoadd=False) +To('xy3') +Set('xData', u'x2') +Set('errorStyle', u'fillvert') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/selftests/sizetest.vsz veusz-1.14/tests/selftests/sizetest.vsz --- veusz-1.10/tests/selftests/sizetest.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/sizetest.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,63 @@ +# Veusz saved document (version 1.12.999) +# Saved at 2011-08-17T21:59:29.985354 + +AddImportPath(u'/home/jss/code/veusz-git/veusz/tests/selftests') +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +To('..') +Add('xy', name=u'pt', autoadd=False) +To(u'pt') +Set('xData', [1.0, 2.1, 2.5]) +Set('yData', [3.0, 2.0, 1.0]) +Set('markerSize', u'5pt') +Set('MarkerFill/color', u'blue') +To('..') +Add('xy', name=u'perc', autoadd=False) +To(u'perc') +Set('xData', [1.0, 2.0, 3.0]) +Set('yData', [1.0, 3.0, 2.0]) +Set('markerSize', u'10%') +Set('MarkerFill/color', u'magenta') +To('..') +Add('xy', name=u'ratio', autoadd=False) +To(u'ratio') +Set('xData', [0.5, 1.5, 2.5]) +Set('yData', [2.5, 0.8, 4.0]) +Set('markerSize', u'1/20') +To('..') +Add('xy', name=u'frac', autoadd=False) +To(u'frac') +Set('xData', [1.4, 1.7, 2.5]) +Set('yData', [3.2, 1.5, 3.0]) +Set('markerSize', u'0.04') +Set('MarkerFill/color', u'cyan') +To('..') +Add('xy', name=u'cm', autoadd=False) +To(u'cm') +Set('xData', [0.4, 1.5, 3.0]) +Set('yData', [3.0, 2.5, 1.0]) +Set('markerSize', u'0.5cm') +Set('MarkerFill/color', u'yellow') +To('..') +Add('xy', name=u'mm', autoadd=False) +To(u'mm') +Set('xData', [1.9, 1.5, 1.7]) +Set('yData', [1.3, 1.7, 1.2]) +Set('markerSize', u'3mm') +Set('MarkerFill/color', u'red') +To('..') +Add('xy', name='xy1', autoadd=False) +To('xy1') +Set('xData', [1.9, 1.5, 1.7]) +Set('yData', [1.5, 2.7, 2.2]) +Set('markerSize', u'0.2in') +Set('MarkerFill/color', u'#aaaaff') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/selftests/testcontour.vsz veusz-1.14/tests/selftests/testcontour.vsz --- veusz-1.10/tests/selftests/testcontour.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/testcontour.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,43 @@ +# Veusz saved document (version 0.99.0) +# User: jss +# Date: Fri, 21 Sep 2007 19:00:30 +0000 + +# A test to make sure 2d arrays are working with different dimensions +# in x and y + +ImportString2D(u'test', ''' +xrange 0.000000e+00 1.000000e+01 +yrange 0.000000e+00 1.200000e+01 +0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 +0.000000e+00 1.000000e+00 2.000000e+00 3.000000e+00 4.000000e+00 5.000000e+00 6.000000e+00 7.000000e+00 8.000000e+00 9.000000e+00 +0.000000e+00 2.000000e+00 4.000000e+00 6.000000e+00 8.000000e+00 1.000000e+01 1.200000e+01 1.400000e+01 1.600000e+01 1.800000e+01 +0.000000e+00 3.000000e+00 6.000000e+00 9.000000e+00 1.200000e+01 1.500000e+01 1.800000e+01 2.100000e+01 2.400000e+01 2.700000e+01 +0.000000e+00 4.000000e+00 8.000000e+00 1.200000e+01 1.600000e+01 2.000000e+01 2.400000e+01 2.800000e+01 3.200000e+01 3.600000e+01 +0.000000e+00 5.000000e+00 1.000000e+01 1.500000e+01 2.000000e+01 2.500000e+01 3.000000e+01 3.500000e+01 4.000000e+01 4.500000e+01 +0.000000e+00 6.000000e+00 1.200000e+01 1.800000e+01 2.400000e+01 3.000000e+01 3.600000e+01 4.200000e+01 4.800000e+01 5.400000e+01 +0.000000e+00 7.000000e+00 1.400000e+01 2.100000e+01 2.800000e+01 3.500000e+01 4.200000e+01 4.900000e+01 5.600000e+01 6.300000e+01 +0.000000e+00 8.000000e+00 1.600000e+01 2.400000e+01 3.200000e+01 4.000000e+01 4.800000e+01 5.600000e+01 6.400000e+01 7.200000e+01 +0.000000e+00 9.000000e+00 1.800000e+01 2.700000e+01 3.600000e+01 4.500000e+01 5.400000e+01 6.300000e+01 7.200000e+01 8.100000e+01 +0.000000e+00 1.000000e+01 2.000000e+01 3.000000e+01 4.000000e+01 5.000000e+01 6.000000e+01 7.000000e+01 8.000000e+01 9.000000e+01 +0.000000e+00 1.100000e+01 2.200000e+01 3.300000e+01 4.400000e+01 5.500000e+01 6.600000e+01 7.700000e+01 8.800000e+01 9.900000e+01 +''') +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +To('..') +Add('contour', name='contour1', autoadd=False) +To('contour1') +Set('data', u'test') +To('..') +Add('image', name='image1', autoadd=False) +To('image1') +Set('data', u'test') +Set('colorMap', u'spectrum2') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/selftests/testcsverr.csv veusz-1.14/tests/selftests/testcsverr.csv --- veusz-1.10/tests/selftests/testcsverr.csv 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/testcsverr.csv 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,10 @@ +"test","test+","test-","foo","+-","foo2ððð","+","-" +1,0.1,2,3,0.3,6,0.6,-0.1 +2,0.2,3,3,0.2,7,0.8,-0.3 +3,0.3,4,4,0.3,4,0.2,-0.4 +4,0.2,5,5,0.2,2,0.1,-0.1 +,,,"a","b","+",,"c" +,,,5,4,0.1,,9 +,,,6,4,0.3,,8 +,,,6,3,0.2,,4 +,,,5,3,0.1,,1 diff -Nru veusz-1.10/tests/selftests/testcsverr.vsz veusz-1.14/tests/selftests/testcsverr.vsz --- veusz-1.10/tests/selftests/testcsverr.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/testcsverr.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,58 @@ +# Veusz saved document (version 1.12.99) +# Saved at 2011-08-14T14:43:33.721297 + +AddImportPath(u'/home/jss/code/veusz-git/veusz') +ImportFileCSV(u'testcsverr.csv', linked=True, blanksaredata=True) +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +To('..') +Add('xy', name=u'footest', autoadd=False) +To(u'footest') +Set('xData', u'foo') +Set('yData', u'test') +To('..') +Add('xy', name=u'foo2foo', autoadd=False) +To(u'foo2foo') +Set('xData', u'foo2\xf0\xf0\xf0') +Set('yData', u'foo') +Set('marker', u'diamond') +Set('MarkerFill/color', u'red') +To('..') +Add('xy', name=u'testplusminus', autoadd=False) +To(u'testplusminus') +Set('xData', u'test+') +Set('yData', u'test-') +Set('marker', u'lineplus') +Set('MarkerFill/color', u'blue') +To('..') +Add('xy', name=u'testplustest', autoadd=False) +To(u'testplustest') +Set('xData', u'test+') +Set('yData', u'test') +Set('marker', u'barvert') +Set('MarkerFill/color', u'cyan') +To('..') +Add('xy', name=u'ab', autoadd=False) +To(u'ab') +Set('xData', u'a') +Set('yData', u'b') +Set('marker', u'pentagon') +Set('errorStyle', u'barends') +Set('MarkerFill/color', u'darkred') +To('..') +Add('xy', name=u'ac', autoadd=False) +To(u'ac') +Set('xData', u'c') +Set('yData', u'a') +Set('PlotLine/steps', u'centre') +Set('PlotLine/color', u'red') +Set('PlotLine/width', u'1pt') +To('..') +To('..') +To('..') Binary files /tmp/PNa_ZWTTVg/veusz-1.10/tests/selftests/testdat.npy and /tmp/3oWkwrHSii/veusz-1.14/tests/selftests/testdat.npy differ Binary files /tmp/PNa_ZWTTVg/veusz-1.10/tests/selftests/testdat.npz and /tmp/3oWkwrHSii/veusz-1.14/tests/selftests/testdat.npz differ diff -Nru veusz-1.10/tests/selftests/test_npy_npz.vsz veusz-1.14/tests/selftests/test_npy_npz.vsz --- veusz-1.10/tests/selftests/test_npy_npz.vsz 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/tests/selftests/test_npy_npz.vsz 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,40 @@ +# Veusz saved document (version 1.13) +# Saved at 2011-09-09T18:43:18.107056 + +ImportFilePlugin(u'Numpy NPZ import', u'testdat.npz', linked=True, errorsin2d=True) +ImportFilePlugin(u'Numpy NPY import', u'testdat.npy', linked=True, errorsin2d=True, name=u'c') +Set('StyleSheet/xy/marker', u'none') +Add('page', name='page1', autoadd=False) +To('page1') +Add('graph', name='graph1', autoadd=False) +To('graph1') +Add('axis', name='x', autoadd=False) +Add('axis', name='y', autoadd=False) +To('y') +Set('direction', 'vertical') +To('..') +Add('xy', name='xy1', autoadd=False) +To('xy1') +Set('xData', u'a') +Set('yData', u'b') +To('..') +Add('xy', name='xy2', autoadd=False) +To('xy2') +Set('xData', u'a') +Set('yData', u'c') +Set('PlotLine/color', u'green') +To('..') +Add('xy', name='xy3', autoadd=False) +To('xy3') +Set('xData', u'a') +Set('yData', u'd') +Set('PlotLine/color', u'blue') +To('..') +Add('xy', name='xy4', autoadd=False) +To('xy4') +Set('xData', u'a') +Set('yData', u'e') +Set('PlotLine/color', u'cyan') +To('..') +To('..') +To('..') diff -Nru veusz-1.10/tests/testcontour.vsz veusz-1.14/tests/testcontour.vsz --- veusz-1.10/tests/testcontour.vsz 2010-12-12 12:41:10.000000000 +0000 +++ veusz-1.14/tests/testcontour.vsz 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -# Veusz saved document (version 0.99.0) -# User: jss -# Date: Fri, 21 Sep 2007 19:00:30 +0000 - -# A test to make sure 2d arrays are working with different dimensions -# in x and y - -ImportString2D(u'test', ''' -xrange 0.000000e+00 1.000000e+01 -yrange 0.000000e+00 1.200000e+01 -0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -0.000000e+00 1.000000e+00 2.000000e+00 3.000000e+00 4.000000e+00 5.000000e+00 6.000000e+00 7.000000e+00 8.000000e+00 9.000000e+00 -0.000000e+00 2.000000e+00 4.000000e+00 6.000000e+00 8.000000e+00 1.000000e+01 1.200000e+01 1.400000e+01 1.600000e+01 1.800000e+01 -0.000000e+00 3.000000e+00 6.000000e+00 9.000000e+00 1.200000e+01 1.500000e+01 1.800000e+01 2.100000e+01 2.400000e+01 2.700000e+01 -0.000000e+00 4.000000e+00 8.000000e+00 1.200000e+01 1.600000e+01 2.000000e+01 2.400000e+01 2.800000e+01 3.200000e+01 3.600000e+01 -0.000000e+00 5.000000e+00 1.000000e+01 1.500000e+01 2.000000e+01 2.500000e+01 3.000000e+01 3.500000e+01 4.000000e+01 4.500000e+01 -0.000000e+00 6.000000e+00 1.200000e+01 1.800000e+01 2.400000e+01 3.000000e+01 3.600000e+01 4.200000e+01 4.800000e+01 5.400000e+01 -0.000000e+00 7.000000e+00 1.400000e+01 2.100000e+01 2.800000e+01 3.500000e+01 4.200000e+01 4.900000e+01 5.600000e+01 6.300000e+01 -0.000000e+00 8.000000e+00 1.600000e+01 2.400000e+01 3.200000e+01 4.000000e+01 4.800000e+01 5.600000e+01 6.400000e+01 7.200000e+01 -0.000000e+00 9.000000e+00 1.800000e+01 2.700000e+01 3.600000e+01 4.500000e+01 5.400000e+01 6.300000e+01 7.200000e+01 8.100000e+01 -0.000000e+00 1.000000e+01 2.000000e+01 3.000000e+01 4.000000e+01 5.000000e+01 6.000000e+01 7.000000e+01 8.000000e+01 9.000000e+01 -0.000000e+00 1.100000e+01 2.200000e+01 3.300000e+01 4.400000e+01 5.500000e+01 6.600000e+01 7.700000e+01 8.800000e+01 9.900000e+01 -''') -Add('page', name='page1', autoadd=False) -To('page1') -Add('graph', name='graph1', autoadd=False) -To('graph1') -Add('axis', name='x', autoadd=False) -Add('axis', name='y', autoadd=False) -To('y') -Set('direction', 'vertical') -To('..') -Add('contour', name='contour1', autoadd=False) -To('contour1') -Set('data', u'test') -To('..') -Add('image', name='image1', autoadd=False) -To('image1') -Set('data', u'test') -Set('colorMap', u'spectrum2') -To('..') -To('..') -To('..') diff -Nru veusz-1.10/utils/action.py veusz-1.14/utils/action.py --- veusz-1.10/utils/action.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/utils/action.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: action.py 1036 2009-07-28 19:41:03Z jeremysanders $ - import veusz.qtall as qt4 import utilfuncs import os.path diff -Nru veusz-1.10/utils/colormap.py veusz-1.14/utils/colormap.py --- veusz-1.10/utils/colormap.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/utils/colormap.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,228 @@ +# Copyright (C) 2011 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################### + +import numpy as N +import veusz.qtall as qt4 + +# use fast or slow helpers +slowfuncs = False +try: + from veusz.helpers.qtloops import numpyToQImage, applyImageTransparancy +except ImportError: + slowfuncs = True + from slowfuncs import slowNumpyToQImage + +# Default colormaps used by widgets. +# Each item in this dict is a colormap entry, with the key the name. +# The values in the dict are tuples of (B, G, R, alpha). +# B, G, R and alpha go from 0 to 255 +# Colors are linearly interpolated in this space. + +defaultcolormaps = { + 'blank': ( + (0, 0, 0, 0), + (0, 0, 0, 0), + ), + 'heat': ( + (0, 0, 0, 255), + (0, 0, 186, 255), + (50, 139, 255, 255), + (19, 239, 248, 255), + (255, 255, 255, 255), + ), + 'spectrum2': ( + (0, 0, 255, 255), + (0, 255, 255, 255), + (0, 255, 0, 255), + (255, 255, 0, 255), + (255, 0, 0, 255), + ), + 'spectrum': ( + (0, 0, 0, 255), + (0, 0, 255, 255), + (0, 255, 255, 255), + (0, 255, 0, 255), + (255, 255, 0, 255), + (255, 0, 0, 255), + (255, 255, 255, 255), + ), + 'grey': ( + (0, 0, 0, 255), + (255, 255, 255, 255), + ), + 'blue': ( + (0, 0, 0, 255), + (255, 0, 0, 255), + (255, 255, 255, 255), + ), + 'red': ( + (0, 0, 0, 255), + (0, 0, 255, 255), + (255, 255, 255, 255), + ), + 'green': ( + (0, 0, 0, 255), + (0, 255, 0, 255), + (255, 255, 255, 255), + ), + 'bluegreen': ( + (0, 0, 0, 255), + (255, 123, 0, 255), + (255, 226, 72, 255), + (161, 255, 0, 255), + (255, 255, 255, 255), + ), + 'transblack': ( + (0, 0, 0, 255), + (0, 0, 0, 0), + ), + 'royal': ( + (0, 0, 0, 255), + (128, 0, 0, 255), + (255, 0, 128, 255), + (0, 255, 255, 255), + (255, 255, 255, 255), + ), + 'complement': ( + (0, 0, 0, 255), + (0, 255, 0, 255), + (255, 0, 255, 255), + (0, 0, 255, 255), + (0, 255, 255, 255), + (255, 255, 255, 255), + ), + } + +def applyScaling(data, mode, minval, maxval): + """Apply a scaling transformation on the data. + data is a numpy array + mode is one of 'linear', 'sqrt', 'log', or 'squared' + minval is the minimum value of the scale + maxval is the maximum value of the scale + + returns transformed data, valid between 0 and 1 + """ + + # catch naughty people by hardcoding a range + if minval == maxval: + minval, maxval = 0., 1. + + if mode == 'linear': + # linear scaling + data = (data - minval) / (maxval - minval) + + elif mode == 'sqrt': + # sqrt scaling + # translate into fractions of range + data = (data - minval) / (maxval - minval) + # clip off any bad sqrts + data[data < 0.] = 0. + # actually do the sqrt transform + data = N.sqrt(data) + + elif mode == 'log': + # log scaling of image + # clip any values less than lowermin + lowermin = data < minval + data = N.log(data - (minval - 1)) / N.log(maxval - (minval - 1)) + data[lowermin] = 0. + + elif mode == 'squared': + # squared scaling + # clip any negative values + lowermin = data < minval + data = (data-minval)**2 / (maxval-minval)**2 + data[lowermin] = 0. + + else: + raise RuntimeError, 'Invalid scaling mode "%s"' % mode + + return data + +def applyColorMap(cmap, scaling, datain, minval, maxval, + trans, transimg=None): + """Apply a colour map to the 2d data given. + + cmap is the color map (numpy of BGRalpha quads) + scaling is scaling mode => 'linear', 'sqrt', 'log' or 'squared' + data are the imaging data + minval and maxval are the extremes of the data for the colormap + trans is a number from 0 to 100 + transimg is an optional image to apply transparency from + Returns a QImage + """ + + cmap = N.array(cmap, dtype=N.intc) + + # invert colour map if min and max are swapped + if minval > maxval: + minval, maxval = maxval, minval + cmap = cmap[::-1] + + # apply transparency + if trans != 0: + cmap = cmap.copy() + cmap[:,3] = (cmap[:,3].astype(N.float32) * (100-trans) / + 100.).astype(N.intc) + + # apply scaling of data + fracs = applyScaling(datain, scaling, minval, maxval) + + if not slowfuncs: + img = numpyToQImage(fracs, cmap, transimg is not None) + if transimg is not None: + applyImageTransparancy(img, transimg) + else: + img = slowNumpyToQImage(fracs, cmap, transimg) + return img + +def makeColorbarImage(minval, maxval, scaling, cmap, transparency, + direction='horz'): + """Make a colorbar for the scaling given.""" + + barsize = 128 + + if scaling in ('linear', 'sqrt', 'squared'): + # do a linear color scaling + vals = N.arange(barsize)/(barsize-1.0)*(maxval-minval) + minval + colorscaling = scaling + coloraxisscale = 'linear' + else: + assert scaling == 'log' + + # a logarithmic color scaling + # we cheat here by actually plotting a linear colorbar + # and telling veusz to put a log axis along it + # (as we only care about the endpoints) + # maybe should do this better... + + vals = N.arange(barsize)/(barsize-1.0)*(maxval-minval) + minval + colorscaling = 'linear' + coloraxisscale = 'log' + + # convert 1d array to 2d image + if direction == 'horizontal': + vals = vals.reshape(1, barsize) + else: + assert direction == 'vertical' + vals = vals.reshape(barsize, 1) + + img = applyColorMap(cmap, colorscaling, vals, + minval, maxval, transparency) + + return (minval, maxval, coloraxisscale, img) diff -Nru veusz-1.10/utils/dates.py veusz-1.14/utils/dates.py --- veusz-1.10/utils/dates.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/utils/dates.py 2011-11-22 20:23:31.000000000 +0000 @@ -86,6 +86,13 @@ frac, sec = math.modf(f - days*24*60*60) return datetime.timedelta(days, sec, frac*1e6) + offsetdate +def dateFloatToString(f): + """Convert date float to string.""" + if N.isfinite(f): + return floatToDateTime(f).isoformat() + else: + return unicode(f) + def datetimeToTuple(dt): """Return tuple (year,month,day,hour,minute,second,microsecond) from datetime object.""" @@ -163,5 +170,101 @@ #print "rounded", timein return tuple(timein) - - + +def dateStrToRegularExpression(instr): + """Convert date-time string to regular expression. + + Converts format yyyy-mm-dd|T|hh:mm:ss to re for date + """ + + # first rename each special string to a unique string (this is a + # unicode character which is in the private use area) then rename + # back again to the regular expression. This avoids the regular + # expression being remapped. + maps = ( + ('YYYY', u'\ue001', r'(?P[0-9]{4})'), + ('YY', u'\ue002', r'(?P[0-9]{2})'), + ('MM', u'\ue003', r'(?P[0-9]{2})'), + ('M', u'\ue004', r'(?P[0-9]{1,2})'), + ('DD', u'\ue005', r'(?P
    [0-9]{2})'), + ('D', u'\ue006', r'(?P
    [0-9]{1,2})'), + ('hh', u'\ue007', r'(?P[0-9]{2})'), + ('h', u'\ue008', r'(?P[0-9]{1,2})'), + ('mm', u'\ue009', r'(?P[0-9]{2})'), + ('m', u'\ue00a', r'(?P[0-9]{1,2})'), + ('ss', u'\ue00b', r'(?P[0-9]{2}(\.[0-9]*)?)'), + ('s', u'\ue00c', r'(?P[0-9]{1,2}(\.[0-9]*)?)'), + ) + + out = [] + for p in instr.split('|'): + # escape special characters (non alpha-num) + p = re.escape(p) + + # replace strings with characters + for search, char, repl in maps: + p = p.replace(search, char) + # replace characters with re strings + for search, char, repl in maps: + p = p.replace(char, repl) + + # save as an optional group + out.append( '(?:%s)?' % p ) + + # return final expression + return '^\s*%s\s*$' % (''.join(out)) + +def dateREMatchToDate(match): + """Take match object for above regular expression, + and convert to float date value.""" + + if match is None: + raise ValueError, "match object is None" + + # remove None matches + grps = {} + for k, v in match.groupdict().iteritems(): + if v is not None: + grps[k] = v + + # bomb out if nothing matches + if len(grps) == 0: + raise ValueError, "no groups matched" + + # get values of offset + oyear = offsetdate.year + omon = offsetdate.month + oday = offsetdate.day + ohour = offsetdate.hour + omin = offsetdate.minute + osec = offsetdate.second + omicrosec = offsetdate.microsecond + + # now convert each element from the re + if 'YYYY' in grps: + oyear = int(grps['YYYY']) + if 'YY' in grps: + y = int(grps['YY']) + if y >= 70: + oyear = int('19' + grps['YY']) + else: + oyear = int('20' + grps['YY']) + if 'MM' in grps: + omon = int(grps['MM']) + if 'DD' in grps: + oday = int(grps['DD']) + if 'hh' in grps: + ohour = int(grps['hh']) + if 'mm' in grps: + omin = int(grps['mm']) + if 'ss' in grps: + s = float(grps['ss']) + osec = int(s) + omicrosec = int(1e6*(s-osec)) + + # convert to python datetime object + d = datetime.datetime( + oyear, omon, oday, ohour, omin, osec, omicrosec) + + # return to veusz float time + return datetimeToFloat(d) diff -Nru veusz-1.10/utils/fitlm.py veusz-1.14/utils/fitlm.py --- veusz-1.10/utils/fitlm.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/utils/fitlm.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: fitlm.py 1302 2010-06-24 15:36:54Z jeremysanders $ - """ Numerical fitting of functions to data. """ diff -Nru veusz-1.10/utils/formatting.py veusz-1.14/utils/formatting.py --- veusz-1.10/utils/formatting.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/utils/formatting.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,224 @@ +# Copyright (C) 2010 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################### + +import re +import math + +import dates +import veusz.qtall as qt4 + +_formaterror = 'FormatError' + +# a format statement in a string +_format_re = re.compile(r'%([-#0-9 +.hlL]*?)([diouxXeEfFgGcrs%])') + +def localeFormat(totfmt, args, locale=None): + """Format using fmt statement fmt, qt QLocale object locale and + arguments to formatting args. + + * arguments are not supported in this formatting, nor is using + a dict to supply values for statement + """ + + # substitute all format statements with string format statements + newfmt = _format_re.sub("%s", totfmt) + + # do formatting separately for all statements + strings = [] + i = 0 + for f in _format_re.finditer(totfmt): + code = f.group(2) + if code == '%': + s = '%' + else: + try: + s = f.group() % args[i] + i += 1 + except IndexError: + raise TypeError, "Not enough arguments for format string" + if locale is not None and code in 'eEfFgG': + s = s.replace('.', qt4.QString(locale.decimalPoint())) + + strings.append(s) + + if i != len(args): + raise TypeError, "Not all arguments converted during string formatting" + + return newfmt % tuple(strings) + +def formatSciNotation(num, formatargs, locale=None): + """Format number into form X \times 10^{Y}. + This function trims trailing zeros and decimal point unless a formatting + argument is supplied + + This is similar to the %e format string + formatargs is the standard argument in a format string to control the + number of decimal places, etc. + + locale is a QLocale object + """ + + # create an initial formatting string + if formatargs: + format = '%' + formatargs + 'e' + else: + format = '%.10e' + + # try to format the number + # this may be user-supplied data, so don't crash hard by returning + # useless output + try: + text = format % num + except: + return _formaterror + + # split around the exponent + leader, exponent = text.split('e') + + # strip off trailing decimal point and zeros if no format args + if not formatargs: + leader = '%.10g' % float(leader) + + # trim off leading 1 + if leader == '1' and not formatargs: + leader = '' + else: + # the unicode string is a small space, multiply and small space + leader += u'\u00d7' + + # do substitution of decimals + if locale is not None: + leader = leader.replace('.', qt4.QString(locale.decimalPoint())) + + return '%s10^{%i}' % (leader, int(exponent)) + +def formatGeneral(num, fmtarg, locale=None): + """General formatting which switches from normal to scientic + notation.""" + + a = abs(num) + # manually choose when to switch from normal to scientific + # as the default isn't very good + if a >= 1e4 or (a < 1e-2 and a > 1e-110): + retn = formatSciNotation(num, fmtarg, locale=locale) + else: + if fmtarg: + f = '%' + fmtarg + 'g' + else: + f = '%.10g' + + try: + retn = f % num + except TypeError: + retn = _formaterror + + if locale is not None: + retn = retn.replace('.', qt4.QString(locale.decimalPoint())) + return retn + +engsuffixes = ( 'y', 'z', 'a', 'f', 'p', 'n', + u'\u03bc', 'm', '', 'k', 'M', 'G', + 'T', 'P', 'E', 'Z', 'Y' ) + +def formatEngineering(num, fmtarg, locale=None): + """Engineering suffix format notation using SI suffixes.""" + + if num != 0.: + logindex = math.log10( abs(num) ) / 3. + + # for numbers < 1 round down suffix + if logindex < 0. and (int(logindex)-logindex) > 1e-6: + logindex -= 1 + + # make sure we don't go out of bounds + logindex = min( max(logindex, -8), + len(engsuffixes) - 9 ) + + suffix = engsuffixes[ int(logindex) + 8 ] + val = num / 10**( int(logindex) *3) + else: + suffix = '' + val = num + + text = ('%' + fmtarg + 'g%s') % (val, suffix) + if locale is not None: + text = text.replace('.', qt4.QString(locale.decimalPoint())) + return text + +# catch general veusz formatting expression +_formatRE = re.compile(r'%([^A-Za-z]*)(VDVS|VD.|V.|[A-Za-z])') + +def formatNumber(num, format, locale=None): + """ Format a number in different ways. + + format is a standard C format string, with some additions: + %Ve scientific notation X \times 10^{Y} + %Vg switches from normal notation to scientific outside 10^-2 to 10^4 + %VE engineering suffix option + + %VDx date formatting, where x is one of the arguments in + http://docs.python.org/lib/module-time.html in the function + strftime + """ + + while True: + # repeatedly try to do string format + m = _formatRE.search(format) + if not m: + break + + # argument and type of formatting + farg, ftype = m.groups() + + # special veusz formatting + if ftype[:1] == 'V': + # special veusz formatting + if ftype == 'Ve': + out = formatSciNotation(num, farg, locale=locale) + elif ftype == 'Vg': + out = formatGeneral(num, farg, locale=locale) + elif ftype == 'VE': + out = formatEngineering(num, farg, locale=locale) + elif ftype[:2] == 'VD': + d = dates.floatToDateTime(num) + # date formatting (seconds since start of epoch) + if ftype[:4] == 'VDVS': + # special seconds operator + out = ('%'+ftype[4:]+'g') % (d.second+d.microsecond*1e-6) + else: + # use date formatting + try: + out = d.strftime(str('%'+ftype[2:])) + except ValueError: + out = _formaterror + else: + out = _formaterror + + # replace hyphen with true - and small space + out = out.replace('-', u'\u2212') + + else: + # standard C formatting + try: + out = localeFormat('%' + farg + ftype, (num,), locale=locale) + except: + out = _formaterror + + format = format[:m.start()] + out + format[m.end():] + + return format diff -Nru veusz-1.10/utils/__init__.py veusz-1.14/utils/__init__.py --- veusz-1.10/utils/__init__.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/utils/__init__.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: __init__.py 1401 2010-09-11 09:28:00Z jeremysanders $ - from version import version from textrender import Renderer, FontMetrics from safe_eval import checkCode @@ -30,6 +28,8 @@ from action import * from pdf import * from dates import * +from formatting import * +from colormap import * try: from veusz.helpers.qtloops import addNumpyToPolygonF, plotPathsToPainter, \ diff -Nru veusz-1.10/utils/pdf.py veusz-1.14/utils/pdf.py --- veusz-1.10/utils/pdf.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/utils/pdf.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: pdf.py 1056 2009-09-05 16:51:59Z jeremysanders $ - import re def scalePDFMediaBox(text, pagewidth, diff -Nru veusz-1.10/utils/points.py veusz-1.14/utils/points.py --- veusz-1.10/utils/points.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/utils/points.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: points.py 1280 2010-06-13 14:53:17Z jeremysanders $ - from itertools import izip import veusz.qtall as qt4 @@ -30,6 +28,8 @@ except ImportError: from slowfuncs import plotPathsToPainter +import colormap + """This is the symbol plotting part of Veusz There are actually several different ways symbols are plotted. @@ -383,7 +383,7 @@ ) def plotMarkers(painter, xpos, ypos, markername, markersize, scaling=None, - clip=None): + clip=None, cmap=None, colorvals=None): """Funtion to plot an array of markers on a painter. painter: QPainter @@ -392,6 +392,8 @@ markersize: size of marker to plot scaling: scale size of markers by array, or don't in None clip: rectangle if clipping wanted + cmap: colormap to use if colorvals is set + colorvals: color values 0-1 of each point if used """ # minor optimization @@ -411,21 +413,18 @@ # turn off brush painter.setBrush( qt4.QBrush() ) - # split up into two loops as this is a critical path - if scaling is None: - plotPathsToPainter(painter, path, xpos, ypos, clip) - else: - # plot markers, scaling each one - s = painter.scale - t = painter.translate - d = painter.drawPath - r = painter.resetTransform - for x, y, sc in izip(xpos, ypos, scaling): - t(x, y) - s(sc, sc) - d(path) - r() - + # if using colored points + colorimg = None + if colorvals is not None: + # convert colors to rgb values via a 2D image and pass to function + trans = (1-painter.brush().color().alphaF())*100 + color2d = colorvals.reshape( 1, len(colorvals) ) + colorimg = colormap.applyColorMap( + cmap, 'linear', color2d, 0., 1., trans) + + # this is the fast (C++) or slow (python) helper + plotPathsToPainter(painter, path, xpos, ypos, scaling, clip, colorimg) + painter.restore() def plotMarker(painter, xpos, ypos, markername, markersize): diff -Nru veusz-1.10/utils/safe_eval.py veusz-1.14/utils/safe_eval.py --- veusz-1.10/utils/safe_eval.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/utils/safe_eval.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: safe_eval.py 1089 2009-10-25 17:59:57Z jeremysanders $ - """ 'Safe' python code evaluation @@ -329,7 +327,9 @@ if securityonly is set, then don't return errors from Python exceptions. """ - + + # compiler can't parse strings with unicode + code = code.encode('utf8') try: ast = compiler.parse(code) except SyntaxError, e: diff -Nru veusz-1.10/utils/slowfuncs.py veusz-1.14/utils/slowfuncs.py --- veusz-1.10/utils/slowfuncs.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/utils/slowfuncs.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: slowfuncs.py 1286 2010-06-16 09:44:53Z jeremysanders $ - """ These are slow versions of routines also implemented in C++ """ @@ -53,7 +51,8 @@ for p in points: pappend( qpointf(*p) ) -def plotPathsToPainter(painter, path, x, y, clip=None): +def plotPathsToPainter(painter, path, x, y, scaling=None, + clip=None, colorimg=None): """Plot array of x, y points.""" if clip is None: @@ -67,12 +66,27 @@ pathbox.bottom(), pathbox.right()) # draw the paths - for xp, yp in izip(x, y): - pt = qt4.QPointF(xp, yp) + numpts = min(len(x), len(y)) + if scaling is not None: + numpts = min(numpts, len(scaling)) + if colorimg is not None: + numpts = min(numpts, colorimg.width()) + + origtrans = painter.worldTransform() + for i in xrange(numpts): + pt = qt4.QPointF(x[i], y[i]) if clip.contains(pt): painter.translate(pt) + # scale if wanted + if scaling is not None: + painter.scale(scaling[i], scaling[i]) + # set color if given + if colorimg is not None: + b = qt4.QBrush( qt4.QColor.fromRgba(colorimg.pixel(i, 0)) ) + painter.setBrush(b) + painter.drawPath(path) - painter.translate(-pt) + painter.setWorldTransform(origtrans) def plotLinesToPainter(painter, x1, y1, x2, y2, clip=None, autoexpand=True): """Plot lines given in numpy arrays to painter.""" @@ -136,3 +150,59 @@ # paint it if rects: painter.drawRects(rects) + +def slowNumpyToQImage(img, cmap, transparencyimg): + """Slow version of routine to convert numpy array to QImage + This is hard work in Python, but it was like this originally. + + img: numpy array to convert to QImage + cmap: 2D array of colors (BGRA rows) + forcetrans: force image to have alpha component.""" + + if struct.pack("h", 1) == "\000\001": + # have to swap colors for big endian architectures + cmap2 = cmap.copy() + cmap2[:,0] = cmap[:,3] + cmap2[:,1] = cmap[:,2] + cmap2[:,2] = cmap[:,1] + cmap2[:,3] = cmap[:,0] + cmap = cmap2 + + fracs = N.clip(N.ravel(img), 0., 1.) + + # Work out which is the minimum colour map. Assumes we have <255 bands. + numbands = cmap.shape[0]-1 + bands = (fracs*numbands).astype(N.uint8) + bands = N.clip(bands, 0, numbands-1) + + # work out fractional difference of data from band to next band + deltafracs = (fracs - bands * (1./numbands)) * numbands + + # need to make a 2-dimensional array to multiply against triplets + deltafracs.shape = (deltafracs.shape[0], 1) + + # calculate BGRalpha quadruplets + # this is a linear interpolation between the band and the next band + quads = (deltafracs*cmap[bands+1] + + (1.-deltafracs)*cmap[bands]).astype(N.uint8) + + # apply transparency if a transparency image is set + if transparencyimg is not None and transparencyimg.shape == img.shape: + quads[:,3] = ( N.clip(N.ravel(transparencyimg), 0., 1.) * + quads[:,3] ).astype(N.uint8) + + # convert 32bit quads to a Qt QImage + s = quads.tostring() + + fmt = qt4.QImage.Format_RGB32 + if N.any(cmap[:,3] != 255) or transparencyimg is not None: + # any transparency + fmt = qt4.QImage.Format_ARGB32 + + img = qt4.QImage(s, img.shape[1], img.shape[0], fmt) + img = img.mirrored() + + # hack to ensure string isn't freed before QImage + img.veusz_string = s + return img + diff -Nru veusz-1.10/utils/textrender.py veusz-1.14/utils/textrender.py --- veusz-1.10/utils/textrender.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/utils/textrender.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,8 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: textrender.py 1403 2010-09-12 13:44:35Z jeremysanders $ - import math import re @@ -40,11 +38,13 @@ r'\}': '}', r'\[': '[', r'\]': ']', + r'\backslash' : u'\u005c', # operators r'\pm': u'\u00b1', r'\mp': u'\u2213', r'\times': u'\u00d7', + r'\cdot': u'\u22c5', r'\ast': u'\u2217', r'\star': u'\u22c6', r'\deg': u'\u00b0', @@ -56,6 +56,7 @@ r'\uplus': u'\u228e', r'\vee': u'\u22c1', r'\wedge': u'\u22c0', + r'\nabla': u'\u2207', r'\lhd': u'\u22b2', r'\rhd': u'\u22b3', r'\unlhd': u'\u22b4', @@ -75,9 +76,14 @@ r'\infty': u'\u221e', r'\int': u'\u222b', r'\leftarrow': u'\u2190', + r'\Leftarrow': u'\u21d0', r'\uparrow': u'\u2191', r'\rightarrow': u'\u2192', + r'\to': u'\u2192', + r'\Rightarrow': u'\u21d2', r'\downarrow': u'\u2193', + r'\leftrightarrow': u'\u2194', + r'\Leftrightarrow': u'\u21d4', r'\circ': u'\u0e50', # relations @@ -190,7 +196,7 @@ painter = self.painter pixperpt = painter.device().logicalDpiY() / 72. try: - pixperpt *= painter.veusz_scaling + pixperpt *= painter.scaling except AttributeError: pass return pixperpt @@ -209,6 +215,9 @@ def __init__(self, text): self.text = text + def addText(self, text): + self.text += text + def render(self, state): """Render some text.""" @@ -387,6 +396,21 @@ font.setPointSizeF(size) state.painter.setFont(font) +class PartMultiScript(Part): + """Represents multiple parts with the same starting x, e.g. a combination of + super- and subscript parts.""" + def render(self, state): + oldx = state.x + newx = oldx + for p in self.children: + state.x = oldx + p.render(state) + newx = max([state.x, newx]) + state.x = newx + + def append(p): + self.children.append(p) + class PartItalic(Part): """Represents italic part.""" def render(self, state): @@ -548,6 +572,7 @@ r'\size': (PartSize, 2), r'\frac': (PartFrac, 2), r'\bar': (PartBar, 1), + r'\overline': (PartBar, 1), r'\dot': (PartDot, 1), } @@ -555,8 +580,7 @@ splitter_re = re.compile(r''' ( \\[A-Za-z]+[ ]* | # normal latex command -\\\{ | \\\} | # escaped {} brackets -\\\[ | \\\] | # escaped [] brackets +\\[\[\]{}_^] | # escaped special characters \\\\ | # line end \{ | # begin block \} | # end block @@ -580,14 +604,12 @@ # we may need to drop excess spaces after \foo commands ps = p.rstrip() if ps in symbols: - # convert to symbol if possible - text = symbols[ps] + # it will become a symbol, so preserve whitespace + doAdd(ps) if ps != p: - # add back spacing - text += p[len(ps)-len(p):] - doAdd(text) + doAdd(p[len(ps)-len(p):]) else: - # add as possible command + # add as possible command, so drop excess whitespace doAdd(ps) elif p == '{': # add a new level @@ -606,6 +628,14 @@ lines = [] itemlist = [] length = len(partlist) + + def addText(text): + """Try to merge consecutive text items for better rendering.""" + if itemlist and isinstance(itemlist[-1], PartText): + itemlist[-1].addText(text) + else: + itemlist.append( PartText(text) ) + i = 0 while i < length: p = partlist[i] @@ -613,13 +643,38 @@ lines.append( Part(itemlist) ) itemlist = [] elif isinstance(p, basestring): - if p in part_commands: + if p in symbols: + addText(symbols[p]) + elif p in part_commands: klass, numargs = part_commands[p] - partargs = [makePartTree(k) for k in partlist[i+1:i+numargs+1]] - itemlist.append( klass(partargs) ) + if numargs == 1 and len(partlist) > i+1 and isinstance(partlist[i+1], basestring): + # coerce a single argument to a partlist so that things + # like "A^\dagger" render correctly without needing + # curly brackets + partargs = [makePartTree([partlist[i+1]])] + else: + partargs = [makePartTree(k) for k in partlist[i+1:i+numargs+1]] + + if (p == '^' or p == '_'): + if len(itemlist) > 0 and ( + isinstance(itemlist[-1], PartSubScript) or + isinstance(itemlist[-1], PartSuperScript) or + isinstance(itemlist[-1], PartMultiScript)): + # combine sequences of multiple sub-/superscript parts into + # a MultiScript item so that a single text item can have + # both super and subscript indicies + # e.g. X^{(q)}_{i} + if isinstance(itemlist[-1], PartMultiScript): + itemlist.append( klass(partargs) ) + else: + itemlist[-1] = PartMultiScript([itemlist[-1], klass(partargs)]) + else: + itemlist.append( klass(partargs) ) + else: + itemlist.append( klass(partargs) ) i += numargs else: - itemlist.append( PartText(p) ) + addText(p) else: itemlist.append( makePartTree(p) ) i += 1 diff -Nru veusz-1.10/utils/treemodel.py veusz-1.14/utils/treemodel.py --- veusz-1.10/utils/treemodel.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/utils/treemodel.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,265 @@ +# Copyright (C) 2011 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################### + +"""A Qt data model show a tree of Python nodes.""" + +import bisect +import veusz.qtall as qt4 + +class TMNode(object): + """Object to represent nodes in TreeModel. + + Each node has a tuple of data items, a parent node and a list of + child nodes. + """ + + def __init__(self, data, parent): + self.data = data + self.parent = parent + self.childnodes = [] + + # model uses these to map objects to qmodelindexes + # self._idx = None + + def toolTip(self, column): + """Return tooltip for column, if any.""" + return qt4.QVariant() + + def doPrint(self, indent=0): + """Print out tree for debugging.""" + print " "*indent, self.data, self + for c in self.childnodes: + c.doPrint(indent=indent+1) + + def deleteFromParent(self): + """Delete this node from its parent.""" + del self.parent.childnodes[self.parent.childnodes.index(self)] + + def nodeData(self, idx): + """Get data with index given.""" + try: + return qt4.QVariant(self.data[idx]) + except: + return qt4.QVariant() + + def childWithData1(self, d): + """Get child node with 1st column data d.""" + for c in self.childnodes: + if c.data[0] == d: + return c + return None + + def insertChildSorted(self, newchild): + """Insert child alphabetically using data d.""" + cdata = [c.data for c in self.childnodes] + idx = bisect.bisect_left(cdata, newchild.data) + newchild.parent = self + self.childnodes.insert(idx, newchild) + + def cloneTo(self, newroot): + """Make a clone of self at the root given.""" + return self.__class__(self.data, newroot) + +class TreeModel(qt4.QAbstractItemModel): + """A Qt model for storing Python nodes in a tree. + + The nodes are TMNode objects above.""" + + def __init__(self, rootdata, *args): + """Construct the model. + rootdata is a tuple of data for the root node - it should have + the same number of columns as other datasets.""" + + qt4.QAbstractItemModel.__init__(self, *args) + self.root = TMNode(rootdata, None) + + # the nodes are stored here when in the tree, + # to be looked up with node._idx + self.nodes = {} + # next index to assign in self.nodes + self.nodeindex = 0 + + def columnCount(self, parent): + """Use root data to get column count.""" + return len(self.root.data) + + def data(self, index, role): + """Get text or tooltip.""" + if index.isValid(): + item = self.objFromIndex(index) + if role == qt4.Qt.DisplayRole: + return item.nodeData(index.column()) + elif role == qt4.Qt.ToolTipRole: + return item.toolTip(index.column()) + + return qt4.QVariant() + + def flags(self, index): + """Return whether node is editable.""" + if not index.isValid(): + return qt4.Qt.NoItemFlags + return qt4.Qt.ItemIsEnabled | qt4.Qt.ItemIsSelectable + + def headerData(self, section, orientation, role): + """Use root node to get headers.""" + if orientation == qt4.Qt.Horizontal and role == qt4.Qt.DisplayRole: + return self.root.nodeData(section) + return qt4.QVariant() + + def objFromIndex(self, idx): + """Given an index, return the node.""" + if idx.isValid(): + try: + return self.nodes[idx.internalId()] + except KeyError: + pass + return None + + def index(self, row, column, parent): + """Return index of node.""" + if not self.hasIndex(row, column, parent): + return qt4.QModelIndex() + + parentitem = self.objFromIndex(parent) + if parentitem is None: + parentitem = self.root + + childitem = parentitem.childnodes[row] + if childitem: + return self.createIndex(row, column, childitem._idx) + return qt4.QModelIndex() + + def parent(self, index): + """Get parent index of index.""" + if not index.isValid(): + return qt4.QModelIndex() + + childitem = self.objFromIndex(index) + parentitem = childitem.parent + + if parentitem is self.root: + return qt4.QModelIndex() + + parentrow = parentitem.parent.childnodes.index(parentitem) + return self.createIndex(parentrow, 0, parentitem._idx) + + def rowCount(self, parent): + """Compute row count of node.""" + if parent.column() > 0: + return 0 + + if not parent.isValid(): + parentitem = self.root + else: + parentitem = self.objFromIndex(parent) + + return len(parentitem.childnodes) + + @staticmethod + def _getdata(theroot): + """Get a set of child node data and a mapping of data to node.""" + lookup = {} + data = [] + for c in theroot.childnodes: + d = c.data[0] + lookup[d] = c + data.append(d) + return lookup, set(data) + + def _syncbranch(self, parentidx, root, rootnew): + """For synchronising branches in node tree.""" + + # FIXME: this doesn't work if there are duplicates + # use LCS - longest common sequence instead + clookup, cdata = self._getdata(root) + nlookup, ndata = self._getdata(rootnew) + if not cdata and not ndata: + return + + common = cdata & ndata + + # items to remove (no longer in new data) + todelete = cdata - common + + # sorted list to add (added to new data) + toadd = list(ndata - common) + toadd.sort() + + # iterate over entries, adding and deleting as necessary + i = 0 + c = root.childnodes + + while i < len(rootnew.childnodes) or i < len(c): + if i < len(c): + k = c[i].data[0] + else: + k = None + + # one to be deleted + if k in todelete: + todelete.remove(k) + self.beginRemoveRows(parentidx, i, i) + del self.nodes[c[i]._idx] + del c[i] + self.endRemoveRows() + continue + + # one to insert + if toadd and (k > toadd[0] or k is None): + self.beginInsertRows(parentidx, i, i) + a = nlookup[toadd[0]].cloneTo(root) + a._idx = self.nodeindex + self.nodeindex += 1 + self.nodes[a._idx] = a + c.insert(i, a) + self.endInsertRows() + del toadd[0] + else: + # neither delete or add + if clookup[k].data != nlookup[k].data: + # the name is the same but data are not + # swap node entry to point to new cloned node + clone = nlookup[k].cloneTo(root) + idx = clookup[k]._idx + clone._idx = idx + self.nodes[idx] = clone + + self.emit(qt4.SIGNAL('dataChanged(const QModelIndex &, ' + 'const QModelIndex &)'), + self.index(i, 0, parentidx), + self.index(i, len(c[i].data)-1, + parentidx)) + + # now recurse to update any subnodes + newindex = self.index(i, 0, parentidx) + self._syncbranch(newindex, c[i], rootnew.childnodes[i]) + + i += 1 + + def syncTree(self, newroot): + """Syncronise the displayed tree with the given tree new.""" + + toreset = self.root.data != newroot.data + if toreset: + # header changed, so do reset + self.beginResetModel() + + self._syncbranch( qt4.QModelIndex(), self.root, newroot ) + if toreset: + self.root.data = newroot.data + self.endResetModel() diff -Nru veusz-1.10/utils/utilfuncs.py veusz-1.14/utils/utilfuncs.py --- veusz-1.10/utils/utilfuncs.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/utils/utilfuncs.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,18 +19,14 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: utilfuncs.py 1447 2010-11-11 20:35:25Z jeremysanders $ - import sys import string -import weakref import re import os.path import threading -import dates import codecs import csv -import math +import StringIO import veusz.qtall as qt4 import numpy as N @@ -42,13 +38,20 @@ """Get installed directory to find files relative to this one.""" if hasattr(sys, 'frozen'): - # for py2exe compatability - return os.path.dirname(os.path.abspath(sys.executable)) + # for pyinstaller/py2app compatability + dirname = os.path.dirname(os.path.abspath(sys.executable)) + if sys.platform == 'darwin': + # py2app + return os.path.join(dirname, '..', 'Resources', 'veusz') + else: + # pyinstaller + return dirname else: # standard installation return os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) veuszDirectory = _getVeuszDirectory() +exampleDirectory = os.path.join(veuszDirectory, 'examples') id_re = re.compile('^[A-Za-z_][A-Za-z0-9_]*$') def validPythonIdentifier(name): @@ -59,7 +62,7 @@ """Validate dataset name is okay. Dataset names can contain anything except back ticks! """ - return len(name) > 0 and name.find('`') == -1 + return len(name.strip()) > 0 and name.find('`') == -1 def validateWidgetName(name): """Validate widget name is okay. @@ -117,25 +120,14 @@ return '#%02x%02x%02x%02x' % (col.red(), col.green(), col.blue(), col.alpha()) -class WeakBoundMethod: - """A weak reference to a bound method. - - Based on code by Frederic Jolliton - See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81253 - """ - - def __init__(self, f): - self.f = f.im_func - self.c = weakref.ref(f.im_self) - - def isEqual(self, f): - """Is the bound method pointed to the same as this one?""" - return f.im_func == self.f and f.im_self == self.c - - def __call__(self , *arg): - if self.c() is None: - raise ValueError, 'Method called on dead object' - self.f(self.c(), *arg) +def pixmapAsHtml(pix): + """Get QPixmap as html image text.""" + ba = qt4.QByteArray() + buf = qt4.QBuffer(ba) + buf.open(qt4.QIODevice.WriteOnly) + pix.toImage().save(buf, "PNG") + b64 = str(buf.data().toBase64()) + return '' % b64 def BoundCaller(function, *params): """Wrap a function with its initial arguments.""" @@ -203,156 +195,6 @@ return out -_formaterror = 'FormatError' - -def formatSciNotation(num, formatargs=''): - """Format number into form X \times 10^{Y}. - This function trims trailing zeros and decimal point unless a formatting - argument is supplied - - This is similar to the %e format string - formatargs is the standard argument in a format string to control the - number of decimal places, etc.""" - - # create an initial formatting string - if formatargs: - format = '%' + formatargs + 'e' - else: - format = '%.10e' - - # try to format the number - # this may be user-supplied data, so don't crash hard by returning - # useless output - try: - text = format % num - except: - return _formaterror - - # split around the exponent - leader, exponent = text.split('e') - - # strip off trailing decimal point and zeros if no format args - if not formatargs: - leader = '%.10g' % float(leader) - - # trim off leading 1 - if leader == '1' and not formatargs: - leader = '' - else: - # the unicode string is a small space, multiply and small space - leader += u'\u00d7' - - return '%s10^{%i}' % (leader, int(exponent)) - -def formatGeneral(num, fmtarg): - """General formatting which switches from normal to scientic - notation.""" - - a = abs(num) - # manually choose when to switch from normal to scientific - # as the default isn't very good - if a >= 1e4 or (a < 1e-2 and a > 1e-110): - return formatSciNotation(num, fmtarg) - else: - if fmtarg: - f = '%' + fmtarg + 'g' - else: - f = '%.10g' - - try: - return f % num - except: - return _formaterror - -engsuffixes = ( 'y', 'z', 'a', 'f', 'p', 'n', - u'\u03bc', 'm', '', 'k', 'M', 'G', - 'T', 'P', 'E', 'Z', 'Y' ) - -def formatEngineering(num, fmtarg): - """Engineering suffix format notation using SI suffixes.""" - - if num != 0.: - logindex = math.log10( abs(num) ) / 3. - - # for numbers < 1 round down suffix - if logindex < 0. and (int(logindex)-logindex) > 1e-6: - logindex -= 1 - - # make sure we don't go out of bounds - logindex = min( max(logindex, -8), - len(engsuffixes) - 9 ) - - suffix = engsuffixes[ int(logindex) + 8 ] - val = num / 10**( int(logindex) *3) - else: - suffix = '' - val = num - - text = ('%' + fmtarg + 'g%s') % (val, suffix) - return text - -_formatRE = re.compile(r'%([^A-Za-z]*)(VDVS|VD.|V.|[A-Za-z])') - -def formatNumber(num, format): - """ Format a number in different ways. - - format is a standard C format string, with some additions: - %Ve scientific notation X \times 10^{Y} - %Vg switches from normal notation to scientific outside 10^-2 to 10^4 - %VE engineering suffix option - - %VDx date formatting, where x is one of the arguments in - http://docs.python.org/lib/module-time.html in the function - strftime - """ - - while True: - # repeatedly try to do string format - m = _formatRE.search(format) - if not m: - break - - # argument and type of formatting - farg, ftype = m.groups() - - # special veusz formatting - if ftype[:1] == 'V': - # special veusz formatting - if ftype == 'Ve': - out = formatSciNotation(num, farg) - elif ftype == 'Vg': - out = formatGeneral(num, farg) - elif ftype == 'VE': - out = formatEngineering(num, farg) - elif ftype[:2] == 'VD': - d = dates.floatToDateTime(num) - # date formatting (seconds since start of epoch) - if ftype[:4] == 'VDVS': - # special seconds operator - out = ('%'+ftype[4:]+'g') % (d.second+d.microsecond*1e-6) - else: - # use date formatting - try: - out = d.strftime(str('%'+ftype[2:])) - except ValueError: - out = _formaterror - else: - out = _formaterror - - # replace hyphen with true - and small space - out = out.replace('-', u'\u2212') - - else: - # standard C formatting - try: - out = ('%' + farg + ftype) % num - except: - out = _formaterror - - format = format[:m.start()] + out + format[m.end():] - - return format - def validLinePoints(x, y): """Take x and y points and split into sets of points which don't have invalid points. @@ -370,93 +212,6 @@ if last < x.shape[0]-1: yield x[last:], y[last:] -# This is Tim Peter's topological sort -# see http://www.python.org/tim_one/000332.html -# adapted to use later python features - -def topsort(pairlist): - """Given a list of pairs, perform a topological sort. - That means, each item has something which needs to be done first. - - topsort( [(1,2), (3,4), (5,6), (1,3), (1,5), (1,6), (2,5)] ) - returns [1, 2, 3, 5, 4, 6] - """ - - numpreds = {} # elt -> # of predecessors - successors = {} # elt -> list of successors - for first, second in pairlist: - # make sure every elt is a key in numpreds - if not numpreds.has_key( first ): - numpreds[first] = 0 - - if not numpreds.has_key( second ): - numpreds[second] = 0 - - # since first < second, second gains a pred ... - numpreds[second] += 1 - - # ... and first gains a succ - if successors.has_key( first ): - successors[first].append( second ) - else: - successors[first] = [second] - - # suck up everything without a predecessor - answer = [key for key, item in numpreds.iteritems() - if item == 0] - - # for everything in answer, knock down the pred count on - # its successors; note that answer grows *in* the loop - - for x in answer: - del numpreds[x] - if successors.has_key( x ): - for y in successors[x]: - numpreds[y] -= 1 - if numpreds[y] == 0: - answer.append( y ) - # following del; isn't needed; just makes - # CycleError details easier to grasp - # del successors[x] - - # assert catches cycle errors - assert not numpreds - - return answer - -class _NoneSoFar: - pass -_NoneSoFar = _NoneSoFar() - -def lazy(func, resultclass): - """A decorator to allow lazy evaluation of functions. - The products of this function is a lazy version of the function - given. - - func is the function to evaluate - resultclass is the class this function returns.""" - - class __proxy__: - def __init__(self, args, kw): - self.__func = func - self.__args = args - self.__kw = kw - self.__result = _NoneSoFar - for (k, v) in resultclass.__dict__.items(): - setattr(self, k, self.__promise__(v)) - - def __promise__(self, func): - def __wrapper__(*args, **kw): - if self.__result is _NoneSoFar: - self.__result = self.__func(*self.__args, **self.__kw) - return func(self.__result, *args, **kw) - return __wrapper__ - - def __wrapper__(*args, **kw): - return __proxy__(args, kw) - - return __wrapper__ - class NonBlockingReaderThread(threading.Thread): """A class to read blocking file objects and return the result. @@ -544,9 +299,16 @@ ] def openEncoding(filename, encoding, mode='r'): - """Convenience function for opening file with encoding given.""" - return codecs.open(filename, mode, encoding, 'ignore') + """Convenience function for opening file with encoding given. + If filename == '{clipboard}', then load the data from the clipboard + instead. + """ + if filename == '{clipboard}': + text = unicode(qt4.QApplication.clipboard().text()) + return StringIO.StringIO(text) + else: + return codecs.open(filename, mode, encoding, 'ignore') # The following two classes are adapted from the Python documentation # they are modified to turn off encoding errors @@ -571,8 +333,18 @@ which is encoded in the given encoding. """ - def __init__(self, f, dialect=csv.excel, encoding='utf-8', **kwds): - f = UTF8Recoder(f, encoding) + def __init__(self, filename, dialect=csv.excel, encoding='utf-8', **kwds): + + if filename != '{clipboard}': + # recode the opened file as utf-8 + f = UTF8Recoder(open(filename), encoding) + else: + # take the unicode clipboard and just put into utf-8 format + s = unicode(qt4.QApplication.clipboard().text()) + s = s.encode('utf-8') + f = StringIO.StringIO(s) + + # the actual csv reader based on the file above self.reader = csv.reader(f, dialect=dialect, **kwds) def next(self): @@ -613,3 +385,31 @@ # get index for current value index = combo.findText(currenttext) combo.setCurrentIndex(index) + +def positionFloatingPopup(popup, widget): + """Position a popped up window (popup) to side and below widget given.""" + pos = widget.parentWidget().mapToGlobal( widget.pos() ) + desktop = qt4.QApplication.desktop() + + # recalculates out position so that size is correct below + popup.adjustSize() + + # is there room to put this widget besides the widget? + if pos.y() + popup.height() + 1 < desktop.height(): + # put below + y = pos.y() + 1 + else: + # put above + y = pos.y() - popup.height() - 1 + + # is there room to the left for us? + if ( (pos.x() + widget.width() + popup.width() < desktop.width()) or + (pos.x() + widget.width() < desktop.width()/2) ): + # put left justified with widget + x = pos.x() + widget.width() + else: + # put extending to left + x = pos.x() - popup.width() - 1 + + popup.move(x, y) + popup.setFocus() diff -Nru veusz-1.10/utils/version.py veusz-1.14/utils/version.py --- veusz-1.10/utils/version.py 2010-12-12 12:41:09.000000000 +0000 +++ veusz-1.14/utils/version.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,8 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: version.py 872 2008-12-29 12:51:59Z jeremysanders $ - """ Return Veusz' version number """ diff -Nru veusz-1.10/VERSION veusz-1.14/VERSION --- veusz-1.10/VERSION 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/VERSION 2011-11-22 20:25:39.000000000 +0000 @@ -1 +1 @@ -1.10 +1.14 diff -Nru veusz-1.10/veusz_listen.py veusz-1.14/veusz_listen.py --- veusz-1.10/veusz_listen.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/veusz_listen.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: veusz_listen.py 1463 2010-12-01 17:06:14Z jeremysanders $ - """ Veusz interface which listens to stdin, and receives commands. Results are written to stdout diff -Nru veusz-1.10/veusz_main.py veusz-1.14/veusz_main.py --- veusz-1.10/veusz_main.py 2010-12-12 12:41:12.000000000 +0000 +++ veusz-1.14/veusz_main.py 2011-11-22 20:23:31.000000000 +0000 @@ -1,8 +1,5 @@ #!/usr/bin/env python -# veusz.py -# Main veusz program file - # Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # @@ -21,7 +18,8 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: veusz_main.py 1460 2010-11-29 21:32:08Z jeremysanders $ +"""Main Veusz executable. +""" import sys import os.path @@ -45,12 +43,12 @@ copyr='''Veusz %s -Copyright (C) Jeremy Sanders 2003-2010 +Copyright (C) Jeremy Sanders 2003-2011 and contributors Licenced under the GNU General Public Licence (version 2 or greater) ''' splashcopyr='''Veusz %s
    -Copyright (C) Jeremy Sanders 2003-2010
    +Copyright (C) Jeremy Sanders 2003-2011 and contributors
    Licenced under the GPL (version 2 or greater) ''' @@ -92,11 +90,6 @@ d = ExceptionDialog((excepttype, exceptvalue, tracebackobj), None) d.exec_() -def embedremote(): - '''For running with --remote-embed option.''' - from veusz.embed_remote import remote - remote.main() - def listen(args, quiet): '''For running with --listen option.''' from veusz.veusz_listen import openWindow @@ -122,9 +115,31 @@ # create blank window MainWindow.CreateWindow() +def convertArgsUnicode(args): + '''Convert set of arguments to unicode. + Arguments in argv use current file system encoding + ''' + enc = sys.getfilesystemencoding() + # bail out if not supported + if enc is None: + return args + out = [] + for a in args: + if isinstance(a, str): + out.append( a.decode(enc) ) + else: + out.append(a) + return out + def run(): '''Run the main application.''' + # jump to the embedding client entry point if required + if len(sys.argv) == 2 and sys.argv[1] == '--embed-remote': + from veusz.embed_remote import runremote + runremote() + return + # this function is spaghetti-like and has nasty code paths. # the idea is to postpone the imports until the splash screen # is shown @@ -155,12 +170,17 @@ ' output image file, exiting when finished') parser.add_option('--embed-remote', action='store_true', help=optparse.SUPPRESS_HELP) - + parser.add_option('--plugin', action='append', metavar='FILE', + help='load the plugin from the file given for ' + 'the session') options, args = parser.parse_args( app.argv() ) - # show splash in normal mode + # convert args to unicode from filesystem strings + args = convertArgsUnicode(args) + splash = None - if not options.embed_remote and not options.listen and not options.export: + if not (options.listen or options.export): + # show the splash screen on normal start splash = qt4.QSplashScreen(makeSplashLogo()) splash.show() app.processEvents() @@ -174,18 +194,24 @@ veusz.setting.transient_settings['unsafe_mode'] = bool( options.unsafe_mode) - # these are the different modes - if options.embed_remote: - embedremote() - elif options.listen: + # load any requested plugins + if options.plugin: + import veusz.document + veusz.document.Document.loadPlugins(pluginlist=options.plugin) + + # different modes + if options.listen: + # listen to incoming commands listen(args, quiet=options.quiet) elif options.export: + # export files to make images if len(options.export) != len(args)-1: parser.error( 'export option needs same number of documents and output files') export(options.export, args) return else: + # standard start main window mainwindow(args) # clear splash when startup done diff -Nru veusz-1.10/widgets/axis.py veusz-1.14/widgets/axis.py --- veusz-1.10/widgets/axis.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/axis.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: axis.py 1412 2010-09-25 08:42:01Z jeremysanders $ - """Widget to plot axes, and to handle conversion of coordinates to plot positions.""" @@ -109,9 +107,10 @@ descr = 'Place axis label close to edge' ' of graph', usertext='At edge') ) - self.add( setting.Bool( 'rotate', False, - descr = 'Rotate the label by 90 degrees', - usertext='Rotate') ) + self.add( setting.RotateInterval( + 'rotate', '0', + descr = 'Angle by which to rotate label by', + usertext='Rotate') ) self.add( setting.DistancePt( 'offset', '0pt', descr = 'Additional offset of axis label' @@ -133,9 +132,10 @@ def __init__(self, name, **args): setting.Text.__init__(self, name, **args) - self.add( setting.Bool( 'rotate', False, - descr = 'Rotate the label by 90 degrees', - usertext='Rotate') ) + self.add( setting.RotateInterval( + 'rotate', '0', + descr = 'Angle by which to rotate label by', + usertext='Rotate') ) self.add( setting.ChoiceOrMore( 'format', TickLabel.formatchoices, 'Auto', @@ -195,12 +195,12 @@ s.add( setting.Str('label', '', descr='Axis label text', usertext='Label') ) - s.add( setting.FloatOrAuto('min', 'Auto', - descr='Minimum value of axis', - usertext='Min') ) - s.add( setting.FloatOrAuto('max', 'Auto', - descr='Maximum value of axis', - usertext='Max') ) + s.add( setting.AxisBound('min', 'Auto', + descr='Minimum value of axis', + usertext='Min') ) + s.add( setting.AxisBound('max', 'Auto', + descr='Maximum value of axis', + usertext='Max') ) s.add( setting.Bool('log', False, descr = 'Whether axis is logarithmic', usertext='Log') ) @@ -324,7 +324,7 @@ # locate widget we're matching # this is ensured to be an Axis try: - widget = s.get('match').getWidget() + widget = s.get('match').getReferredWidget() except setting.InvalidType: widget = None @@ -359,9 +359,9 @@ # make sure log axes don't blow up if s.log: - if self.plottedrange[0] <= 0.: + if self.plottedrange[0] < 1e-99: self.plottedrange[0] = 1e-99 - if self.plottedrange[1] <= 0.: + if self.plottedrange[1] < 1e-99: self.plottedrange[1] = 1e-99 if self.plottedrange[0] == self.plottedrange[1]: self.plottedrange[1] = self.plottedrange[0]*2 @@ -378,9 +378,12 @@ extendzero = s.autoExtendZero, logaxis = s.log ) - (self.plottedrange[0],self.plottedrange[1], - self.majortickscalc, self.minortickscalc, - self.autoformat) = axs.getTicks() + axs.getTicks() + self.plottedrange[0] = axs.minval + self.plottedrange[1] = axs.maxval + self.majortickscalc = axs.tickvals + self.minortickscalc = axs.minorticks + self.autoformat = axs.autoformat # override values if requested if len(s.MajorTicks.manualTicks) > 0: @@ -426,8 +429,8 @@ # other axis coordinates self.coordPerp = y2 - dy*otherposition - self.coordPerp1 = y2 - dy*p1 - self.coordPerp2 = y2 - dy*p2 + self.coordPerp1 = y1 + self.coordPerp2 = y2 else: # vertical self.coordParr1 = y2 - dy*p1 @@ -435,8 +438,8 @@ # other axis coordinates self.coordPerp = x1 + dx*otherposition - self.coordPerp1 = x1 + dx*p1 - self.coordPerp2 = x1 + dx*p2 + self.coordPerp1 = x1 + self.coordPerp2 = x2 # is this axis reflected if otherposition > 0.5: @@ -575,9 +578,21 @@ a = (b1, a1, b2, a2) utils.plotLinesToPainter(painter, a[0], a[1], a[2], a[3]) - def _drawGridLines(self, subset, painter, coordticks): + def _drawGridLines(self, subset, painter, coordticks, parentposn): """Draw grid lines on the plot.""" painter.setPen( self.settings.get(subset).makeQPen(painter) ) + + # drop points which overlap with graph box (if used) + if self.parent.typename == 'graph': + if not self.parent.settings.Border.hide: + if self.settings.direction == 'horizontal': + ok = ( (N.abs(coordticks-parentposn[0]) > 1e-3) & + (N.abs(coordticks-parentposn[2]) > 1e-3) ) + else: + ok = ( (N.abs(coordticks-parentposn[1]) > 1e-3) & + (N.abs(coordticks-parentposn[3]) > 1e-3) ) + coordticks = coordticks[ok] + self.swaplines(painter, coordticks, coordticks*0.+self.coordPerp1, coordticks, coordticks*0.+self.coordPerp2) @@ -610,9 +625,9 @@ delta *= -1 y = coordminorticks*0.+self.coordPerp - self.swaplines(painter, - coordminorticks, y, - coordminorticks, y-delta) + self.swaplines( painter, + coordminorticks, y, + coordminorticks, y-delta ) def _drawMajorTicks(self, painter, tickcoords): """Draw major ticks on the plot.""" @@ -633,19 +648,19 @@ delta *= -1 y = tickcoords*0.+self.coordPerp - self.swaplines(painter, - tickcoords, y, - tickcoords, y-delta) + self.swaplines( painter, + tickcoords, y, + tickcoords, y-delta ) # account for ticks if they are in the direction of the label if s.outerticks and not self.coordReflected: self._delta_axis += abs(delta) - def generateLabelLabels(self, painter): + def generateLabelLabels(self, phelper): """Generate list of positions and labels from widgets using this axis.""" try: - plotters = painter.veusz_axis_plotter_map[self] + plotters = phelper.axisplottermap[self] except (AttributeError, KeyError): return @@ -663,7 +678,7 @@ if N.isfinite(coord) and (minval <= coord <= maxval): yield pcoord, lab - def _drawTickLabels(self, painter, coordticks, sign, outerbounds, + def _drawTickLabels(self, phelper, painter, coordticks, sign, outerbounds, texttorender): """Draw tick labels on the plot. @@ -679,13 +694,9 @@ tl_spacing = fm.leading() + fm.descent() # work out font alignment - if s.TickLabels.rotate: - if self.coordReflected: - angle = 90 - else: - angle = 270 - else: - angle = 0 + angle = int(s.TickLabels.rotate) + if not self.coordReflected and angle != 0: + angle = 360-angle if vertical: # limit tick labels to be directly below/besides axis @@ -717,7 +728,8 @@ # generate positions and labels for posn, tickval in izip(coordticks, self.majortickscalc): - text = utils.formatNumber(tickval*scale, format) + text = utils.formatNumber(tickval*scale, format, + locale=self.document.locale) yield posn, text # position of label perpendicular to axis @@ -725,7 +737,7 @@ # use generator function to get labels and positions if s.mode == 'labels': - ticklabels = self.generateLabelLabels(painter) + ticklabels = self.generateLabelLabels(phelper) else: ticklabels = generateTickLabels() @@ -795,15 +807,16 @@ if reflected: ax, ay = -ax, -ay - # angle of text - if ( (horz and not sl.rotate) or - (not horz and sl.rotate) ): - angle = 0 + # angle of text (logic is slightly complex) + angle = int(sl.rotate) + if horz: + if not reflected: + angle = 360-angle else: + angle = angle+270 if reflected: - angle = 90 - else: - angle = 270 + angle = 360-angle + angle = angle % 360 x = 0.5*(self.coordParr1 + self.coordParr2) y = self.coordPerp + sign*(self._delta_axis+al_spacing) @@ -912,10 +925,13 @@ return True return False - def draw(self, parentposn, painter, outerbounds=None): + def draw(self, parentposn, phelper, outerbounds=None, + useexistingpainter=None): """Plot the axis on the painter. - if suppresstext is True, then we don't number or label the axis + useexistingpainter is a hack so that a colorbar can reuse the + drawing code here. If set to a painter, it will use this rather + than opening a new one. """ s = self.settings @@ -924,17 +940,19 @@ if self.docchangeset != self.document.changeset: self._computePlottedRange() - posn = widget.Widget.draw(self, parentposn, painter, outerbounds) + posn = widget.Widget.draw(self, parentposn, phelper, outerbounds) self._updatePlotRange(posn) + # get ready to draw + if useexistingpainter is not None: + painter = useexistingpainter + else: + painter = phelper.painter(self, posn) + # make control item for axis - self.controlgraphitems = [ - controlgraph.ControlAxisLine(self, s.direction, - self.coordParr1, - self.coordParr2, - self.coordPerp, - posn) - ] + phelper.setControlGraph(self, [ controlgraph.ControlAxisLine( + self, s.direction, self.coordParr1, + self.coordParr2, self.coordPerp, posn) ]) # get tick vals coordticks = self._graphToPlotter(self.majortickscalc) @@ -944,10 +962,6 @@ if s.hide: return - # save the state of the painter for later - painter.beginPaintingWidget(self, posn) - painter.save() - texttorender = [] # multiplication factor if reflection on the axis is requested @@ -959,9 +973,11 @@ # plot gridlines if not s.MinorGridLines.hide: - self._drawGridLines('MinorGridLines', painter, coordminorticks) + self._drawGridLines('MinorGridLines', painter, coordminorticks, + parentposn) if not s.GridLines.hide: - self._drawGridLines('GridLines', painter, coordticks) + self._drawGridLines('GridLines', painter, coordticks, + parentposn) # plot the line along the axis if not s.Line.hide: @@ -978,19 +994,11 @@ if not s.MajorTicks.hide: self._drawMajorTicks(painter, coordticks) - # debugging - #painter.save() - #painter.setPen(qt4.QPen(qt4.Qt.blue)) - #painter.drawRect( - # qt4.QRectF(qt4.QPointF(outerbounds[0], outerbounds[1]), - # qt4.QPointF(outerbounds[2], outerbounds[3])) ) - #painter.restore() - # plot tick labels suppresstext = self._suppressText(painter, parentposn, outerbounds) if not s.TickLabels.hide and not suppresstext: - self._drawTickLabels(painter, coordticks, sign, outerbounds, - texttorender) + self._drawTickLabels(phelper, painter, coordticks, sign, + outerbounds, texttorender) # draw an axis label if not s.Label.hide and not suppresstext: @@ -1012,36 +1020,48 @@ painter.setPen(pen) box = r.render() drawntext.addRect(rect) - - # restore the state of the painter - painter.restore() - - painter.endPaintingWidget() - + def updateControlItem(self, cgi): """Update axis position from control item.""" s = self.settings p = cgi.maxposn - if s.direction == 'horizontal': - minfrac = abs((cgi.minpos - p[0]) / (p[2] - p[0])) - maxfrac = abs((cgi.maxpos - p[0]) / (p[2] - p[0])) - axisfrac = abs((cgi.axispos - p[3]) / (p[1] - p[3])) - else: - minfrac = abs((cgi.minpos - p[3]) / (p[1] - p[3])) - maxfrac = abs((cgi.maxpos - p[3]) / (p[1] - p[3])) - axisfrac = abs((cgi.axispos - p[0]) / (p[2] - p[0])) - - if minfrac > maxfrac: - minfrac, maxfrac = maxfrac, minfrac - - operations = ( - document.OperationSettingSet(s.get('lowerPosition'), minfrac), - document.OperationSettingSet(s.get('upperPosition'), maxfrac), - document.OperationSettingSet(s.get('otherPosition'), axisfrac), - ) - self.document.applyOperation( - document.OperationMultiple(operations, descr='adjust axis')) + + if cgi.zoomed(): + # zoom axis scale + c1, c2 = self.plotterToGraphCoords( + cgi.maxposn, N.array([cgi.minzoom, cgi.maxzoom])) + if c1 > c2: + c1, c2 = c2, c1 + operations = ( + document.OperationSettingSet(s.get('min'), float(c1)), + document.OperationSettingSet(s.get('max'), float(c2)), + document.OperationSettingSet(s.get('autoExtend'), False), + document.OperationSettingSet(s.get('autoExtendZero'), False), + ) + self.document.applyOperation( + document.OperationMultiple(operations, descr='zoom axis')) + elif cgi.moved(): + # move axis + # convert positions to fractions + pt1, pt2, ppt1, ppt2 = ( (3, 1, 0, 2), (0, 2, 3, 1) + ) [s.direction == 'horizontal'] + minfrac = abs((cgi.minpos - p[pt1]) / (p[pt2] - p[pt1])) + maxfrac = abs((cgi.maxpos - p[pt1]) / (p[pt2] - p[pt1])) + axisfrac = abs((cgi.axispos - p[ppt1]) / (p[ppt2] - p[ppt1])) + + # swap if wrong way around + if minfrac > maxfrac: + minfrac, maxfrac = maxfrac, minfrac + + # update doc + operations = ( + document.OperationSettingSet(s.get('lowerPosition'), minfrac), + document.OperationSettingSet(s.get('upperPosition'), maxfrac), + document.OperationSettingSet(s.get('otherPosition'), axisfrac), + ) + self.document.applyOperation( + document.OperationMultiple(operations, descr='adjust axis')) # allow the factory to instantiate an axis document.thefactory.register( Axis ) diff -Nru veusz-1.10/widgets/axisticks.py veusz-1.14/widgets/axisticks.py --- veusz-1.10/widgets/axisticks.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/axisticks.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,8 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: axisticks.py 1120 2010-01-16 22:34:31Z jeremysanders $ - import math import numpy as N @@ -41,7 +39,8 @@ def __init__( self, minval, maxval, numticks, numminorticks, logaxis = False, prefermore = True, - extendbounds = True, extendzero = True ): + extendbounds = True, extendzero = True, + forceinterval = None ): """Initialise the class. minval and maxval are the range of the data to be plotted @@ -49,7 +48,10 @@ logaxis: axis logarithmic? prefermore: prefer more ticks rather than fewer extendbounds: extend minval and maxval to nearest tick if okay - extendzero: extend one end to zero if it is okay""" + extendzero: extend one end to zero if it is okay + forceinterval: force interval to one given (if allowed). interval + is tuple as returned in self.interval after calling getTicks() + """ self.minval = minval self.maxval = maxval @@ -59,11 +61,14 @@ self.prefermore = prefermore self.extendbounds = extendbounds self.extendzero = extendzero + self.forceinterval = forceinterval def getTicks( self ): """Calculate and return the position of the major ticks. - Returns a tuple (minval, maxval, majorticks, minorticks)""" + Results are returned as attributes of this object in + interval, minval, maxval, tickvals, minorticks, autoformat + """ class AxisTicks(AxisTicksBase): """Class to work out at what values axis major ticks should appear.""" @@ -94,7 +99,7 @@ startmult = int( math.ceil( minval / delta ) ) stopmult = int( math.floor( maxval / delta ) ) - + return N.arange(startmult, stopmult+1) * delta def _tickNums(self, minval, maxval, delta): @@ -201,9 +206,40 @@ ticks.append(v) return N.array( ticks ) - - def _axisScaler(self, allowed_intervals): - """With minval and maxval find best tick positions.""" + + def _selectBestTickFromSelection(self, selection): + """Choose best tick from selection given.""" + # we now try to find the best matching value + minabsdelta = 1e99 + mindelta = 1e99 + bestsel = () + + # find the best set of tick labels + for s in selection: + # difference between what we want and what we have + delta = s[0] - self.numticks + absdelta = abs(delta) + + # if it matches better choose this + if absdelta < minabsdelta: + minabsdelta = absdelta + mindelta = delta + bestsel = s + + # if we find two closest matching label sets, we + # test whether we prefer too few to too many labels + if absdelta == minabsdelta: + if (self.prefermore and (delta > mindelta)) or \ + (not self.prefermore and (delta < mindelta)): + minabsdelta = absdelta + mindelta = delta + bestsel = s + + return bestsel + + def _getBestTickSelection(self, allowed_intervals): + """Go through allowed tick intervals and find one best matching + requested parameters.""" # work out range and log range therange = self.maxval - self.minval @@ -217,7 +253,9 @@ # Maybe a better algorithm is required selection = [] + # keep track of largest number of ticks calculated largestno = 0 + while True: for interval in allowed_intervals: no, minval, maxval = self._calcNoTicks( interval, logstep ) @@ -235,48 +273,32 @@ if logstep < 0 and self.logaxis: break - # we now try to find the best matching value - minabsdelta = 1e99 - mindelta = 1e99 - bestsel = () + return selection - # find the best set of tick labels - for s in selection: - # difference between what we want and what we have - delta = s[0] - self.numticks - absdelta = abs(delta) - - # if it matches better choose this - if absdelta < minabsdelta: - minabsdelta = absdelta - mindelta = delta - bestsel = s - - # if we find two closest matching label sets, we - # test whether we prefer too few to too many labels - if absdelta == minabsdelta: - if (self.prefermore and (delta > mindelta)) or \ - (not self.prefermore and (delta < mindelta)): - minabsdelta = absdelta - mindelta = delta - bestsel = s + def _tickSelector(self, allowed_intervals): + """With minval and maxval find best tick positions.""" - # now we have the best, we work out the ticks and return - interval = bestsel[1] - loginterval = bestsel[2] + if self.forceinterval is None: + # get selection of closely matching ticks + selection = self._getBestTickSelection(allowed_intervals) + + # now we have the best, we work out the ticks and return + bestsel = self._selectBestTickFromSelection(selection) + dummy, interval, loginterval, minval, maxval = bestsel + else: + # forced specific interval requested + interval, loginterval = self.forceinterval + no, minval, maxval = self._calcNoTicks(interval, loginterval) + # calculate the positions of the ticks from parameters tickdelta = interval * 10.**loginterval - minval = bestsel[3] - maxval = bestsel[4] - - # calculate the positions of the ticks ticks = self._calcTickValues( minval, maxval, tickdelta ) + return (minval, maxval, ticks, interval, loginterval) - def getTicks( self ): + def getTicks(self): """Calculate and return the position of the major ticks. - - Returns a tuple (minval, maxval, majorticks, minorticks)""" + """ if self.logaxis: # which intervals we'll accept for major ticks @@ -285,30 +307,31 @@ # transform range into log space self.minval = N.log10( self.minval ) self.maxval = N.log10( self.maxval ) - else: # which linear intervals we'll allow intervals = AxisTicks.allowed_intervals_linear - - minval, maxval, tickvals, interval, loginterval = self._axisScaler( intervals ) + + minval, maxval, tickvals, interval, loginterval = self._tickSelector( + intervals ) # work out the most appropriate minor tick intervals if not self.logaxis: # just plain minor ticks # try to achieve no of minors close to value requested - + minorticks = self._calcLinearMinorTickValues( minval, maxval, interval, loginterval, AxisTicks.allowed_minorintervals_linear[interval] ) else: + # log axis if interval == 1.: # calculate minor ticks # here we use 'conventional' minor log tick spacing # e.g. 0.9, 1, 2, .., 8, 9, 10, 20, 30 ... - minorticks = self._calcLogMinorTickValues(10.**minval, - 10.**maxval) + minorticks = self._calcLogMinorTickValues( + 10.**minval, 10.**maxval) # Here we test whether more log major tick values are needed... # often we might only have one tick value, and so we add 2, then 5 @@ -323,25 +346,30 @@ n = low10 + math.log10(i) if n >= minval and n <= maxval: tickvals = N.concatenate( (tickvals, N.array([n]) )) - + else: # if we increase by more than one power of 10 on the # axis, we can't do the above, so we do linear ticks # in log space # aim is to choose powers of 3 for majors and minors # to make it easy to read the axis. comments? - - minorticks = self._calcLinearMinorTickValues\ - (minval, maxval, interval, loginterval, - AxisTicks.allowed_minorintervals_log) + + minorticks = self._calcLinearMinorTickValues( + minval, maxval, interval, loginterval, + AxisTicks.allowed_minorintervals_log) minorticks = 10.**minorticks - + # transform normal ticks back to real space minval = 10.**minval maxval = 10.**maxval tickvals = 10.**tickvals - - return (minval, maxval, tickvals, minorticks, '%Vg') + + self.interval = (interval, loginterval) + self.minorticks = minorticks + self.minval = minval + self.maxval = maxval + self.tickvals = tickvals + self.autoformat = '%Vg' class DateTicks(AxisTicksBase): """For formatting dates. We want something that chooses appropriate @@ -349,58 +377,58 @@ So we want to choose most apropriate interval depending on number of ticks requested """ - + # possible intervals for a time/date axis # tuples of ((y, m, d, h, m, s, msec), autoformat) intervals = ( - ((200, 0, 0, 0, 0, 0, 0), '%VDY'), - ((100, 0, 0, 0, 0, 0, 0), '%VDY'), - ((50, 0, 0, 0, 0, 0, 0), '%VDY'), + ((200, 0, 0, 0, 0, 0, 0), '%VDY'), + ((100, 0, 0, 0, 0, 0, 0), '%VDY'), + ((50, 0, 0, 0, 0, 0, 0), '%VDY'), ((20, 0, 0, 0, 0, 0, 0), '%VDY'), - ((10, 0, 0, 0, 0, 0, 0), '%VDY'), - ((5, 0, 0, 0, 0, 0, 0), '%VDY'), - ((2, 0, 0, 0, 0, 0, 0), '%VDY'), - ((1, 0, 0, 0, 0, 0, 0), '%VDY'), - ((0, 6, 0, 0, 0, 0, 0), '%VDY-%VDm'), + ((10, 0, 0, 0, 0, 0, 0), '%VDY'), + ((5, 0, 0, 0, 0, 0, 0), '%VDY'), + ((2, 0, 0, 0, 0, 0, 0), '%VDY'), + ((1, 0, 0, 0, 0, 0, 0), '%VDY'), + ((0, 6, 0, 0, 0, 0, 0), '%VDY-%VDm'), ((0, 4, 0, 0, 0, 0, 0), '%VDY-%VDm'), - ((0, 3, 0, 0, 0, 0, 0), '%VDY-%VDm'), + ((0, 3, 0, 0, 0, 0, 0), '%VDY-%VDm'), ((0, 2, 0, 0, 0, 0, 0), '%VDY-%VDm'), - ((0, 1, 0, 0, 0, 0, 0), '%VDY-%VDm'), - ((0, 0, 28, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), - ((0, 0, 14, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), - ((0, 0, 7, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), - ((0, 0, 2, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), - ((0, 0, 1, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), - ((0, 0, 0, 12, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), - ((0, 0, 0, 6, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), - ((0, 0, 0, 4, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), - ((0, 0, 0, 3, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), - ((0, 0, 0, 2, 0, 0, 0), '%VDH:%VDM'), - ((0, 0, 0, 1, 0, 0, 0), '%VDH:%VDM'), - ((0, 0, 0, 0, 30, 0, 0), '%VDH:%VDM'), - ((0, 0, 0, 0, 15, 0, 0), '%VDH:%VDM'), - ((0, 0, 0, 0, 10, 0, 0), '%VDH:%VDM'), - ((0, 0, 0, 0, 5, 0, 0), '%VDH:%VDM'), - ((0, 0, 0, 0, 2, 0, 0), '%VDH:%VDM'), + ((0, 1, 0, 0, 0, 0, 0), '%VDY-%VDm'), + ((0, 0, 28, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), + ((0, 0, 14, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), + ((0, 0, 7, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), + ((0, 0, 2, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), + ((0, 0, 1, 0, 0, 0, 0), '%VDY-%VDm-%VDd'), + ((0, 0, 0, 12, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), + ((0, 0, 0, 6, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), + ((0, 0, 0, 4, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), + ((0, 0, 0, 3, 0, 0, 0), '%VDY-%VDm-%VDd\\\\%VDH:%VDM'), + ((0, 0, 0, 2, 0, 0, 0), '%VDH:%VDM'), + ((0, 0, 0, 1, 0, 0, 0), '%VDH:%VDM'), + ((0, 0, 0, 0, 30, 0, 0), '%VDH:%VDM'), + ((0, 0, 0, 0, 15, 0, 0), '%VDH:%VDM'), + ((0, 0, 0, 0, 10, 0, 0), '%VDH:%VDM'), + ((0, 0, 0, 0, 5, 0, 0), '%VDH:%VDM'), + ((0, 0, 0, 0, 2, 0, 0), '%VDH:%VDM'), ((0, 0, 0, 0, 1, 0, 0), '%VDH:%VDM'), - ((0, 0, 0, 0, 0, 30, 0), '%VDH:%VDM:%VDS'), - ((0, 0, 0, 0, 0, 15, 0), '%VDH:%VDM:%VDS'), - ((0, 0, 0, 0, 0, 10, 0), '%VDH:%VDM:%VDS'), - ((0, 0, 0, 0, 0, 5, 0), '%VDH:%VDM:%VDS'), - ((0, 0, 0, 0, 0, 2, 0), '%VDH:%VDM:%VDS'), - ((0, 0, 0, 0, 0, 1, 0), '%VDH:%VDM:%VDS'), - ((0, 0, 0, 0, 0, 0, 500000), '%VDH:%VDM:%VDVS'), - ((0, 0, 0, 0, 0, 0, 200000), '%VDVS'), - ((0, 0, 0, 0, 0, 0, 100000), '%VDVS'), - ((0, 0, 0, 0, 0, 0, 50000), '%VDVS'), - ((0, 0, 0, 0, 0, 0, 10000), '%VDVS'), + ((0, 0, 0, 0, 0, 30, 0), '%VDH:%VDM:%VDS'), + ((0, 0, 0, 0, 0, 15, 0), '%VDH:%VDM:%VDS'), + ((0, 0, 0, 0, 0, 10, 0), '%VDH:%VDM:%VDS'), + ((0, 0, 0, 0, 0, 5, 0), '%VDH:%VDM:%VDS'), + ((0, 0, 0, 0, 0, 2, 0), '%VDH:%VDM:%VDS'), + ((0, 0, 0, 0, 0, 1, 0), '%VDH:%VDM:%VDS'), + ((0, 0, 0, 0, 0, 0, 500000), '%VDH:%VDM:%VDVS'), + ((0, 0, 0, 0, 0, 0, 200000), '%VDVS'), + ((0, 0, 0, 0, 0, 0, 100000), '%VDVS'), + ((0, 0, 0, 0, 0, 0, 50000), '%VDVS'), + ((0, 0, 0, 0, 0, 0, 10000), '%VDVS'), ) - + intervals_sec = N.array([(ms*1e-6+s+mi*60+hr*60*60+dy*24*60*60+ mn*(365/12.)*24*60*60+ yr*365*24*60*60) for (yr, mn, dy, hr, mi, s, ms), fmt in intervals]) - + def bestTickFinder(self, minval, maxval, numticks, extendbounds, intervals, intervals_sec): """Try to find best choice of numticks ticks between minval and maxval @@ -416,10 +444,10 @@ tick1 = max(estimated.searchsorted(numticks)-1, 0) tick2 = min(tick1+1, len(estimated)-1) - + del1 = abs(estimated[tick1] - numticks) del2 = abs(estimated[tick2] - numticks) - + if del1 < del2: best = tick1 else: @@ -432,7 +460,7 @@ # round min and max to nearest minround = utils.tupleToDateTime(utils.roundDownToTimeTuple(mindate, besttt)) maxround = utils.tupleToDateTime(utils.roundDownToTimeTuple(maxdate, besttt)) - + if minround == mindate: mintick = minround else: @@ -450,7 +478,7 @@ if extendbounds and (deltamax != 0. and deltamax < delta*0.15): maxdate = utils.addTimeTupleToDateTime(maxtick, besttt) maxtick = maxdate - + # make ticks ticks = [] dt = mintick @@ -458,11 +486,11 @@ ticks.append( utils.datetimeToFloat(dt)) dt = utils.addTimeTupleToDateTime(dt, besttt) - return ( utils.datetimeToFloat(mindate), + return ( utils.datetimeToFloat(mindate), utils.datetimeToFloat(maxdate), - intervals_sec[best], + intervals_sec[best], N.array(ticks), format ) - + def filterIntervals(self, estint): """Filter intervals and intervals_sec to be multiples of estint seconds.""" @@ -477,20 +505,23 @@ def getTicks(self): """Calculate and return the position of the major ticks. - - Returns a tuple (minval, maxval, majorticks, minorticks, format)""" + """ # find minor ticks - mindate, maxdate, est, ticks, format = self.bestTickFinder( - self.minval, self.maxval, self.numticks, self.extendbounds, + mindate, maxdate, est, ticks, format = self.bestTickFinder( + self.minval, self.maxval, self.numticks, self.extendbounds, self.intervals, self.intervals_sec) # try to make minor ticks divide evenly into major ticks intervals, intervals_sec = self.filterIntervals(est) # get minor ticks ig, ig, ig, minorticks, ig = self.bestTickFinder( - mindate, maxdate, self.numminorticks, False, + mindate, maxdate, self.numminorticks, False, intervals, intervals_sec) - return (mindate, maxdate, ticks, minorticks, format) - + self.interval = (intervals, intervals_sec) + self.minval = mindate + self.maxval = maxdate + self.minorticks = minorticks + self.tickvals = ticks + self.autoformat = format diff -Nru veusz-1.10/widgets/bar.py veusz-1.14/widgets/bar.py --- veusz-1.10/widgets/bar.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/bar.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: bar.py 1449 2010-11-22 09:26:58Z jeremysanders $ - """For plotting bar graphs.""" from itertools import izip, repeat @@ -156,11 +154,11 @@ positions = s.get('posn').getData(doc) if positions is None: lengths = s.get('lengths').getData(doc) - if lengths is None: + if not lengths: return (None, None) p = N.arange( max([len(d.data) for d in lengths]) )+1. else: - p = positions + p = positions.data return (labels, p) @@ -427,10 +425,10 @@ qt4.QPointF(x+width, y+height*0.8)) ) - def draw(self, parentposn, painter, outerbounds=None): + def draw(self, parentposn, phelper, outerbounds=None): """Plot the data on a plotter.""" - widgetposn = GenericPlotter.draw(self, parentposn, painter, + widgetposn = GenericPlotter.draw(self, parentposn, phelper, outerbounds=outerbounds) s = self.settings @@ -477,17 +475,13 @@ dsvals.append(vals) # clip data within bounds of plotter - painter.beginPaintingWidget(self, widgetposn) - painter.save() - clip = self.clipAxesBounds(painter, axes, widgetposn) + clip = self.clipAxesBounds(axes, widgetposn) + painter = phelper.painter(self, widgetposn, clip=clip) # actually do the drawing fn = {'stacked': self.barDrawStacked, 'grouped': self.barDrawGroup}[s.mode] fn(painter, barposns, maxwidth, dsvals, axes, widgetposn, clip) - painter.restore() - painter.endPaintingWidget() - # allow the factory to instantiate a bar plotter document.thefactory.register( BarPlotter ) diff -Nru veusz-1.10/widgets/boxplot.py veusz-1.14/widgets/boxplot.py --- veusz-1.10/widgets/boxplot.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/boxplot.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: boxplot.py 1470 2010-12-09 09:39:12Z jeremysanders $ - """For making box plots.""" import math @@ -41,7 +39,8 @@ # interpolate between indices frac, index = math.modf(index) index = int(index) - interpol = (1-frac)*sortedds[index] + frac*sortedds[index+1] + indexplus1 = min(index+1, sortedds.shape[0]-1) + interpol = (1-frac)*sortedds[index] + frac*sortedds[indexplus1] return interpol def swapline(painter, x1, y1, x2, y2, swap): @@ -374,18 +373,18 @@ markersize, clip=clip ) # draw mean - meanplt = axes[not horz].dataToPlotterCoords(posn, - N.array([stats.mean]))[0] + meanplt = axes[not horz].dataToPlotterCoords( + posn, N.array([stats.mean]))[0] if horz: x, y = meanplt, boxposn else: x, y = boxposn, meanplt utils.plotMarker( painter, x, y, s.meanmarker, markersize ) - def draw(self, parentposn, painter, outerbounds=None): + def draw(self, parentposn, phelper, outerbounds=None): """Plot the data on a plotter.""" - widgetposn = GenericPlotter.draw(self, parentposn, painter, + widgetposn = GenericPlotter.draw(self, parentposn, phelper, outerbounds=outerbounds) s = self.settings @@ -418,10 +417,8 @@ axes[1].settings.direction != 'vertical' ): return - # clip data within bounds of plotter - painter.beginPaintingWidget(self, widgetposn) - painter.save() - clip = self.clipAxesBounds(painter, axes, widgetposn) + clip = self.clipAxesBounds(axes, widgetposn) + painter = phelper.painter(self, widgetposn, clip=clip) # get boxes visible along direction of boxes to work out width horz = (s.direction == 'horizontal') @@ -468,8 +465,5 @@ self.plotBox(painter, axes, vals[6][i], widgetposn, width, clip, stats) - painter.restore() - painter.endPaintingWidget() - # allow the factory to instantiate a boxplot document.thefactory.register( BoxPlot ) diff -Nru veusz-1.10/widgets/colorbar.py veusz-1.14/widgets/colorbar.py --- veusz-1.10/widgets/colorbar.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/colorbar.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: colorbar.py 1401 2010-09-11 09:28:00Z jeremysanders $ - """A colorbar widget for the image widget. Should show the scale of the image.""" @@ -56,9 +54,10 @@ """Construct list of settings.""" axis.Axis.addSettings(s) - s.add( setting.Image('image', '', - descr = 'Corresponding image', - usertext = 'Image'), 0 ) + s.add( setting.WidgetChoice('widgetName', '', + descr='Corresponding widget', + widgettypes=('image', 'xy'), + usertext = 'Widget'), 0 ) s.get('log').readonly = True s.get('datascale').readonly = True @@ -97,13 +96,15 @@ usertext='Border'), pixmap='settings_border') + s.add( setting.SettingBackwardCompat('image', 'widgetName', None) ) + def chooseName(self): """Get name of widget.""" # override axis naming of x and y return widget.Widget.chooseName(self) - def draw(self, parentposn, painter, outerbounds = None): + def draw(self, parentposn, phelper, outerbounds = None): '''Update the margins before drawing.''' s = self.settings @@ -113,11 +114,13 @@ return # get height of label font - font = s.get('Label').makeQFont(painter) + bounds = self.computeBounds(parentposn, phelper) + painter = phelper.painter(self, parentposn) + + font = s.get('Label').makeQFont(phelper) painter.setFont(font) fontheight = utils.FontMetrics(font, painter.device()).height() - bounds = self.computeBounds(parentposn, painter) horz = s.direction == 'horizontal' # use above to estimate width and height if necessary @@ -171,30 +174,41 @@ bounds[1] += (bounds[3]-bounds[1])*s.vertManual bounds[3] = bounds[1] + totalheight + # this is ugly - update bounds in helper state + phelper.states[self].bounds = bounds + # do no painting if hidden or no image - imgwidget = s.get('image').findImage() - if s.hide or not imgwidget: + imgwidget = s.get('widgetName').findWidget() + if s.hide: return bounds # update image if necessary with new settings - (minval, maxval, - axisscale, img) = imgwidget.makeColorbarImage(s.direction) + if imgwidget is not None: + # could find widget + (minval, maxval, + axisscale, img) = imgwidget.makeColorbarImage(s.direction) + else: + # couldn't find widget + minval, maxval, axisscale = 0., 1., 'linear' + img = None + self.setAutoRange([minval, maxval]) s.get('log').setSilent(axisscale == 'log') - painter.beginPaintingWidget(self, bounds) - # now draw image on axis... - minpix, maxpix = self.graphToPlotterCoords( bounds, - N.array([minval, maxval]) ) + minpix, maxpix = self.graphToPlotterCoords( + bounds, N.array([minval, maxval]) ) if s.direction == 'horizontal': c = [ minpix, bounds[1], maxpix, bounds[3] ] else: c = [ bounds[0], maxpix, bounds[2], minpix ] r = qt4.QRectF(c[0], c[1], c[2]-c[0], c[3]-c[1]) - painter.drawImage(r, img) + + # really draw the img + if img is not None: + painter.drawImage(r, img) # if there's a border if not s.Border.hide: @@ -209,10 +223,10 @@ # will mess up range if called twice savedposition = self.position self.position = (0., 0., 1., 1.) - axis.Axis.draw(self, bounds, painter, outerbounds=outerbounds) - self.position = savedposition - painter.endPaintingWidget() - + axis.Axis.draw(self, bounds, phelper, outerbounds=outerbounds, + useexistingpainter=painter) + self.position = savedposition + # allow the factory to instantiate a colorbar document.thefactory.register( ColorBar ) diff -Nru veusz-1.10/widgets/contour.py veusz-1.14/widgets/contour.py --- veusz-1.10/widgets/contour.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/contour.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: contour.py 1401 2010-09-11 09:28:00Z jeremysanders $ - """Contour plotting from 2d datasets. Contour plotting requires that the veusz_helpers package is installed, @@ -238,15 +236,15 @@ s = self.settings d = self.document - if s.data not in d.data: - # this dataset doesn't exist - minval = 0. - maxval = 1. - else: + minval, maxval = 0., 1. + if s.data in d.data: # scan data - data = d.data[s.data] - minval = data.data.min() - maxval = data.data.max() + data = d.data[s.data].data + minval, maxval = N.nanmin(data), N.nanmax(data) + if not N.isfinite(minval): + minval = 0. + if not N.isfinite(maxval): + maxval = 1. # override if not auto if s.min != 'Auto': @@ -293,7 +291,7 @@ """Calculate sublevels between contours.""" s = self.settings num = s.SubLines.numLevels - if s.SubLines.hide or len(s.SubLines.lines) == 0: + if s.SubLines.hide or len(s.SubLines.lines) == 0 or len(levels) <= 1: return N.array([]) # indices where contour levels should be placed @@ -364,7 +362,8 @@ if s.keyLevels: cl = s.get('ContourLabels') return utils.formatNumber( s.levelsOut[number] * cl.scale, - cl.format ) + cl.format, + locale=self.document.locale ) else: return '' @@ -384,7 +383,7 @@ # return if no data or if the dataset isn't two dimensional data = d.data.get(s.data, None) - if data is None or data.dimensions != 2: + if data is None or data.dimensions != 2 or data.data.size == 0: self.contsettings = self.lastdataset = None s.levelsOut = [] return False @@ -402,10 +401,10 @@ return True - def draw(self, parentposn, painter, outerbounds = None): + def draw(self, parentposn, phelper, outerbounds = None): """Draw the contours.""" - posn = plotters.GenericPlotter.draw(self, parentposn, painter, + posn = plotters.GenericPlotter.draw(self, parentposn, phelper, outerbounds = outerbounds) s = self.settings @@ -427,17 +426,13 @@ return # plot the precalculated contours - painter.beginPaintingWidget(self, posn) - painter.save() - clip = self.clipAxesBounds(painter, axes, posn) + clip = self.clipAxesBounds(axes, posn) + painter = phelper.painter(self, posn, clip=clip) self.plotContourFills(painter, posn, axes, clip) self.plotContours(painter, posn, axes, clip) self.plotSubContours(painter, posn, axes, clip) - painter.restore() - painter.endPaintingWidget() - def updateContours(self): """Update calculated contours.""" @@ -452,6 +447,9 @@ rangex, rangey = data.getDataRanges() yw, xw = data.data.shape + if xw == 0 or yw == 0: + return + # arrays containing coordinates of pixels in x and y xpts = N.fromfunction(lambda y,x: (x+0.5)*((rangex[1]-rangex[0])/xw) + rangex[0], @@ -460,13 +458,16 @@ (y+0.5)*((rangey[1]-rangey[0])/yw) + rangey[0], (yw, xw)) + # only keep finite data points + mask = N.logical_not(N.isfinite(data.data)) + # iterate over the levels and trace the contours self._cachedcontours = None self._cachedpolygons = None self._cachedsubcontours = None if self.Cntr is not None: - c = self.Cntr(xpts, ypts, data.data) + c = self.Cntr(xpts, ypts, data.data, mask) # trace the contour levels if len(s.Lines.lines) != 0: @@ -490,13 +491,17 @@ self._cachedsubcontours.append( finitePoly(linelist) ) def plotContourLabel(self, painter, number, xplt, yplt, showline): + """Draw a label on a contour. + This clips when drawing the line, plotting the label on top. + """ s = self.settings cl = s.get('ContourLabels') painter.save() # get text and font - text = utils.formatNumber(number * cl.scale, cl.format) + text = utils.formatNumber(number * cl.scale, cl.format, + locale=self.document.locale) font = cl.makeQFont(painter) descent = utils.FontMetrics(font, painter.device()).descent() @@ -535,8 +540,8 @@ painter.restore() - def _plotContours(self, painter, posn, axes, linestyles, contours, - showlabels, hidelines, clip): + def _plotContours(self, painter, posn, axes, linestyles, + contours, showlabels, hidelines, clip): """Plot a set of contours. """ @@ -562,8 +567,8 @@ utils.addNumpyToPolygonF(pts, xplt, yplt) if showlabels: - self.plotContourLabel(painter, s.levelsOut[num], xplt, yplt, - not hidelines) + self.plotContourLabel(painter, s.levelsOut[num], + xplt, yplt, not hidelines) else: # actually draw the curve to the plotter if not hidelines: diff -Nru veusz-1.10/widgets/controlgraph.py veusz-1.14/widgets/controlgraph.py --- veusz-1.10/widgets/controlgraph.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/controlgraph.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: controlgraph.py 1411 2010-09-25 08:38:27Z jeremysanders $ - """ Classes for moving widgets around @@ -87,14 +85,14 @@ ############################################################################## class ControlMarginBox(object): - def __init__(self, widget, posn, maxposn, painter, + def __init__(self, widget, posn, maxposn, painthelper, ismovable = True, isresizable = True): """Create control box item. widget: widget this is controllng posn: coordinates of box [x1, y1, x2, y2] maxposn: coordinates of biggest possibe box - painter: painter to get scaling from + painthelper: painterhelper to get scaling from ismovable: box can be moved isresizable: box can be resized """ @@ -107,9 +105,9 @@ self.isresizable = isresizable # we need these later to convert back to original units - self.page_size = painter.veusz_page_size - self.scaling = painter.veusz_scaling - self.pixperpt = painter.veusz_pixperpt + self.pagesize = painthelper.pagesize + self.scaling = painthelper.scaling + self.dpi = painthelper.dpi def createGraphicsItem(self): return _GraphMarginBox(self) @@ -118,8 +116,7 @@ """A helpful routine for setting widget margins after moving or resizing. - This is called by the widget after receiving - updateControlItem + This is called by the widget after receiving updateControlItem """ s = self.widget.settings @@ -129,17 +126,15 @@ top = self.posn[1] - self.maxposn[1] bottom = self.maxposn[3] - self.posn[3] - # set up fake painter containing veusz scalings - fakepainter = qt4.QPainter() - fakepainter.veusz_page_size = self.page_size - fakepainter.veusz_scaling = self.scaling - fakepainter.veusz_pixperpt = self.pixperpt + # set up fake painthelper containing veusz scalings + helper = document.PaintHelper(self.pagesize, scaling=self.scaling, + dpi=self.dpi) # convert to physical units - left = s.get('leftMargin').convertInverse(left, fakepainter) - right = s.get('rightMargin').convertInverse(right, fakepainter) - top = s.get('topMargin').convertInverse(top, fakepainter) - bottom = s.get('bottomMargin').convertInverse(bottom, fakepainter) + left = s.get('leftMargin').convertInverse(left, helper) + right = s.get('rightMargin').convertInverse(right, helper) + top = s.get('topMargin').convertInverse(top, helper) + bottom = s.get('bottomMargin').convertInverse(bottom, helper) # modify widget margins operations = ( @@ -151,6 +146,33 @@ self.widget.document.applyOperation( document.OperationMultiple(operations, descr='resize margins')) + def setPageSize(self): + """Helper for setting document/page widget size. + + This is called by the widget after receiving updateControlItem + """ + s = self.widget.settings + + # get margins in pixels + width = self.posn[2] - self.posn[0] + height = self.posn[3] - self.posn[1] + + # set up fake painter containing veusz scalings + helper = document.PaintHelper(self.pagesize, scaling=self.scaling, + dpi=self.dpi) + + # convert to physical units + width = s.get('width').convertInverse(width, helper) + height = s.get('height').convertInverse(height, helper) + + # modify widget margins + operations = ( + document.OperationSettingSet(s.get('width'), width), + document.OperationSettingSet(s.get('height'), height), + ) + self.widget.document.applyOperation( + document.OperationMultiple(operations, descr='change page size')) + class _GraphMarginBox(qt4.QGraphicsItem): """A box which can be moved or resized. @@ -410,10 +432,10 @@ the real position of the widget is """ - def __init__(self, widget, posn, painter, crosspos=None): + def __init__(self, widget, posn, painthelper, crosspos=None): ControlMarginBox.__init__(self, widget, posn, [-10000, -10000, 10000, 10000], - painter, isresizable=False) + painthelper, isresizable=False) self.deltacrosspos = (crosspos[0] - self.posn[0], crosspos[1] - self.posn[1]) @@ -528,11 +550,22 @@ maxposn): self.widget = widget self.direction = direction - self.minpos = minpos - self.maxpos = maxpos - self.axispos = axispos + if minpos > maxpos: + minpos, maxpos = maxpos, minpos + self.minpos = self.minzoom = self.minorig = minpos + self.maxpos = self.maxzoom = self.maxorig = maxpos + self.axisorigpos = self.axispos = axispos self.maxposn = maxposn + def zoomed(self): + """Is this a zoom?""" + return self.minzoom != self.minorig or self.maxzoom != self.maxorig + + def moved(self): + """Has axis moved?""" + return ( self.minpos != self.minorig or self.maxpos != self.maxorig or + self.axisorigpos != self.axispos ) + def createGraphicsItem(self): return _GraphAxisLine(self) @@ -540,21 +573,27 @@ curs = {True: qt4.Qt.SizeVerCursor, False: qt4.Qt.SizeHorCursor} + curs_zoom = {True: qt4.Qt.SplitVCursor, + False: qt4.Qt.SplitHCursor} def __init__(self, params): """Line is about to be shown.""" qt4.QGraphicsItem.__init__(self) self.params = params - self.pts = [ _ShapeCorner(self), - _ShapeCorner(self) ] + self.pts = [ _ShapeCorner(self), _ShapeCorner(self), + _ShapeCorner(self), _ShapeCorner(self) ] self.line = _AxisGraphicsLineItem(self) - # set correct coordinates + # set cursors and tooltips for items self.horz = (params.direction == 'horizontal') - endcurs = self.curs[not self.horz] - self.pts[0].setCursor(endcurs) - self.pts[1].setCursor(endcurs) + for p in self.pts[0:2]: + p.setCursor(self.curs[not self.horz]) + p.setToolTip("Move axis ends") + for p in self.pts[2:]: + p.setCursor(self.curs_zoom[not self.horz]) + p.setToolTip("Change axis scale") self.line.setCursor( self.curs[self.horz] ) + self.line.setToolTip("Move axis position") self.setZValue(2.) self.updatePos() @@ -563,47 +602,58 @@ """Set ends of line and line positions from stored values.""" par = self.params mxp = par.maxposn + + def _clip(*args): + """Clip positions to bounds of box given coords.""" + par.minpos = max(par.minpos, mxp[args[0]]) + par.maxpos = min(par.maxpos, mxp[args[1]]) + par.axispos = max(par.axispos, mxp[args[2]]) + par.axispos = min(par.axispos, mxp[args[3]]) + if self.horz: - # clip to box bounds - par.minpos = max(par.minpos, mxp[0]) - par.maxpos = min(par.maxpos, mxp[2]) - par.axispos = max(par.axispos, mxp[1]) - par.axispos = min(par.axispos, mxp[3]) + _clip(0, 2, 1, 3) # set positions - self.line.setPos(par.minpos, par.axispos) - self.line.setLine(0, 0, par.maxpos-par.minpos, 0) + if par.zoomed(): + self.line.setPos(par.minzoom, par.axispos) + self.line.setLine(0, 0, par.maxzoom-par.minzoom, 0) + else: + self.line.setPos(par.minpos, par.axispos) + self.line.setLine(0, 0, par.maxpos-par.minpos, 0) self.pts[0].setPos(par.minpos, par.axispos) self.pts[1].setPos(par.maxpos, par.axispos) + self.pts[2].setPos(par.minzoom, par.axispos-15) + self.pts[3].setPos(par.maxzoom, par.axispos-15) else: - # clip to box bounds - par.minpos = max(par.minpos, mxp[1]) - par.maxpos = min(par.maxpos, mxp[3]) - par.axispos = max(par.axispos, mxp[0]) - par.axispos = min(par.axispos, mxp[2]) + _clip(1, 3, 0, 2) # set positions - self.line.setPos(par.axispos, par.minpos) - self.line.setLine(0, 0, 0, par.maxpos-par.minpos) + if par.zoomed(): + self.line.setPos(par.axispos, par.minzoom) + self.line.setLine(0, 0, 0, par.maxzoom-par.minzoom) + else: + self.line.setPos(par.axispos, par.minpos) + self.line.setLine(0, 0, 0, par.maxpos-par.minpos) self.pts[0].setPos(par.axispos, par.minpos) self.pts[1].setPos(par.axispos, par.maxpos) + self.pts[2].setPos(par.axispos+15, par.minzoom) + self.pts[3].setPos(par.axispos+15, par.maxzoom) def updateFromCorner(self, corner, event): """Ends of axis have moved, so update values.""" par = self.params + pt = (corner.y(), corner.x())[self.horz] # which end has moved? if corner is self.pts[0]: # horizonal or vertical axis? - if self.horz: - par.minpos = corner.x() - else: - par.minpos = corner.y() - else: - if self.horz: - par.maxpos = corner.x() - else: - par.maxpos = corner.y() + par.minpos = pt + elif corner is self.pts[1]: + par.maxpos = pt + elif corner is self.pts[2]: + par.minzoom = pt + elif corner is self.pts[3]: + par.maxzoom = pt # swap round end points if min > max if par.minpos > par.maxpos: diff -Nru veusz-1.10/widgets/data/colormaps.dat veusz-1.14/widgets/data/colormaps.dat --- veusz-1.10/widgets/data/colormaps.dat 2010-12-12 12:41:10.000000000 +0000 +++ veusz-1.14/widgets/data/colormaps.dat 1970-01-01 00:00:00.000000000 +0000 @@ -1,62 +0,0 @@ -# Veusz colour maps -# Format is following: -# name -# B G R alpha -# ... -# name: name of colourmap -# B G R alpha are quads for each point on map - -# $Id: colormaps.dat 598 2007-05-10 19:00:13Z jeremysanders $ - -heat -0 0 0 255 -0 0 186 255 -50 139 255 255 -19 239 248 255 -255 255 255 255 - -spectrum2 -0 0 255 255 -0 255 255 255 -0 255 0 255 -255 255 0 255 -255 0 0 255 - -spectrum -0 0 0 255 -0 0 255 255 -0 255 255 255 -0 255 0 255 -255 255 0 255 -255 0 0 255 -255 255 255 255 - -grey -0 0 0 255 -255 255 255 255 - -blue -0 0 0 255 -255 0 0 255 -255 255 255 255 - -red -0 0 0 255 -0 0 255 255 -255 255 255 255 - -green -0 0 0 255 -0 255 0 255 -255 255 255 255 - -bluegreen -0 0 0 255 -255 123 0 255 -255 226 72 255 -161 255 0 255 -255 255 255 255 - -transblack -0 0 0 255 -0 0 0 0 diff -Nru veusz-1.10/widgets/fit.py veusz-1.14/widgets/fit.py --- veusz-1.10/widgets/fit.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/fit.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,8 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: fit.py 1450 2010-11-24 15:56:05Z jeremysanders $ - import re import sys @@ -33,6 +31,76 @@ from function import FunctionPlotter import widget +try: + import minuit +except ImportError: + minuit = None + +def minuitFit(evalfunc, params, names, values, xvals, yvals, yserr): + """Do fitting with minuit (if installed).""" + + def chi2(params): + """generate a lambda function to impedance-match between PyMinuit's + use of multiple parameters versus our use of a single numpy vector.""" + c = ((evalfunc(params, xvals) - yvals)**2 / yserr**2).sum() + if chi2.runningFit: + chi2.iters += 1 + p = [chi2.iters, c] + params.tolist() + str = ("%5i " + "%8g " * (len(params)+1)) % tuple(p) + print str + + return c + + namestr = ', '.join(names) + fnstr = 'lambda %s: chi2(N.array([%s]))' % (namestr, namestr) + + # this is safe because the only user-controlled variable is len(names) + fn = eval(fnstr, {'chi2' : chi2, 'N' : N}) + + print 'Fitting via Minuit:' + m = minuit.Minuit(fn, fix_x=True, **values) + + # run the fit + chi2.runningFit = True + chi2.iters = 0 + m.migrad() + + # do some error analysis + have_symerr, have_err = False, False + try: + chi2.runningFit = False + m.hesse() + have_symerr = True + m.minos() + have_err = True + except minuit.MinuitError, e: + print e + if str(e).startswith('Discovered a new minimum'): + # the initial fit really failed + raise + + # print the results + retchi2 = m.fval + dof = len(yvals) - len(params) + redchi2 = retchi2 / dof + + if have_err: + print 'Fit results:\n', "\n".join([ + u" %s = %g \u00b1 %g (+%g / %g)" + % (n, m.values[n], m.errors[n], m.merrors[(n, 1.0)], m.merrors[(n, -1.0)]) for n in names]) + elif have_symerr: + print 'Fit results:\n', "\n".join([ + u" %s = %g \u00b1 %g" % (n, m.values[n], m.errors[n]) for n in names]) + print 'MINOS error estimate not available.' + else: + print 'Fit results:\n', "\n".join([' %s = %g' % (n, m.values[n]) for n in names]) + print 'No error analysis available: fit quality uncertain' + + print "chi^2 = %g, dof = %i, reduced-chi^2 = %g" % (retchi2, dof, redchi2) + + vals = m.values + return vals, retchi2, dof + class Fit(FunctionPlotter): """A plotter to fit a function to data.""" @@ -71,22 +139,29 @@ 'the function variable', usertext='Fit only range'), 4 ) + s.add( setting.WidgetChoice( + 'outLabel', '', + descr='Write best fit parameters to this text label ' + 'after fitting', + widgettypes=('label',), + usertext='Output label'), + 5 ) s.add( setting.Str('outExpr', '', descr = 'Output best fitting expression', usertext='Output expression'), - 5, readonly=True ) + 6, readonly=True ) s.add( setting.Float('chi2', -1, descr = 'Output chi^2 from fitting', usertext='Fit χ2'), - 6, readonly=True ) + 7, readonly=True ) s.add( setting.Int('dof', -1, descr = 'Output degrees of freedom from fitting', usertext='Fit d.o.f.'), - 7, readonly=True ) + 8, readonly=True ) s.add( setting.Float('redchi2', -1, descr = 'Output reduced-chi-squared from fitting', usertext='Fit reduced χ2'), - 8, readonly=True ) + 9, readonly=True ) f = s.get('function') f.newDefault('a + b*x') @@ -117,6 +192,29 @@ env.update( self.settings.values ) return env + def updateOutputLabel(self, ops, vals, chi2, dof): + """Use best fit parameters to update text label.""" + s = self.settings + labelwidget = s.get('outLabel').findWidget() + + if labelwidget is not None: + # build up a set of X=Y values + loc = self.document.locale + txt = [] + for l, v in sorted(vals.iteritems()): + val = utils.formatNumber(v, '%.4Vg', locale=loc) + txt.append( '%s = %s' % (l, val) ) + # add chi2 output + txt.append( r'\chi^{2}_{\nu} = %s/%i = %s' % ( + utils.formatNumber(chi2, '%.4Vg', locale=loc), + dof, + utils.formatNumber(chi2/dof, '%.4Vg', locale=loc) )) + + # update label with text + text = r'\\'.join(txt) + ops.append( document.OperationSettingSet( + labelwidget.settings.get('label') , text ) ) + def actionFit(self): """Fit the data.""" @@ -203,18 +301,25 @@ sys.stderr.write('No degrees of freedom for fit. Not fitting\n') return - # actually do the fit - retn, chi2, dof = utils.fitLM(self.evalfunc, params, - xvals, - yvals, yserr) + # actually do the fit, either via Minuit or our own LM fitter + chi2 = 1 + dof = 1 + + if minuit is not None: + vals, chi2, dof = minuitFit(self.evalfunc, params, names, s.values, xvals, yvals, yserr) + else: + print 'Minuit not available, falling back to simple L-M fitting:' + retn, chi2, dof = utils.fitLM(self.evalfunc, params, + xvals, + yvals, yserr) + vals = {} + for i, v in zip(names, retn): + vals[i] = float(v) # list of operations do we can undo the changes operations = [] # populate the return parameters - vals = {} - for i, v in zip(names, retn): - vals[i] = float(v) operations.append( document.OperationSettingSet(s.get('values'), vals) ) # populate the read-only fit quality params @@ -231,8 +336,11 @@ expr = self.generateOutputExpr(vals) operations.append( document.OperationSettingSet(s.get('outExpr'), expr) ) + self.updateOutputLabel(operations, vals, chi2, dof) + # actually change all the settings - d.applyOperation( document.OperationMultiple(operations, descr='fit') ) + d.applyOperation( + document.OperationMultiple(operations, descr='fit') ) def evalfunc(self, params, xvals): diff -Nru veusz-1.10/widgets/function.py veusz-1.14/widgets/function.py --- veusz-1.10/widgets/function.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/function.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: function.py 1449 2010-11-22 09:26:58Z jeremysanders $ - """For plotting numerical functions.""" import veusz.qtall as qt4 @@ -28,6 +26,7 @@ import veusz.setting as setting import veusz.utils as utils +import pickable from plotters import GenericPlotter class FunctionChecker(object): @@ -178,15 +177,19 @@ varaxrange[1] = min(s.max, varaxrange[1]) # work out function in steps - if axis.settings.log: - # log spaced steps - l1, l2 = N.log(varaxrange[1]), N.log(varaxrange[0]) - delta = (l2-l1)/20. - points = N.exp(N.arange(l1, l2+delta, delta)) - else: - # linear spaced steps - delta = (varaxrange[1] - varaxrange[0])/20. - points = N.arange(varaxrange[0], varaxrange[1]+delta, delta) + try: + if axis.settings.log: + # log spaced steps + l1, l2 = N.log(varaxrange[1]), N.log(varaxrange[0]) + delta = (l2-l1)/20. + points = N.exp(N.arange(l1, l2+delta, delta)) + else: + # linear spaced steps + delta = (varaxrange[1] - varaxrange[0])/20. + points = N.arange(varaxrange[0], varaxrange[1]+delta, delta) + except ZeroDivisionError: + # delta is zero + return env = self.initEnviron() env[s.variable] = points @@ -289,16 +292,15 @@ def initEnviron(self): """Set up function environment.""" return self.document.eval_context.copy() - - def calcFunctionPoints(self, axes, posn): - """Calculate the pixels to plot for the function - returns (pxpts, pypts).""" + + def getIndependentPoints(self, axes, posn): + """Calculate the real and screen points to plot for the independent axis""" s = self.settings - try: - self.checker.check(s.function, s.variable) - except RuntimeError, e: - self.logEvalError(e) + + if ( None in axes or + axes[0].settings.direction != 'horizontal' or + axes[1].settings.direction != 'vertical' ): return None, None # get axes function is plotted along and on and @@ -323,27 +325,77 @@ axispts = axispts[ axispts <= s.max ] plotpts = axis1.dataToPlotterCoords(posn, axispts) + return axispts, plotpts + + def calcDependentPoints(self, axispts, axes, posn): + """Calculate the real and screen points to plot for the dependent axis""" + + s = self.settings + + if ( None in axes or + axes[0].settings.direction != 'horizontal' or + axes[1].settings.direction != 'vertical' ): + return None, None + + if axispts is None: + return None, None + + try: + self.checker.check(s.function, s.variable) + except RuntimeError, e: + self.logEvalError(e) + return None, None + + axis2 = axes[1] if s.variable == 'x' else axes[0] + # evaluate function env = self.initEnviron() env[s.variable] = axispts try: - results = eval(self.checker.compiled, env) - resultpts = axis2.dataToPlotterCoords( - posn, results + N.zeros(axispts.shape)) + results = eval(self.checker.compiled, env) + N.zeros(axispts.shape) + resultpts = axis2.dataToPlotterCoords(posn, results) except Exception, e: self.logEvalError(e) + results = None resultpts = None - # return x and y coordinates for plot + return results, resultpts + + def calcFunctionPoints(self, axes, posn): + ipts, pipts = self.getIndependentPoints(axes, posn) + dpts, pdpts = self.calcDependentPoints(ipts, axes, posn) + + if self.settings.variable == 'x': + return (ipts, dpts), (pipts, pdpts) + else: + return (dpts, ipts), (pdpts, pipts) + + def _pickable(self, posn): + s = self.settings + + axisnames = [s.xAxis, s.yAxis] + axes = self.parent.getAxes(axisnames) + if s.variable == 'x': - return plotpts, resultpts + axisnames[1] = axisnames[1] + '(' + axisnames[0] + ')' else: - return resultpts, plotpts + axisnames[0] = axisnames[0] + '(' + axisnames[1] + ')' - def draw(self, parentposn, painter, outerbounds = None): + (xpts, ypts), (pxpts, pypts) = self.calcFunctionPoints(axes, posn) + + return pickable.GenericPickable( + self, axisnames, (xpts, ypts), (pxpts, pypts) ) + + def pickPoint(self, x0, y0, bounds, distance='radial'): + return self._pickable(bounds).pickPoint(x0, y0, bounds, distance) + + def pickIndex(self, oldindex, direction, bounds): + return self._pickable(bounds).pickIndex(oldindex, direction, bounds) + + def draw(self, parentposn, painthelper, outerbounds = None): """Draw the function.""" - posn = GenericPlotter.draw(self, parentposn, painter, + posn = GenericPlotter.draw(self, parentposn, painthelper, outerbounds = outerbounds) x1, y1, x2, y2 = posn s = self.settings @@ -362,12 +414,11 @@ return # clip data within bounds of plotter - painter.beginPaintingWidget(self, posn) - painter.save() - cliprect = self.clipAxesBounds(painter, axes, posn) + cliprect = self.clipAxesBounds(axes, posn) + painter = painthelper.painter(self, posn, clip=cliprect) # get the points to plot by evaluating the function - pxpts, pypts = self.calcFunctionPoints(axes, posn) + (xpts, ypts), (pxpts, pypts) = self.calcFunctionPoints(axes, posn) # draw the function line if pxpts is None or pypts is None: @@ -395,9 +446,6 @@ painter.setPen( s.Line.makeQPen(painter) ) self._plotLine(painter, pxpts, pypts, posn, cliprect) - painter.restore() - painter.endPaintingWidget() - # allow the factory to instantiate an function plotter document.thefactory.register( FunctionPlotter ) diff -Nru veusz-1.10/widgets/graph.py veusz-1.14/widgets/graph.py --- veusz-1.10/widgets/graph.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/graph.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,10 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: graph.py 1416 2010-09-25 09:14:16Z jeremysanders $ - -from itertools import izip - import veusz.qtall as qt4 import veusz.setting as setting import veusz.utils as utils @@ -113,29 +109,29 @@ # return list of found widgets return [widgets[n] for n in axesnames] - def draw(self, parentposn, painter, outerbounds = None): + def draw(self, parentposn, painthelper, outerbounds = None): '''Update the margins before drawing.''' s = self.settings - margins = ( s.get('leftMargin').convert(painter), - s.get('topMargin').convert(painter), - s.get('rightMargin').convert(painter), - s.get('bottomMargin').convert(painter) ) - bounds = self.computeBounds(parentposn, painter, margins=margins) - maxbounds = self.computeBounds(parentposn, painter) + margins = ( s.get('leftMargin').convert(painthelper), + s.get('topMargin').convert(painthelper), + s.get('rightMargin').convert(painthelper), + s.get('bottomMargin').convert(painthelper) ) + + bounds = self.computeBounds(parentposn, painthelper, margins=margins) + maxbounds = self.computeBounds(parentposn, painthelper) # controls for adjusting graph margins - self.controlgraphitems = [ - controlgraph.ControlMarginBox(self, bounds, maxbounds, painter) - ] + painter = painthelper.painter(self, bounds) + painthelper.setControlGraph(self, [ + controlgraph.ControlMarginBox(self, bounds, maxbounds, + painthelper) ]) # do no painting if hidden if s.hide: return bounds - painter.beginPaintingWidget(self, bounds) - # set graph rectangle attributes painter.setBrush( s.get('Background').makeQBrushWHide() ) painter.setPen( s.get('Border').makeQPenWHide(painter) ) @@ -144,17 +140,10 @@ painter.drawRect( qt4.QRectF(qt4.QPointF(bounds[0], bounds[1]), qt4.QPointF(bounds[2], bounds[3])) ) - painter.endPaintingWidget() - - # set default pen/brush - # this is probably sticking plaster - painter.setPen( qt4.QPen() ) - painter.setBrush( qt4.QBrush() ) - # do normal drawing of children # iterate over children in reverse order for c in reversed(self.children): - c.draw(bounds, painter, outerbounds=outerbounds) + c.draw(bounds, painthelper, outerbounds=outerbounds) # now need to find axes which aren't children, and draw those again axestodraw = set() @@ -173,7 +162,7 @@ axeswidgets = self.getAxes(axestodraw) for w in axeswidgets: if w is not None: - w.draw(bounds, painter, outerbounds=outerbounds) + w.draw(bounds, painthelper, outerbounds=outerbounds) return bounds diff -Nru veusz-1.10/widgets/grid.py veusz-1.14/widgets/grid.py --- veusz-1.10/widgets/grid.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/grid.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: grid.py 1413 2010-09-25 08:48:02Z jeremysanders $ - """The grid class allows graphs to be arranged in a regular grid. The graphs may share axes if they are stored in the grid widget. """ @@ -298,7 +296,7 @@ self.document.applyOperation( document.OperationMultiple(operations, descr='zero margins') ) - def _drawChild(self, painter, child, bounds, parentposn): + def _drawChild(self, phelper, child, bounds, parentposn): """Draw child at correct position, with correct bounds.""" # save old position, then update with calculated @@ -326,7 +324,7 @@ coutbound[3] = parentposn[3] # draw widget - child.draw(bounds, painter, outerbounds=coutbound) + child.draw(bounds, phelper, outerbounds=coutbound) # debugging #painter.setPen(qt4.QPen(qt4.Qt.red)) @@ -336,7 +334,7 @@ # restore position child.position = oldposn - def draw(self, parentposn, painter, outerbounds=None): + def draw(self, parentposn, phelper, outerbounds=None): """Draws the widget's children.""" s = self.settings @@ -353,27 +351,25 @@ self.lastdimensions = dimensions self.lastscalings = scalings - margins = ( s.get('leftMargin').convert(painter), - s.get('topMargin').convert(painter), - s.get('rightMargin').convert(painter), - s.get('bottomMargin').convert(painter) ) + margins = ( s.get('leftMargin').convert(phelper), + s.get('topMargin').convert(phelper), + s.get('rightMargin').convert(phelper), + s.get('bottomMargin').convert(phelper) ) - bounds = self.computeBounds(parentposn, painter, margins=margins) - maxbounds = self.computeBounds(parentposn, painter) + bounds = self.computeBounds(parentposn, phelper, margins=margins) + maxbounds = self.computeBounds(parentposn, phelper) - painter.beginPaintingWidget(self, bounds) - painter.endPaintingWidget() + painter = phelper.painter(self, bounds) # controls for adjusting grid margins - self.controlgraphitems = [ - controlgraph.ControlMarginBox(self, bounds, maxbounds, painter) - ] + phelper.setControlGraph(self,[ + controlgraph.ControlMarginBox(self, bounds, maxbounds, phelper)]) for child in self.children: if child.typename != 'axis': - self._drawChild(painter, child, bounds, parentposn) + self._drawChild(phelper, child, bounds, parentposn) - # do not call widget.Widget.draw + # do not call widget.Widget.draw, do not collect 200 pounds pass def updateControlItem(self, cgi): diff -Nru veusz-1.10/widgets/image.py veusz-1.14/widgets/image.py --- veusz-1.10/widgets/image.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/image.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,14 +16,8 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: image.py 1368 2010-08-21 11:12:13Z jeremysanders $ - """Image plotting from 2d datasets.""" -import string -import os.path -import struct - import veusz.qtall as qt4 import numpy as N @@ -33,120 +27,10 @@ import plotters -slowfuncs = False -try: - from veusz.helpers.qtloops import numpyToQImage, applyImageTransparancy -except ImportError: - slowfuncs = True - -def applyScaling(data, mode, minval, maxval): - """Apply a scaling transformation on the data. - - mode is one of 'linear', 'sqrt', 'log', or 'squared' - minval is the minimum value of the scale - maxval is the maximum value of the scale - - returns transformed data, valid between 0 and 1 - """ - - # catch naughty people by hardcoding a range - if minval == maxval: - minval, maxval = 0., 1. - - if mode == 'linear': - # linear scaling - data = (data - minval) / (maxval - minval) - - elif mode == 'sqrt': - # sqrt scaling - # translate into fractions of range - data = (data - minval) / (maxval - minval) - # clip off any bad sqrts - data[data < 0.] = 0. - # actually do the sqrt transform - data = N.sqrt(data) - - elif mode == 'log': - # log scaling of image - # clip any values less than lowermin - lowermin = data < minval - data = N.log(data - (minval - 1)) / N.log(maxval - (minval - 1)) - data[lowermin] = 0. - - elif mode == 'squared': - # squared scaling - # clip any negative values - lowermin = data < minval - data = (data-minval)**2 / (maxval-minval)**2 - data[lowermin] = 0. - - else: - raise RuntimeError, 'Invalid scaling mode "%s"' % mode - - return data - -def slowNumpyToQImage(img, cmap, transparencyimg): - """Slow version of routine to convert numpy array to QImage - This is hard work in Python, but it was like this originally. - - img: numpy array to convert to QImage - cmap: 2D array of colors (BGRA rows) - forcetrans: force image to have alpha component.""" - - if struct.pack("h", 1) == "\000\001": - # have to swap colors for big endian architectures - cmap2 = cmap.copy() - cmap2[:,0] = cmap[:,3] - cmap2[:,1] = cmap[:,2] - cmap2[:,2] = cmap[:,1] - cmap2[:,3] = cmap[:,0] - cmap = cmap2 - - fracs = N.clip(N.ravel(img), 0., 1.) - - # Work out which is the minimum colour map. Assumes we have <255 bands. - numbands = cmap.shape[0]-1 - bands = (fracs*numbands).astype(N.uint8) - bands = N.clip(bands, 0, numbands-1) - - # work out fractional difference of data from band to next band - deltafracs = (fracs - bands * (1./numbands)) * numbands - - # need to make a 2-dimensional array to multiply against triplets - deltafracs.shape = (deltafracs.shape[0], 1) - - # calculate BGRalpha quadruplets - # this is a linear interpolation between the band and the next band - quads = (deltafracs*cmap[bands+1] + - (1.-deltafracs)*cmap[bands]).astype(N.uint8) - - # apply transparency if a transparency image is set - if transparencyimg is not None and transparencyimg.shape == img.shape: - quads[:,3] = ( N.clip(N.ravel(transparencyimg), 0., 1.) * - quads[:,3] ).astype(N.uint8) - - # convert 32bit quads to a Qt QImage - s = quads.tostring() - - fmt = qt4.QImage.Format_RGB32 - if N.any(cmap[:,3] != 255) or transparencyimg is not None: - # any transparency - fmt = qt4.QImage.Format_ARGB32 - - img = qt4.QImage(s, img.shape[1], img.shape[0], fmt) - img = img.mirrored() - - # hack to ensure string isn't freed before QImage - img.veusz_string = s - return img - class Image(plotters.GenericPlotter): """A class which plots an image on a graph with a specified coordinate system.""" - # a dict of colormaps loaded in from external file - colormaps = None - typename='image' allowusercreation=True description='Plot a 2d dataset as an image' @@ -172,10 +56,6 @@ """Construct list of settings.""" plotters.GenericPlotter.addSettings(s) - # lazy read of colormap file (Let's help startup times) - if klass.colormaps is None: - klass.readColorMaps() - s.add( setting.Dataset('data', '', dimensions = 2, descr = 'Dataset to plot', @@ -203,11 +83,11 @@ usertext='Transparent data'), 4 ) - s.add( ColormapSetting('colorMap', - 'grey', - descr = 'Set of colors to plot data with', - usertext='Colormap', - formatting=True), + s.add( setting.Colormap('colorMap', + 'grey', + descr = 'Set of colors to plot data with', + usertext='Colormap', + formatting=True), 5 ) s.add( setting.Bool('colorInvert', False, descr = 'Invert color map', @@ -237,90 +117,6 @@ return ', '.join(out) userdescription = property(_getUserDescription) - def readColorMaps(cls): - """Read color maps data file (a class method) - - File is made up of: - comments (prefaced by # on separate line) - colormapname - list of colors with B G R alpha order from 0->255 on separate lines - [colormapname ...] - """ - - name = '' - vals = [] - cls.colormaps = {} - - # locate file holding colormap data - filename = os.path.join(utils.veuszDirectory, 'widgets', 'data', - 'colormaps.dat') - - # iterate over file - for l in open(filename): - p = l.split() - if len(p) == 0 or p[0][0] == '#': - # blank or commented line - pass - elif p[0][0] not in string.digits: - # new colormap follows - if name != '': - cls.colormaps[name] = N.array(vals).astype(N.intc) - name = p[0] - vals = [] - else: - # add value to current colormap - assert name != '' - assert len(p) == 4 - vals.append( [int(i) for i in p] ) - - # add on final colormap - if name != '': - cls.colormaps[name] = N.array(vals).astype(N.intc) - - # collect names and sort alphabetically - names = cls.colormaps.keys() - names.sort() - cls.colormapnames = names - - readColorMaps = classmethod(readColorMaps) - - def applyColorMap(self, cmap, scaling, datain, minval, maxval, - trans, transimg=None): - """Apply a colour map to the 2d data given. - - cmap is the color map (numpy of BGRalpha quads) - scaling is scaling mode => 'linear', 'sqrt', 'log' or 'squared' - data are the imaging data - minval and maxval are the extremes of the data for the colormap - trans is a number from 0 to 100 - transimg is an optional image to apply transparency from - Returns a QImage - """ - - # invert colour map if min and max are swapped - if minval > maxval: - minval, maxval = maxval, minval - cmap = cmap[::-1] - - # apply transparency - if trans != 0: - cmap = cmap.copy() - cmap[:,3] = (cmap[:,3].astype(N.float32) * (100-trans) / - 100.).astype(N.intc) - - # apply scaling of data - fracs = applyScaling(datain, scaling, minval, maxval) - - if not slowfuncs: - img = numpyToQImage(fracs, cmap, transimg is not None) - if transimg is not None: - applyImageTransparancy(img, transimg) - else: - img = slowNumpyToQImage(fracs, cmap, transimg) - return img - - applyColorMap = classmethod(applyColorMap) - def updateImage(self): """Update the image with new contents.""" @@ -342,14 +138,12 @@ # this is used currently by colorbar objects self.cacheddatarange = (minval, maxval) - cmap = self.colormaps[s.colorMap] - if s.colorInvert: - cmap = cmap[::-1] - - self.image = self.applyColorMap(cmap, s.colorScaling, - data.data, - minval, maxval, s.transparency, - transimg=transimg) + # get color map + cmap = self.document.getColormap(s.colorMap, s.colorInvert) + + self.image = utils.applyColorMap( + cmap, s.colorScaling, data.data, minval, maxval, + s.transparency, transimg=transimg) def providesAxesDependency(self): """Range information provided by widget.""" @@ -443,44 +237,15 @@ """ self.recomputeInternals() - - barsize = 128 - s = self.settings minval, maxval = self.cacheddatarange + s = self.settings - if s.colorScaling in ('linear', 'sqrt', 'squared'): - # do a linear color scaling - vals = N.arange(barsize)/(barsize-1.0)*(maxval-minval) + minval - colorscaling = s.colorScaling - coloraxisscale = 'linear' - else: - assert s.colorScaling == 'log' - - # a logarithmic color scaling - # we cheat here by actually plotting a linear colorbar - # and telling veusz to put a log axis along it - # (as we only care about the endpoints) - # maybe should do this better... - - vals = N.arange(barsize)/(barsize-1.0)*(maxval-minval) + minval - colorscaling = 'linear' - coloraxisscale = 'log' - - # convert 1d array to 2d image - if direction == 'horizontal': - vals = vals.reshape(1, barsize) - else: - assert direction == 'vertical' - vals = vals.reshape(barsize, 1) - - cmap = self.colormaps[s.colorMap] - if s.colorInvert: - cmap = cmap[::-1] - - img = self.applyColorMap(cmap, colorscaling, vals, - minval, maxval, s.transparency) + # get colormap + cmap = self.document.getColormap(s.colorMap, s.colorInvert) - return (minval, maxval, coloraxisscale, img) + return utils.makeColorbarImage( + minval, maxval, s.colorScaling, cmap, s.transparency, + direction=direction) def recomputeInternals(self): """Recompute the internals if required. @@ -507,10 +272,10 @@ else: return None - def draw(self, parentposn, painter, outerbounds = None): + def draw(self, parentposn, phelper, outerbounds = None): """Draw the image.""" - posn = plotters.GenericPlotter.draw(self, parentposn, painter, + posn = plotters.GenericPlotter.draw(self, parentposn, phelper, outerbounds = outerbounds) x1, y1, x2, y2 = posn s = self.settings @@ -548,9 +313,8 @@ image = self.image # clip data within bounds of plotter - painter.beginPaintingWidget(self, posn) - painter.save() - self.clipAxesBounds(painter, axes, posn) + clip = self.clipAxesBounds(axes, posn) + painter = phelper.painter(self, posn, clip=clip) # optionally smooth images before displaying if s.smooth: @@ -558,61 +322,30 @@ qt4.Qt.IgnoreAspectRatio, qt4.Qt.SmoothTransformation ) - # now draw pixmap - painter.drawImage( qt4.QRectF(coordsx[0], coordsy[1], - coordsx[1]-coordsx[0], - coordsy[0]-coordsy[1]), - image ) - painter.restore() - painter.endPaintingWidget() + # get position and size of output image + xp, yp = coordsx[0], coordsy[1] + xw = coordsx[1]-coordsx[0] + yw = coordsy[0]-coordsy[1] + + # invert output drawing if axes go from positive->negative + # we only translate the coordinate system if this is the case + xscale = yscale = 1 + if xw < 0: + xscale = -1 + if yw < 0: + yscale = -1 + if xscale != 1 or yscale != 1: + painter.save() + painter.translate(xp, yp) + xp = yp = 0 + painter.scale(xscale, yscale) + + # draw image + painter.drawImage(qt4.QRectF(xp, yp, abs(xw), abs(yw)), image) + + # restore painter if image was inverted + if xscale != 1 or yscale != 1: + painter.restore() # allow the factory to instantiate an image document.thefactory.register( Image ) - -class ColormapSetting(setting.Choice): - """A setting to set the colour map used in an image.""" - - def __init__(self, name, value, **args): - setting.Choice.__init__(self, name, Image.colormapnames, value, **args) - - def copy(self): - """Make a copy of the setting.""" - return self._copyHelper((), (), {}) - - def makeControl(self, *args): - return ColormapControl(self, *args) - -class ColormapControl(setting.controls.Choice): - """Give the user a preview of colourmaps.""" - - _icons = [] - - size = (32, 12) - - def __init__(self, setn, parent): - if not self._icons: - self._generateIcons() - - setting.controls.Choice.__init__(self, setn, False, - Image.colormapnames, parent, - icons=self._icons) - self.setIconSize( qt4.QSize(*self.size) ) - - def _generateIcons(cls): - """Generate a list of icons for drop down menu.""" - size = cls.size - - # create a fake dataset smoothly varying from 0 to size[0]-1 - fakedataset = N.fromfunction(lambda x, y: y, - (size[1], size[0])) - - # iterate over colour maps - for cmap in Image.colormapnames: - image = Image.applyColorMap(Image.colormaps[cmap], 'linear', - fakedataset, - 0., size[0]-1., 0) - pixmap = qt4.QPixmap.fromImage(image) - cls._icons.append( qt4.QIcon(pixmap) ) - - _generateIcons = classmethod(_generateIcons) - diff -Nru veusz-1.10/widgets/__init__.py veusz-1.14/widgets/__init__.py --- veusz-1.10/widgets/__init__.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/__init__.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: __init__.py 1447 2010-11-11 20:35:25Z jeremysanders $ - """Widgets are defined in this module.""" from widget import Widget, Action @@ -25,6 +23,7 @@ from graph import Graph from grid import Grid from plotters import GenericPlotter, FreePlotter +from pickable import PickInfo from point import PointPlotter from function import FunctionPlotter from textlabel import TextLabel @@ -42,5 +41,6 @@ from vectorfield import VectorField from boxplot import BoxPlot from polar import Polar +from ternary import Ternary from nonorthpoint import NonOrthPoint from nonorthfunction import NonOrthFunction diff -Nru veusz-1.10/widgets/key.py veusz-1.14/widgets/key.py --- veusz-1.10/widgets/key.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/key.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: key.py 1451 2010-11-25 21:25:41Z jeremysanders $ - import veusz.qtall as qt4 import veusz.document as document import veusz.setting as setting @@ -29,6 +27,8 @@ import graph import controlgraph +import math + ############################################################################# # classes for controlling key position interactively @@ -197,6 +197,12 @@ descr = 'Length of line to show in sample', usertext='Key length', formatting=True) ) + + s.add( setting.AlignVert( 'keyAlign', + 'top', + descr = 'Alignment of key symbols relative to text', + usertext = 'Key alignment', + formatting = True) ) s.add( setting.Float( 'horzManual', 0., @@ -223,16 +229,102 @@ minval = 1, maxval = 100, formatting = True) ) - - def draw(self, parentposn, painter, outerbounds = None): + + @staticmethod + def _layoutChunk(entries, start, dims): + """Layout the entries into the given box, starting at start""" + row, col = start + numrows, numcols = dims + colstats = [0] * numcols + layout = [] + for (plotter, num, lines) in entries: + if row+lines > numrows: + # this item doesn't fit in this column, so move to the next + col += 1 + row = 0 + if col >= numcols: + # this layout failed, suggest expanding the box by 1 row + return ([], [], numrows+1) + if lines > numrows: + # this layout failed, suggest expanding the box to |lines| + return ([], [], lines) + + # col -> yp, row -> xp + layout.append( (plotter, num, col, row, lines) ) + row += lines + colstats[col] += 1 + + return (layout, colstats, numrows) + + def _layout(self, entries, totallines): + """Layout the items, trying to keep the box as small as possible + while still filling the columns""" + + maxcols = self.settings.columns + numcols = min(maxcols, max(len(entries), 1)) + + if not entries: + return (list(), (0, 0)) + + # start with evenly-sized rows and expand to fit + numrows = totallines / numcols + layout = [] + + while not layout: + # try to do a first cut of the layout, and expand the box until + # everything fits + (layout, colstats, newrows) = self._layoutChunk(entries, (0, 0), (numrows, numcols)) + if not layout: + numrows = newrows + + # ok, we've got a layout where everything fits, now pull items right + # to fill the remaining columns, if need be + while colstats[-1] == 0: + # shift 1 item to the right, up to the first column that has + # excess items + meanoccupation = max(1, sum(colstats)/float(numcols)) + + # loop until we find a victim item which can be safely moved + victimcol = numcols + while True: + # find the right-most column with excess occupation number + for i in reversed(xrange(victimcol)): + if colstats[i] > meanoccupation: + victimcol = i + break + + # find the last item in the victim column + victim = 0 + for i in reversed(xrange(len(layout))): + if layout[i][2] == victimcol: + victim = i + break + + # try to relayout with the victim item shoved to the next column + (newlayout, newcolstats, newrows) = self._layoutChunk(entries[victim:], + (0, victimcol+1), (numrows, numcols)) + if newlayout: + # the relayout worked, so accept it + layout = layout[0:victim] + newlayout + colstats[victimcol] -= 1 + del colstats[victimcol+1:] + colstats += newcolstats[victimcol+1:] + break + + # if we've run out of potential victims, just return what we have + if victimcol == 0: + return (layout, (numrows, numcols)) + + return (layout, (numrows, numcols)) + + def draw(self, parentposn, phelper, outerbounds = None): """Plot the key on a plotter.""" s = self.settings if s.hide: return - painter.beginPaintingWidget(self, parentposn) - painter.save() + painter = phelper.painter(self, parentposn) font = s.get('Text').makeQFont(painter) painter.setFont(font) @@ -240,10 +332,10 @@ showtext = not s.Text.hide - # keep track of widgets to place - keywidgets = [] # maximum width of text required maxwidth = 1 + # total number of layout lines required + totallines = 0 entries = [] # iterate over children and find widgets which are suitable @@ -255,19 +347,18 @@ if not c.settings.hide: # add an entry for each key entry for each widget for i in xrange(num): - entries.append( (c, i) ) - + lines = 1 if showtext: w, h = utils.Renderer(painter, font, 0, 0, c.getKeyText(i)).getDimensions() maxwidth = max(maxwidth, w) + lines = max(1, math.ceil(float(h)/float(height))) + + totallines += lines + entries.append( (c, i, lines) ) - # get number of columns - count = len(entries) - numcols = min(s.columns, max(count, 1)) - numrows = count / numcols - if count % numcols != 0: - numrows += 1 + # layout the box + layout, (numrows, numcols) = self._layout(entries, totallines) # total size of box symbolwidth = s.get('keyLength').convert(painter) @@ -321,14 +412,19 @@ textpen = s.get('Text').makeQPen() # plot dataset entries - for index, (plotter, num) in enumerate(entries): - xp, yp = index / numrows, index % numrows + for (plotter, num, xp, yp, lines) in layout: xpos = x + xp*(maxwidth+2*height+symbolwidth) ypos = y + yp*height # plot key symbol painter.save() - plotter.drawKeySymbol(num, painter, xpos, ypos, + keyoffset = 0 + if s.keyAlign == 'centre': + keyoffset = (lines-1)*height/2.0 + elif s.keyAlign == 'bottom': + keyoffset = (lines-1)*height + + plotter.drawKeySymbol(num, painter, xpos, ypos+keyoffset, symbolwidth, height) painter.restore() @@ -340,11 +436,7 @@ plotter.getKeyText(num), -1, 1).render() - self.controlgraphitems = [ - ControlKey(self, parentposn, boxposn, boxdims, height) - ] - - painter.restore() - painter.endPaintingWidget() + phelper.setControlGraph( + self, [ControlKey(self, parentposn, boxposn, boxdims, height)] ) document.thefactory.register( Key ) diff -Nru veusz-1.10/widgets/line.py veusz-1.14/widgets/line.py --- veusz-1.10/widgets/line.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/line.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: line.py 1387 2010-08-29 15:24:57Z jeremysanders $ - """Plotting a line with arrowheads or labels.""" import itertools @@ -80,7 +78,7 @@ usertext='Arrow left', formatting=True), 0) - def draw(self, posn, painter, outerbounds = None): + def draw(self, posn, phelper, outerbounds = None): """Plot the key on a plotter.""" s = self.settings @@ -105,12 +103,13 @@ not s.get('yPos').isDataset(d) and not s.get('length').isDataset(d) and not s.get('angle').isDataset(d) ) - self.controlgraphitems = [] - arrowsize = s.get('arrowSize').convert(painter) + # now do the drawing + painter = phelper.painter(self, posn) - painter.beginPaintingWidget(self, posn) - painter.save() + # adjustable positions for the lines + controlgraphitems = [] + arrowsize = s.get('arrowSize').convert(painter) # drawing settings for line if not s.Line.hide: @@ -145,10 +144,9 @@ cgi.index = index cgi.widgetposn = posn index += 1 - self.controlgraphitems.append(cgi) + controlgraphitems.append(cgi) - painter.restore() - painter.endPaintingWidget() + phelper.setControlGraph(self, controlgraphitems) def updateControlItem(self, cgi, pt1, pt2): """If control items are moved, update line.""" diff -Nru veusz-1.10/widgets/nonorthfunction.py veusz-1.14/widgets/nonorthfunction.py --- veusz-1.10/widgets/nonorthfunction.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/nonorthfunction.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: nonorthfunction.py 1447 2010-11-11 20:35:25Z jeremysanders $ - '''Non orthogonal function plotting.''' import numpy as N @@ -27,6 +25,7 @@ import veusz.setting as setting import veusz.utils as utils +import pickable from nonorthgraph import NonOrthGraph, FillBrush from widget import Widget from function import FunctionChecker @@ -130,13 +129,28 @@ def updateDataRanges(self, inrange): '''Update ranges of data given function.''' - def draw(self, parentposn, painter, outerbounds=None): + def _pickable(self): + apts, bpts = self.getFunctionPoints() + px, py = self.parent.graphToPlotCoords(apts, bpts) + + if self.settings.variable == 'a': + labels = ('a', 'b(a)') + else: + labels = ('a(b)', 'b') + + return pickable.GenericPickable( self, labels, (apts, bpts), (px, py) ) + + def pickPoint(self, x0, y0, bounds, distance='radial'): + return self._pickable().pickPoint(x0, y0, bounds, distance) + + def pickIndex(self, oldindex, direction, bounds): + return self._pickable().pickIndex(oldindex, direction, bounds) + + def draw(self, parentposn, phelper, outerbounds=None): '''Plot the function on a plotter.''' - posn = Widget.draw(self, parentposn, painter, + posn = Widget.draw(self, parentposn, phelper, outerbounds=outerbounds) - x1, y1, x2, y2 = posn - cliprect = qt4.QRectF( qt4.QPointF(x1, y1), qt4.QPointF(x2, y2) ) s = self.settings d = self.document @@ -152,8 +166,10 @@ self.logEvalError(e) return - painter.beginPaintingWidget(self, posn) - painter.save() + x1, y1, x2, y2 = posn + cliprect = qt4.QRectF( qt4.QPointF(x1, y1), qt4.QPointF(x2, y2) ) + painter = phelper.painter(self, posn) + self.parent.setClip(painter, posn) apts, bpts = self.getFunctionPoints() px, py = self.parent.graphToPlotCoords(apts, bpts) @@ -179,7 +195,4 @@ painter.setPen( s.PlotLine.makeQPen(painter) ) utils.plotClippedPolyline(painter, cliprect, p) - painter.restore() - painter.endPaintingWidget() - document.thefactory.register( NonOrthFunction ) diff -Nru veusz-1.10/widgets/nonorthgraph.py veusz-1.14/widgets/nonorthgraph.py --- veusz-1.10/widgets/nonorthgraph.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/nonorthgraph.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: nonorthgraph.py 1447 2010-11-11 20:35:25Z jeremysanders $ - """Non orthogonal graph root.""" import controlgraph @@ -27,7 +25,9 @@ import veusz.setting as setting -filloptions = ('center', 'outside', 'top', 'bottom', 'left', 'right') +filloptions = ('center', 'outside', 'top', 'bottom', 'left', 'right', + 'polygon') + class FillBrush(setting.Brush): '''Brush for filling point region.''' def __init__(self, *args, **argsv): @@ -107,40 +107,36 @@ c.updateDataRanges(drange) return drange - def draw(self, parentposn, painter, outerbounds=None): + def draw(self, parentposn, phelper, outerbounds=None): '''Update the margins before drawing.''' s = self.settings - margins = ( s.get('leftMargin').convert(painter), - s.get('topMargin').convert(painter), - s.get('rightMargin').convert(painter), - s.get('bottomMargin').convert(painter) ) - bounds = self.computeBounds(parentposn, painter, margins=margins) - maxbounds = self.computeBounds(parentposn, painter) - - # controls for adjusting graph margins - self.controlgraphitems = [ - controlgraph.ControlMarginBox(self, bounds, maxbounds, painter) - ] + margins = ( s.get('leftMargin').convert(phelper), + s.get('topMargin').convert(phelper), + s.get('rightMargin').convert(phelper), + s.get('bottomMargin').convert(phelper) ) + bounds = self.computeBounds(parentposn, phelper, margins=margins) + maxbounds = self.computeBounds(parentposn, phelper) + + painter = phelper.painter(self, bounds) + + # controls for adjusting margins + phelper.setControlGraph(self, [ + controlgraph.ControlMarginBox(self, bounds, maxbounds, phelper)]) # do no painting if hidden if s.hide: return bounds # plot graph - painter.beginPaintingWidget(self, bounds) datarange = self.getDataRange() self.drawGraph(painter, bounds, datarange, outerbounds=outerbounds) self.drawAxes(painter, bounds, datarange, outerbounds=outerbounds) - painter.endPaintingWidget() # paint children - painter.save() - self.setClip(painter, bounds) for c in reversed(self.children): - c.draw(bounds, painter, outerbounds=outerbounds) - painter.restore() + c.draw(bounds, phelper, outerbounds=outerbounds) return bounds diff -Nru veusz-1.10/widgets/nonorthpoint.py veusz-1.14/widgets/nonorthpoint.py --- veusz-1.10/widgets/nonorthpoint.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/nonorthpoint.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,10 +16,9 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: nonorthpoint.py 1447 2010-11-11 20:35:25Z jeremysanders $ - """Non orthogonal point plotting.""" +import itertools import numpy as N import veusz.qtall as qt4 @@ -27,6 +26,8 @@ import veusz.setting as setting import veusz.utils as utils +import pickable + from nonorthgraph import NonOrthGraph, FillBrush from widget import Widget from point import MarkerFillBrush @@ -63,6 +64,9 @@ 'scalePoints', '', descr = 'Scale size of plotted markers by this dataset or' ' list of values', usertext='Scale markers') ) + s.add( setting.DatasetOrStr('labels', '', + descr='Dataset or string to label points', + usertext='Labels', datatype='text') ) s.add( setting.DistancePt('markerSize', '3pt', @@ -92,6 +96,10 @@ descr = 'Fill settings (2)', usertext = 'Area fill 2'), pixmap = 'settings_plotfillbelow' ) + s.add( setting.PointLabel('Label', + descr = 'Label settings', + usertext='Label'), + pixmap = 'settings_axislabel' ) def updateDataRanges(self, inrange): '''Extend inrange to range of data.''' @@ -105,24 +113,62 @@ inrange[2] = min( N.nanmin(d2.data), inrange[2] ) inrange[3] = max( N.nanmax(d2.data), inrange[3] ) - def plotMarkers(self, painter, plta, pltb, scaling, clip): + def pickPoint(self, x0, y0, bounds, distance = 'radial'): + p = pickable.DiscretePickable(self, 'data1', 'data2', + lambda v1, v2: self.parent.graphToPlotCoords(v1, v2)) + return p.pickPoint(x0, y0, bounds, distance) + + def pickIndex(self, oldindex, direction, bounds): + p = pickable.DiscretePickable(self, 'data1', 'data2', + lambda v1, v2: self.parent.graphToPlotCoords(v1, v2)) + return p.pickIndex(oldindex, direction, bounds) + + def plotMarkers(self, painter, plta, pltb, scaling, markersize, clip): '''Draw markers in widget.''' s = self.settings if not s.MarkerLine.hide or not s.MarkerFill.hide: painter.setBrush( s.MarkerFill.makeQBrushWHide() ) painter.setPen( s.MarkerLine.makeQPenWHide(painter) ) - - size = s.get('markerSize').convert(painter) - utils.plotMarkers(painter, plta, pltb, s.marker, size, + + utils.plotMarkers(painter, plta, pltb, s.marker, markersize, scaling=scaling, clip=clip) - def draw(self, parentposn, painter, outerbounds=None): + def drawLabels(self, painter, xplotter, yplotter, + textvals, markersize): + """Draw labels for the points. + + This is copied from the xy (point) widget class, so it + probably should be somehow be shared. + + FIXME: sane automatic placement of labels + """ + + s = self.settings + lab = s.get('Label') + + # work out offset an alignment + deltax = markersize*1.5*{'left':-1, 'centre':0, 'right':1}[lab.posnHorz] + deltay = markersize*1.5*{'top':-1, 'centre':0, 'bottom':1}[lab.posnVert] + alignhorz = {'left':1, 'centre':0, 'right':-1}[lab.posnHorz] + alignvert = {'top':-1, 'centre':0, 'bottom':1}[lab.posnVert] + + # make font and len + textpen = lab.makeQPen() + painter.setPen(textpen) + font = lab.makeQFont(painter) + angle = lab.angle + + # iterate over each point and plot each label + for x, y, t in itertools.izip(xplotter+deltax, yplotter+deltay, + textvals): + utils.Renderer( painter, font, x, y, t, + alignhorz, alignvert, angle ).render() + + def draw(self, parentposn, phelper, outerbounds=None): '''Plot the data on a plotter.''' - posn = Widget.draw(self, parentposn, painter, + posn = Widget.draw(self, parentposn, phelper, outerbounds=outerbounds) - x1, y1, x2, y2 = posn - cliprect = qt4.QRectF( qt4.QPointF(x1, y1), qt4.QPointF(x2, y2) ) s = self.settings d = self.document @@ -134,14 +180,18 @@ d1 = s.get('data1').getData(d) d2 = s.get('data2').getData(d) dscale = s.get('scalePoints').getData(d) + text = s.get('labels').getData(d, checknull=True) if not d1 or not d2: return - painter.beginPaintingWidget(self, posn) - painter.save() + x1, y1, x2, y2 = posn + cliprect = qt4.QRectF( qt4.QPointF(x1, y1), qt4.QPointF(x2, y2) ) + painter = phelper.painter(self, posn) + self.parent.setClip(painter, posn) # split parts separated by NaNs - for v1, v2, vs in document.generateValidDatasetParts(d1, d2, dscale): + for v1, v2, scalings, textitems in document.generateValidDatasetParts( + d1, d2, dscale, text): # convert data (chopping down length) v1d, v2d = v1.data, v2.data minlen = min(v1d.shape[0], v2d.shape[0]) @@ -170,13 +220,15 @@ utils.plotClippedPolyline(painter, cliprect, pts) # markers + markersize = s.get('markerSize').convert(painter) pscale = None - if vs: - pscale = vs.data - self.plotMarkers(painter, px, py, pscale, cliprect) - - painter.restore() - painter.endPaintingWidget() + if scalings: + pscale = scalings.data + self.plotMarkers(painter, px, py, pscale, markersize, cliprect) + + # finally plot any labels + if textitems and not s.Label.hide: + self.drawLabels(painter, px, py, textitems, markersize) # allow the factory to instantiate plotter document.thefactory.register( NonOrthPoint ) diff -Nru veusz-1.10/widgets/page.py veusz-1.14/widgets/page.py --- veusz-1.10/widgets/page.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/page.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,15 +16,15 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: page.py 1023 2009-07-11 17:52:27Z jeremysanders $ - """Widget that represents a page in the document.""" import veusz.qtall as qt4 import veusz.document as document +import veusz.setting as setting import widget import root +import controlgraph # x, y, fplot, xyplot # x-> xyplot(x) @@ -121,8 +121,11 @@ dwidget, dwidget_dep = dep if hasattr(dwidget, 'isplotter'): # update range of axis with (dwidget, dwidget_dep) - dwidget.updateAxisRange(widget, dwidget_dep, - self.ranges[widget]) + # do not do this if the widget is hidden + if ( not dwidget.settings.isSetting('hide') or + not dwidget.settings.hide ): + dwidget.updateAxisRange(widget, dwidget_dep, + self.ranges[widget]) elif hasattr(dwidget, 'isaxis'): # set actual range on axis, as axis no longer has a # dependency @@ -185,14 +188,25 @@ if type(self) == Page: self.readDefaults() - def draw(self, parentposn, painter, outerbounds=None): + @classmethod + def addSettings(klass, s): + widget.Widget.addSettings(s) + + # page sizes are initially linked to the document page size + s.add( setting.Distance('width', + setting.Reference('/width'), + descr='Width of page', + usertext='Page width', + formatting=True) ) + s.add( setting.Distance('height', + setting.Reference('/height'), + descr='Height of page', + usertext='Page height', + formatting=True) ) + + def draw(self, parentposn, painthelper, outerbounds=None): """Draw the plotter. Clip graph inside bounds.""" - # special scaling properties are stored in painter - if not hasattr(painter, 'veusz_scaling'): - painter.veusz_scaling = 1. - painter.veusz_pixperpt = painter.device().logicalDpiY() / 72. - # document should pass us the page bounds x1, y1, x2, y2 = parentposn @@ -202,27 +216,37 @@ axisdependhelper.findAxisRanges() # store axis->plotter mappings in painter too (is this nasty?) - painter.veusz_axis_plotter_map = axisdependhelper.axis_plotter_map - - # page size is stored in painter - painter.veusz_page_size = (x2-x1, y2-y1) + painthelper.axisplottermap.update(axisdependhelper.axis_plotter_map) if self.settings.hide: - bounds = self.computeBounds(parentposn, painter) + bounds = self.computeBounds(parentposn, painthelper) return bounds - painter.beginPaintingWidget(self, parentposn) - painter.save() + clip = qt4.QRectF( qt4.QPointF(parentposn[0], parentposn[1]), + qt4.QPointF(parentposn[2], parentposn[3]) ) + painter = painthelper.painter(self, parentposn, clip=clip) # clip to page - painter.setClipRect( qt4.QRectF(x1, y1, x2-x1, y2-y1) ) - bounds = widget.Widget.draw(self, parentposn, painter, + bounds = widget.Widget.draw(self, parentposn, painthelper, parentposn) - painter.restore() - painter.endPaintingWidget() + + # w and h are non integer + w = self.settings.get('width').convert(painter) + h = self.settings.get('height').convert(painter) + painthelper.setControlGraph(self, [ + controlgraph.ControlMarginBox(self, [0, 0, w, h], + [-10000, -10000, + 10000, 10000], + painthelper, + ismovable = False) + ] ) return bounds + def updateControlItem(self, cgi): + """Call helper to set page size.""" + cgi.setPageSize() + # allow the factory to instantiate this document.thefactory.register( Page ) diff -Nru veusz-1.10/widgets/pickable.py veusz-1.14/widgets/pickable.py --- veusz-1.10/widgets/pickable.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/widgets/pickable.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,253 @@ +# pickable.py +# stuff related to the Picker (aka Read Data) tool + +# Copyright (C) 2011 Benjamin K. Stuhl +# Email: Benjamin K. Stuhl +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################### + +import numpy as N + +import veusz.document as document + +class PickInfo: + """Encapsulates the results of a Pick operation. screenpos and coords are + numeric (x,y) tuples, labels are the textual labels for the x and y + datasets, and index is some object that the picker can use to figure out + what the 'next' and 'previous' points are. index must implement __str__(); + return '' if it has no user-visible meaning.""" + def __init__(self, widget=None, screenpos=None, labels=None, coords=None, index=None): + self.widget = widget + self.screenpos = screenpos + self.labels = labels + self.coords = coords + self.index = index + self.distance = float('inf') + self.displaytype = ('numeric', 'numeric') + + def __nonzero__(self): + if self.widget and self.screenpos and self.labels and self.coords: + return True + return False + +class Index: + """A class containing all the state a GenericPickable needs to find the + next or previous point""" + def __init__(self, ivar, index, sign): + self.ivar = ivar + self.index = index + self.sign = sign + + # default to not trusting the actual index to be meaningful + self.useindex = False + + def __str__(self): + if not self.useindex: + return '' + else: + return str(self.index) + +def _chooseOrderingSign(m, c, p): + """Figures out whether p or m is visually right of c""" + assert c is not None + + if p is not None and m is not None: + if p[0] > m[0] or (p[0] == m[0] and p[1] < m[1]): + # p is visually to the right of or above m + return 1 + else: + return -1 + elif p is not None: + if p[0] > c[0]: + # p is visually right of c + return 1 + else: + return -1 + elif m is not None: + if m[0] < c[0]: + # m is visually left of c + return 1 + else: + return -1 + else: + assert m is not None or p is not None + +class GenericPickable: + """Utility class which abstracts the math of picking the closest point out + of a list of points""" + + def __init__(self, widget, labels, vals, screenvals): + self.widget = widget + self.labels = labels + self.xvals, self.yvals = vals + self.xscreen, self.yscreen = screenvals + + def _pickSign(self, i): + if len(self.xscreen) <= 1: + # we only have one element, so it doesn't matter anyways + return 1 + + if i == 0: + m = None + else: + m = self.xscreen[i-1], self.yscreen[i-1] + + c = self.xscreen[i], self.yscreen[i] + + if i+1 == len(self.xscreen): + p = None + else: + p = self.xscreen[i+1], self.yscreen[i+1] + + return _chooseOrderingSign(m, c, p) + + def pickPoint(self, x0, y0, bounds, distance_direction): + info = PickInfo(self.widget, labels=self.labels) + + if self.widget.settings.hide: + return info + + if None in (self.xvals, self.yvals): + return info + + # calculate distances + if distance_direction == 'vertical': + # measure distance along y + dist = N.abs(self.yscreen - y0) + elif distance_direction == 'horizontal': + # measure distance along x + dist = N.abs(self.xscreen - x0) + elif distance_direction == 'radial': + # measure radial distance + dist = N.sqrt((self.xscreen - x0)**2 + (self.yscreen - y0)**2) + else: + # programming error + assert (distance_direction == 'radial' or + distance_direction == 'vertical' or + distance_direction == 'horizontal') + + # ignore points which are offscreen + outofbounds = ( (self.xscreen < bounds[0]) | (self.xscreen > bounds[2]) | + (self.yscreen < bounds[1]) | (self.yscreen > bounds[3]) ) + dist[outofbounds] = float('inf') + + m = N.min(dist) + # if there are multiple equidistant points, arbitrarily take + # the first one + i = N.nonzero(dist == m)[0][0] + + info.screenpos = self.xscreen[i], self.yscreen[i] + info.coords = self.xvals[i], self.yvals[i] + info.distance = m + info.index = Index(self.xvals[i], i, self._pickSign(i)) + + return info + + def pickIndex(self, oldindex, direction, bounds): + info = PickInfo(self.widget, labels=self.labels) + + if self.widget.settings.hide: + return info + + if None in (self.xvals, self.yvals): + return info + + if oldindex.index is None: + # no explicit index, so find the closest location to the previous + # independent variable value + i = N.logical_not( N.logical_or( + self.xvals < oldindex.ivar, self.xvals > oldindex.ivar) ) + + # and pick the next + if oldindex.sign == 1: + i = max(N.nonzero(i)[0]) + else: + i = min(N.nonzero(i)[0]) + else: + i = oldindex.index + + if direction == 'right': + incr = oldindex.sign + elif direction == 'left': + incr = -oldindex.sign + else: + assert direction == 'right' or direction == 'left' + + i += incr + + # skip points that are outside of the bounds + while ( i >= 0 and i < len(self.xscreen) and + (self.xscreen[i] < bounds[0] or self.xscreen[i] > bounds[2] or + self.yscreen[i] < bounds[1] or self.yscreen[i] > bounds[3]) ): + i += incr + + if i < 0 or i >= len(self.xscreen): + return info + + info.screenpos = self.xscreen[i], self.yscreen[i] + info.coords = self.xvals[i], self.yvals[i] + info.index = Index(self.xvals[i], i, oldindex.sign) + + return info + +class DiscretePickable(GenericPickable): + """A specialization of GenericPickable that knows how to deal with widgets + with axes and data sets""" + def __init__(self, widget, xdata_propname, ydata_propname, mapdata_fn): + s = widget.settings + doc = widget.document + self.xdata = xdata = s.get(xdata_propname).getData(doc) + self.ydata = ydata = s.get(ydata_propname).getData(doc) + labels = s.__getattr__(xdata_propname), s.__getattr__(ydata_propname) + + if not xdata or not ydata or not mapdata_fn: + GenericPickable.__init__( self, widget, labels, (None, None), (None, None) ) + return + + # map all the valid data + x, y = N.array([]), N.array([]) + xs, ys = N.array([]), N.array([]) + for xvals, yvals in document.generateValidDatasetParts(xdata, ydata): + chunklen = min(len(xvals.data), len(yvals.data)) + + x = N.append(x, xvals.data[:chunklen]) + y = N.append(y, yvals.data[:chunklen]) + + xs, ys = mapdata_fn(x, y) + + # and set us up with the mapped data + GenericPickable.__init__( self, widget, labels, (x, y), (xs, ys) ) + + def pickPoint(self, x0, y0, bounds, distance_direction): + info = GenericPickable.pickPoint(self, x0, y0, bounds, distance_direction) + info.displaytype = (self.xdata.displaytype, self.ydata.displaytype) + + if not info: + return info + + # indicies are persistent + info.index.useindex = True + return info + + def pickIndex(self, oldindex, direction, bounds): + info = GenericPickable.pickIndex(self, oldindex, direction, bounds) + + if not info: + return info + + # indicies are persistent + info.index.useindex = True + return info diff -Nru veusz-1.10/widgets/plotters.py veusz-1.14/widgets/plotters.py --- veusz-1.10/widgets/plotters.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/plotters.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,8 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: plotters.py 1280 2010-06-13 14:53:17Z jeremysanders $ - """A generic plotter widget which is inherited by function and point.""" import veusz.qtall as qt4 @@ -109,10 +107,8 @@ """ pass - def clipAxesBounds(self, painter, axes, bounds): - """Clip painter to start and stop values of axis. - Returns clipping rectangle - """ + def clipAxesBounds(self, axes, bounds): + """Returns clipping rectange for start and stop values of axis.""" # update cached coordinates of axes axes[0].plotterToDataCoords(bounds, N.array([])) @@ -128,7 +124,6 @@ # actually clip the data cliprect = qt4.QRectF(qt4.QPointF(x1, y1), qt4.QPointF(x2, y2)) - painter.setClipRect(cliprect) return cliprect def getAxisLabels(self, direction): diff -Nru veusz-1.10/widgets/point.py veusz-1.14/widgets/point.py --- veusz-1.10/widgets/point.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/point.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: point.py 1449 2010-11-22 09:26:58Z jeremysanders $ - """For plotting xy points.""" import veusz.qtall as qt4 @@ -28,6 +26,7 @@ import veusz.setting as setting import veusz.utils as utils +import pickable from plotters import GenericPlotter try: @@ -143,7 +142,7 @@ hidevert = True # keep track of what's shown hidehorz = True - if ( (style == 'fillvert' or style == 'linevert') and + if ( 'vert' in style and (ymin is not None and ymax is not None) and not s.ErrorBarLine.hideVert ): hidevert = False @@ -151,7 +150,7 @@ utils.addNumpyToPolygonF(ptsbelow, xplotter, ymin) utils.addNumpyToPolygonF(ptsabove, xplotter, ymax) - elif ( (style == 'fillhorz' or style == 'linehorz') and + elif ( 'horz' in style and (xmin is not None and xmax is not None) and not s.ErrorBarLine.hideHorz ): hidehorz = False @@ -160,8 +159,7 @@ utils.addNumpyToPolygonF(ptsabove, xmax, yplotter) # draw filled regions above/left and below/right - if ( (style == 'fillvert' or style == 'fillhorz') and - not (hidehorz and hidevert) ): + if 'fill' in style and not (hidehorz and hidevert): # construct points for error bar regions retnpts = qt4.QPolygonF() utils.addNumpyToPolygonF(retnpts, xplotter[::-1], yplotter[::-1]) @@ -198,16 +196,53 @@ 'fillvert': (_errorBarsFilled,), 'linehorz': (_errorBarsFilled,), 'linevert': (_errorBarsFilled,), + 'linehorzbar': (_errorBarsBar, _errorBarsFilled), + 'linevertbar': (_errorBarsBar, _errorBarsFilled), } - class MarkerFillBrush(setting.Brush): def __init__(self, name, **args): setting.Brush.__init__(self, name, **args) self.get('color').newDefault( setting.Reference( '../PlotLine/color') ) - + + self.add( setting.Colormap( + 'colorMap', 'grey', + descr = 'If color markers dataset is given, use this colormap ' + 'instead of the fill color', + usertext='Color map', + formatting=True) ) + self.add( setting.Bool( + 'colorMapInvert', False, + descr = 'Invert color map', + usertext = 'Invert map', + formatting=True) ) + +class ColorSettings(setting.Settings): + """Settings for a coloring points using data values.""" + + def __init__(self, name): + setting.Settings.__init__(self, name, setnsmode='groupedsetting') + self.add( setting.DatasetOrFloatList( + 'points', '', + descr = 'Use color value (0-1) in dataset to paint points', + usertext='Color markers'), 7 ) + self.add( setting.Float( + 'min', 0., + descr = 'Minimum value of color dataset', + usertext = 'Min val' )) + self.add( setting.Float( + 'max', 1., + descr = 'Maximum value of color dataset', + usertext = 'Max val' )) + self.add( setting.Choice( + 'scaling', + ['linear', 'sqrt', 'log', 'squared'], + 'linear', + descr = 'Scaling to transform numbers to color', + usertext='Scaling')) + class PointPlotter(GenericPlotter): """A class for plotting points and their errors.""" @@ -251,6 +286,8 @@ descr = 'Scale size of plotted markers by this dataset or' ' list of values', usertext='Scale markers'), 6 ) + s.add( ColorSettings('Color') ) + s.add( setting.DatasetOrFloatList( 'yData', 'y', descr='Dataset containing y data or list of values', @@ -349,13 +386,26 @@ return ( (s.xAxis, 'sx'), (s.yAxis, 'sy') ) def updateAxisRange(self, axis, depname, axrange): + """Compute the effect of data on the axis range.""" dataname = {'sx': 'xData', 'sy': 'yData'}[depname] - data = self.settings.get(dataname).getData(self.document) + dsetn = self.settings.get(dataname) + data = dsetn.getData(self.document) + if data: drange = data.getRange() if drange: axrange[0] = min(axrange[0], drange[0]) axrange[1] = max(axrange[1], drange[1]) + elif dsetn.isEmpty(): + # no valid dataset. + # check if there a valid dataset for the other axis. + # if there is, treat this as a row number + dataname = {'sy': 'xData', 'sx': 'yData'}[depname] + data = self.settings.get(dataname).getData(self.document) + if data: + length = data.data.shape[0] + axrange[0] = min(axrange[0], 1) + axrange[1] = max(axrange[1], length) def _getLinePoints( self, xvals, yvals, posn, xdata, ydata ): """Get the points corresponding to the line connecting the points.""" @@ -436,7 +486,8 @@ return path painter.strokePath(p, painter.pen()) - def _drawBezierLine( self, painter, xvals, yvals, posn, xdata, ydata): + def _drawBezierLine( self, painter, xvals, yvals, posn, + xdata, ydata): """Handle bezier lines and fills.""" pts = self._getLinePoints(xvals, yvals, posn, xdata, ydata) @@ -533,8 +584,8 @@ # plot error bar painter.setPen( s.ErrorBarLine.makeQPenWHide(painter) ) for function in _errorBarFunctionMap[style]: - function(style, xneg, xpos, yneg, ypos, xpts, ypts, s, painter, - cliprect) + function(style, xneg, xpos, yneg, ypos, xpts, ypts, s, + painter, cliprect) # draw line if not s.PlotLine.hide: @@ -555,7 +606,8 @@ painter.restore() - def drawLabels(self, painter, xplotter, yplotter, textvals, markersize): + def drawLabels(self, painter, xplotter, yplotter, + textvals, markersize): """Draw labels for the points.""" s = self.settings @@ -594,10 +646,53 @@ else: return (text, yv.data) - def draw(self, parentposn, painter, outerbounds=None): + def _fetchAxes(self): + """Returns the axes for this widget""" + + s = self.settings + axes = self.parent.getAxes( (s.xAxis, s.yAxis) ) + + # fail if we don't have good axes + if ( None in axes or + axes[0].settings.direction != 'horizontal' or + axes[1].settings.direction != 'vertical' ): + return None + + return axes + + def _pickable(self, bounds): + axes = self._fetchAxes() + + if axes is None: + map_fn = None + else: + map_fn = lambda x, y: ( axes[0].dataToPlotterCoords(bounds, x), + axes[1].dataToPlotterCoords(bounds, y) ) + + return pickable.DiscretePickable(self, 'xData', 'yData', map_fn) + + def pickPoint(self, x0, y0, bounds, distance = 'radial'): + return self._pickable(bounds).pickPoint(x0, y0, bounds, distance) + + def pickIndex(self, oldindex, direction, bounds): + return self._pickable(bounds).pickIndex(oldindex, direction, bounds) + + def makeColorbarImage(self, direction='horz'): + """Make a QImage colorbar for the current plot.""" + + s = self.settings + c = s.Color + cmap = self.document.getColormap( + s.MarkerFill.colorMap, s.MarkerFill.colorMapInvert) + + return utils.makeColorbarImage( + c.min, c.max, c.scaling, cmap, 0, + direction=direction) + + def draw(self, parentposn, phelper, outerbounds=None): """Plot the data on a plotter.""" - posn = GenericPlotter.draw(self, parentposn, painter, + posn = GenericPlotter.draw(self, parentposn, phelper, outerbounds=outerbounds) x1, y1, x2, y2 = posn @@ -613,8 +708,20 @@ yv = s.get('yData').getData(doc) text = s.get('labels').getData(doc, checknull=True) scalepoints = s.get('scalePoints').getData(doc) + colorpoints = s.Color.get('points').getData(doc) + # if a missing dataset, make a fake dataset for the second one + # based on a row number + if xv and not yv and s.get('yData').isEmpty(): + # use index for y data + length = xv.data.shape[0] + yv = document.DatasetRange(length, (1,length)) + elif yv and not xv and s.get('xData').isEmpty(): + # use index for x data + length = yv.data.shape[0] + xv = document.DatasetRange(length, (1,length)) if not xv or not yv: + # no valid dataset, so exit return # if text entered, then multiply up to get same number of values @@ -624,22 +731,19 @@ text = text*(length / len(text)) + text[:length % len(text)] # get axes widgets - axes = self.parent.getAxes( (s.xAxis, s.yAxis) ) - - # return if there's no proper axes - if ( None in axes or - axes[0].settings.direction != 'horizontal' or - axes[1].settings.direction != 'vertical' ): + axes = self._fetchAxes() + if not axes: + # no valid axes, so exit return # clip data within bounds of plotter - painter.beginPaintingWidget(self, posn) - painter.save() - cliprect = self.clipAxesBounds(painter, axes, posn) + cliprect = self.clipAxesBounds(axes, posn) + painter = phelper.painter(self, posn, clip=cliprect) # loop over chopped up values - for xvals, yvals, tvals, ptvals in document.generateValidDatasetParts( - xv, yv, text, scalepoints): + for xvals, yvals, tvals, ptvals, cvals in ( + document.generateValidDatasetParts( + xv, yv, text, scalepoints, colorpoints)): #print "Calculating coordinates" # calc plotter coords of x and y points @@ -689,20 +793,26 @@ yplotter[::s.thinfactor]) # whether to scale markers - scaling = None + scaling = colorvals = cmap = None if ptvals: scaling = ptvals.data + # color point individually + if cvals: + colorvals = utils.applyScaling( + cvals.data, s.Color.scaling, + s.Color.min, s.Color.max) + cmap = self.document.getColormap( + s.MarkerFill.colorMap, s.MarkerFill.colorMapInvert) # actually plot datapoints utils.plotMarkers(painter, xplt, yplt, s.marker, markersize, - scaling=scaling, clip=cliprect) + scaling=scaling, clip=cliprect, + cmap=cmap, colorvals=colorvals) # finally plot any labels if tvals and not s.Label.hide: - self.drawLabels(painter, xplotter, yplotter, tvals, markersize) - - painter.restore() - painter.endPaintingWidget() + self.drawLabels(painter, xplotter, yplotter, + tvals, markersize) # allow the factory to instantiate an x,y plotter document.thefactory.register( PointPlotter ) diff -Nru veusz-1.10/widgets/polar.py veusz-1.14/widgets/polar.py --- veusz-1.10/widgets/polar.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/polar.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: polar.py 1474 2010-12-12 12:40:43Z jeremysanders $ - """Polar plot widget.""" import numpy as N @@ -168,6 +166,8 @@ pp.arcTo(cliprect, 0, 360) pp.addPolygon(pts) painter.fillPath(pp, painter.brush()) + elif filltype == 'polygon': + utils.plotClippedPolygon(painter, cliprect, pts) def drawGraph(self, painter, bounds, datarange, outerbounds=None): '''Plot graph area and axes.''' @@ -204,10 +204,10 @@ if self._maxradius <= 0.: self._maxradius = 1. - atick = AxisTicks(0, self._maxradius, t.number, - t.number*4, + atick = AxisTicks(0, self._maxradius, t.number, t.number*4, extendbounds=False, extendzero=False) - minval, maxval, majtick, mintick, tickformat = atick.getTicks() + atick.getTicks() + majtick = atick.tickvals # draw ticks as circles if not t.hideannuli: @@ -226,14 +226,15 @@ tl = s.TickLabels scale, format = tl.scale, tl.format if format == 'Auto': - format = tickformat + format = atick.autoformat painter.setPen( tl.makeQPen() ) font = tl.makeQFont(painter) # draw radial axis if not s.TickLabels.hideradial: for tick in majtick[1:]: - num = utils.formatNumber(tick*scale, format) + num = utils.formatNumber(tick*scale, format, + locale=self.document.locale) x = tick / self._maxradius * self._xscale + self._xc r = utils.Renderer(painter, font, x, self._yc, num, alignhorz=-1, diff -Nru veusz-1.10/widgets/polygon.py veusz-1.14/widgets/polygon.py --- veusz-1.10/widgets/polygon.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/polygon.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: polygon.py 1087 2009-10-06 18:44:36Z jeremysanders $ - from itertools import izip import veusz.document as document @@ -51,7 +49,7 @@ usertext = 'Fill'), pixmap = 'settings_plotfillbelow' ) - def draw(self, posn, painter, outerbounds=None): + def draw(self, posn, phelper, outerbounds=None): """Plot the data on a plotter.""" s = self.settings @@ -67,11 +65,10 @@ # we can't calculate coordinates return - painter.beginPaintingWidget(self, posn) - painter.save() - painter.setClipRect( qt4.QRectF(posn[0], posn[1], - posn[2]-posn[0], - posn[3]-posn[1]) ) + x1, y1, x2, y2 = posn + cliprect = qt4.QRectF( qt4.QPointF(x1, y1), qt4.QPointF(x2, y2) ) + painter = phelper.painter(self, posn, clip=cliprect) + painter.setPen( s.Line.makeQPenWHide(painter) ) painter.setBrush( s.Fill.makeQBrushWHide() ) @@ -87,8 +84,5 @@ # draw it painter.drawPolygon(poly) - painter.restore() - painter.endPaintingWidget() - # allow the factory to instantiate this document.thefactory.register( Polygon ) diff -Nru veusz-1.10/widgets/root.py veusz-1.14/widgets/root.py --- veusz-1.10/widgets/root.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/root.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,8 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: root.py 1023 2009-07-11 17:52:27Z jeremysanders $ - import veusz.qtall as qt4 import veusz.document as document @@ -52,6 +50,8 @@ if type(self) == Root: self.readDefaults() + s.get('englishlocale').setOnModified(self.changeLocale) + @classmethod def addSettings(klass, s): widget.Widget.addSettings(s) @@ -67,57 +67,48 @@ descr='Height of the pages', usertext='Page height', formatting=True) ) + s.add( setting.Bool('englishlocale', False, + descr='Use US/English number formatting for ' + 'document', + usertext='English locale', + formatting=True) ) - def getSize(self, painter): - """Get dimensions of widget in painter coordinates.""" - return ( self.settings.get('width').convert(painter), - self.settings.get('height').convert(painter) ) - - def draw(self, painter, pagenum): + def changeLocale(self): + """Update locale of document if changed by user.""" + + if self.settings.englishlocale: + self.document.locale = qt4.QLocale.c() + else: + self.document.locale = qt4.QLocale() + self.document.locale.setNumberOptions(qt4.QLocale.OmitGroupSeparator) + + def getPage(self, pagenum): + """Get page widget.""" + return self.children[pagenum] + + def draw(self, painthelper, pagenum): """Draw the page requested on the painter.""" - xw, yw = self.getSize(painter) + xw, yw = painthelper.pagesize posn = [0, 0, xw, yw] - painter.beginPaintingWidget(self, posn) + painter = painthelper.painter(self, posn) page = self.children[pagenum] - page.draw( posn, painter ) + page.draw( posn, painthelper ) - self.controlgraphitems = [ - controlgraph.ControlMarginBox(self, posn, - [-10000, -10000, - 10000, 10000], - painter, - ismovable = False) - ] - - painter.endPaintingWidget() + # w and h are non integer + w = self.settings.get('width').convert(painter) + h = self.settings.get('height').convert(painter) + painthelper.setControlGraph(self, [ + controlgraph.ControlMarginBox(self, [0, 0, w, h], + [-10000, -10000, + 10000, 10000], + painthelper, + ismovable = False) + ] ) def updateControlItem(self, cgi): - """Graph resized or moved - call helper routine to move self.""" - - s = self.settings - - # get margins in pixels - width = cgi.posn[2] - cgi.posn[0] - height = cgi.posn[3] - cgi.posn[1] - - # set up fake painter containing veusz scalings - fakepainter = qt4.QPainter() - fakepainter.veusz_page_size = cgi.page_size - fakepainter.veusz_scaling = cgi.scaling - fakepainter.veusz_pixperpt = cgi.pixperpt - - # convert to physical units - width = s.get('width').convertInverse(width, fakepainter) - height = s.get('height').convertInverse(height, fakepainter) - - # modify widget margins - operations = ( - document.OperationSettingSet(s.get('width'), width), - document.OperationSettingSet(s.get('height'), height), - ) - self.document.applyOperation( - document.OperationMultiple(operations, descr='change page size')) + """Call helper to set page size.""" + cgi.setPageSize() def fillStylesheet(self, stylesheet): """Register widgets with stylesheet.""" diff -Nru veusz-1.10/widgets/shape.py veusz-1.14/widgets/shape.py --- veusz-1.10/widgets/shape.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/shape.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: shape.py 1322 2010-07-10 20:32:38Z jeremysanders $ - """For plotting shapes.""" import itertools @@ -27,7 +25,7 @@ import veusz.setting as setting import veusz.document as document import veusz.utils as utils - +import widget import controlgraph import plotters @@ -82,7 +80,7 @@ def drawShape(self, painter, rect): pass - def draw(self, posn, painter, outerbounds = None): + def draw(self, posn, phelper, outerbounds = None): """Plot the key on a plotter.""" s = self.settings @@ -104,15 +102,14 @@ return # if a dataset is used, we can't use control items - isnotdataset = ( not s.get('xPos').isDataset(d) and + isnotdataset = ( not s.get('xPos').isDataset(d) and not s.get('yPos').isDataset(d) and not s.get('width').isDataset(d) and not s.get('height').isDataset(d) and not s.get('rotate').isDataset(d) ) - self.controlgraphitems = [] + controlgraphitems = [] - painter.beginPaintingWidget(self, posn) - painter.save() + painter = phelper.painter(self, posn) # drawing settings for shape if not s.Border.hide: @@ -145,10 +142,9 @@ cgi.index = index cgi.widgetposn = posn index += 1 - self.controlgraphitems.append(cgi) + controlgraphitems.append(cgi) - painter.restore() - painter.endPaintingWidget() + phelper.setControlGraph(self, controlgraphitems) def updateControlItem(self, cgi): """If control item is moved or resized, this is called.""" @@ -238,9 +234,15 @@ if type(self) == ImageFile: self.readDefaults() - self.cachepixmap = None + self.cacheimage = None self.cachefilename = None self.cachestat = None + self.cacheembeddata = None + + self.addAction( widget.Action('embed', self.actionEmbed, + descr = 'Embed image in Veusz document ' + 'to remove dependency on external file', + usertext = 'Embed image') ) @classmethod def addSettings(klass, s): @@ -252,6 +254,13 @@ usertext='Filename', formatting=False), posn=0 ) + + s.add( setting.Str('embeddedImageData', '', + descr='Embedded base 64-encoded image data, ' + 'used if filename set to {embedded}', + usertext='Embedded data', + hidden=True) ) + s.add( setting.Bool('aspect', True, descr='Preserve aspect ratio', usertext='Preserve aspect', @@ -259,56 +268,108 @@ posn=0 ) s.Border.get('hide').newDefault(True) - def updateCachedPixmap(self): + def actionEmbed(self): + """Embed external image into veusz document.""" + + s = self.settings + + if s.filename == '{embedded}': + print "Data already embedded" + return + + # get data from external file + try: + f = open(s.filename, 'rb') + data = f.read() + f.close() + except IOError: + print "Could not find file. Not embedding." + return + + # convert to base 64 to make it nicer in the saved file + encoded = str(qt4.QByteArray(data).toBase64()) + + # now put embedded data in hidden setting + ops = [ + document.OperationSettingSet(s.get('filename'), '{embedded}'), + document.OperationSettingSet(s.get('embeddedImageData'), + encoded) + ] + self.document.applyOperation( + document.OperationMultiple(ops, descr='embed image') ) + + def updateCachedImage(self): """Update cache.""" s = self.settings self.cachestat = os.stat(s.filename) - self.cachepixmap = qt4.QPixmap(s.filename) + self.cacheimage = qt4.QImage(s.filename) self.cachefilename = s.filename - return self.cachepixmap + + def updateCachedEmbedded(self): + """Update cached image from embedded data.""" + s = self.settings + self.cacheimage = qt4.QImage() + + # convert the embedded data from base64 and load into the image + decoded = qt4.QByteArray.fromBase64(s.embeddedImageData) + self.cacheimage.loadFromData(decoded) + + # we cache the data we have decoded + self.cacheembeddata = s.embeddedImageData def drawShape(self, painter, rect): - """Draw pixmap.""" + """Draw image.""" s = self.settings # draw border and fill painter.drawRect(rect) - # cache pixmap - pixmap = None + # check to see whether image needs reloading + image = None if s.filename != '' and os.path.isfile(s.filename): - if (self.cachefilename != s.filename or + if (self.cachefilename != s.filename or os.stat(s.filename) != self.cachestat): - self.updateCachedPixmap() - pixmap = self.cachepixmap + # update the image cache + self.updateCachedImage() + # clear any embedded image data + self.settings.get('embeddedImageData').set('') + image = self.cacheimage + + # or needs recreating from embedded data + if s.filename == '{embedded}': + if s.embeddedImageData is not self.cacheembeddata: + self.updateCachedEmbedded() + image = self.cacheimage + + # if no image, then use default image + if ( not image or image.isNull() or + image.width() == 0 or image.height() == 0 ): + # load replacement image + fname = os.path.join(utils.imagedir, 'button_imagefile.svg') + r = qt4.QSvgRenderer(fname) + r.render(painter, rect) + + else: + # image rectangle + irect = qt4.QRectF(image.rect()) - # if no pixmap, then use default image - if not pixmap or pixmap.width() == 0 or pixmap.height() == 0: - pixmap = utils.getIcon('button_imagefile').pixmap(64, 64) - - # pixmap rectangle - prect = qt4.QRectF(pixmap.rect()) - - # preserve aspect ratio - if s.aspect: - xr = rect.width() / prect.width() - yr = rect.height() / prect.height() - - if xr > yr: - rect = qt4.QRectF(rect.left()+(rect.width()- - prect.width()*yr)*0.5, - rect.top(), - prect.width()*yr, - rect.height()) - else: - rect = qt4.QRectF(rect.left(), - rect.top()+(rect.height()- - prect.height()*xr)*0.5, - rect.width(), - prect.height()*xr) + # preserve aspect ratio + if s.aspect: + xr = rect.width() / irect.width() + yr = rect.height() / irect.height() + + if xr > yr: + rect = qt4.QRectF( + rect.left()+(rect.width()-irect.width()*yr)*0.5, + rect.top(), irect.width()*yr, rect.height()) + else: + rect = qt4.QRectF( + rect.left(), + rect.top()+(rect.height()-irect.height()*xr)*0.5, + rect.width(), irect.height()*xr) - # finally draw pixmap - painter.drawPixmap(rect, pixmap, prect) + # finally draw image + painter.drawImage(rect, image, irect) document.thefactory.register( Ellipse ) document.thefactory.register( Rectangle ) diff -Nru veusz-1.10/widgets/ternary.py veusz-1.14/widgets/ternary.py --- veusz-1.10/widgets/ternary.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/widgets/ternary.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,492 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################## + +"""Ternary plot widget.""" + +import numpy as N +import math +from itertools import izip + +from nonorthgraph import NonOrthGraph +from axisticks import AxisTicks +from axis import MajorTick, MinorTick, GridLine, MinorGridLine, AxisLabel, \ + TickLabel + +import veusz.qtall as qt4 +import veusz.document as document +import veusz.setting as setting +import veusz.utils as utils + +def rotatePts(x, y, theta): + '''Rotate points by theta degrees.''' + s = math.sin(theta*math.pi/180.) + c = math.cos(theta*math.pi/180.) + return x*c-y*s, x*s+y*c + +# translate coordinates a,b,c from user to plot +# user can select different coordinate systems +coord_lookup = { + 'bottom-left': (0, 1, 2), + 'bottom-right': (0, 2, 1), + 'left-bottom': (1, 0, 2), + 'left-right': (2, 0, 1), + 'right-bottom': (1, 2, 0), + 'right-left': (2, 1, 0) +} + +# useful trigonometric identities +sin30 = 0.5 +sin60 = cos30 = 0.86602540378 +tan30 = 0.5773502691 + +class Ternary(NonOrthGraph): + '''Ternary plotter.''' + + typename='ternary' + allowusercreation = True + description = 'Ternary graph' + + def __init__(self, parent, name=None): + '''Initialise ternary plot.''' + NonOrthGraph.__init__(self, parent, name=name) + if type(self) == NonOrthGraph: + self.readDefaults() + + @classmethod + def addSettings(klass, s): + '''Construct list of settings.''' + NonOrthGraph.addSettings(s) + + s.add( setting.Choice('mode', + ('percentage', 'fraction'), + 'percentage', + descr='Show percentages or fractions', + usertext='Mode') ) + + s.add( setting.Choice('coords', + ('bottom-left', 'bottom-right', + 'left-bottom', 'left-right', + 'right-left', 'right-bottom'), + 'bottom-left', + descr='Axes to use for plotting coordinates', + usertext='Coord system') ) + + s.add( setting.Str('labelbottom', '', + descr='Bottom axis label text', + usertext='Label bottom') ) + s.add( setting.Str('labelleft', '', + descr='Left axis label text', + usertext='Label left') ) + s.add( setting.Str('labelright', '', + descr='Right axis label text', + usertext='Label right') ) + + s.add( setting.Float('originleft', 0., + descr='Fractional origin of left axis at its top', + usertext='Left origin') ) + s.add( setting.Float('originbottom', 0., + descr='Fractional origin of bottom axis at its ' + 'left', usertext='Bottom origin') ) + s.add( setting.Float('fracsize', 1., + descr='Fractional size of plot', + usertext='Size') ) + + s.add( AxisLabel('Label', + descr = 'Axis label settings', + usertext = 'Axis label'), + pixmap='settings_axislabel' ) + s.add( TickLabel('TickLabels', + descr = 'Tick label settings', + usertext = 'Tick labels'), + pixmap='settings_axisticklabels' ) + s.add( MajorTick('MajorTicks', + descr = 'Major tick line settings', + usertext = 'Major ticks'), + pixmap='settings_axismajorticks' ) + s.add( MinorTick('MinorTicks', + descr = 'Minor tick line settings', + usertext = 'Minor ticks'), + pixmap='settings_axisminorticks' ) + s.add( GridLine('GridLines', + descr = 'Grid line settings', + usertext = 'Grid lines'), + pixmap='settings_axisgridlines' ) + s.add( MinorGridLine('MinorGridLines', + descr = 'Minor grid line settings', + usertext = 'Grid lines for minor ticks'), + pixmap='settings_axisminorgridlines' ) + + s.get('leftMargin').newDefault('1cm') + s.get('rightMargin').newDefault('1cm') + s.get('topMargin').newDefault('1cm') + s.get('bottomMargin').newDefault('1cm') + + s.MajorTicks.get('number').newDefault(10) + s.MinorTicks.get('number').newDefault(50) + s.GridLines.get('hide').newDefault(False) + s.TickLabels.remove('rotate') + + def _maxVal(self): + '''Get maximum value on axis.''' + if self.settings.mode == 'percentage': + return 100. + else: + return 1. + + def coordRanges(self): + '''Get ranges of coordinates.''' + + mv = self._maxVal() + + # ranges for each coordinate + ra = [self._orgbot*mv, (self._orgbot+self._size)*mv] + rb = [self._orgleft*mv, (self._orgleft+self._size)*mv] + rc = [self._orgright*mv, (self._orgright+self._size)*mv] + ranges = [ra, rb, rc] + + lookup = coord_lookup[self.settings.coords] + return ranges[lookup.index(0)], ranges[lookup.index(1)] + + def graphToPlotCoords(self, coorda, coordb): + '''Convert coordinates in r, theta to x, y.''' + + s = self.settings + + # normalize coordinates + maxval = self._maxVal() + coordan = coorda / maxval + coordbn = coordb / maxval + + # the three coordinates on the plot + clist = (coordan, coordbn, 1.-coordan-coordbn) + + # select the right coordinates for a, b and c given the system + # requested by the user + # normalise by origins and plot size + lookup = coord_lookup[s.coords] + cbot = ( clist[ lookup[0] ] - self._orgbot ) / self._size + cleft = ( clist[ lookup[1] ] - self._orgleft ) / self._size + cright = ( clist[ lookup[2] ] - self._orgright ) / self._size + + # from Ingram, 1984, Area, 16, 175 + # remember that y goes in the opposite direction here + x = (0.5*cright + cbot)*self._width + self._box[0] + y = self._box[3] - cright * sin60 * self._width + + return x, y + + def drawFillPts(self, painter, cliprect, ptsx, ptsy, filltype): + '''Draw points for plotting a fill.''' + pts = qt4.QPolygonF() + utils.addNumpyToPolygonF(pts, ptsx, ptsy) + + # this is broken: FIXME + if filltype == 'left': + dyend = ptsy[-1]-self._box[1] + pts.append( qt4.QPointF(ptsx[-1]-dyend*tan30, self._box[1]) ) + dystart = ptsy[0]-self._box[1] + pts.append( qt4.QPointF(ptsx[0]-dystart*tan30, self._box[1]) ) + elif filltype == 'right': + pts.append( qt4.QPointF(self._box[2], ptsy[-1]) ) + pts.append( qt4.QPointF(self._box[2], ptsy[0]) ) + elif filltype == 'bottom': + dyend = self._box[3]-ptsy[-1] + pts.append( qt4.QPointF(ptsx[-1]-dyend*tan30, self._box[3]) ) + dystart = self._box[3]-ptsy[0] + pts.append( qt4.QPointF(ptsx[0]-dystart*tan30, self._box[3]) ) + elif filltype == 'polygon': + pass + else: + pts = None + + if pts is not None: + utils.plotClippedPolygon(painter, cliprect, pts) + + def drawGraph(self, painter, bounds, datarange, outerbounds=None): + '''Plot graph area and axes.''' + + s = self.settings + + xw, yw = bounds[2]-bounds[0], bounds[3]-bounds[1] + + d60 = 60./180.*math.pi + ang = math.atan2(yw, xw/2.) + if ang > d60: + # taller than wider + widthh = xw/2 + height = math.tan(d60) * widthh + else: + # wider than taller + height = yw + widthh = height / math.tan(d60) + + # box for equilateral triangle + self._box = ( (bounds[2]+bounds[0])/2 - widthh, + (bounds[1]+bounds[3])/2 - height/2, + (bounds[2]+bounds[0])/2 + widthh, + (bounds[1]+bounds[3])/2 + height/2 ) + self._width = widthh*2 + self._height = height + + # triangle shaped polygon for graph + self._tripoly = p = qt4.QPolygonF() + p.append( qt4.QPointF(self._box[0], self._box[3]) ) + p.append( qt4.QPointF(self._box[0]+widthh, self._box[1]) ) + p.append( qt4.QPointF(self._box[2], self._box[3]) ) + + painter.setPen( s.Border.makeQPenWHide(painter) ) + painter.setBrush( s.Background.makeQBrushWHide() ) + painter.drawPolygon(p) + + # work out origins and size + self._size = max(min(s.fracsize, 1.), 0.) + + # make sure we don't go past the ends of the allowed range + # value of origin of left axis at top + self._orgleft = min(s.originleft, 1.-self._size) + # value of origin of bottom axis at left + self._orgbot = min(s.originbottom, 1.-self._size) + # origin of right axis at bottom + self._orgright = 1. - self._orgleft - (self._orgbot + self._size) + + def _computeTickVals(self): + """Compute tick values.""" + + s = self.settings + + # this is a hack as we lose ends off the axis otherwise + d = 1e-6 + + # get ticks along left axis + atickleft = AxisTicks(self._orgleft-d, self._orgleft+self._size+d, + s.MajorTicks.number, s.MinorTicks.number, + extendbounds=False, extendzero=False) + atickleft.getTicks() + # use the interval from above to calculate ticks for right + atickright = AxisTicks(self._orgright-d, self._orgright+self._size+d, + s.MajorTicks.number, s.MinorTicks.number, + extendbounds=False, extendzero=False, + forceinterval = atickleft.interval) + atickright.getTicks() + # then calculate for bottom + atickbot = AxisTicks(self._orgbot-d, self._orgbot+self._size+d, + s.MajorTicks.number, s.MinorTicks.number, + extendbounds=False, extendzero=False, + forceinterval = atickleft.interval) + atickbot.getTicks() + + return atickbot, atickleft, atickright + + def setClip(self, painter, bounds): + '''Set clipping for graph.''' + p = qt4.QPainterPath() + p.addPolygon( self._tripoly ) + painter.setClipPath(p) + + def _getLabels(self, ticks, autoformat): + """Return tick labels.""" + labels = [] + tl = self.settings.TickLabels + format = tl.format + scale = tl.scale + if format.lower() == 'auto': + format = autoformat + for v in ticks: + l = utils.formatNumber(v*scale, format, locale=self.document.locale) + labels.append(l) + return labels + + def _drawTickSet(self, painter, tickSetn, gridSetn, + tickbot, tickleft, tickright, + tickLabelSetn=None, labelSetn=None): + '''Draw a set of ticks (major or minor). + + tickSetn: tick setting to get line details + gridSetn: setting for grid line (if any) + tickXXX: tick arrays for each axis + tickLabelSetn: setting used to label ticks, or None if minor ticks + labelSetn: setting for labels, if any + ''' + + # this is mostly a lot of annoying trigonometry + # compute line ends for ticks and grid lines + + tl = tickSetn.get('length').convert(painter) + mv = self._maxVal() + + # bottom ticks + x1 = (tickbot - self._orgbot)/self._size*self._width + self._box[0] + x2 = x1 - tl * sin30 + y1 = self._box[3] + N.zeros(x1.shape) + y2 = y1 + tl * cos30 + tickbotline = (x1, y1, x2, y2) + + # bottom grid (removing lines at edge of plot) + scaletick = 1 - (tickbot-self._orgbot)/self._size + gx = x1 + scaletick*self._width*sin30 + gy = y1 - scaletick*self._width*cos30 + ne = (scaletick > 1e-6) & (scaletick < (1-1e-6)) + gridbotline = (x1[ne], y1[ne], gx[ne], gy[ne]) + + # left ticks + x1 = -(tickleft - self._orgleft)/self._size*self._width*sin30 + ( + self._box[0] + self._box[2])*0.5 + x2 = x1 - tl * sin30 + y1 = (tickleft - self._orgleft)/self._size*self._width*cos30 + self._box[1] + y2 = y1 - tl * cos30 + tickleftline = (x1, y1, x2, y2) + + # left grid + scaletick = 1 - (tickleft-self._orgleft)/self._size + gx = x1 + scaletick*self._width*sin30 + gy = self._box[3] + N.zeros(y1.shape) + ne = (scaletick > 1e-6) & (scaletick < (1-1e-6)) + gridleftline = (x1[ne], y1[ne], gx[ne], gy[ne]) + + # right ticks + x1 = -(tickright - self._orgright)/self._size*self._width*sin30+self._box[2] + x2 = x1 + tl + y1 = -(tickright - self._orgright)/self._size*self._width*cos30+self._box[3] + y2 = y1 + tickrightline = (x1, y1, x2, y2) + + # right grid + scaletick = 1 - (tickright-self._orgright)/self._size + gx = x1 - scaletick*self._width + gy = y1 + gridrightline = (x1[ne], y1[ne], gx[ne], gy[ne]) + + if not gridSetn.hide: + # draw the grid + pen = gridSetn.makeQPen(painter) + painter.setPen(pen) + utils.plotLinesToPainter(painter, *gridbotline) + utils.plotLinesToPainter(painter, *gridleftline) + utils.plotLinesToPainter(painter, *gridrightline) + + # calculate deltas for ticks + bdelta = ldelta = rdelta = 0 + + if not tickSetn.hide: + # draw ticks themselves + pen = tickSetn.makeQPen(painter) + pen.setCapStyle(qt4.Qt.FlatCap) + painter.setPen(pen) + utils.plotLinesToPainter(painter, *tickbotline) + utils.plotLinesToPainter(painter, *tickleftline) + utils.plotLinesToPainter(painter, *tickrightline) + + ldelta += tl*sin30 + bdelta += tl*cos30 + rdelta += tl + + if tickLabelSetn is not None and not tickLabelSetn.hide: + # compute the labels for the ticks + tleftlabels = self._getLabels(tickleft*mv, '%Vg') + trightlabels = self._getLabels(tickright*mv, '%Vg') + tbotlabels = self._getLabels(tickbot*mv, '%Vg') + + painter.setPen( tickLabelSetn.makeQPen() ) + font = tickLabelSetn.makeQFont(painter) + painter.setFont(font) + + fm = utils.FontMetrics(font, painter.device()) + sp = fm.leading() + fm.descent() + off = tickLabelSetn.get('offset').convert(painter) + + # draw tick labels in each direction + hlabbot = wlableft = wlabright = 0 + for l, x, y in izip(tbotlabels, tickbotline[2], tickbotline[3]+off): + r = utils.Renderer(painter, font, x, y, l, 0, 1, 0) + bounds = r.render() + hlabbot = max(hlabbot, bounds[3]-bounds[1]) + for l, x, y in izip(tleftlabels, tickleftline[2]-off-sp, tickleftline[3]): + r = utils.Renderer(painter, font, x, y, l, 1, 0, 0) + bounds = r.render() + wlableft = max(wlableft, bounds[2]-bounds[0]) + for l, x, y in izip(trightlabels,tickrightline[2]+off+sp, tickrightline[3]): + r = utils.Renderer(painter, font, x, y, l, -1, 0, 0) + bounds = r.render() + wlabright = max(wlabright, bounds[2]-bounds[0]) + + bdelta += hlabbot+off+sp + ldelta += wlableft+off+sp + rdelta += wlabright+off+sp + + if labelSetn is not None and not labelSetn.hide: + # draw label on edges (if requested) + painter.setPen( labelSetn.makeQPen() ) + font = labelSetn.makeQFont(painter) + painter.setFont(font) + + fm = utils.FontMetrics(font, painter.device()) + sp = fm.leading() + fm.descent() + off = labelSetn.get('offset').convert(painter) + + # bottom label + r = utils.Renderer(painter, font, + self._box[0]+self._width/2, + self._box[3] + bdelta + off, + self.settings.labelbottom, + 0, 1) + r.render() + + # left label - rotate frame before drawing so we can get + # the bounds correct + r = utils.Renderer(painter, font, 0, -sp, + self.settings.labelleft, + 0, -1) + painter.save() + painter.translate(self._box[0]+self._width*0.25 - ldelta - off, + 0.5*(self._box[1]+self._box[3])) + painter.rotate(-60) + r.render() + painter.restore() + + # right label + r = utils.Renderer(painter, font, 0, -sp, + self.settings.labelright, + 0, -1) + painter.save() + painter.translate(self._box[0]+self._width*0.75 + ldelta + off, + 0.5*(self._box[1]+self._box[3])) + painter.rotate(60) + r.render() + painter.restore() + + def drawAxes(self, painter, bounds, datarange, outerbounds=None): + '''Draw plot axes.''' + + s = self.settings + + # compute tick values for later when plotting axes + tbot, tleft, tright = self._computeTickVals() + + # draw the major ticks + self._drawTickSet(painter, s.MajorTicks, s.GridLines, + tbot.tickvals, tleft.tickvals, tright.tickvals, + tickLabelSetn=s.TickLabels, + labelSetn=s.Label) + + # now draw the minor ones + self._drawTickSet(painter, s.MinorTicks, s.MinorGridLines, + tbot.minorticks, tleft.minorticks, tright.minorticks) + +document.thefactory.register(Ternary) diff -Nru veusz-1.10/widgets/textlabel.py veusz-1.14/widgets/textlabel.py --- veusz-1.10/widgets/textlabel.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/textlabel.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: textlabel.py 1064 2009-09-19 19:05:01Z jeremysanders $ - """For plotting one or more text labels on a graph.""" import itertools @@ -77,7 +75,7 @@ cnvtalignhorz = { 'left': -1, 'centre': 0, 'right': 1 } cnvtalignvert = { 'top': 1, 'centre': 0, 'bottom': -1 } - def draw(self, posn, painter, outerbounds = None): + def draw(self, posn, phelper, outerbounds = None): """Draw the text label.""" s = self.settings @@ -94,17 +92,16 @@ # we can't calculate coordinates return - painter.beginPaintingWidget(self, posn) - painter.save() + painter = phelper.painter(self, posn) textpen = s.get('Text').makeQPen() painter.setPen(textpen) font = s.get('Text').makeQFont(painter) # we should only be able to move non-dataset labels - self.controlgraphitems = [] isnotdataset = ( not s.get('xPos').isDataset(d) and not s.get('yPos').isDataset(d) ) + controlgraphitems = [] for index, (x, y, t) in enumerate(itertools.izip( xp, yp, itertools.cycle(text))): # render the text @@ -115,16 +112,14 @@ # add cgi for adjustable positions if isnotdataset: - cgi = controlgraph.ControlMovableBox(self, tbounds, - painter, + cgi = controlgraph.ControlMovableBox(self, tbounds, phelper, crosspos = (x, y)) cgi.labelpt = (x, y) cgi.widgetposn = posn cgi.index = index - self.controlgraphitems.append(cgi) + controlgraphitems.append(cgi) - painter.restore() - painter.endPaintingWidget() + phelper.setControlGraph(self, controlgraphitems) def updateControlItem(self, cgi): """Update position of point given new name and vals.""" diff -Nru veusz-1.10/widgets/vectorfield.py veusz-1.14/widgets/vectorfield.py --- veusz-1.10/widgets/vectorfield.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/vectorfield.py 2011-11-22 20:23:31.000000000 +0000 @@ -18,8 +18,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: vectorfield.py 1387 2010-08-29 15:24:57Z jeremysanders $ - import itertools import numpy as N @@ -126,10 +124,24 @@ axrange[0] = min( axrange[0], dyrange[0] ) axrange[1] = max( axrange[1], dyrange[1] ) - def draw(self, parentposn, painter, outerbounds = None): + def drawKeySymbol(self, number, painter, x, y, width, height): + """Draw the plot symbol and/or line.""" + painter.save() + + s = self.settings + painter.setPen( s.Line.makeQPenWHide(painter) ) + painter.setBrush( s.get('Fill').makeQBrushWHide() ) + utils.plotLineArrow(painter, x+width, y+height*0.5, + width, 180, height*0.25, + arrowleft=s.arrowfront, + arrowright=s.arrowback) + + painter.restore() + + def draw(self, parentposn, phelper, outerbounds = None): """Draw the widget.""" - posn = plotters.GenericPlotter.draw(self, parentposn, painter, + posn = plotters.GenericPlotter.draw(self, parentposn, phelper, outerbounds = outerbounds) x1, y1, x2, y2 = posn s = self.settings @@ -160,9 +172,8 @@ return # clip data within bounds of plotter - painter.beginPaintingWidget(self, posn) - painter.save() - cliprect = self.clipAxesBounds(painter, axes, posn) + cliprect = self.clipAxesBounds(axes, posn) + painter = phelper.painter(self, posn, clip=cliprect) baselength = s.get('baselength').convert(painter) @@ -218,10 +229,7 @@ utils.plotLineArrow(painter, x, y, l, a, asize, arrowleft=s.arrowfront, arrowright=s.arrowback) - - painter.restore() - painter.endPaintingWidget() - + # allow the factory to instantiate a vector field document.thefactory.register( VectorField ) diff -Nru veusz-1.10/widgets/widget.py veusz-1.14/widgets/widget.py --- veusz-1.10/widgets/widget.py 2010-12-12 12:41:11.000000000 +0000 +++ veusz-1.14/widgets/widget.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,8 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: widget.py 1325 2010-07-12 13:04:13Z jeremysanders $ - import itertools import veusz.document as document @@ -53,6 +51,9 @@ class Widget(object): """ Fundamental plotting widget interface.""" + # differentiate widgets, settings and setting + nodetype = 'widget' + typename = 'generic' allowusercreation = False @@ -85,7 +86,8 @@ self.position = (0., 0., 1., 1.) # settings for widget - self.settings = setting.Settings( 'Widget_' + self.typename ) + self.settings = setting.Settings( 'Widget_' + self.typename, + setnsmode='widgetsettings' ) self.settings.parent = self self.addSettings(self.settings) @@ -93,9 +95,6 @@ # actions for widget self.actions = [] - # pts user can move around - self.controlgraphitems = [] - @classmethod def addSettings(klass, s): """Add items to settings s.""" @@ -129,7 +128,6 @@ raise ValueError, 'New name "%s" already exists' % name self.name = name - self.document.setModified() def addDefaultSubWidgets(self): '''Add default sub widgets to widget, if any''' @@ -257,6 +255,13 @@ raise ValueError, \ "Cannot remove graph '%s' - does not exist" % name + def widgetSiblingIndex(self): + """Get index of widget in its siblings.""" + if self.parent is None: + return 0 + else: + return self.parent.children.index(self) + def _getPath(self): """Returns a path for the object, e.g. /plot1/x.""" @@ -272,7 +277,8 @@ return build path = property(_getPath) - def computeBounds(self, parentposn, painter, margins = (0., 0., 0., 0.)): + def computeBounds(self, parentposn, painthelper, + margins = (0., 0., 0., 0.)): """Compute a bounds array, giving the bounding box for the widget.""" # get parent's position @@ -285,20 +291,20 @@ dx1, dy1, dx2, dy2 = margins return [ x1+dx1, y1+dy1, x2-dx2, y2-dy2 ] - def draw(self, parentposn, painter, outerbounds = None): + def draw(self, parentposn, painthelper, outerbounds = None): """Draw the widget and its children in posn (a tuple with x1,y1,x2,y2). painter is the widget.Painter to draw on outerbounds contains "ultimate" bounds we don't go outside """ - bounds = self.computeBounds(parentposn, painter) + bounds = self.computeBounds(parentposn, painthelper) if not self.settings.hide: # iterate over children in reverse order for c in reversed(self.children): - c.draw(bounds, painter, outerbounds=outerbounds) + c.draw(bounds, painthelper, outerbounds=outerbounds) # return our final bounds return bounds @@ -401,7 +407,6 @@ if existingname: w.name = w.chooseName() - self.document.setModified(True) return True def updateControlItem(self, controlitem, pos): diff -Nru veusz-1.10/windows/consolewindow.py veusz-1.14/windows/consolewindow.py --- veusz-1.10/windows/consolewindow.py 2010-12-12 12:41:08.000000000 +0000 +++ veusz-1.14/windows/consolewindow.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,8 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: consolewindow.py 1180 2010-02-27 18:03:13Z jeremysanders $ - import codeop import traceback import sys @@ -60,6 +58,7 @@ qt4.QLineEdit.__init__(self, *args) self.history = [] self.history_posn = 0 + self.entered_text = '' qt4.QObject.connect( self, qt4.SIGNAL("returnPressed()"), self.slotReturnPressed ) @@ -73,9 +72,10 @@ command = unicode( self.text() ) self.setText("") - # keep the command for history (and move back to top) + # keep the command for history self.history.append( command ) - self.history_posn = 0 + self.history_posn = len(self.history) + self.entered_text = '' # tell the console we have a command self.emit( qt4.SIGNAL("sigEnter"), command) @@ -91,29 +91,50 @@ # check whether one of the "history keys" has been pressed if code in _CommandEdit.historykeys: - # move up or down in the history list + # look for the next or previous history item which our current text + # is a prefix of + if self.isModified(): + text = unicode(self.text()) + self.history_posn = len(self.history) + else: + text = self.entered_text + if code == qt4.Qt.Key_Up: - self.history_posn += 1 + step = -1 elif code == qt4.Qt.Key_Down: - self.history_posn -= 1 + step = 1 + + newpos = self.history_posn + step - # make sure counter is within bounds - self.history_posn = max(self.history_posn, 0) - self.history_posn = min(self.history_posn, len(self.history)) + while True: + if newpos >= len(self.history): + break + if newpos < 0: + return + if self.history[newpos].startswith(text): + break + + newpos += step + + if newpos >= len(self.history): + # go back to whatever the user had typed in + self.history_posn = len(self.history) + self.setText(self.entered_text) + return + + # found a relevant history item + self.history_posn = newpos # user has modified text since last set if self.isModified(): - self.history.append( unicode(self.text()) ) - self.history_posn += 1 + self.entered_text = text # replace the text in the control - text = '' - if self.history_posn > 0: - text = self.history[ -self.history_posn ] + text = self.history[ self.history_posn ] self.setText(text) -introtext=u'''Welcome to Veusz --- a scientific plotting application.
    -Veusz version %s, Copyright \u00a9 2003-2010 Jeremy Sanders <jeremy@jeremysanders.net>
    +introtext=u'''Welcome to Veusz %s --- a scientific plotting application.
    +Copyright \u00a9 2003-2011 Jeremy Sanders <jeremy@jeremysanders.net> and contributors.
    Veusz comes with ABSOLUTELY NO WARRANTY. Veusz is Free Software, and you are
    welcome to redistribute it under certain conditions. Enter "GPL()" for details.
    This window is a Python command line console and acts as a calculator.
    @@ -136,6 +157,7 @@ # start an interpreter instance to the document self.interpreter = document.CommandInterpreter(thedocument) + self.document = thedocument # output from the interpreter goes to self.output_stdxxx self.con_stdout = _Writer(self.output_stdout) @@ -179,6 +201,33 @@ self.connect( thedocument, qt4.SIGNAL("sigLog"), self.slotDocumentLog ) + def _makeTextFormat(self, cursor, color): + fmt = cursor.charFormat() + + if color is not None: + brush = qt4.QBrush(color) + fmt.setForeground(brush) + else: + # use the default foreground color + fmt.clearForeground() + + return fmt + + def appendOutput(self, text, style): + """Add text to the tail of the error log, with a specified style""" + if style == 'error': + color = setting.settingdb.color('error') + elif style == 'command': + color = setting.settingdb.color('command') + else: + color = None + + cursor = self._outputdisplay.textCursor() + cursor.movePosition(qt4.QTextCursor.End) + cursor.insertText(text, self._makeTextFormat(cursor, color)) + self._outputdisplay.setTextCursor(cursor) + self._outputdisplay.ensureCursorVisible() + def runFunction(self, func): """Execute the function within the console window, trapping exceptions.""" @@ -190,14 +239,16 @@ sys.stderr = _Writer(self.output_stderr) # catch any exceptions, printing problems to stderr + self.document.suspendUpdates() try: func() - except Exception, e: + except: # print out the backtrace to stderr i = sys.exc_info() backtrace = traceback.format_exception( *i ) for l in backtrace: sys.stderr.write(l) + self.document.enableUpdates() # return output streams sys.stdout = temp_stdout @@ -223,24 +274,16 @@ def output_stdout(self, text): """ Write text in stdout font to the log.""" self.checkVisible() - self._outputdisplay.insertPlainText(text) - self._outputdisplay.ensureCursorVisible() + self.appendOutput(text, 'normal') def output_stderr(self, text): """ Write text in stderr font to the log.""" self.checkVisible() - - # insert text as bright error color - self._outputdisplay.setTextColor( setting.settingdb.color('error') ) - self._outputdisplay.insertPlainText(text) - self._outputdisplay.setTextColor( - qt4.qApp.palette().color(qt4.QPalette.Text) ) - self._outputdisplay.ensureCursorVisible() + self.appendOutput(text, 'error') def insertTextInOutput(self, text): """ Inserts the text into the log.""" - self._outputdisplay.append( text ) - self._outputdisplay.ensureCursorVisible() + self.appendOutput(text, 'normal') def slotEnter(self, command): """ Called if the return key is pressed in the edit control.""" @@ -261,12 +304,7 @@ prompt = '...' # output the command in the log pane - self._outputdisplay.setTextColor( setting.settingdb.color('command') ) - self._outputdisplay.insertPlainText('%s %s\n' % (prompt, command)) - self._outputdisplay.setTextColor( - qt4.qApp.palette().color(qt4.QPalette.Text) ) - - self._outputdisplay.ensureCursorVisible() + self.appendOutput('%s %s\n' % (prompt, command), 'command') # are we ready to run this? if c is None or (len(command) != 0 and diff -Nru veusz-1.10/windows/datanavigator.py veusz-1.14/windows/datanavigator.py --- veusz-1.10/windows/datanavigator.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/windows/datanavigator.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2011 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################### + +import veusz.qtall as qt4 +from veusz.qtwidgets.datasetbrowser import DatasetBrowser + +class DataNavigatorWindow(qt4.QDockWidget): + """A dock window containing a dataset browsing widget.""" + + def __init__(self, thedocument, mainwin, *args): + qt4.QDockWidget.__init__(self, *args) + self.setWindowTitle("Data - Veusz") + self.setObjectName("veuszdatawindow") + + self.nav = DatasetBrowser(thedocument, mainwin, self) + self.setWidget(self.nav) diff -Nru veusz-1.10/windows/icons/button_ternary.svg veusz-1.14/windows/icons/button_ternary.svg --- veusz-1.10/windows/icons/button_ternary.svg 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/windows/icons/button_ternary.svg 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,88 @@ + + + + + + + + + + image/svg+xml + + + + Jeremy Sanders + + + + + Copyright (C) Jeremy Sanders + + + + + + + + + + + + + diff -Nru veusz-1.10/windows/icons/error_linehorzbar.svg veusz-1.14/windows/icons/error_linehorzbar.svg --- veusz-1.10/windows/icons/error_linehorzbar.svg 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/windows/icons/error_linehorzbar.svg 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,99 @@ + + + + + + + + + + image/svg+xml + + + + Jeremy Sanders + + + + + Copyright (C) Jeremy Sanders + + + + + + + + + + + + + + + + + + diff -Nru veusz-1.10/windows/icons/error_linevertbar.svg veusz-1.14/windows/icons/error_linevertbar.svg --- veusz-1.10/windows/icons/error_linevertbar.svg 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/windows/icons/error_linevertbar.svg 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,99 @@ + + + + + + + + + + image/svg+xml + + + + Jeremy Sanders + + + + + Copyright (C) Jeremy Sanders + + + + + + + + + + + + + + + + + + diff -Nru veusz-1.10/windows/icons/kde-clipboard.svg veusz-1.14/windows/icons/kde-clipboard.svg --- veusz-1.10/windows/icons/kde-clipboard.svg 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/windows/icons/kde-clipboard.svg 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,132 @@ + + + + + + + + Part of the Flat Icon Collection (Thu Aug 26 14:38:01 2004) + + + +
  • + + + + + + </Agent> + </publisher> + <creator + id="creator28"> + <Agent + id="Agent29" + about=""> + <title + id="title30">Danny Allen + + + + + Danny Allen + + + + image/svg+xml + + + + + en + + + + + image/svg+xml + + + + + + + + + + diff -Nru veusz-1.10/windows/icons/veusz-pick-data.svg veusz-1.14/windows/icons/veusz-pick-data.svg --- veusz-1.10/windows/icons/veusz-pick-data.svg 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/windows/icons/veusz-pick-data.svg 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,73 @@ + + + + + + + + image/svg+xml + + + + + + + + + + diff -Nru veusz-1.10/windows/__init__.py veusz-1.14/windows/__init__.py --- veusz-1.10/windows/__init__.py 2010-12-12 12:41:08.000000000 +0000 +++ veusz-1.14/windows/__init__.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,6 +16,4 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: __init__.py 872 2008-12-29 12:51:59Z jeremysanders $ - """Veusz windows module.""" diff -Nru veusz-1.10/windows/mainwindow.py veusz-1.14/windows/mainwindow.py --- veusz-1.10/windows/mainwindow.py 2010-12-12 12:41:08.000000000 +0000 +++ veusz-1.14/windows/mainwindow.py 2011-11-22 20:23:31.000000000 +0000 @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2003 Jeremy S. Sanders # Email: Jeremy Sanders # @@ -16,13 +17,12 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: mainwindow.py 1455 2010-11-26 22:35:03Z jeremysanders $ - """Implements the main window of the application.""" import os.path import sys import traceback +import glob import veusz.qtall as qt4 @@ -34,6 +34,7 @@ import consolewindow import plotwindow import treeeditwindow +from datanavigator import DataNavigatorWindow from veusz.dialogs.aboutdialog import AboutDialog from veusz.dialogs.reloaddata import ReloadData @@ -83,6 +84,13 @@ win.treeedit.doInitialWidgetSelect() cls.windows.append(win) + + # check if tutorial wanted + if not setting.settingdb['ask_tutorial']: + win.askTutorial() + # don't ask again + setting.settingdb['ask_tutorial'] = True + return win def __init__(self, *args): @@ -121,6 +129,8 @@ self.formatdock = treeeditwindow.FormatDock(self.document, self.treeedit, self) self.addDockWidget(qt4.Qt.LeftDockWidgetArea, self.formatdock) + self.datadock = DataNavigatorWindow(self.document, self, self) + self.addDockWidget(qt4.Qt.RightDockWidgetArea, self.datadock) # make the console window a dock self.console = consolewindow.ConsoleWindow(self.document, @@ -129,15 +139,36 @@ self.interpreter = self.console.interpreter self.addDockWidget(qt4.Qt.BottomDockWidgetArea, self.console) - # keep page number up to date + # assemble the statusbar statusbar = self.statusbar = qt4.QStatusBar(self) self.setStatusBar(statusbar) self.updateStatusbar('Ready') - self.pagelabel = qt4.QLabel(statusbar) - statusbar.addWidget(self.pagelabel) + + # a label for the picker readout + self.pickerlabel = qt4.QLabel(statusbar) + self._setPickerFont(self.pickerlabel) + statusbar.addPermanentWidget(self.pickerlabel) + self.pickerlabel.hide() + + # plot queue - how many plots are currently being drawn + self.plotqueuecount = 0 + self.connect( self.plot, qt4.SIGNAL("queuechange"), + self.plotQueueChanged ) + self.plotqueuelabel = qt4.QLabel() + self.plotqueuelabel.setToolTip("Number of rendering jobs remaining") + statusbar.addWidget(self.plotqueuelabel) + self.plotqueuelabel.show() + + # a label for the cursor position readout self.axisvalueslabel = qt4.QLabel(statusbar) - statusbar.addWidget(self.axisvalueslabel) + statusbar.addPermanentWidget(self.axisvalueslabel) self.axisvalueslabel.show() + self.slotUpdateAxisValues(None) + + # a label for the page number readout + self.pagelabel = qt4.QLabel(statusbar) + statusbar.addPermanentWidget(self.pagelabel) + self.pagelabel.show() # working directory - use previous one self.dirname = setdb.get('dirname', qt4.QDir.homePath()) @@ -150,7 +181,11 @@ self.slotUpdatePage ) self.connect( self.plot, qt4.SIGNAL("sigAxisValuesFromMouse"), self.slotUpdateAxisValues ) - + self.connect( self.plot, qt4.SIGNAL("sigPickerEnabled"), + self.slotPickerEnabled ) + self.connect( self.plot, qt4.SIGNAL("sigPointPicked"), + self.slotUpdatePickerLabel ) + # disable save if already saved self.connect( self.document, qt4.SIGNAL("sigModified"), self.slotModifiedDoc ) @@ -161,8 +196,8 @@ # if a widget in the plot window is clicked by the user self.connect( self.plot, qt4.SIGNAL("sigWidgetClicked"), self.treeedit.selectWidget ) - self.connect( self.treeedit, qt4.SIGNAL("widgetSelected"), - self.plot.selectedWidget ) + self.connect( self.treeedit, qt4.SIGNAL("widgetsSelected"), + self.plot.selectedWidgets ) # enable/disable undo/redo self.connect(self.menus['edit'], qt4.SIGNAL('aboutToShow()'), @@ -267,6 +302,7 @@ """Undo the previous operation""" if self.document.canUndo(): self.document.undoOperation() + self.treeedit.checkWidgetSelected() def slotEditRedo(self): """Redo the previous operation""" @@ -405,6 +441,10 @@ 'view.console': a(self, 'Show or hide console window', 'Console window', None, checkable=True), + 'view.datanav': + a(self, 'Show or hide data navigator window', 'Data navigator window', + None, checkable=True), + 'view.maintool': a(self, 'Show or hide main toolbar', 'Main toolbar', None, checkable=True), @@ -452,6 +492,9 @@ 'help.bug': a(self, 'Report a bug on the internet', 'Suggestions and bugs', self.slotHelpBug), + 'help.tutorial': + a(self, 'An interactive Veusz tutorial', + 'Tutorial', self.slotHelpTutorial), 'help.about': a(self, 'Displays information about the program', 'About...', self.slotHelpAbout, icon='veusz') @@ -491,12 +534,14 @@ editmenu = [ 'edit.undo', 'edit.redo', '', + ['edit.select', '&Select', []], + '', 'edit.prefs', 'edit.stylesheet', 'edit.custom', '' ] viewwindowsmenu = [ 'view.edit', 'view.props', 'view.format', - 'view.console', + 'view.console', 'view.datanav', '', 'view.maintool', 'view.viewtool', 'view.addtool', 'view.edittool' @@ -521,6 +566,10 @@ helpmenu = [ 'help.home', 'help.project', 'help.bug', '', + 'help.tutorial', + '', + ['help.examples', '&Example documents', []], + '', 'help.about' ] @@ -541,6 +590,28 @@ self.menus = {} utils.constructMenus(self.menuBar(), self.menus, menus, self.vzactions) + self.populateExamplesMenu() + + def _setPickerFont(self, label): + f = label.font() + f.setBold(True) + f.setPointSizeF(f.pointSizeF() * 1.2) + label.setFont(f) + + def populateExamplesMenu(self): + """Add examples to help menu.""" + + examples = glob.glob(os.path.join(utils.exampleDirectory, '*.vsz')) + menu = self.menus["help.examples"] + for ex in sorted(examples): + name = os.path.splitext(os.path.basename(ex))[0] + + def _openexample(ex=ex): + MainWindow.CreateWindow(ex) + + a = menu.addAction(name, _openexample) + a.setStatusTip("Open %s example document" % name) + def defineViewWindowMenu(self): """Setup View -> Window menu.""" @@ -557,6 +628,7 @@ (self.propdock, 'view.props'), (self.formatdock, 'view.format'), (self.console, 'view.console'), + (self.datadock, 'view.datanav'), (self.maintoolbar, 'view.maintool'), (self.datatoolbar, 'view.datatool'), (self.treeedit.edittoolbar, 'view.edittool'), @@ -584,6 +656,7 @@ self.connect(dialog, qt4.SIGNAL('dialogFinished'), self.deleteDialog) self.dialogs.append(dialog) dialog.show() + self.emit( qt4.SIGNAL('dialogShown'), dialog ) def deleteDialog(self, dialog): """Remove dialog from list of dialogs.""" @@ -599,10 +672,15 @@ self.showDialog(dialog) return dialog - def slotDataEdit(self): - """Edit existing datasets.""" + def slotDataEdit(self, editdataset=None): + """Edit existing datasets. + + If editdataset is set to a dataset name, edit this dataset + """ dialog = dataeditdialog.DataEditDialog(self, self.document) self.showDialog(dialog) + if editdataset is not None: + dialog.selectDataset(editdataset) return dialog def slotDataCreate(self): @@ -648,6 +726,32 @@ qt4.QDesktopServices.openUrl( qt4.QUrl('https://gna.org/bugs/?group=veusz') ) + def askTutorial(self): + """Ask if tutorial wanted.""" + retn = qt4.QMessageBox.question( + self, "Veusz Tutorial", + "Veusz includes a tutorial to help get you started.\n" + "Would you like to start the tutorial now?\n" + "If not, you can access it later through the Help menu.", + qt4.QMessageBox.Yes | qt4.QMessageBox.No + ) + + if retn == qt4.QMessageBox.Yes: + self.slotHelpTutorial() + + def slotHelpTutorial(self): + """Show a Veusz tutorial.""" + if self.document.isBlank(): + # run the tutorial + from veusz.windows.tutorial import TutorialDock + tutdock = TutorialDock(self.document, self, self) + self.addDockWidget(qt4.Qt.RightDockWidgetArea, tutdock) + tutdock.show() + else: + # open up a blank window for tutorial + win = self.CreateWindow() + win.slotHelpTutorial() + def slotHelpAbout(self): """Show about dialog.""" AboutDialog(self).exec_() @@ -688,8 +792,8 @@ self.slotFileSave() # store working directory - setdb['dirname'] = unicode(self.dirname) - setdb['dirname_export'] = unicode(self.dirname_export) + setdb['dirname'] = self.dirname + setdb['dirname_export'] = self.dirname_export # store the current geometry in the settings database geometry = ( self.x(), self.y(), self.width(), self.height() ) @@ -699,6 +803,9 @@ data = str(self.saveState()) setdb['geometry_mainwindowstate'] = data + # save current setting db + setdb.writeSettings() + event.accept() def setupWindowGeometry(self): @@ -756,6 +863,11 @@ self.setWindowTitle( "%s - Veusz" % os.path.basename(self.filename) ) + def plotQueueChanged(self, incr): + self.plotqueuecount += incr + text = u'•' * self.plotqueuecount + self.plotqueuelabel.setText(text) + def _fileSaveDialog(self, filetype, filedescr, dialogtitle): """A generic file save dialog for exporting / saving.""" @@ -768,7 +880,7 @@ # okay was selected (and is okay to overwrite if it exists) if fd.exec_() == qt4.QDialog.Accepted: # save directory for next time - self.dirname = fd.directory() + self.dirname = fd.directory().absolutePath() # update the edit box filename = unicode( fd.selectedFiles()[0] ) if os.path.splitext(filename)[1] == '': @@ -789,7 +901,7 @@ # if the user chooses a file if fd.exec_() == qt4.QDialog.Accepted: # save directory for next time - self.dirname = fd.directory() + self.dirname = fd.directory().absolutePath() filename = unicode( fd.selectedFiles()[0] ) try: @@ -966,6 +1078,8 @@ self.dirname = os.path.dirname( os.path.abspath(filename) ) self.dirname_export = self.dirname + # notify cmpts which need notification that doc has finished opening + self.emit(qt4.SIGNAL("documentopened")) qt4.QApplication.restoreOverrideCursor() def addRecentFile(self, filename): @@ -1030,41 +1144,37 @@ # File types we can export to in the form ([extensions], Name) fd = qt4.QFileDialog(self, 'Export page') fd.setDirectory( self.dirname_export ) - + fd.setFileMode( qt4.QFileDialog.AnyFile ) fd.setAcceptMode( qt4.QFileDialog.AcceptSave ) # Create a mapping between a format string and extensions filtertoext = {} + # convert extensions to filter + exttofilter = {} filters = [] # a list of extensions which are allowed validextns = [] - formats = self.document.getExportFormats() + formats = document.Export.formats for extns, name in formats: extensions = " ".join(["*." + item for item in extns]) # join eveything together to make a filter string filterstr = '%s (%s)' % (name, extensions) filtertoext[filterstr] = extns + for e in extns: + exttofilter[e] = filterstr filters.append(filterstr) validextns += extns - - try: - # Qt >= 4.4 (reqd for Fedora 12 Qt 4.6) - fd.setNameFilters(filters) - except AttributeError: - fd.setFilters(filters) + fd.setNameFilters(filters) # restore last format if possible try: filt = setdb['export_lastformat'] - try: - # Qt >= 4.4 (reqd for Fedora 12 Qt 4.6) - fd.selectNameFilter(filt) - except AttributeError: - fd.selectFilter(filt) + fd.selectNameFilter(filt) extn = formats[filters.index(filt)][0][0] except (KeyError, IndexError, ValueError): - extn = 'eps' + extn = 'pdf' + fd.selectNameFilter( exttofilter[extn] ) if self.filename: # try to convert current filename to export name @@ -1074,7 +1184,7 @@ if fd.exec_() == qt4.QDialog.Accepted: # save directory for next time - self.dirname_export = unicode(fd.directory().absolutePath()) + self.dirname_export = fd.directory().absolutePath() filterused = str(fd.selectedFilter()) setdb['export_lastformat'] = filterused @@ -1090,19 +1200,23 @@ # this is the extension without the dot ext = os.path.splitext(filename)[1][1:] if (ext not in validextns) and (ext not in chosenextns): - filename = filename + "." + chosenextns[0] + filename += "." + chosenextns[0] + e = document.Export( self.document, + filename, + self.plot.getPageNumber(), + bitmapdpi=setdb['export_DPI'], + pdfdpi=setdb['export_DPI_PDF'], + antialias=setdb['export_antialias'], + color=setdb['export_color'], + quality=setdb['export_quality'], + backcolor=setdb['export_background'] ) try: - self.document.export(filename, self.plot.getPageNumber(), - dpi=setdb['export_DPI'], - antialias=setdb['export_antialias'], - color=setdb['export_color'], - quality=setdb['export_quality'], - backcolor=setdb['export_background']) + e.export() except (IOError, RuntimeError), inst: qt4.QMessageBox.critical(self, "Veusz", "Error exporting file:\n%s" % inst) - + # restore the cursor qt4.QApplication.restoreOverrideCursor() @@ -1182,6 +1296,40 @@ else: self.axisvalueslabel.setText('No position') + def slotPickerEnabled(self, enabled): + if enabled: + self.pickerlabel.setText('No point selected') + self.pickerlabel.show() + else: + self.pickerlabel.hide() + + def slotUpdatePickerLabel(self, info): + """Display the picked point""" + xv, yv = info.coords + xn, yn = info.labels + xt, yt = info.displaytype + ix = str(info.index) + if ix: + ix = '[' + ix + ']' + + # format values for display + def fmt(val, dtype): + if dtype == 'date': + return utils.dateFloatToString(val) + elif dtype == 'numeric': + return '%0.5g' % val + elif dtype == 'text': + return val + else: + raise RuntimeError + + xtext = fmt(xv, xt) + ytext = fmt(yv, yt) + + t = '%s: %s%s = %s, %s%s = %s' % ( + info.widget.name, xn, ix, xtext, yn, ix, ytext) + self.pickerlabel.setText(t) + def slotAllowedImportsDoc(self, module, names): """Are allowed imports?""" diff -Nru veusz-1.10/windows/plotwindow.py veusz-1.14/windows/plotwindow.py --- veusz-1.10/windows/plotwindow.py 2010-12-12 12:41:08.000000000 +0000 +++ veusz-1.14/windows/plotwindow.py 2011-11-22 20:23:31.000000000 +0000 @@ -19,10 +19,9 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: plotwindow.py 1274 2010-06-10 19:00:47Z jeremysanders $ - import sys from itertools import izip +import traceback import veusz.qtall as qt4 import numpy as N @@ -33,123 +32,176 @@ import veusz.utils as utils import veusz.widgets as widgets -class RecordingPainter(document.Painter): - """A painter to remember where the positions of the - painted widgets.""" - - def __init__(self, device): - """Start painting on device.""" - document.Painter.__init__(self) - self.widgetpositions = [] - self.widgetpositionslookup = {} - self.begin(device) - - def beginPaintingWidget(self, widget, bounds): - """Record the widget and position.""" - self.widgetpositions.append( (widget, bounds) ) - self.widgetpositionslookup[widget] = bounds - -class PointPainter(document.Painter): - """A simple painter variant which works out the last widget - to overlap with the point specified.""" - - def __init__(self, pixmap, x, y): - """Watch the point x, y.""" - document.Painter.__init__(self) - self.x = x - self.y = y - self.widget = None - self.bounds = {} - - self.pixmap = pixmap - self.begin(pixmap) - - def beginPaintingWidget(self, widget, bounds): - - if (isinstance(widget, widgets.Graph) and - bounds[0] <= self.x and bounds[1] <= self.y and - bounds[2] >= self.x and bounds[3] >= self.y): - self.widget = widget - - # record bounds of each widget - self.bounds[widget] = bounds - -class ClickPainter(document.Painter): - """A variant of a painter which checks to see whether a certain - sized area is drawn over each time a widget is drawn. This allows - the program to identify clicks with a widget. +class PickerCrosshairItem( qt4.QGraphicsPathItem ): + """The picker cross widget: it moves from point to point and curve to curve + with the arrow keys, and hides itself when it looses focus""" + def __init__(self, parent=None): + path = qt4.QPainterPath() + path.addRect(-4, -4, 8, 8) + path.addRect(-5, -5, 10, 10) + path.moveTo(-8, 0) + path.lineTo(8, 0) + path.moveTo(0, -8) + path.lineTo(0, 8) + + qt4.QGraphicsPathItem.__init__(self, path, parent) + self.setBrush(qt4.QBrush(qt4.Qt.black)) + self.setFlags(self.flags() | qt4.QGraphicsItem.ItemIsFocusable) + + def paint(self, painter, option, widget): + """Override this to enforce the global antialiasing setting""" + aa = setting.settingdb['plot_antialias'] + painter.save() + painter.setRenderHint(qt4.QPainter.Antialiasing, aa) + qt4.QGraphicsPathItem.paint(self, painter, option, widget) + painter.restore() + + def focusOutEvent(self, event): + qt4.QGraphicsPathItem.focusOutEvent(self, event) + self.hide() + +class RenderControl(qt4.QObject): + """Object for rendering plots in a separate thread.""" + + def __init__(self, plotwindow): + """Start up numthreads rendering threads.""" + qt4.QObject.__init__(self) + self.sem = qt4.QSemaphore() + self.mutex = qt4.QMutex() + self.threads = [] + self.exit = False + self.latestjobs = [] + self.latestaddedjob = -1 + self.latestdrawnjob = -1 + self.plotwindow = plotwindow + + self.updateNumberThreads() + + def updateNumberThreads(self, num=None): + """Changes the number of rendering threads.""" + if num is None: + if qt4.QFontDatabase.supportsThreadedFontRendering(): + # use number of threads in preference + num = setting.settingdb['plot_numthreads'] + else: + # disable threads + num = 0 - The painter monitors a certain sized region in the output pixmap - """ + if self.threads: + # delete old ones + self.exit = True + self.sem.release(len(self.threads)) + for t in self.threads: + t.wait() + del self.threads[:] + self.exit = False + + # start new ones + for i in xrange(num): + t = RenderThread(self) + t.start() + self.threads.append(t) + + def exitThreads(self): + """Exit threads started.""" + self.updateNumberThreads(num=0) - def __init__(self, pixmap, xmin, ymin, xw, yw): - """Monitor the region from (xmin, ymin) to (xmin+xw, ymin+yw). + def processNextJob(self): + """Take a job from the queue and process it. - pixmap is the region the painter monitors + emits renderfinished(jobid, img, painthelper) + when done, if job has not been superseded """ - - document.Painter.__init__(self) - self.pixmap = pixmap - self.xmin = xmin - self.ymin = ymin - self.xw = xw - self.yw = yw - - # a stack keeping track of the widgets being painted currently - self.widgets = [] - # a stack of starting state pixmaps of the widgets - self.pixmaps = [] - # a list of widgets which change the region - self.foundwidgets = [] - - # we hope this color isn't actually used by the user - # if a pixel changes from this color, a widget has drawn something - self.specialcolor = qt4.QColor(254, 255, 254) - self.pixmap.fill(self.specialcolor) - self.begin(self.pixmap) - - def beginPaintingWidget(self, widget, bounds): - self.widgets.append(widget) - - # make a small pixmap of the starting state of the image - # we can compare this after the widget is painted - pixmap = self.pixmap.copy(self.xmin, self.ymin, self.xw, self.yw) - self.pixmaps.append(pixmap) - - def endPaintingWidget(self): - """When a widget has finished.""" - - oldpixmap = self.pixmaps.pop() - widget = self.widgets.pop() - - # compare current pixmap for region with initial contents - # hope this is not needed - #self.flush() - newpixmap = self.pixmap.copy(self.xmin, self.ymin, self.xw, self.yw) - - if oldpixmap.toImage() != newpixmap.toImage(): - # drawn here, so make a note - self.foundwidgets.append(widget) - - # copy back original - self.drawPixmap(qt4.QRect(self.xmin, self.ymin, self.xw, self.yw), - oldpixmap, - qt4.QRect(0, 0, self.xw, self.yw)) + self.mutex.lock() + jobid, helper = self.latestjobs[-1] + del self.latestjobs[-1] + lastadded = self.latestaddedjob + self.mutex.unlock() + + # don't process jobs which have been superseded + if lastadded == jobid: + img = qt4.QImage(helper.pagesize[0], helper.pagesize[1], + qt4.QImage.Format_ARGB32_Premultiplied) + img.fill( setting.settingdb.color('page').rgb() ) + + painter = qt4.QPainter(img) + aa = self.plotwindow.antialias + painter.setRenderHint(qt4.QPainter.Antialiasing, aa) + painter.setRenderHint(qt4.QPainter.TextAntialiasing, aa) + helper.renderToPainter(painter) + painter.end() + + self.mutex.lock() + # just throw away result if it older than the latest one + if jobid > self.latestdrawnjob: + self.emit( qt4.SIGNAL("renderfinished"), + jobid, img, helper ) + self.latestdrawnjob = jobid + self.mutex.unlock() + + # tell any listeners that a job has been processed + self.plotwindow.emit( qt4.SIGNAL("queuechange"), -1 ) + + def addJob(self, helper): + """Process drawing job in PaintHelper given.""" + + # indicate that there is a new item to be processed to listeners + self.plotwindow.emit( qt4.SIGNAL("queuechange"), 1 ) + + # add the job to the queue + self.mutex.lock() + self.latestaddedjob += 1 + self.latestjobs.append( (self.latestaddedjob, helper) ) + self.mutex.unlock() + + if self.threads: + # tell a thread to process job + self.sem.release(1) + else: + # process job in current thread if multithreading disabled + self.processNextJob() + +class RenderThread( qt4.QThread ): + """A thread for processing rendering jobs. + This is controlled by a RenderControl object + """ - def getFoundWidget(self): - """Return the widget lowest in the tree near the click of the mouse. + def __init__(self, rendercontrol): + qt4.QThread.__init__(self) + self.rc = rendercontrol + + def run(self): + """Repeat forever until told to exit. + If it aquires 1 resource from the semaphore it will process + the next job. """ - - if self.foundwidgets: - return self.foundwidgets[-1] - else: - return None + while True: + # wait until we can aquire the resources + self.rc.sem.acquire(1) + if self.rc.exit: + break + try: + self.rc.processNextJob() + except Exception: + sys.stderr.write("Error in rendering thread\n") + traceback.print_exc(file=sys.stderr) class PlotWindow( qt4.QGraphicsView ): """Class to show the plot(s) in a scrollable window.""" - intervals = [0, 100, 250, 500, 1000, 2000, 5000, 10000] + # how often the document can update + updateintervals = ( + (0, 'Disable'), + (-1, 'On document change'), + (100, 'Every 0.1s'), + (250, 'Every 0.25s'), + (500, 'Every 0.5s'), + (1000, 'Every 1s'), + (2000, 'Every 2s'), + (5000, 'Every 5s'), + (10000, 'Every 10s'), + ) def __init__(self, document, parent, menu=None): """Initialise the window. @@ -163,47 +215,65 @@ self.setScene(self.scene) # this graphics scene item is the actual graph - self.pixmapitem = self.scene.addPixmap( qt4.QPixmap(1, 1) ) - self.controlgraphs = [] - self.widgetcontrolgraphs = {} - self.selwidget = None + pixmap = qt4.QPixmap(1, 1) + self.dpi = (pixmap.logicalDpiX(), pixmap.logicalDpiY()) + self.pixmapitem = self.scene.addPixmap(pixmap) + + # set to be parent's actions self.vzactions = None + # for controlling plot elements + g = self.controlgraphgroup = qt4.QGraphicsItemGroup() + g.setHandlesChildEvents(False) + self.scene.addItem(g) + # zoom rectangle for zooming into graph (not shown normally) self.zoomrect = self.scene.addRect( 0, 0, 100, 100, qt4.QPen(qt4.Qt.DotLine) ) self.zoomrect.setZValue(2.) self.zoomrect.hide() + # picker graphicsitem for marking the picked point + self.pickeritem = PickerCrosshairItem() + self.scene.addItem(self.pickeritem) + self.pickeritem.setZValue(2.) + self.pickeritem.hide() + + # all the widgets that picker key-navigation might cycle through + self.pickerwidgets = [] + + # the picker state + self.pickerinfo = widgets.PickInfo() + # set up so if document is modified we are notified self.document = document self.docchangeset = -100 + self.oldpagenumber = -1 + self.connect(self.document, qt4.SIGNAL("sigModified"), + self.slotDocModified) + + # state of last plot from painthelper + self.painthelper = None - self.size = (1, 1) + self.lastwidgetsselected = [] self.oldzoom = -1. self.zoomfactor = 1. self.pagenumber = 0 - self.forceupdate = False self.ignoreclick = False - # work out dpi - self.widgetdpi = self.logicalDpiY() - - # convert size to pixels - self.setOutputSize() + # for rendering plots in separate threads + self.rendercontrol = RenderControl(self) + self.connect(self.rendercontrol, qt4.SIGNAL("renderfinished"), + self.slotRenderFinished) # mode for clicking self.clickmode = 'select' self.currentclickmode = None - # list of widgets and positions last painted - self.widgetpositions = [] - self.widgetpositionslookup = {} - # set up redrawing timer self.timer = qt4.QTimer(self) self.connect( self.timer, qt4.SIGNAL('timeout()'), - self.slotTimeout ) + self.checkPlotUpdate ) # for drag scrolling self.grabpos = None @@ -214,15 +284,19 @@ self.connect( self.scrolltimer, qt4.SIGNAL('timeout()'), self.slotBecomeScrollClick ) - # get update period from setting database - self.interval = setting.settingdb['plot_updateinterval'] - - # load antialias settings - self.antialias = setting.settingdb['plot_antialias'] + # get plot view updating policy + # -1: update on document changes + # 0: never update automatically + # >0: check for updates every x ms + self.interval = setting.settingdb['plot_updatepolicy'] + # if using a time-based document update checking, start timer if self.interval > 0: self.timer.start(self.interval) + # load antialias settings + self.antialias = setting.settingdb['plot_antialias'] + # allow window to get focus, to allow context menu self.setFocusPolicy(qt4.Qt.StrongFocus) @@ -235,6 +309,19 @@ # make the context menu object self._constructContextMenu() + def hideEvent(self, event): + """Window closing, so exit rendering threads.""" + self.rendercontrol.exitThreads() + qt4.QGraphicsView.hideEvent(self, event) + + def sizeHint(self): + """Return size hint for window.""" + p = self.pixmapitem.pixmap() + if p.width() <= 1 and p.height() <= 1: + # if the document has been uninitialized, get the doc size + return qt4.QSize(*self.document.docSize()) + return p.size() + def showToolbar(self, show=True): """Show or hide toolbar""" self.viewtoolbar.setVisible(show) @@ -299,6 +386,11 @@ 'Select items or scroll', None, icon='kde-mouse-pointer'), + 'view.pick': + a(self, 'Read data points on the graph', + 'Read data points', + None, + icon='veusz-pick-data'), 'view.zoomgraph': a(self, 'Zoom into graph', 'Zoom graph', None, @@ -314,7 +406,7 @@ 'view.zoomheight', 'view.zoompage', '', 'view.prevpage', 'view.nextpage', - 'view.select', 'view.zoomgraph', + 'view.select', 'view.pick', 'view.zoomgraph', ]), ] utils.constructMenus(menu, {'view': menu}, menuitems, @@ -339,13 +431,13 @@ # add items to toolbar utils.addToolbarActions(self.viewtoolbar, actions, ('view.prevpage', 'view.nextpage', - 'view.select', 'view.zoomgraph', - 'view.zoommenu')) + 'view.select', 'view.pick', + 'view.zoomgraph', 'view.zoommenu')) # define action group for various different selection models grp = self.selectactiongrp = qt4.QActionGroup(self) grp.setExclusive(True) - for a in ('view.select', 'view.zoomgraph'): + for a in ('view.select', 'view.pick', 'view.zoomgraph'): actions[a].setActionGroup(grp) actions[a].setCheckable(True) actions['view.select'].setChecked(True) @@ -396,17 +488,8 @@ return # try to work out in which widget the first point is in - bufferpixmap = qt4.QPixmap( *self.size ) - painter = PointPainter(bufferpixmap, pt1.x(), pt1.y()) - pagenumber = min( self.document.getNumberPages() - 1, - self.pagenumber ) - if pagenumber >= 0: - self.document.paintTo(painter, self.pagenumber, - scaling=self.zoomfactor, dpi=self.widgetdpi) - painter.end() - - # get widget - widget = painter.widget + widget = self.painthelper.pointInWidgetBounds( + pt1.x(), pt1.y(), widgets.Graph) if widget is None: return @@ -440,7 +523,11 @@ # convert points on plotter to axis coordinates # FIXME: Need To Trap Conversion Errors! - r = axis.plotterToGraphCoords(painter.bounds[axis], p) + try: + r = axis.plotterToGraphCoords( + self.painthelper.widgetBounds(axis), p) + except KeyError: + continue # invert if min and max are inverted if r[1] < r[0]: @@ -457,7 +544,69 @@ # finally change the axes self.document.applyOperation( document.OperationMultiple(operations,descr='zoom axes') ) - + + def axesForPoint(self, mousepos): + """Find all the axes which contain the given mouse position""" + + if self.painthelper is None: + return [] + + pos = self.mapToScene(mousepos) + px, py = pos.x(), pos.y() + + axes = [] + for widget, bounds in self.painthelper.widgetBoundsIterator( + widgettype=widgets.Axis): + # if widget is axis, and point lies within bounds + if ( px>=bounds[0] and px<=bounds[2] and + py>=bounds[1] and py<=bounds[3] ): + + # convert correct pointer position + if widget.settings.direction == 'horizontal': + val = px + else: + val = py + coords=widget.plotterToGraphCoords(bounds, N.array([val])) + axes.append( (widget, coords[0]) ) + + return axes + + def emitPicked(self, pickinfo): + """Report that a new point has been picked""" + + self.pickerinfo = pickinfo + self.pickeritem.setPos(pickinfo.screenpos[0], pickinfo.screenpos[1]) + self.emit(qt4.SIGNAL("sigPointPicked"), pickinfo) + + def doPick(self, mousepos): + """Find the point on any plot-like widget closest to the cursor""" + + self.pickerwidgets = [] + + pickinfo = widgets.PickInfo() + pos = self.mapToScene(mousepos) + + for w, bounds in self.painthelper.widgetBoundsIterator(): + try: + # ask the widget for its (visually) closest point to the cursor + info = w.pickPoint(pos.x(), pos.y(), bounds) + + # this is a pickable widget, so remember it for future key navigation + self.pickerwidgets.append(w) + + if info.distance < pickinfo.distance: + # and remember the overall closest + pickinfo = info + except AttributeError: + # ignore widgets that don't support axes or picking + continue + + if not pickinfo: + self.pickeritem.hide() + return + + self.emitPicked(pickinfo) + def slotBecomeScrollClick(self): """If the click is still down when this timer is reached then we turn the click into a scrolling click.""" @@ -472,7 +621,12 @@ qt4.QGraphicsView.mousePressEvent(self, event) # work out whether user is clicking on a control point - self.ignoreclick = self.itemAt(event.pos()) is not self.pixmapitem + # we have to ignore the item group which seems to be above + # its constituents + items = self.items(event.pos()) + if len(items) > 0 and isinstance(items[0], qt4.QGraphicsItemGroup): + del items[0] + self.ignoreclick = len(items)==0 or items[0] is not self.pixmapitem if event.button() == qt4.Qt.LeftButton and not self.ignoreclick: @@ -487,6 +641,11 @@ # select widgets! self.scrolltimer.start(400) + elif self.clickmode == 'pick': + self.pickeritem.show() + self.pickeritem.setFocus(qt4.Qt.MouseFocusReason) + self.doPick(event.pos()) + elif self.clickmode == 'scroll': qt4.QApplication.setOverrideCursor( qt4.QCursor(qt4.Qt.SizeAllCursor)) @@ -528,33 +687,23 @@ self.zoomrect.setRect( r.x(), r.y(), pos.x()-r.x(), pos.y()-r.y() ) - elif self.clickmode == 'select': + elif self.clickmode == 'select' or self.clickmode == 'pick': # find axes which map to this position - pos = self.mapToScene(event.pos()) - px, py = pos.x(), pos.y() - - vals = {} - for widget, bounds in self.widgetpositions: - # if widget is axis, and point lies within bounds - if ( isinstance(widget, widgets.Axis) and - px>=bounds[0] and px<=bounds[2] and - py>=bounds[1] and py<=bounds[3] ): - - # convert correct pointer position - if widget.settings.direction == 'horizontal': - val = px - else: - val = py - coords=widget.plotterToGraphCoords(bounds, N.array([val])) - vals[widget.name] = coords[0] + axes = self.axesForPoint(event.pos()) + vals = dict([ (a[0].name, a[1]) for a in axes ]) self.emit( qt4.SIGNAL('sigAxisValuesFromMouse'), vals ) + if self.currentclickmode == 'pick': + # drag the picker around + self.doPick(event.pos()) + def mouseReleaseEvent(self, event): """If the mouse button is released, check whether the mouse clicked on a widget, and emit a sigWidgetClicked(widget).""" qt4.QGraphicsView.mouseReleaseEvent(self, event) + if event.button() == qt4.Qt.LeftButton and not self.ignoreclick: event.accept() self.scrolltimer.stop() @@ -573,6 +722,62 @@ self.grabpos = None elif self.currentclickmode == 'viewgetclick': self.clickmode = 'select' + elif self.currentclickmode == 'pick': + self.currentclickmode = None + + def keyPressEvent(self, event): + """Keypad motion moves the picker if it has focus""" + if self.pickeritem.hasFocus(): + event.accept() + k = event.key() + if k == qt4.Qt.Key_Left or k == qt4.Qt.Key_Right: + # navigate to the previous or next point on the curve + dir = 'right' if k == qt4.Qt.Key_Right else 'left' + ix = self.pickerinfo.index + pickinfo = self.pickerinfo.widget.pickIndex( + ix, dir, self.painthelper.widgetBounds( + self.pickerinfo.widget)) + if not pickinfo: + # no more points visible in this direction + return + + self.emitPicked(pickinfo) + elif k == qt4.Qt.Key_Up or k == qt4.Qt.Key_Down: + # navigate to the next plot up or down on the screen + p = self.pickeritem.pos() + + oldw = self.pickerinfo.widget + pickinfo = widgets.PickInfo() + + dist = float('inf') + for w in self.pickerwidgets: + if w == oldw: + continue + + # ask the widgets to pick their point which is closest horizontally + # to the last (screen) x value picked + pi = w.pickPoint(self.pickerinfo.screenpos[0], p.y(), + self.painthelper.widgetBounds(w), + distance='horizontal') + if not pi: + continue + + dy = p.y() - pi.screenpos[1] + + # take the new point which is closest vertically to the current + # one and either above or below it as appropriate + if abs(dy) < dist and ( (k == qt4.Qt.Key_Up and dy > 0) + or (k == qt4.Qt.Key_Down and dy < 0) ): + pickinfo = pi + dist = abs(dy) + + if pickinfo: + oldx = self.pickerinfo.screenpos[0] + self.emitPicked(pickinfo) + + # restore the previous x-position, so that vertical navigation + # stays repeatable + pickinfo.screenpos = (oldx, pickinfo.screenpos[1]) def locateClickWidget(self, x, y): """Work out which widget was clicked, and if necessary send @@ -581,41 +786,15 @@ if self.document.getNumberPages() == 0: return - # now crazily draw the whole thing again - # see which widgets change the region in the small box given below - bufferpixmap = qt4.QPixmap( *self.size ) - painter = ClickPainter(bufferpixmap, x-3, y-3, 7, 7) - - pagenumber = min( self.document.getNumberPages() - 1, - self.pagenumber ) - self.document.paintTo(painter, self.pagenumber, - scaling=self.zoomfactor, dpi=self.widgetdpi) - painter.end() - - widget = painter.getFoundWidget() - if not widget: - widget = self.document.getPage(self.pagenumber) + widget = self.painthelper.identifyWidgetAtPoint( + x, y, antialias=self.antialias) + if widget is None: + # select page if nothing clicked + widget = self.document.basewidget.getPage(self.pagenumber) # tell connected objects that widget was clicked - self.emit( qt4.SIGNAL('sigWidgetClicked'), widget ) - - def setOutputSize(self): - """Set the ouput display size.""" - - # convert distances into pixels - pix = qt4.QPixmap(1, 1) - painter = document.Painter(pix, - scaling = self.zoomfactor, - dpi = self.widgetdpi) - size = self.document.basewidget.getSize(painter) - painter.end() - - # make new buffer and resize widget - if size != self.size: - self.size = size - self.bufferpixmap = qt4.QPixmap( *self.size ) - self.forceupdate = True - self.setSceneRect( 0, 0, size[0], size[1] ) + if widget is not None: + self.emit( qt4.SIGNAL('sigWidgetClicked'), widget ) def setPageNumber(self, pageno): """Move the the selected page.""" @@ -630,72 +809,79 @@ pageno = max(0, pageno) self.pagenumber = pageno - self.forceupdate = True + if self.pagenumber != self.oldpagenumber and self.interval != 0: + self.checkPlotUpdate() def getPageNumber(self): """Get the the selected page.""" return self.pagenumber - def slotTimeout(self): - """Called after timer times out, to check for updates to window.""" + def slotDocModified(self, ismodified): + """Update plot on document being modified.""" + # only update if doc is modified and the update policy is set + # to update on document updates + if ismodified and self.interval == -1: + self.checkPlotUpdate() + + def checkPlotUpdate(self): + """Check whether plot needs updating.""" + # print >>sys.stderr, "checking update" # no threads, so can't get interrupted here # draw data into background pixmap if modified if ( self.zoomfactor != self.oldzoom or self.document.changeset != self.docchangeset or - self.forceupdate ): + self.pagenumber != self.oldpagenumber ): - self.setOutputSize() + # print >>sys.stderr, "updating" + self.pickeritem.hide() - # fill pixmap with proper background colour - self.bufferpixmap.fill( setting.settingdb.color('page') ) - self.pagenumber = min( self.document.getNumberPages() - 1, self.pagenumber ) + self.oldpagenumber = self.pagenumber + if self.pagenumber >= 0: + size = self.document.pageSize( + self.pagenumber, scaling=self.zoomfactor) + # draw the data into the buffer # errors cause an exception window to pop up try: - painter = RecordingPainter(self.bufferpixmap) - painter.setRenderHint(qt4.QPainter.Antialiasing, - self.antialias) - painter.setRenderHint(qt4.QPainter.TextAntialiasing, - self.antialias) - self.document.paintTo( painter, self.pagenumber, - scaling = self.zoomfactor, - dpi = self.widgetdpi ) - painter.end() - self.widgetpositions = painter.widgetpositions - self.widgetpositionslookup = painter.widgetpositionslookup - - # collect all controlgraphs (in case these change later - # from e.g. printing) - self.widgetcontrolgraphs = dict( - [ (w[0], w[0].controlgraphitems) - for w in self.widgetpositions ]) - - # update selected widget items - self.selectedWidget(self.selwidget) - + phelper = document.PaintHelper( + size, scaling=self.zoomfactor, dpi=self.dpi) + self.document.paintTo(phelper, self.pagenumber) + except Exception: # stop updates this time round and show exception dialog d = exceptiondialog.ExceptionDialog(sys.exc_info(), self) self.oldzoom = self.zoomfactor - self.forceupdate = False self.docchangeset = self.document.changeset d.exec_() - + + self.painthelper = phelper + self.rendercontrol.addJob(phelper) else: + self.painthelper = None self.pagenumber = 0 + size = self.document.docSize() + pixmap = qt4.QPixmap(*size) + pixmap.fill( setting.settingdb.color('page') ) + self.setSceneRect(0, 0, *size) + self.pixmapitem.setPixmap(pixmap) self.emit( qt4.SIGNAL("sigUpdatePage"), self.pagenumber ) self.updatePageToolbar() + self.updateControlGraphs(self.lastwidgetsselected) self.oldzoom = self.zoomfactor - self.forceupdate = False self.docchangeset = self.document.changeset - self.pixmapitem.setPixmap(self.bufferpixmap) + def slotRenderFinished(self, jobid, img, helper): + """Update image on display if rendering (usually in other + thread) finished.""" + bufferpixmap = qt4.QPixmap.fromImage(img) + self.setSceneRect(0, 0, bufferpixmap.width(), bufferpixmap.height()) + self.pixmapitem.setPixmap(bufferpixmap) def _constructContextMenu(self): """Construct the context menu.""" @@ -709,26 +895,18 @@ menu.addAction( self.vzactions['view.nextpage'] ) menu.addSeparator() - # update NOW! + # force an update now menu item menu.addAction('Force update', self.actionForceUpdate) - # Update submenu + # Update policy submenu submenu = menu.addMenu('Updates') intgrp = qt4.QActionGroup(self) - inttext = ['Disable'] - for intv in self.intervals[1:]: - inttext.append('Every %gs' % (intv * 0.001)) - - # need to keep copies of bound objects otherwise they are collected - self._intfuncs = [] - # bind interval options to actions - for intv, text in izip(self.intervals, inttext): + for intv, text in self.updateintervals: act = intgrp.addAction(text) act.setCheckable(True) fn = utils.BoundCaller(self.actionSetTimeout, intv) - self._intfuncs.append(fn) self.connect(act, qt4.SIGNAL('triggered(bool)'), fn) if intv == self.interval: act.setChecked(True) @@ -742,8 +920,9 @@ def updatePlotSettings(self): """Update plot window settings from settings.""" - self.setTimeout(setting.settingdb['plot_updateinterval']) + self.setTimeout(setting.settingdb['plot_updatepolicy']) self.antialias = setting.settingdb['plot_antialias'] + self.rendercontrol.updateNumberThreads() self.actionForceUpdate() def contextMenuEvent(self, event): @@ -753,36 +932,29 @@ def actionForceUpdate(self): """Force an update for the graph.""" self.docchangeset = -100 - self.slotTimeout() + self.checkPlotUpdate() def setTimeout(self, interval): """Change timer setting without changing save value.""" - if interval == 0: + self.interval = interval + if interval <= 0: + # stop updates if self.timer.isActive(): self.timer.stop() else: + # change interval to one selected self.timer.setInterval(interval) + # start timer if it was stopped if not self.timer.isActive(): self.timer.start() def actionSetTimeout(self, interval, checked): """Called by setting the interval.""" - if interval == 0: - # stop updates - self.interval = 0 - if self.timer.isActive(): - self.timer.stop() - else: - # change interval to one selected - self.interval = interval - self.timer.setInterval(interval) - # start timer if it was stopped - if not self.timer.isActive(): - self.timer.start() + self.setTimeout(interval) # remember changes for next time - setting.settingdb['plot_updateinterval'] = self.interval + setting.settingdb['plot_updatepolicy'] = self.interval def actionAntialias(self): """Toggle antialias.""" @@ -793,7 +965,7 @@ def setZoomFactor(self, zoomfactor): """Set the zoom factor of the window.""" self.zoomfactor = float(zoomfactor) - self.update() + self.checkPlotUpdate() def slotViewZoomIn(self): """Zoom into the plot.""" @@ -809,14 +981,15 @@ # need to take account of scroll bars when deciding size viewportsize = self.maximumViewportSize() aspectwin = viewportsize.width()*1./viewportsize.height() - aspectplot = self.size[0]*1./self.size[1] + r = self.pixmapitem.boundingRect() + aspectplot = r.width() / r.height() width = viewportsize.width() if aspectwin > aspectplot: # take account of scroll bar width -= self.verticalScrollBar().width() - mult = width*1./self.size[0] + mult = width / r.width() self.setZoomFactor(self.zoomfactor * mult) def slotViewZoomHeight(self): @@ -825,22 +998,24 @@ # need to take account of scroll bars when deciding size viewportsize = self.maximumViewportSize() aspectwin = viewportsize.width()*1./viewportsize.height() - aspectplot = self.size[0]*1./self.size[1] + r = self.pixmapitem.boundingRect() + aspectplot = r.width() / r.height() height = viewportsize.height() if aspectwin < aspectplot: # take account of scroll bar height -= self.horizontalScrollBar().height() - mult = height*1./self.size[1] + mult = height / r.height() self.setZoomFactor(self.zoomfactor * mult) def slotViewZoomPage(self): """Make the zoom factor correct to show the whole page.""" viewportsize = self.maximumViewportSize() - multw = viewportsize.width()*1./self.size[0] - multh = viewportsize.height()*1./self.size[1] + r = self.pixmapitem.boundingRect() + multw = viewportsize.width()*1./r.width() + multh = viewportsize.height()*1./r.height() self.setZoomFactor(self.zoomfactor * min(multw, multh)) def slotViewZoom11(self): @@ -868,17 +1043,25 @@ """Called when the selection mode has changed.""" modecnvt = { self.vzactions['view.select'] : 'select', + self.vzactions['view.pick'] : 'pick', self.vzactions['view.zoomgraph'] : 'graphzoom' } + # close the current picker + self.pickeritem.hide() + self.emit(qt4.SIGNAL('sigPickerEnabled'), False) + # convert action into clicking mode self.clickmode = modecnvt[action] if self.clickmode == 'select': - pass + self.pixmapitem.unsetCursor() #self.label.setCursor(qt4.Qt.ArrowCursor) elif self.clickmode == 'graphzoom': - pass + self.pixmapitem.unsetCursor() #self.label.setCursor(qt4.Qt.CrossCursor) + elif self.clickmode == 'pick': + self.pixmapitem.setCursor(qt4.Qt.CrossCursor) + self.emit(qt4.SIGNAL('sigPickerEnabled'), True) def getClick(self): """Return a click point from the graph.""" @@ -896,17 +1079,8 @@ pt = self.grabpos # try to work out in which widget the first point is in - bufferpixmap = qt4.QPixmap( *self.size ) - painter = PointPainter(bufferpixmap, pt.x(), pt.y()) - pagenumber = min( self.document.getNumberPages() - 1, - self.pagenumber ) - if pagenumber >= 0: - self.document.paintTo(painter, self.pagenumber, - scaling=self.zoomfactor, dpi=self.widgetdpi) - painter.end() - - # get widget - widget = painter.widget + widget = self.painthelper.pointInWidgetBounds( + pt.x(), pt.y(), widgets.Graph) if widget is None: return [] @@ -933,25 +1107,33 @@ # convert point on plotter to axis coordinate # FIXME: Need To Trap Conversion Errors! - r = axis.plotterToGraphCoords(painter.bounds[axis], p) + r = axis.plotterToGraphCoords( + self.painthelper.widgetBounds(axis), p) axesretn.append( (axis.path, r[0]) ) return axesretn - def selectedWidget(self, widget): - """Update control items on screen associated with widget.""" + def selectedWidgets(self, widgets): + """Update control items on screen associated with widget. + Called when widgets have been selected in the tree edit window + """ + self.updateControlGraphs(widgets) + self.lastwidgetsselected = widgets + + def updateControlGraphs(self, widgets): + """Add control graphs for the widgets given.""" - self.selwidget = widget + cgg = self.controlgraphgroup - # remove old items from scene - for item in self.controlgraphs: - self.scene.removeItem(item) - del self.controlgraphs[:] - - # put in new items - if widget is not None and widget in self.widgetcontrolgraphs: - for control in self.widgetcontrolgraphs[widget]: - graphitem = control.createGraphicsItem() - self.controlgraphs.append(graphitem) - self.scene.addItem(graphitem) + # delete old items + for c in cgg.childItems(): + cgg.removeFromGroup(c) + self.scene.removeItem(c) + + # add each item to the group + for widget in widgets: + if self.painthelper and widget in self.painthelper.states: + for control in self.painthelper.states[widget].cgis: + graphitem = control.createGraphicsItem() + cgg.addToGroup(graphitem) diff -Nru veusz-1.10/windows/simplewindow.py veusz-1.14/windows/simplewindow.py --- veusz-1.10/windows/simplewindow.py 2010-12-12 12:41:08.000000000 +0000 +++ veusz-1.14/windows/simplewindow.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################## -# $Id: simplewindow.py 1222 2010-05-09 21:35:19Z jeremysanders $ - import veusz.qtall as qt4 import veusz.document as document diff -Nru veusz-1.10/windows/treeeditwindow.py veusz-1.14/windows/treeeditwindow.py --- veusz-1.10/windows/treeeditwindow.py 2010-12-12 12:41:08.000000000 +0000 +++ veusz-1.14/windows/treeeditwindow.py 2011-11-22 20:23:31.000000000 +0000 @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2004 Jeremy S. Sanders # Email: Jeremy Sanders # @@ -16,8 +17,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: treeeditwindow.py 1469 2010-12-08 22:15:00Z jeremysanders $ - """Window to edit the document using a tree, widget properties and formatting properties.""" @@ -30,26 +29,396 @@ from widgettree import WidgetTreeModel, WidgetTreeView +class SettingsProxy(object): + """Object to handle communication between widget/settings + or sets of widgets/settings.""" + + def childProxyList(self): + """Return a list settings and setting variables proxified.""" + + def settingsProxyList(self): + """Return list of SettingsProxy objects for sub Settings.""" + + def settingList(self): + """Return list of Setting objects.""" + + def actionsList(self): + """Return list of Action objects.""" + + def onSettingChanged(self, control, setting, val): + """Called when a setting has been modified.""" + + def onAction(self, action, console): + """Called if action pressed. Console window is given.""" + + def name(self): + """Return name of Settings.""" + + def pixmap(self): + """Return pixmap for Settings.""" + + def usertext(self): + """Return text for user.""" + + def setnsmode(self): + """Return setnsmode of Settings.""" + + def multivalued(self, name): + """Is setting with name multivalued?""" + return False + + def resetToDefault(self, name): + """Reset setting to default.""" + +class SettingsProxySingle(SettingsProxy): + """A proxy wrapping settings for a single widget.""" + + def __init__(self, document, settings, actions=None): + """Initialise settings proxy. + settings is the widget settings, actions is its actions.""" + self.document = document + self.settings = settings + self.actions = actions + + def childProxyList(self): + """Return a list settings and setting variables proxified.""" + retn = [] + s = self.settings + for n in s.getNames(): + o = s.get(n) + if isinstance(o, setting.Settings): + retn.append( SettingsProxySingle(self.document, o) ) + else: + retn.append(o) + return retn + + def settingsProxyList(self): + """Return list of SettingsProxy objects.""" + return [ SettingsProxySingle(self.document, s) + for s in self.settings.getSettingsList() ] + + def settingList(self): + """Return list of Setting objects.""" + return self.settings.getSettingList() + + def actionsList(self): + """Return list of actions.""" + return self.actions + + def onSettingChanged(self, control, setting, val): + """Change setting in document.""" + if setting.val != val: + self.document.applyOperation( + document.OperationSettingSet(setting, val)) + + def onAction(self, action, console): + """Run action on console.""" + console.runFunction(action.function) + + def name(self): + """Return name.""" + return self.settings.name + + def pixmap(self): + """Return pixmap.""" + return self.settings.pixmap + + def usertext(self): + """Return text for user.""" + return self.settings.usertext + + def setnsmode(self): + """Return setnsmode of Settings.""" + return self.settings.setnsmode + + def resetToDefault(self, name): + """Reset setting to default.""" + setn = self.settings.get(name) + self.document.applyOperation( + document.OperationSettingSet(setn, setn.default)) + +class SettingsProxyMulti(SettingsProxy): + """A proxy wrapping settings for multiple widgets.""" + + def __init__(self, document, widgets, _root=''): + """Initialise settings proxy. + widgets is a list of widgets to proxy for.""" + self.document = document + self.widgets = widgets + self._root = _root + + self._settingsatlevel = self._getSettingsAtLevel() + self._cachesettings = self._cachesetting = self._cachechild = None + + def _getSettingsAtLevel(self): + """Return settings of widgets at level given.""" + if self._root: + levels = self._root.split('/') + else: + levels = [] + setns = [] + for w in self.widgets: + s = w.settings + for lev in levels: + s = s.get(lev) + setns.append(s) + return setns + + def _objList(self, filterclasses): + """Return a list of objects with the type in filterclasses.""" + + setns = self._settingsatlevel + + # get list of names with appropriate class + names = [] + for n in setns[0].getNames(): + o = setns[0].get(n) + for c in filterclasses: + if isinstance(o, c): + names.append(n) + break + + sset = set(names) + for s in setns[1:]: + sset &= set(s.getNames()) + names = [n for n in names if n in sset] + + proxylist = [] + for n in names: + o = setns[0].get(n) + if isinstance(o, setting.Settings): + # construct new proxy settings (adding on name of root) + newroot = n + if self._root: + newroot = self._root + '/' + newroot + v = SettingsProxyMulti(self.document, self.widgets, + _root=newroot) + else: + # use setting from first settings as template + v = o + + proxylist.append(v) + return proxylist + + def childProxyList(self): + """Make a list of proxy settings.""" + if self._cachechild is None: + self._cachechild = self._objList( (setting.Settings, + setting.Setting) ) + return self._cachechild + + def settingsProxyList(self): + """Get list of settings proxy.""" + if self._cachesettings is None: + self._cachesettings = self._objList( (setting.Settings,) ) + return self._cachesettings + + def settingList(self): + """Set list of common Setting objects for each widget.""" + if self._cachesetting is None: + self._cachesetting = self._objList( (setting.Setting,) ) + return self._cachesetting + + def actionsList(self): + """Get list of common actions.""" + anames = None + for widget in self.widgets: + a = set([a.name for a in widget.actions]) + if anames is None: + anames = a + else: + anames &= a + actions = [a for a in self.widgets[0].actions if a.name in anames] + return actions + + def onSettingChanged(self, control, setting, val): + """Change setting in document.""" + # construct list of operations to change each setting + ops = [] + sname = setting.name + if self._root: + sname = self._root + '/' + sname + for w in self.widgets: + s = self.document.resolveFullSettingPath(w.path + '/' + sname) + if s.val != val: + ops.append(document.OperationSettingSet(s, val)) + # apply all operations + if ops: + self.document.applyOperation( + document.OperationMultiple(ops, descr='change settings')) + + def onAction(self, action, console): + """Run actions with same name.""" + aname = action.name + for w in self.widgets: + for a in w.actions: + if a.name == aname: + console.runFunction(a.function) + + def name(self): + return self._settingsatlevel[0].name + + def pixmap(self): + """Return pixmap.""" + return self._settingsatlevel[0].pixmap + + def usertext(self): + """Return text for user.""" + return self._settingsatlevel[0].usertext + + def setnsmode(self): + """Return setnsmode.""" + return self._settingsatlevel[0].setnsmode + + def multivalued(self, name): + """Is setting multivalued?""" + slist = [s.get(name) for s in self._settingsatlevel] + first = slist[0].get() + for s in slist[1:]: + if s.get() != first: + return True + return False + + def resetToDefault(self, name): + """Reset settings to default.""" + ops = [] + for s in self._settingsatlevel: + setn = s.get(name) + ops.append(document.OperationSettingSet(setn, setn.default)) + self.document.applyOperation( + document.OperationMultiple(ops, descr="reset to default")) + class PropertyList(qt4.QWidget): """Edit the widget properties using a set of controls.""" - def __init__(self, document, showsubsettings=True, + def __init__(self, document, showformatsettings=True, *args): qt4.QWidget.__init__(self, *args) self.document = document - self.showsubsettings = showsubsettings + self.showformatsettings = showformatsettings self.layout = qt4.QGridLayout(self) - self.layout.setSpacing( self.layout.spacing()/2 ) self.layout.setMargin(4) self.childlist = [] self.setncntrls = {} # map setting name to controls - def updateProperties(self, settings, title=None, showformatting=True, + def getConsole(self): + """Find console window. This is horrible: HACK.""" + win = self.parent() + while not hasattr(win, 'console'): + win = win.parent() + return win.console + + def _addActions(self, setnsproxy, row): + """Add a list of actions.""" + for action in setnsproxy.actionsList(): + text = action.name + if action.usertext: + text = action.usertext + + lab = qt4.QLabel(text) + self.layout.addWidget(lab, row, 0) + self.childlist.append(lab) + + button = qt4.QPushButton(text) + button.setToolTip(action.descr) + # need to save reference to caller object + button.caller = utils.BoundCaller(setnsproxy.onAction, action, + self.getConsole()) + self.connect(button, qt4.SIGNAL('clicked()'), button.caller) + + self.layout.addWidget(button, row, 1) + self.childlist.append(button) + + row += 1 + return row + + def _addControl(self, setnsproxy, setn, row): + """Add a control for a setting.""" + cntrl = setn.makeControl(None) + if cntrl: + lab = SettingLabel(self.document, setn, setnsproxy) + self.layout.addWidget(lab, row, 0) + self.childlist.append(lab) + + self.connect(cntrl, qt4.SIGNAL('settingChanged'), + setnsproxy.onSettingChanged) + self.layout.addWidget(cntrl, row, 1) + self.childlist.append(cntrl) + self.setncntrls[setn.name] = (lab, cntrl) + + row += 1 + return row + + def _addGroupedSettingsControl(self, grpdsetting, row): + """Add a control for a set of grouped settings.""" + + slist = grpdsetting.settingList() + + # make first widget with expandable button + + # this is a label with a + button by this side + setnlab = SettingLabel(self.document, slist[0], grpdsetting) + expandbutton = qt4.QPushButton("+", checkable=True, flat=True, + maximumWidth=16) + + l = qt4.QHBoxLayout(spacing=0) + l.setContentsMargins(0,0,0,0) + l.addWidget( expandbutton ) + l.addWidget( setnlab ) + lw = qt4.QWidget() + lw.setLayout(l) + self.layout.addWidget(lw, row, 0) + self.childlist.append(lw) + + # make main control + cntrl = slist[0].makeControl(None) + self.connect(cntrl, qt4.SIGNAL('settingChanged'), + grpdsetting.onSettingChanged) + self.layout.addWidget(cntrl, row, 1) + self.childlist.append(cntrl) + + row += 1 + + # set of controls for remaining settings + l = qt4.QGridLayout() + grp_row = 0 + for setn in slist[1:]: + cntrl = setn.makeControl(None) + if cntrl: + lab = SettingLabel(self.document, setn, grpdsetting) + l.addWidget(lab, grp_row, 0) + self.connect(cntrl, qt4.SIGNAL('settingChanged'), + grpdsetting.onSettingChanged) + l.addWidget(cntrl, grp_row, 1) + grp_row += 1 + + grpwidget = qt4.QFrame( frameShape = qt4.QFrame.Panel, + frameShadow = qt4.QFrame.Raised, + visible=False ) + grpwidget.setLayout(l) + + def ontoggle(checked): + """Toggle button text and make grp visible/invisible.""" + expandbutton.setText( ("+","-")[checked] ) + grpwidget.setVisible( checked ) + + self.connect(expandbutton, qt4.SIGNAL("toggled(bool)"), ontoggle) + + # add group to standard layout + self.layout.addWidget(grpwidget, row, 0, 1, -1) + self.childlist.append(grpwidget) + row += 1 + return row + + def updateProperties(self, setnsproxy, title=None, showformatting=True, onlyformatting=False): - """Update the list of controls with new ones for the settings.""" + """Update the list of controls with new ones for the SettingsProxy.""" + + # keep a reference to keep it alive + self._setnsproxy = setnsproxy # delete all child widgets self.setUpdatesEnabled(False) @@ -60,7 +429,7 @@ c.deleteLater() del c - if settings is None: + if setnsproxy is None: self.setUpdatesEnabled(True) return @@ -70,71 +439,41 @@ # add a title if requested if title is not None: - lab = qt4.QLabel(title[0]) - lab.setFrameShape(qt4.QFrame.Panel) - lab.setFrameShadow(qt4.QFrame.Sunken) - lab.setToolTip(title[1]) + lab = qt4.QLabel(title[0], frameShape=qt4.QFrame.Panel, + frameShadow=qt4.QFrame.Sunken, toolTip=title[1]) self.layout.addWidget(lab, row, 0, 1, -1) row += 1 # add actions if parent is widget - if settings.parent.isWidget() and not showformatting: - widget = settings.parent - for action in widget.actions: - text = action.name - if action.usertext: - text = action.usertext - - lab = qt4.QLabel(text) - self.layout.addWidget(lab, row, 0) - self.childlist.append(lab) - - button = qt4.QPushButton(text) - button.setToolTip(action.descr) - # need to save reference to caller object - button.caller = utils.BoundCaller(self.slotActionPressed, - action) - self.connect(button, qt4.SIGNAL('clicked()'), button.caller) - - self.layout.addWidget(button, row, 1) - self.childlist.append(button) - - row += 1 + if setnsproxy.actionsList() and not showformatting: + row = self._addActions(setnsproxy, row) - if settings.getSettingsList() and self.showsubsettings: + if setnsproxy.settingsProxyList() and self.showformatsettings: # if we have subsettings, use tabs - tabbed = TabbedFormatting(self.document, settings) + tabbed = TabbedFormatting(self.document, setnsproxy) self.layout.addWidget(tabbed, row, 1, 1, 2) row += 1 self.childlist.append(tabbed) else: # else add settings proper as a list - for setn in settings.getSettingList(): - # skip if not to show formatting - if not showformatting and setn.formatting: - continue - # skip if only to show formatting and not formatting - if onlyformatting and not setn.formatting: - continue - - cntrl = setn.makeControl(None) - if cntrl: - lab = SettingLabel(self.document, setn, None) - self.layout.addWidget(lab, row, 0) - self.childlist.append(lab) - - self.connect(cntrl, qt4.SIGNAL('settingChanged'), - self.slotSettingChanged) - self.layout.addWidget(cntrl, row, 1) - self.childlist.append(cntrl) - self.setncntrls[setn.name] = (lab, cntrl) - - row += 1 + for setn in setnsproxy.childProxyList(): + + # add setting + # only add if formatting setting and formatting allowed + # and not formatting and not formatting not allowed + if ( isinstance(setn, setting.Setting) and ( + (setn.formatting and (showformatting or onlyformatting)) + or (not setn.formatting and not onlyformatting)) and + not setn.hidden ): + row = self._addControl(setnsproxy, setn, row) + elif ( isinstance(setn, SettingsProxy) and + setn.setnsmode() == 'groupedsetting' and + not onlyformatting ): + row = self._addGroupedSettingsControl(setn, row) # add empty widget to take rest of space - w = qt4.QWidget() - w.setSizePolicy(qt4.QSizePolicy.Maximum, - qt4.QSizePolicy.MinimumExpanding) + w = qt4.QWidget( sizePolicy=qt4.QSizePolicy( + qt4.QSizePolicy.Maximum, qt4.QSizePolicy.MinimumExpanding) ) self.layout.addWidget(w, row, 0) self.childlist.append(w) @@ -148,65 +487,49 @@ if setn in self.setncntrls: for cntrl in self.setncntrls[setn]: cntrl.setVisible(vis) - - def slotSettingChanged(self, widget, setting, val): - """Called when a setting is changed by the user. - - This updates the setting to the value using an operation so that - it can be undone. - """ - - self.document.applyOperation(document.OperationSettingSet(setting, val)) - - def slotActionPressed(self, action): - """Activate the action.""" - - # find console window, this is horrible: HACK - win = self - while not hasattr(win, 'console'): - win = win.parent() - console = win.console - - console.runFunction(action.function) class TabbedFormatting(qt4.QTabWidget): """Class to have tabbed set of settings.""" - def __init__(self, document, settings, shownames=False): + def __init__(self, document, setnsproxy, shownames=False): qt4.QTabWidget.__init__(self) + self.document = document - if settings is None: + if setnsproxy is None: return # get list of settings - setnslist = settings.getSettingsList() + self.setnsproxy = setnsproxy + setnslist = setnsproxy.settingsProxyList() # add formatting settings if necessary - numformat = len( [setn for setn in settings.getSettingList() + numformat = len( [setn for setn in setnsproxy.settingList() if setn.formatting] ) if numformat > 0: # add on a formatting tab - setnslist.insert(0, settings) + setnslist.insert(0, setnsproxy) + + self.connect( self, qt4.SIGNAL('currentChanged(int)'), + self.slotCurrentChanged ) + + # subsettings for tabs + self.tabsubsetns = [] + + # collected titles and tooltips for tabs + self.tabtitles = [] + self.tabtooltips = [] + + # tabs which have been initialized + self.tabinit = set() # add tab for each subsettings for subset in setnslist: - if subset.name == 'StyleSheet': + if subset.setnsmode() not in ('formatting', 'widgetsettings'): continue - - # create tab - tab = qt4.QWidget() - layout = qt4.QVBoxLayout() - layout.setMargin(2) - tab.setLayout(layout) - - # create scrollable area - scroll = qt4.QScrollArea(None) - layout.addWidget(scroll) - scroll.setWidgetResizable(True) + self.tabsubsetns.append(subset) # details of tab - mainsettings = (subset == settings) - if mainsettings: + if subset is setnsproxy: # main tab formatting, so this is special pixmap = 'settings_main' tabname = title = 'Main' @@ -214,26 +537,54 @@ else: # others if hasattr(subset, 'pixmap'): - pixmap = subset.pixmap + pixmap = subset.pixmap() else: pixmap = None - tabname = subset.name - tooltip = title = subset.usertext + tabname = subset.name() + tooltip = title = subset.usertext() - # create list of properties - plist = PropertyList(document, showsubsettings=not mainsettings) - plist.updateProperties(subset, title=(title, tooltip), - onlyformatting=mainsettings) - scroll.setWidget(plist) - plist.show() - # hide name in tab if not shownames: tabname = '' - indx = self.addTab(tab, utils.getIcon(pixmap), tabname) + self.tabtitles.append(title) + self.tabtooltips.append(tooltip) + + # create tab + indx = self.addTab(qt4.QWidget(), utils.getIcon(pixmap), tabname) self.setTabToolTip(indx, tooltip) + def slotCurrentChanged(self, tab): + """Lazy loading of tab when displayed.""" + if tab in self.tabinit: + # already initialized + return + self.tabinit.add(tab) + + # settings to show + subsetn = self.tabsubsetns[tab] + # whether these are the main settings + mainsettings = subsetn is self.setnsproxy + + # add this property list to the scroll widget for tab + plist = PropertyList(self.document, showformatsettings=not mainsettings) + plist.updateProperties(subsetn, title=(self.tabtitles[tab], + self.tabtooltips[tab]), + onlyformatting=mainsettings) + + # create scrollable area + scroll = qt4.QScrollArea() + scroll.setWidgetResizable(True) + scroll.setWidget(plist) + + # layout for tab widget + layout = qt4.QVBoxLayout() + layout.setMargin(2) + layout.addWidget(scroll) + + # finally use layout containing items for tab + self.widget(tab).setLayout(layout) + class FormatDock(qt4.QDockWidget): """A window for formatting the current widget. Provides tabbed formatting properties @@ -248,14 +599,11 @@ self.tabwidget = None # update our view when the tree edit window selection changes - self.connect(treeedit, qt4.SIGNAL('widgetSelected'), - self.selectedWidget) + self.connect(treeedit, qt4.SIGNAL('widgetsSelected'), + self.selectedWidgets) - # do initial selection - self.selectedWidget(treeedit.selwidget) - - def selectedWidget(self, widget): - """Created tabbed widget for formatting for each subsettings.""" + def selectedWidgets(self, widgets, setnsproxy): + """Created tabbed widgets for formatting for each subsettings.""" # get current tab (so we can set it afterwards) if self.tabwidget: @@ -268,12 +616,7 @@ self.tabwidget.deleteLater() self.tabwidget = None - # create new tabbed widget showing formatting - settings = None - if widget is not None: - settings = widget.settings - - self.tabwidget = TabbedFormatting(self.document, settings) + self.tabwidget = TabbedFormatting(self.document, setnsproxy) self.setWidget(self.tabwidget) # wrap tab from zero to max number @@ -291,8 +634,8 @@ self.document = document # update our view when the tree edit window selection changes - self.connect(treeedit, qt4.SIGNAL('widgetSelected'), - self.slotWidgetSelected) + self.connect(treeedit, qt4.SIGNAL('widgetsSelected'), + self.slotWidgetsSelected) # construct scrollable area self.scroll = qt4.QScrollArea() @@ -300,31 +643,23 @@ self.setWidget(self.scroll) # construct properties list in scrollable area - self.proplist = PropertyList(document, showsubsettings=False) + self.proplist = PropertyList(document, showformatsettings=False) self.scroll.setWidget(self.proplist) - # do initial selection - self.slotWidgetSelected(treeedit.selwidget) - - def slotWidgetSelected(self, widget): - """Update properties when selected widget changes.""" - - settings = None - if widget is not None: - settings = widget.settings - self.proplist.updateProperties(settings, showformatting=False) + def slotWidgetsSelected(self, widgets, setnsproxy): + """Update properties when selected widgets change.""" + self.proplist.updateProperties(setnsproxy, showformatting=False) class TreeEditDock(qt4.QDockWidget): - """A window for editing the document as a tree.""" - - # mime type when widgets are stored on the clipboard + """A dock window presenting widgets as a tree.""" - def __init__(self, document, parent): - qt4.QDockWidget.__init__(self, parent) - self.parent = parent + def __init__(self, document, parentwin): + """Initialise dock given document and parent widget.""" + qt4.QDockWidget.__init__(self, parentwin) + self.parentwin = parentwin self.setWindowTitle("Editing - Veusz") self.setObjectName("veuszeditingwindow") - self.selwidget = None + self.selwidgets = [] self.document = document self.connect( self.document, qt4.SIGNAL("sigWiped"), @@ -336,27 +671,28 @@ # receive change in selection self.connect(self.treeview.selectionModel(), - qt4.SIGNAL('selectionChanged(const QItemSelection &, const QItemSelection &)'), - self.slotTreeItemSelected) + qt4.SIGNAL('selectionChanged(const QItemSelection &,' + ' const QItemSelection &)'), + self.slotTreeItemsSelected) # set tree as main widget self.setWidget(self.treeview) # toolbar to create widgets self.addtoolbar = qt4.QToolBar("Insert toolbar - Veusz", - parent) + parentwin) # note wrong description!: backwards compatibility self.addtoolbar.setObjectName("veuszeditingtoolbar") # toolbar for editting widgets self.edittoolbar = qt4.QToolBar("Edit toolbar - Veusz", - parent) + parentwin) self.edittoolbar.setObjectName("veuszedittoolbar") self._constructToolbarMenu() - parent.addToolBarBreak(qt4.Qt.TopToolBarArea) - parent.addToolBar(qt4.Qt.TopToolBarArea, self.addtoolbar) - parent.addToolBar(qt4.Qt.TopToolBarArea, self.edittoolbar) + parentwin.addToolBarBreak(qt4.Qt.TopToolBarArea) + parentwin.addToolBar(qt4.Qt.TopToolBarArea, self.addtoolbar) + parentwin.addToolBar(qt4.Qt.TopToolBarArea, self.edittoolbar) # this sets various things up self.selectWidget(document.basewidget) @@ -371,53 +707,78 @@ """If the document is wiped, reselect root widget.""" self.selectWidget(self.document.basewidget) - def slotTreeItemSelected(self, current, previous): + def slotTreeItemsSelected(self, current, previous): """New item selected in tree. This updates the list of properties """ - indexes = current.indexes() - - if len(indexes) > 1: - index = indexes[0] - self.selwidget = self.treemodel.getWidget(index) - settings = self.treemodel.getSettings(index) + # get selected widgets + self.selwidgets = widgets = [ + self.treemodel.getWidget(idx) + for idx in self.treeview.selectionModel().selectedRows() ] + + if len(widgets) == 0: + setnsproxy = None + elif len(widgets) == 1: + setnsproxy = SettingsProxySingle(self.document, widgets[0].settings, + actions=widgets[0].actions) else: - self.selwidget = None - settings = None + setnsproxy = SettingsProxyMulti(self.document, widgets) self._enableCorrectButtons() self._checkPageChange() - self.emit( qt4.SIGNAL('widgetSelected'), self.selwidget ) + self.emit( qt4.SIGNAL('widgetsSelected'), self.selwidgets, setnsproxy ) def contextMenuEvent(self, event): """Bring up context menu.""" + # no widgets selected + if not self.selwidgets: + return + m = qt4.QMenu(self) + + # selection + m.addMenu(self.parentwin.menus['edit.select']) + m.addSeparator() + + # actions on widget(s) for act in ('edit.cut', 'edit.copy', 'edit.paste', 'edit.moveup', 'edit.movedown', 'edit.delete', 'edit.rename'): m.addAction(self.vzactions[act]) # allow show or hides of selected widget - if self.selwidget and 'hide' in self.selwidget.settings: - m.addSeparator() - hide = self.selwidget.settings.hide - act = qt4.QAction( ('Hide object', 'Show object')[hide], m ) - self.connect(act, qt4.SIGNAL('triggered()'), - (self.slotWidgetHide, self.slotWidgetShow)[hide]) - m.addAction(act) + anyhide = False + anyshow = False + for w in self.selwidgets: + if 'hide' in w.settings: + if w.settings.hide: + anyshow = True + else: + anyhide = True - m.exec_(self.mapToGlobal(event.pos())) + for (enabled, menutext, showhide) in ( + (anyhide, 'Hide', True), (anyshow, 'Show', False) ): + if enabled: + m.addSeparator() + act = qt4.QAction(menutext, self) + self.connect(act, qt4.SIGNAL('triggered()'), + utils.BoundCaller(self.slotWidgetHideShow, + self.selwidgets, showhide)) + m.addAction(act) + m.exec_(self.mapToGlobal(event.pos())) event.accept() def _checkPageChange(self): """Check to see whether page has changed.""" - w = self.selwidget + w = None + if self.selwidgets: + w = self.selwidgets[0] while w is not None and not isinstance(w, widgets.Page): w = w.parent @@ -432,7 +793,9 @@ def _enableCorrectButtons(self): """Make sure the create graph buttons are correctly enabled.""" - selw = self.selwidget + selw = None + if self.selwidgets: + selw = self.selwidgets[0] # has to be visible if is to be enabled (yuck) nonorth = self.vzactions['add.nonorthpoint'].setVisible(True) @@ -453,8 +816,9 @@ self.vzactions['add.nonorthfunc'].setVisible(nonorth) self.vzactions['add.function'].setVisible(not nonorth) - # certain actions shouldn't allow root to be deleted - isnotroot = not isinstance(selw, widgets.Root) + # certain actions shouldn't work on root + isnotroot = not any([isinstance(w, widgets.Root) + for w in self.selwidgets]) for act in ('edit.cut', 'edit.copy', 'edit.delete', 'edit.moveup', 'edit.movedown', 'edit.rename'): @@ -470,13 +834,13 @@ self.edittoolbar.setIconSize( qt4.QSize(iconsize, iconsize) ) self.addslots = {} - self.vzactions = actions = self.parent.vzactions + self.vzactions = actions = self.parentwin.vzactions for widgettype in ('page', 'grid', 'graph', 'axis', 'xy', 'bar', 'fit', 'function', 'boxplot', 'image', 'contour', 'vectorfield', 'key', 'label', 'colorbar', 'rect', 'ellipse', 'imagefile', - 'line', 'polygon', 'polar', + 'line', 'polygon', 'polar', 'ternary', 'nonorthpoint', 'nonorthfunc'): wc = document.thefactory.getWidgetClass(widgettype) @@ -493,31 +857,31 @@ a = utils.makeAction actions.update({ 'edit.cut': - a(self, 'Cut the selected item', 'Cu&t', + a(self, 'Cut the selected widget', 'Cu&t', self.slotWidgetCut, icon='veusz-edit-cut', key='Ctrl+X'), 'edit.copy': - a(self, 'Copy the selected item', '&Copy', + a(self, 'Copy the selected widget', '&Copy', self.slotWidgetCopy, icon='kde-edit-copy', key='Ctrl+C'), 'edit.paste': - a(self, 'Paste item from the clipboard', '&Paste', + a(self, 'Paste widget from the clipboard', '&Paste', self.slotWidgetPaste, icon='kde-edit-paste', key='Ctrl+V'), 'edit.moveup': - a(self, 'Move the selected item up', 'Move &up', + a(self, 'Move the selected widget up', 'Move &up', utils.BoundCaller(self.slotWidgetMove, -1), icon='kde-go-up'), 'edit.movedown': - a(self, 'Move the selected item down', 'Move d&own', + a(self, 'Move the selected widget down', 'Move d&own', utils.BoundCaller(self.slotWidgetMove, 1), icon='kde-go-down'), 'edit.delete': - a(self, 'Remove the selected item', '&Delete', + a(self, 'Remove the selected widget', '&Delete', self.slotWidgetDelete, icon='kde-edit-delete'), 'edit.rename': - a(self, 'Renames the selected item', '&Rename', + a(self, 'Renames the selected widget', '&Rename', self.slotWidgetRename, icon='kde-edit-rename'), @@ -533,7 +897,7 @@ 'xy', 'nonorthpoint', 'bar', 'fit', 'function', 'nonorthfunc', 'boxplot', 'image', 'contour', 'vectorfield', - 'key', 'label', 'colorbar', 'polar')] + 'key', 'label', 'colorbar', 'polar', 'ternary')] menuitems = [ ('insert', '', addact + [ @@ -547,14 +911,14 @@ 'edit.delete', 'edit.rename' ]), ] - utils.constructMenus( self.parent.menuBar(), - self.parent.menus, + utils.constructMenus( self.parentwin.menuBar(), + self.parentwin.menus, menuitems, actions ) # create shape toolbar button # attach menu to insert shape button - actions['add.shapemenu'].setMenu(self.parent.menus['insert.shape']) + actions['add.shapemenu'].setMenu(self.parentwin.menus['insert.shape']) # add actions to toolbar to create widgets utils.addToolbarActions(self.addtoolbar, actions, @@ -566,6 +930,9 @@ 'edit.moveup', 'edit.movedown', 'edit.delete', 'edit.rename')) + self.connect( self.parentwin.menus['edit.select'], + qt4.SIGNAL('aboutToShow()'), self.updateSelectMenu ) + def slotMakeWidgetButton(self, wc): """User clicks button to make widget.""" self.makeWidget(wc.typename) @@ -582,9 +949,9 @@ """ # if no widget selected, bomb out - if self.selwidget is None: + if not self.selwidgets: return - parent = document.getSuitableParent(widgettype, self.selwidget) + parent = document.getSuitableParent(widgettype, self.selwidgets[0]) assert parent is not None @@ -607,8 +974,8 @@ def slotWidgetCopy(self): """Copy selected widget to the clipboard.""" - if self.selwidget: - mimedata = document.generateWidgetsMime([self.selwidget]) + if self.selwidgets: + mimedata = document.generateWidgetsMime(self.selwidgets) clipboard = qt4.QApplication.clipboard() clipboard.setMimeData(mimedata) @@ -617,7 +984,10 @@ selected widget? If so, enable paste button""" data = document.getClipboardWidgetMime() - show = document.isMimePastable(self.selwidget, data) + if len(self.selwidgets) == 0: + show = False + else: + show = document.isWidgetMimePastable(self.selwidgets[0], data) self.vzactions['edit.paste'].setEnabled(show) def doInitialWidgetSelect(self): @@ -637,7 +1007,7 @@ data = document.getClipboardWidgetMime() if data: - op = document.OperationWidgetPaste(self.selwidget, data) + op = document.OperationWidgetPaste(self.selwidgets[0], data) widgets = self.document.applyOperation(op) if widgets: self.selectWidget(widgets[0]) @@ -645,28 +1015,32 @@ def slotWidgetDelete(self): """Delete the widget selected.""" - # no item selected, so leave - w = self.selwidget - if w is None: + widgets = self.selwidgets + # if no item selected, leave + if not widgets: return # get list of widgets in order widgetlist = [] self.document.basewidget.buildFlatWidgetList(widgetlist) - widgetnum = widgetlist.index(w) - assert widgetnum >= 0 + # find indices of widgets to be deleted - find one to select after + indexes = [widgetlist.index(w) for w in widgets] + if -1 in indexes: + raise RuntimeError, "Invalid widget in list of selected widgets" + minindex = min(indexes) # delete selected widget - self.document.applyOperation( document.OperationWidgetDelete(w) ) + self.document.applyOperation( + document.OperationWidgetsDelete(widgets)) # rebuild list widgetlist = [] self.document.basewidget.buildFlatWidgetList(widgetlist) # find next to select - if widgetnum < len(widgetlist): - nextwidget = widgetlist[widgetnum] + if minindex < len(widgetlist): + nextwidget = widgetlist[minindex] else: nextwidget = widgetlist[-1] @@ -686,8 +1060,13 @@ listview.""" index = self.treemodel.getWidgetIndex(widget) - self.treeview.scrollTo(index) - self.treeview.setCurrentIndex(index) + if index is not None: + self.treeview.scrollTo(index) + self.treeview.selectionModel().select( + index, qt4.QItemSelectionModel.Clear | + qt4.QItemSelectionModel.Current | + qt4.QItemSelectionModel.Rows | + qt4.QItemSelectionModel.Select ) def slotWidgetMove(self, direction): """Move the selected widget up/down in the hierarchy. @@ -696,26 +1075,79 @@ direction is -1 for 'up' and +1 for 'down' """ + if not self.selwidgets: + return # widget to move - w = self.selwidget + w = self.selwidgets[0] # actually move the widget self.document.applyOperation( document.OperationWidgetMoveUpDown(w, direction) ) - # rehilight moved widget + # re-highlight moved widget self.selectWidget(w) - def slotWidgetHide(self): - """Hide the selected widget.""" - self.document.applyOperation( - document.OperationSettingSet(self.selwidget.settings.get('hide'), - True) ) - def slotWidgetShow(self): - """Show the selected widget.""" + def slotWidgetHideShow(self, widgets, hideshow): + """Hide or show selected widgets. + hideshow is True for hiding, False for showing + """ + ops = [ document.OperationSettingSet(w.settings.get('hide'), hideshow) + for w in widgets ] + descr = ('show', 'hide')[hideshow] self.document.applyOperation( - document.OperationSettingSet(self.selwidget.settings.get('hide'), - False) ) + document.OperationMultiple(ops, descr=descr)) + + def checkWidgetSelected(self): + """Check widget is selected.""" + if len(self.treeview.selectionModel().selectedRows()) == 0: + self.selectWidget(self.document.basewidget) + + def _selectWidgetsTypeAndOrName(self, wtype, wname): + """Select widgets with type or name given. + Give None if you don't care for either.""" + def selectwidget(path, w): + """Select widget if of type or name given.""" + if ( (wtype is None or w.typename == wtype) and + (wname is None or w.name == wname) ): + idx = self.treemodel.getWidgetIndex(w) + self.treeview.selectionModel().select( + idx, qt4.QItemSelectionModel.Select | + qt4.QItemSelectionModel.Rows) + + self.document.walkNodes(selectwidget, nodetypes=('widget',)) + + def _selectWidgetSiblings(self, w, wtype): + """Select siblings of widget given with type.""" + for c in w.parent.children: + if c is not w and c.typename == wtype: + idx = self.treemodel.getWidgetIndex(c) + self.treeview.selectionModel().select( + idx, qt4.QItemSelectionModel.Select | + qt4.QItemSelectionModel.Rows) + + def updateSelectMenu(self): + """Update edit.select menu.""" + menu = self.parentwin.menus['edit.select'] + menu.clear() + + if len(self.selwidgets) == 0: + return + + wtype = self.selwidgets[0].typename + name = self.selwidgets[0].name + + menu.addAction( + "All '%s' widgets" % wtype, + lambda: self._selectWidgetsTypeAndOrName(wtype, None)) + menu.addAction( + "Siblings of '%s' with type '%s'" % (name, wtype), + lambda: self._selectWidgetSiblings(self.selwidgets[0], wtype)) + menu.addAction( + "All '%s' widgets called '%s'" % (wtype, name), + lambda: self._selectWidgetsTypeAndOrName(wtype, name)) + menu.addAction( + "All widgets called '%s'" % name, + lambda: self._selectWidgetsTypeAndOrName(None, name)) class SettingLabel(qt4.QWidget): """A label to describe a setting. @@ -724,16 +1156,17 @@ access to the context menu """ - def __init__(self, document, setting, parent): + def __init__(self, document, setting, setnsproxy): """Initialise button, passing document, setting, and parent widget.""" - qt4.QWidget.__init__(self, parent) + qt4.QWidget.__init__(self) self.setFocusPolicy(qt4.Qt.StrongFocus) self.document = document self.connect(document, qt4.SIGNAL('sigModified'), self.slotDocModified) self.setting = setting + self.setnsproxy = setnsproxy self.layout = qt4.QHBoxLayout(self) self.layout.setMargin(2) @@ -786,9 +1219,10 @@ self.setToolTip(tooltip) # if not default, make label bold - bold = not self.setting.isDefault() f = qt4.QFont(self.labelicon.font()) - f.setBold(bold) + multivalued = self.setnsproxy.multivalued(self.setting.name) + f.setBold( (not self.setting.isDefault()) or multivalued ) + f.setItalic( multivalued ) self.labelicon.setFont(f) def updateHighlight(self): @@ -911,10 +1345,7 @@ def actionResetDefault(self): """Reset setting to default.""" - self.document.applyOperation( - document.OperationSettingSet( - self.setting, - self.setting.default)) + self.setnsproxy.resetToDefault(self.setting.name) def actionCopyTypedWidgets(self): """Copy setting to widgets of same type.""" @@ -935,37 +1366,11 @@ widgetname= self._clickwidget.name) ) - def actionDefaultTyped(self): - """Make default for widgets with the same type.""" - self.setting.setAsDefault(False) - - def actionDefaultTypedNamed(self): - """Make default for widgets with the same name and type.""" - self.setting.setAsDefault(True) - - def actionDefaultForget(self): - """Forget any default setting.""" - self.setting.removeDefault() - def actionUnlinkSetting(self): """Unlink the setting if it is a reference.""" self.document.applyOperation( document.OperationSettingSet(self.setting, self.setting.get()) ) - def actionEditLinkedSetting(self): - """Edit the linked setting rather than the setting.""" - - realsetn = self.setting.getReference().resolve(self.setting) - widget = realsetn - while not isinstance(widget, widgets.Widget) and widget is not None: - widget = widget.parent - - # need to select widget, so need to find treeditwindow :-( - window = self - while not hasattr(window, 'treeedit'): - window = window.parent() - window.treeedit.selectWidget(widget) - def actionSetStyleSheet(self): """Use the setting as the default in the stylesheet.""" @@ -979,4 +1384,3 @@ self.setting.default) ], descr="make default style") ) - diff -Nru veusz-1.10/windows/tutorial.py veusz-1.14/windows/tutorial.py --- veusz-1.10/windows/tutorial.py 1970-01-01 00:00:00.000000000 +0000 +++ veusz-1.14/windows/tutorial.py 2011-11-22 20:23:31.000000000 +0000 @@ -0,0 +1,855 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2011 Jeremy S. Sanders +# Email: Jeremy Sanders +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################### + +import os.path + +import veusz.qtall as qt4 +import veusz.utils as utils +import veusz.setting as setting + +class TutorialStep(qt4.QObject): + def __init__(self, text, mainwin, + nextstep=None, flash=None, + disablenext=False, + closestep=False, + nextonsetting=None, + nextonselected=None): + + """ + nextstep is class next TutorialStep class to use + If flash is set, flash widget + disablenext: wait until nextStep is emitted before going to next slide + closestep: add a close button + nextonsetting: (setnpath, lambda val: ok) - + check setting to go to next slide + nextonselected: go to next if widget with name is selected + """ + + qt4.QObject.__init__(self) + self.text = text + self.nextstep = nextstep + self.flash = flash + self.disablenext = disablenext + self.closestep = closestep + self.mainwin = mainwin + + self.nextonsetting = nextonsetting + if nextonsetting is not None: + self.connect( mainwin.document, + qt4.SIGNAL('sigModified'), self.slotNextSetting ) + + self.nextonselected = nextonselected + if nextonselected is not None: + self.connect(mainwin.treeedit, qt4.SIGNAL('widgetsSelected'), + self.slotWidgetsSelected) + + def slotNextSetting(self, *args): + """Check setting to emit next.""" + try: + setn = self.mainwin.document.basewidget.prefLookup( + self.nextonsetting[0]).get() + if self.nextonsetting[1](setn): + self.emit( qt4.SIGNAL('nextStep') ) + except ValueError: + pass + + def slotWidgetsSelected(self, widgets, *args): + """Go to next page if widget selected.""" + if len(widgets) == 1 and widgets[0].name == self.nextonselected: + self.emit( qt4.SIGNAL('nextStep') ) + +########################## +## Introduction to widgets + +class StepIntro(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Welcome to Veusz!

    + +

    This tutorial aims to get you working with Veusz as quickly as +possible.

    + +

    You can close this tutorial at any time using the close button to +the top-right of this panel. The tutorial can be replayed in the help +menu.

    + +

    Press Next to go to the next step

    +''', mainwin, nextstep=StepWidgets1) + +class StepWidgets1(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Widgets

    + +

    Plots in Veusz are constructed from widgets. Different +types of widgets are used to make different parts of a plot. For +example, there are widgets for axes, for a graph, for plotting data +and for plotting functions.

    + +

    There are also special widgets. The grid widget arranges graphs +inside it in a grid arrangement.

    +''', mainwin, nextstep=StepWidgets2) + +class StepWidgets2(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Widget can often be placed inside each other. For instance, a graph +widget is placed in a page widget or a grid widget. Plotting widgets +are placed in graph widget.

    + +

    You can have multiple widgets of different types. For example, you +can have several graphs on the page, optionally arranged in a +grid. Several plotting widgets and axis widgets can be put in a +graph.

    +''', mainwin, nextstep=StepWidgetWin) + +class StepWidgetWin(TutorialStep): + def __init__(self, mainwin): + t = mainwin.treeedit + TutorialStep.__init__( + self, ''' +

    Widget editing

    + +

    The flashing window is the Editing window, which shows the widgets +currently in the plot in a hierarchical tree. Each widget has a name +(the left column) and a type (the right column).

    + +

    Press Next to continue.

    +''', mainwin, + nextstep=StepWidgetWinExpand, + flash=t) + +class StepWidgetWinExpand(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    The graph widget is the currently selected widget.

    + +

    Expand the graph widget - click the arrow or plus +to its left in the editing window - and select the x axis widget.

    +''', mainwin, + disablenext=True, + nextonselected='x', + nextstep=StepPropertiesWin) + +class StepPropertiesWin(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Widget properties

    + +

    This window shows the properties of the currently selected widget, +the x axis widget of the graph.

    + +

    Enter a new label for the widget, by clicking in the +text edit box to the right of "Label", typing some text and press the +Enter key.

    +''', mainwin, + flash = mainwin.propdock, + disablenext = True, + nextonsetting = ('/page1/graph1/x/label', + lambda val: val != ''), + nextstep = StepPropertiesWin2) + +class StepPropertiesWin2(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Notice that the x axis label of your plot has now been updated. +Veusz supports LaTeX style formatting for labels, so you could include +superscripts, subscripts and fractions.

    + +

    Other important axis properties include the minimum, maximum values +of the axis and whether the axis is logarithmic.

    + +

    Click Next to continue.

    +''', mainwin, nextstep=WidgetAdd) + +class WidgetAdd(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Adding widgets

    + +

    The flashing Add Widget toolbar and the Insert menu add widgets to +the document. New widgets are inserted in the currently selected +widget, if possible, or its parents.

    + +

    Hold your mouse pointer over one of the toolbar buttons to +see a description of a widget type.

    + +

    Press Next to continue.

    +''', mainwin, + flash=mainwin.treeedit.addtoolbar, + nextstep=FunctionAdd ) + +class FunctionAdd(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Add a function

    + +

    We will now add a function plotting widget to the current +graph.

    + +

    Click on the flashing icon, or go to the Insert menu +and choosing "Add function".

    +''', mainwin, + flash=mainwin.treeedit.addtoolbar.widgetForAction( + mainwin.vzactions['add.function']), + disablenext=True, + nextonsetting = ('/page1/graph1/function1/function', + lambda val: val != ''), + nextstep=FunctionSet) + +class FunctionSet(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    You have now added a function widget to the graph widget. By +default function widgets plot y=x.

    + +

    Go to the Function property and change the function to +be x**2, plotting x squared.

    + +

    (Veusz uses Python syntax for its functions, so the power operator +is **, rather than ^)

    +''', mainwin, + nextonsetting = ('/page1/graph1/function1/function', + lambda val: val.strip() == 'x**2'), + disablenext = True, + nextstep=FunctionFormatting) + +class FunctionFormatting(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Formatting

    + +

    Widgets have a number of formatting options. The Formatting window +(flashing) shows the options for the currently selected widget, here +the function widget.

    + +

    Press Next to continue

    +''', mainwin, + flash=mainwin.formatdock, + nextstep=FunctionFormatLine) + +class FunctionFormatLine(TutorialStep): + def __init__(self, mainwin): + + tb = mainwin.formatdock.tabwidget.tabBar() + label = qt4.QLabel(" ") + tb.setTabButton(1, qt4.QTabBar.LeftSide, label) + + TutorialStep.__init__( + self, ''' +

    Different types of formatting properties are grouped under separate +tables. The options for drawing the function line are grouped under +the flashing Line tab (%s).

    + +

    Click on the Line tab to continue.

    +''' % utils.pixmapAsHtml(utils.getPixmap('settings_plotline.png')), + mainwin, + flash=label, + disablenext=True, + nextstep=FunctionLineFormatting) + + self.connect(tb, qt4.SIGNAL('currentChanged(int)'), + self.slotCurrentChanged) + + def slotCurrentChanged(self, idx): + if idx == 1: + self.emit( qt4.SIGNAL('nextStep') ) + +class FunctionLineFormatting(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Veusz lets you choose a line style, thickness and color for the +function line.

    + +

    Choose a new line color for the line.

    +''', + mainwin, + disablenext=True, + nextonsetting = ('/page1/graph1/function1/Line/color', + lambda val: val.strip() != 'black'), + nextstep=DataStart) + +########### +## Datasets + +class DataStart(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Datasets

    + +

    Many widgets in Veusz plot datasets. Datasets can be imported from +files, entered manually or created from existing datasets using +operations or expressions.

    + +

    Imported data can be linked to an external file or embedded in the +document.

    + +

    Press Next to continue

    +''', mainwin, + nextstep=DataImport) + +class DataImport(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Importing data

    + +

    Let us start by importing data.

    + +

    Click the flashing Data Import icon, or choose +"Import..." From the Data menu.

    +''', mainwin, + flash=mainwin.datatoolbar.widgetForAction( + mainwin.vzactions['data.import']), + disablenext=True, + nextstep=DataImportDialog) + + # make sure we have the default delimiters + for k in ( 'importdialog_csvdelimitercombo_HistoryCombo', + 'importdialog_csvtextdelimitercombo_HistoryCombo' ): + if k in setting.settingdb: + del setting.settingdb[k] + + self.connect(mainwin, qt4.SIGNAL('dialogShown'), self.slotDialogShown ) + + def slotDialogShown(self, dialog): + """Called when a dialog is opened in the main window.""" + from veusz.dialogs.importdialog import ImportDialog + if isinstance(dialog, ImportDialog): + # make life easy by sticking in filename + dialog.filenameedit.setText( + os.path.join(utils.exampleDirectory, 'tutorialdata.csv')) + # and choosing tab + dialog.guessImportTab() + # get rid of existing values + self.emit( qt4.SIGNAL('nextStep') ) + +class DataImportDialog(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    This is the data import dialog. In this tutorial, we have selected +an example CSV (comma separated value) file for you, but you would +normally browse to find your data file.

    + +

    This example file defines three datasets, alpha, beta +and gamma, entered as columns in the CSV file.

    + +

    Press Next to continue

    +''', mainwin, nextstep=DataImportDialog2) + +class DataImportDialog2(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Veusz will try to guess the datatype - numeric, text or date - from +the data in the file or you can specify it manually.

    + +

    Several different data formats are supported in Veusz and plugins +can be defined to import any data format. The Link option links data +to the original file.

    + +

    Click the Import button in the dialog.

    +''', mainwin, + nextstep=DataImportDialog3, + disablenext=True) + self.connect( mainwin.document, + qt4.SIGNAL('sigModified'), self.slotDocModified ) + + def slotDocModified(self): + if 'alpha' in self.mainwin.document.data: + self.emit( qt4.SIGNAL('nextStep') ) + +class DataImportDialog3(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Notice how Veusz has loaded the three different datasets from the +file. You could carry on importing new datasets from the Import dialog +box or reopen it later.

    + +

    Close the Import dialog box.

    +''', mainwin, + disablenext=True, + nextstep=DataImportDialog4) + + self.timer = qt4.QTimer() + self.connect( self.timer, qt4.SIGNAL('timeout()'), + self.slotTimeout ) + self.timer.start(200) + + def slotTimeout(self): + from veusz.dialogs.importdialog import ImportDialog + closed = True + for dialog in self.mainwin.dialogs: + if isinstance(dialog, ImportDialog): + closed = False + if closed: + # move forward if no import dialog open + self.emit( qt4.SIGNAL('nextStep') ) + +class DataImportDialog4(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    The Data viewing window (flashing) shows the currently loaded +datasets in the document.

    + +

    Hover your mouse over datasets to get information about them. You +can see datasets in more detail in the Data Edit dialog box.

    + +

    Click Next to continue

    +''', mainwin, + flash=mainwin.datadock, + nextstep=AddXYPlotter) + +############## +## XY plotting + +class AddXYPlotter(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Plotting data

    + +

    The point plotting widget plots datasets loaded in Veusz.

    + +

    The flashing icon adds a point plotting (xy) +widget. Click on this, or go to the Add menu and choose "Add xy".

    +''', mainwin, + flash=mainwin.treeedit.addtoolbar.widgetForAction( + mainwin.vzactions['add.xy']), + disablenext=True, + nextonsetting = ('/page1/graph1/xy1/xData', + lambda val: val != ''), + nextstep=SetXY_X) + +class SetXY_X(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    The datasets to be plotted are in the widget's properties.

    + +

    Change the "X data" setting to be the +alpha dataset. You can choose this from the drop down +menu or type it.

    +''', mainwin, + disablenext=True, + nextonsetting = ('/page1/graph1/xy1/xData', + lambda val: val == 'alpha'), + nextstep=SetXY_Y) + +class SetXY_Y(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Change the "Y data" setting to be the +beta dataset.

    +''', mainwin, + disablenext=True, + nextonsetting = ('/page1/graph1/xy1/yData', + lambda val: val == 'beta'), + nextstep=SetXYLine) + +class SetXYLine(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Veusz has now plotted the data on the graph. You can manipulate how +the data are shown using the formatting settings.

    + +

    Make sure that the line Formatting tab (%s) for the +widget is selected.

    + +

    Click on the check box next to the Hide option at +the bottom, to hide the line plotted between the data points.

    +''' % utils.pixmapAsHtml(utils.getPixmap('settings_plotline.png')), + mainwin, + disablenext=True, + nextonsetting = ('/page1/graph1/xy1/PlotLine/hide', + lambda val: val), + nextstep=SetXYFill) + +class SetXYFill(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Now we will change the point color.

    + +

    Click on the "Marker fill (%s)" formatting tab. +Change the fill color of the plotted data.

    +''' % utils.pixmapAsHtml(utils.getPixmap('settings_plotmarkerfill.png')), + mainwin, + disablenext=True, + nextonsetting = ('/page1/graph1/xy1/MarkerFill/color', + lambda val: val != 'black'), + nextstep=AddXY2nd) + +class AddXY2nd(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Adding a second dataset

    + +

    We will now plot dataset alpha against +gamma on the same graph.

    + +

    Add a second point plotting (xy) widget using the +flashing icon, or go to the Add menu and choose "Add xy".

    +''', mainwin, + flash=mainwin.treeedit.addtoolbar.widgetForAction( + mainwin.vzactions['add.xy']), + disablenext=True, + nextonsetting = ('/page1/graph1/xy2/xData', + lambda val: val != ''), + nextstep=AddXY2nd_2) + +class AddXY2nd_2(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Change the "X data" setting to be the +alpha dataset.

    +''', mainwin, + disablenext=True, + nextonsetting = ('/page1/graph1/xy2/xData', + lambda val: val == 'alpha'), + nextstep=AddXY2nd_3) + +class AddXY2nd_3(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Next, change the "Y data" setting to be the +gamma dataset.

    +''', mainwin, + disablenext=True, + nextonsetting = ('/page1/graph1/xy2/yData', + lambda val: val == 'gamma'), + nextstep=AddXY2nd_4) + +class AddXY2nd_4(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    We can fill regions under plots using the Fill Below Formatting tab +(%s).

    + +

    Go to this tab, and unselect the "Hide edge fill" +option.

    +''' % utils.pixmapAsHtml(utils.getPixmap('settings_plotfillbelow.png')), + mainwin, + disablenext=True, + nextonsetting = ('/page1/graph1/xy2/FillBelow/hide', + lambda val: not val), + nextstep=File1) + +class File1(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Saving

    + +

    The document can be saved under the File menu, choosing "Save +as...", or by clicking on the Save icon (flashing).

    + +

    Veusz documents are simple text files which can be easily modified +outside the program.

    + +

    Click Next to continue

    +''', mainwin, + flash=mainwin.maintoolbar.widgetForAction( + mainwin.vzactions['file.save']), + nextstep=File2) + +class File2(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Exporting

    + +

    The document can be exported in scalable (EPS, PDF, SVG and EMF) or +bitmap formats.

    + +

    The "Export..." command under the File menu exports the selected +page. Alternatively, click on the Export icon (flashing).

    + +

    Click Next to continue

    +''', mainwin, + flash=mainwin.maintoolbar.widgetForAction( + mainwin.vzactions['file.export']), + nextstep=Cut1, + ) + +class Cut1(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Cut and paste

    + +

    Widgets can be cut and pasted to manipulate the document.

    + +

    Select the "graph1" widget in the Editing window.

    +''', mainwin, + disablenext=True, + nextonselected='graph1', + nextstep=Cut2) + +class Cut2(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Now click the Cut icon (flashing) or choose "Cut" +from the Edit menu.

    + +

    This copies the currently selected widget to the clipboard and +deletes it from the document.

    +''', mainwin, + disablenext=True, + flash=mainwin.treeedit.edittoolbar.widgetForAction( + mainwin.vzactions['edit.cut']), + nextstep=AddGrid) + self.connect( mainwin.document, + qt4.SIGNAL('sigModified'), self.slotCheckDelete ) + + def slotCheckDelete(self, *args): + d = self.mainwin.document + try: + d.resolve(d.basewidget, '/page1/graph1') + except ValueError: + # success! + self.emit( qt4.SIGNAL('nextStep') ) + +class AddGrid(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Adding a grid

    + +

    Now we will add a grid widget to paste the graph back into.

    + +

    Click on the flashing Grid widget icon, or choose +"Add grid" from the Insert menu.

    +''', mainwin, + flash=mainwin.treeedit.addtoolbar.widgetForAction( + mainwin.vzactions['add.grid']), + disablenext=True, + nextonsetting = ('/page1/grid1/rows', + lambda val: val != ''), + nextstep=Paste1) + +class Paste1(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Now click the Paste icon (flashing) or choose "Paste" +from the Edit menu.

    + +

    This pastes back the widget from the clipboard.

    +''', mainwin, + disablenext=True, + flash=mainwin.treeedit.edittoolbar.widgetForAction( + mainwin.vzactions['edit.paste']), + nextonsetting = ('/page1/grid1/graph1/leftMargin', + lambda val: val != ''), + nextstep=Paste2) + +class Paste2(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    For a second time, click the Paste icon (flashing) +or choose "Paste" from the Edit menu.

    + +

    This adds a second copy of the original graph to the grid.

    +''', mainwin, + disablenext=True, + flash=mainwin.treeedit.edittoolbar.widgetForAction( + mainwin.vzactions['edit.paste']), + nextonsetting = ('/page1/grid1/graph2/leftMargin', + lambda val: val != ''), + nextstep=Paste3) + +class Paste3(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    Having the graphs side-by-side looks a bit messy. We would like to +change the graphs to be arranged in rows.

    + +

    Navigate to the grid1 widget properties. Change the +number of columns to 1.

    +''', mainwin, + disablenext=True, + nextonsetting = ('/page1/grid1/columns', + lambda val: val == 1), + nextstep=Paste4) + +class Paste4(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    We could now adjust the margins of the graphs and the grid.

    + +

    Axes can also be shared by the graphs of the grid by moving them +into the grid widget. This shares the same axis scale for graphs.

    + +

    Click Next to continue

    +''', mainwin, nextstep=EndStep) + +class EndStep(TutorialStep): + def __init__(self, mainwin): + TutorialStep.__init__( + self, ''' +

    The End

    + +

    Thank you for working through this Veusz tutorial. We hope you +enjoy using Veusz!

    + +

    Please send comments, bug reports and suggestions to the +developers via the mailing list.

    + +

    You can try this tutorial again from the Help menu.

    +''', mainwin, closestep=True, disablenext=True) + +class TutorialDock(qt4.QDockWidget): + '''A dock tutorial window.''' + + def __init__(self, document, mainwin, *args): + qt4.QDockWidget.__init__(self, *args) + self.setAttribute(qt4.Qt.WA_DeleteOnClose) + self.setMinimumHeight(300) + self.setWindowTitle('Tutorial - Veusz') + self.setObjectName('veusztutorialwindow') + + self.setStyleSheet('background: lightyellow;') + + self.document = document + self.mainwin = mainwin + + self.layout = l = qt4.QVBoxLayout() + + txtdoc = qt4.QTextDocument(self) + txtdoc.setDefaultStyleSheet( + "p.usercmd { color: blue; } " + "h1 { font-size: x-large;} " + "code { color: green;} " + ) + self.textedit = qt4.QTextEdit(readOnly=True) + self.textedit.setDocument(txtdoc) + + l.addWidget(self.textedit) + + self.buttonbox = qt4.QDialogButtonBox() + self.nextb = self.buttonbox.addButton( + 'Next', qt4.QDialogButtonBox.ActionRole) + self.connect(self.nextb, qt4.SIGNAL('clicked()'), self.slotNext) + + l.addWidget(self.buttonbox) + + # have to use a separate widget as dialog already has layout + self.widget = qt4.QWidget() + self.widget.setLayout(l) + self.setWidget(self.widget) + + # timer for controlling flashing + self.flashtimer = qt4.QTimer(self) + self.connect(self.flashtimer, qt4.SIGNAL('timeout()'), + self.slotFlashTimeout) + self.flash = self.oldflash = None + self.flashon = False + self.flashct = 0 + self.flashtimer.start(500) + + self.changeStep(StepIntro) + + def ensureShowFlashWidgets(self): + '''Ensure we can see the widgets flashing.''' + w = self.flash + while w is not None: + w.show() + w = w.parent() + + def changeStep(self, stepklass): + '''Apply the next step.''' + + # this is the current text + self.step = stepklass(self.mainwin) + + # listen to step for next step + self.connect(self.step, qt4.SIGNAL('nextStep'), self.slotNext) + + # update text + self.textedit.setHtml(self.step.text) + + # handle requests for flashing + self.flashct = 20 + self.flashon = True + self.flash = self.step.flash + if self.flash is not None: + self.ensureShowFlashWidgets() + + # enable/disable next button + self.nextb.setEnabled(not self.step.disablenext) + + # add a close button if requested + if self.step.closestep: + closeb = self.buttonbox.addButton( + 'Close', qt4.QDialogButtonBox.ActionRole) + self.connect(closeb, qt4.SIGNAL('clicked()'), self.close) + + def slotFlashTimeout(self): + '''Handle flashing of UI components.''' + + if self.flash is not self.oldflash and self.oldflash is not None: + # clear any flashing on previous widget + self.oldflash.setStyleSheet('') + self.oldflash = None + + if self.flash: + # set flash state and toggle variable + if self.flashon: + self.flash.setStyleSheet('background: yellow;') + else: + self.flash.setStyleSheet('') + self.flashon = not self.flashon + self.oldflash = self.flash + + # stop flashing after N iterations + self.flashct -= 1 + if self.flashct == 0: + self.flash = None + + def slotNext(self): + """Move to the next page of the tutorial.""" + nextstepklass = self.step.nextstep + if nextstepklass is not None: + self.changeStep( nextstepklass ) diff -Nru veusz-1.10/windows/widgettree.py veusz-1.14/windows/widgettree.py --- veusz-1.10/windows/widgettree.py 2010-12-12 12:41:08.000000000 +0000 +++ veusz-1.14/windows/widgettree.py 2011-11-22 20:23:31.000000000 +0000 @@ -16,8 +16,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ############################################################################### -# $Id: widgettree.py 1330 2010-07-15 19:57:32Z jeremysanders $ - """Contains a model and view for handling a tree of widgets.""" import veusz.qtall as qt4 @@ -147,83 +145,45 @@ return qt4.QVariant() - def _getChildren(self, parent): - """Get a list of children for the parent given (None selects root).""" - - if parent is None: - return [self.document.basewidget] - else: - return parent.children - def index(self, row, column, parent): """Construct an index for a child of parent.""" - if not parent.isValid(): - parentobj = None + if parent.isValid(): + # normal widget + try: + child = parent.internalPointer().children[row] + except IndexError: + return qt4.QModelIndex() else: - parentobj = parent.internalPointer() - - children = self._getChildren(parentobj) - - try: - c = children[row] - except IndexError: - # sometimes this function gets called with an invalid row - # when deleting, so we return an error result - return qt4.QModelIndex() - - return self.createIndex(row, column, c) + # root widget + child = self.document.basewidget + return self.createIndex(row, column, child) def getWidgetIndex(self, widget): """Returns index for widget specified.""" - - # walk index tree back to widget from root - widgetlist = [] - w = widget - while w is not None: - widgetlist.append(w) - w = w.parent - - # now iteratively look up indices - parent = qt4.QModelIndex() - while widgetlist: - w = widgetlist.pop() - row = self._getChildren(w.parent).index(w) - parent = self.index(row, 0, parent) - - return parent + return self.createIndex(widget.widgetSiblingIndex(), 0, widget) def parent(self, index): """Find the parent of the index given.""" - if not index.isValid(): - return qt4.QModelIndex() - - thisobj = index.internalPointer() - parentobj = thisobj.parent - + parentobj = index.internalPointer().parent if parentobj is None: return qt4.QModelIndex() else: - # lookup parent in grandparent's children - grandparentchildren = self._getChildren(parentobj.parent) try: - parentrow = grandparentchildren.index(parentobj) + return self.createIndex(parentobj.widgetSiblingIndex(), 0, + parentobj) except ValueError: return qt4.QModelIndex() - return self.createIndex(parentrow, 0, parentobj) - - def rowCount(self, parent): - """Return number of rows of children.""" + def rowCount(self, index): + """Return number of rows of children of index.""" - if not parent.isValid(): - parentobj = None + if index.isValid(): + return len(index.internalPointer().children) else: - parentobj = parent.internalPointer() - - children = self._getChildren(parentobj) - return len(children) + # always 1 root node + return 1 def getSettings(self, index): """Return the settings for the index selected.""" @@ -320,7 +280,7 @@ hdr.setResizeMode(1, qt4.QHeaderView.Custom) # setup drag and drop - self.setSelectionMode(qt4.QAbstractItemView.SingleSelection) + self.setSelectionMode(qt4.QAbstractItemView.ExtendedSelection) self.setDragEnabled(True) self.viewport().setAcceptDrops(True) self.setDropIndicatorShown(True)